fix(auth): derive clientIp server-side, drop it from sign-in input
clientIp was part of the public signInInputSchema, so any client could spoof its own rate-limit bucket or dodge IP throttling entirely (audit finding B2). The schema no longer carries it (strict parsing rejects it with BAD_REQUEST); instead the web-next tRPC fetch adapter derives it in createTrpcContext from x-forwarded-for (first hop) / x-real-ip — trust caveat documented — and the router threads ctx.clientIp to the controller as a second, server-only argument typed outside the input schema (SignInRequestContext). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,15 +11,27 @@ import type { IUsersRepository } from "../repositories/users.repository.interfac
|
||||
import type { IAuthenticationService } from "../services/authentication.service.interface";
|
||||
|
||||
// ── Input ────────────────────────────────────────────────────────────────
|
||||
// `.strict()` + no clientIp field: a client submitting clientIp is rejected
|
||||
// at the procedure boundary (audit finding B2).
|
||||
export const signInInputSchema = z
|
||||
.object({
|
||||
username: z.string().min(3).max(31),
|
||||
password: z.string().min(6).max(255),
|
||||
clientIp: z.string().optional(),
|
||||
})
|
||||
.strict();
|
||||
export type SignInInput = z.infer<typeof signInInputSchema>;
|
||||
|
||||
/**
|
||||
* Server-derived per-request context, typed OUTSIDE the public input schema
|
||||
* so it can never be client-supplied (audit finding B2). The tRPC adapter
|
||||
* derives `clientIp` from trusted proxy headers and the controller threads
|
||||
* it through; `undefined` means "no proxy header present" and falls into a
|
||||
* shared bucket.
|
||||
*/
|
||||
export type SignInRequestContext = {
|
||||
clientIp?: string;
|
||||
};
|
||||
|
||||
// ── Output ───────────────────────────────────────────────────────────────
|
||||
export const signInOutputSchema = z.object({
|
||||
session: sessionSchema,
|
||||
@@ -36,7 +48,7 @@ export const signInUseCase =
|
||||
authenticationService: IAuthenticationService,
|
||||
rateLimit: IRateLimit,
|
||||
) =>
|
||||
async (input: SignInInput): Promise<SignInOutput> => {
|
||||
async (input: SignInInput & SignInRequestContext): Promise<SignInOutput> => {
|
||||
const { allowed: ipAllowed } = await rateLimit.consume(
|
||||
"ip",
|
||||
`signIn:ip:${input.clientIp ?? ""}`,
|
||||
|
||||
Reference in New Issue
Block a user