fix(authz): stop encodeURI from double-encoding a percent-escaped redirectTo
Fix round 3 for Task 7 (N5, minor-to-important): fix round 2's encodeURI(options.redirectTo) fixed the non-ASCII crash but broke the most common real use of redirectTo -- a return-path query param that's already percent-encoded (e.g. /login?next=%2Fdash) -- because encodeURI also escapes "%", double-encoding it to %252Fdash. Replaced with headerSafePath(), a codepoint loop that encodes only codepoints above 0x7f (matching isLocalPath's style: no regex, no source escapes) and leaves "%" alone. Added tests: an already-percent-encoded target round-trips unchanged; a non-ASCII target still 303s without throwing and the location is ASCII-only; a plain ASCII target passes through byte-identical.
This commit is contained in:
@@ -166,6 +166,20 @@ function isLocalPath(target: string): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Header values must be Latin-1, so a localized path would otherwise throw
|
||||
* inside `new Response` and 500 on a denial path. Encode ONLY the codepoints
|
||||
* that cannot be sent: encodeURI would also escape "%", corrupting a target
|
||||
* that already carries a percent-encoded return path.
|
||||
*/
|
||||
function headerSafePath(value: string): string {
|
||||
let out = "";
|
||||
for (const character of value) {
|
||||
out += character.codePointAt(0)! <= 0x7f ? character : encodeURIComponent(character);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
const NO_STORE_HEADERS = { "cache-control": "private, no-store" } as const;
|
||||
|
||||
export interface GuardOptions {
|
||||
@@ -203,9 +217,11 @@ export function guardPermission(permission: string, options: GuardOptions = {}):
|
||||
if (isLocalPath(options.redirectTo)) {
|
||||
return new Response(null, {
|
||||
status: 303,
|
||||
// encodeURI: a non-ASCII local path (e.g. a localized login route)
|
||||
// is valid config but not a valid raw header value.
|
||||
headers: { location: encodeURI(options.redirectTo), ...NO_STORE_HEADERS },
|
||||
// headerSafePath, not encodeURI: a non-ASCII local path (e.g. a
|
||||
// localized login route) is valid config but not a valid raw
|
||||
// header value, while encodeURI would also mangle a target that
|
||||
// already carries a percent-encoded return path.
|
||||
headers: { location: headerSafePath(options.redirectTo), ...NO_STORE_HEADERS },
|
||||
});
|
||||
}
|
||||
// JSON.stringify, not string interpolation: this branch exists precisely
|
||||
|
||||
Reference in New Issue
Block a user