UNPKG

agentfootprint

Version:

The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.

69 lines • 3.06 kB
"use strict"; /** * hosting/signin/returnTo — where a sign-in sends the browser afterwards, and * why it can never be off the public origin (design §5.5; RFC 9700 §4.11.1: * "Clients MUST NOT expose open redirectors"). * * Written down so nobody improvises "starts with `/`", which passes * `//evil.example`, `/\evil.example` (browsers read `\` as `/`) and * `/<TAB>/evil.example` (the URL parser drops tab, CR and LF): * * 1. Only a relative path: exactly one leading `/`; no `\` and no control * character anywhere in the raw input; never an absolute URL, even one * naming our own origin. * 2. Resolved against the public URL anyway, and refused unless the result's * origin IS the public origin — a second check for a parser quirk step 1 * missed. * 3. Only the re-serialised `pathname + search + hash` is kept: that string, * not the input, is what gets sealed and redirected to. It must still * start with exactly one `/`: dot segments (`/.//evil.example`, * `/%2e%2e//evil.example`) resolve to the pathname `//evil.example`, a * protocol-relative redirect — this check is their only guard. * 4. The KEPT form is at most 512 bytes (review idI57 S-2): it rides in a * sealed cookie, and percent-encoding can grow 900 raw characters to * 5 KB, past a browser's per-cookie cap. * 5. Never the door itself (`/auth/login` would loop through a silent-SSO * IdP, minting a sign-in per lap — review idI57 N-3). * 6. Anything refused becomes `/`. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.safeReturnTo = exports.MAX_RETURN_TO_BYTES = void 0; /** The longest kept `returnTo`, in bytes after encoding. */ exports.MAX_RETURN_TO_BYTES = 512; /** * The safe place to send the browser: a same-origin path, or `/`. * `doorPrefix` (the sign-in door's, `/auth`): a path under it becomes `/`. */ function safeReturnTo(input, publicUrl, doorPrefix) { if (typeof input !== 'string' || input.length === 0 || input.length > 2048) return '/'; if (!input.startsWith('/') || input.startsWith('//')) return '/'; // eslint-disable-next-line no-control-regex if (/[\\\u0000-\u001f\u007f]/.test(input)) return '/'; let resolved; try { resolved = new URL(input, publicUrl.origin); } catch { return '/'; } if (resolved.origin !== publicUrl.origin) return '/'; const kept = `${resolved.pathname}${resolved.search}${resolved.hash}`; if (!kept.startsWith('/') || kept.startsWith('//')) return '/'; if (Buffer.byteLength(kept, 'utf8') > exports.MAX_RETURN_TO_BYTES) return '/'; if (doorPrefix !== undefined && underPrefix(resolved.pathname, doorPrefix)) return '/'; return kept; } exports.safeReturnTo = safeReturnTo; function underPrefix(pathname, prefix) { const lower = pathname.toLowerCase(); const door = prefix.toLowerCase(); return lower === door || lower.startsWith(`${door}/`); } //# sourceMappingURL=returnTo.js.map