From 96cb1519dec32869406c1f4745c86c27fc13d22f Mon Sep 17 00:00:00 2001 From: "bert.hausmans" Date: Wed, 6 May 2026 00:20:12 +0200 Subject: [PATCH] feat(security): full A13-3 open-redirect validation in postLoginRedirect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the WS-3 PR-B2a minimum precaution (`startsWith('/') && !startsWith('//')`) with a layered validator that rejects every input that is not a strict relative path. isSafeRelativePath rejects: - Empty / null / undefined input - Non-`/`-prefixed paths (including leading whitespace) - Protocol-relative URLs (`//evil.com`) - Backslash anywhere (browsers normalise `\` → `/` in some contexts; `/\evil.com` parses as `//evil.com`) - ASCII control characters `\x00`–`\x1F` and `\x7F` (NUL, tab, LF, CR, DEL, etc. — header-injection vectors) - Anything the URL constructor parses to a different origin than the synthetic invalid origin used as the resolution base The URL-constructor check is the authoritative guard; the prefix and character checks are fast pre-filters that short-circuit common attack shapes without paying the URL allocation. Test coverage expands from 6 → 16 cases. New cases pin the backslash, control-character, leading-whitespace, and positive- character-set contracts. The URL-encoded-slash-in-query case documents that we don't false-positive on `%2F` in query strings. Closes A13-3 (open-redirect on post-login). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../utils/__tests__/postLoginRedirect.spec.ts | Bin 1751 -> 3477 bytes apps/app/src/utils/postLoginRedirect.ts | 46 +++++++++++++++--- 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/apps/app/src/utils/__tests__/postLoginRedirect.spec.ts b/apps/app/src/utils/__tests__/postLoginRedirect.spec.ts index 343df18e825593c6b3e9ea2bbd3b1ed5b0d030a4..d5bd67c57cfdbff708d01f14b7c71b8683c975a4 100644 GIT binary patch delta 881 zcmaiz!HN?>5QZhj;6@}YxS*&@5s{t9PDTV379|(WDR>YTF9XukUCA^%=^ndllFdQ) z=2;x{?jv~jJ-quQc6Smt0U?)xo~io!|EvD_ee>JhFF)>&tD*p1qKXX)X^<1lSRn-? zsnyS4w_Ei=vop*XKeGk1d0ZMC%X1VURap^4nbC8L#)8sj>KZLVngVG^jv+}?oXMik z^%VYm{tESr=1C(>>rYpo);C)1`d;&T52qOl;duY9KnqMQ<~bU4mjLAmC8NCFZ}j() zvDRstF;fp3@tSc+ECZf1!#H^*5!Z^0F2Xz8^Nst7N2D~D{^{!NCZMZuUPy&IAO}l` z3J|sG*#bS{%oxcg=!Qv^Q9FR0CC~(*;ajHm-kgpgTM$~WznxrBZc+ZcDg4%}*Q3ij zAbNcMd>H&*OD+T!o2AbRWJ0P*Y)i)dV8ix=cGFgxz`A|g@d7L{L-w5;Q`Akmy#VV# zvemrC4jAWHk`3#z;9aazaH+|jfN-%vdkmElZaZ8Xe-N62F^`@|3WG>IAzqfhXG-)# z(f8Kj#g3YzUK`+JukD_aqNBB})3>7-71uspA-vA3R8|Uv4;313*0_ndz>qHE0%uqR sD string, ): string { const to = rawTo ?? '' - if (to.startsWith('/') && !to.startsWith('//')) - return to - return fallback() + return isSafeRelativePath(to) ? to : fallback() }