WS-3 PR-B2b: A13-3 + single-cookie + single-host (incl. flatpickr precursor) #6

Merged
bert.hausmans merged 8 commits from feat/ws-3-pr-b2b-single-cookie-deploy into main 2026-05-06 01:16:07 +02:00
2 changed files with 38 additions and 8 deletions
Showing only changes of commit 96cb1519de - Show all commits

View File

@@ -1,19 +1,49 @@
/**
* Resolve the post-login redirect target. If the caller supplied a `?to=`
* query that's a same-origin relative path, honour it; otherwise fall back
* to the auth-store's resolveLandingRoute().
* query that is a same-origin, well-formed relative path, honour it;
* otherwise fall back to the auth-store's resolveLandingRoute().
*
* The `startsWith('/')` + `!startsWith('//')` guard is the **minimum**
* A13-3 (open-redirect) precaution. Full domain-validation lands in
* WS-3 PR-B2b.
* `isSafeRelativePath` rejects every input that is not a strict relative
* path: missing/empty, absolute, protocol-relative (`//`), backslash-bearing
* (browsers normalise `\` → `/` in some contexts), control-character-bearing,
* or anything the URL constructor parses to a different origin than our
* synthetic invalid origin. The URL-constructor check is the authoritative
* guard — the prefix and character checks are fast pre-filters.
*
* Closes A13-3 (open-redirect on post-login). The minimum precaution from
* WS-3 PR-B2a (`startsWith('/') && !startsWith('//')`) is now superseded.
*/
const SYNTHETIC_ORIGIN = 'https://__crewli_safe_relative_check__.invalid'
function isSafeRelativePath(to: string): boolean {
if (!to || !to.startsWith('/') || to.startsWith('//'))
return false
if (to.includes('\\'))
return false
// eslint-disable-next-line no-control-regex
if (/[\x00-\x1F\x7F]/.test(to))
return false
try {
const url = new URL(to, SYNTHETIC_ORIGIN)
if (url.origin !== SYNTHETIC_ORIGIN)
return false
}
catch {
return false
}
return true
}
export function resolvePostLoginTarget(
rawTo: string | null | undefined,
fallback: () => string,
): string {
const to = rawTo ?? ''
if (to.startsWith('/') && !to.startsWith('//'))
return to
return fallback()
return isSafeRelativePath(to) ? to : fallback()
}