fix: secure authentication request identity
Replace Better Auth's default trust of a caller-controlled X-Forwarded-For with a platform-owned client-address resolution boundary: direct mode (the safe default) trusts only the real socket peer, and an explicit trusted-proxy mode requires a header name, a concrete trusted-proxy list, and only honors a forwarded chain from an actual trusted peer. This closes a rate-limit bypass where rotating a spoofed header let a caller choose a fresh bucket on every request. Public sign-up is also disabled by default and requires an explicit typed opt-in, using Better Auth's own disableSignUp gate so no email-existence leak or second signup path is introduced. Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
5e09330001
commit
cafd513019
@@ -0,0 +1,66 @@
|
||||
import { type ClientAddressResolver, TRUSTED_CLIENT_ADDRESS_HEADER } from "@ekklesia/core/identity";
|
||||
import type { Http2Bindings, HttpBindings } from "@hono/node-server";
|
||||
|
||||
type NodeServerBindings = HttpBindings | Http2Bindings;
|
||||
|
||||
export {
|
||||
type ClientAddressConfig,
|
||||
ClientAddressConfigError,
|
||||
type ClientAddressConfigErrorCode,
|
||||
type ClientAddressConfigInput,
|
||||
type ClientAddressFailureReason,
|
||||
type ClientAddressMode,
|
||||
type ClientAddressResolution,
|
||||
type ClientAddressResolver,
|
||||
createClientAddressConfig,
|
||||
createClientAddressResolver,
|
||||
type DirectClientAddressConfig,
|
||||
findInvalidTrustedProxies,
|
||||
normalizeClientAddress,
|
||||
type TrustedProxyClientAddressConfig,
|
||||
} from "@ekklesia/core/identity";
|
||||
|
||||
/** A fixed, generic diagnostic — never the peer address, forwarded header content, request path, or
|
||||
* any other request detail — logged only when client-address resolution fails closed. */
|
||||
export const CLIENT_ADDRESS_UNRESOLVED_LOG_MESSAGE =
|
||||
"[client-address] A request's caller address could not be resolved safely; rejecting with 503.";
|
||||
|
||||
function sanitizedClientAddressUnresolvedResponse(): Response {
|
||||
return new Response(JSON.stringify({ code: "CLIENT_ADDRESS_UNRESOLVED" }), {
|
||||
headers: { "cache-control": "no-store", "content-type": "application/json" },
|
||||
status: 503,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps a `@hono/node-server` fetch callback at the exact Node/API transport boundary — the one
|
||||
* place in this repository below `apps/api` that reads the raw Node `IncomingMessage` — with
|
||||
* Core's framework-agnostic client-address resolution policy (`@ekklesia/core/identity`). Resolves
|
||||
* the caller's trusted address from the real socket peer (`env.incoming.socket.remoteAddress`)
|
||||
* before the wrapped callback — and therefore Better Auth's own rate limiter — ever sees the
|
||||
* request, then overwrites (never merely appends) any externally supplied copy of
|
||||
* `TRUSTED_CLIENT_ADDRESS_HEADER` with that resolved value. A caller can therefore never choose its
|
||||
* own resolved address, whether by spoofing a forwarded header or by sending the internal header
|
||||
* directly. Resolution failure never reaches the wrapped callback at all — no Better Auth logic, no
|
||||
* Core mutation, and no module handler ever runs for a request whose caller address could not be
|
||||
* resolved safely.
|
||||
*/
|
||||
export function wrapFetchWithClientAddress(
|
||||
fetchCallback: (request: Request, env: NodeServerBindings) => Response | Promise<Response>,
|
||||
resolver: ClientAddressResolver,
|
||||
): (request: Request, env: NodeServerBindings) => Promise<Response> {
|
||||
return async (request: Request, env: NodeServerBindings): Promise<Response> => {
|
||||
const resolution = resolver.resolve({
|
||||
headers: request.headers,
|
||||
peerAddress: env.incoming.socket.remoteAddress,
|
||||
});
|
||||
if (!resolution.ok) {
|
||||
console.error(CLIENT_ADDRESS_UNRESOLVED_LOG_MESSAGE);
|
||||
return sanitizedClientAddressUnresolvedResponse();
|
||||
}
|
||||
const headers = new Headers(request.headers);
|
||||
headers.set(TRUSTED_CLIENT_ADDRESS_HEADER, resolution.address);
|
||||
const sanitizedRequest = new Request(request, { headers });
|
||||
return fetchCallback(sanitizedRequest, env);
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user