ANT-2026-REDYDMAE · supabase/supabase

xss medium

Severity Claude medium · Security research firm - · Maintainer -

Discovered by Claude Mythos Preview

REPORT

Anthropic's analysis of this finding, sealed at approval.

ANT-2026-REDYDMAE: DOM-based XSS via javascript: scheme in URL hostname check

The isVercelUrl() helper checks only that new URL(url).hostname === 'vercel.com' and never inspects the protocol. Because non-special schemes like javascript: parse an authority after //, a URL such as javascript://vercel.com/%0a yields hostname 'vercel.com' and passes validation. The attacker-controlled next query parameter flows through this check to window.location.href in three Vercel integration pages (choose-project.tsx:85/142 and new-project.tsx:246). Assigning a javascript: URL to location.href executes the decoded body after the %0a newline in the Studio origin, and the app's CSP permits 'unsafe-inline' so it does not block the navigation. An attacker who lures an authenticated admin to a crafted link and gets one click on the 'Skip' button gains arbitrary JS execution in the victim's dashboard session.

Target

Project: supabase/supabase
Location: apps/studio/components/interfaces/Integrations/Vercel/VercelIntegration.utils.ts:3
Discovery: static analysis — not yet dynamically reproduced

Technical Details

Root cause is a protocol-blind URL allowlist: validating only new URL(url).hostname is insufficient because the WHATWG URL parser will populate hostname for non-special schemes when the input contains //authority. Thus new URL('javascript://vercel.com/%0aalert(1)').hostname === 'vercel.com' is true, the check passes, and the javascript: URL is later assigned to window.location.href, where the browser treats //vercel.com/ as a JS line comment and executes everything after the %0a newline.

Reproduction

  1. Craft a URL to /dashboard/integrations/vercel//marketplace/choose-project with next=javascript%3A%2F%2Fvercel.com%2F%250a
  2. Deliver the link to an authenticated Studio user
  3. Victim loads the page; isVercelUrl() approves the next value because hostname === 'vercel.com'
  4. Victim clicks 'Skip', triggering window.location.href = next
  5. Browser executes the javascript: body after the %0a newline in the Studio origin

[No reproducer or sanitizer output attached — request from security-cvd@anthropic.com if needed.]

Suggested Fix

URL validators that gate navigation must enforce a protocol allowlist (http/https) in addition to hostname checks; hostname-only validation of new URL() output is bypassable via non-special schemes like javascript:.

Acknowledgement

This vulnerability was discovered by Claude, Anthropic's AI assistant, and triaged by the Anthropic security team in collaboration with Anthropic Research. Please direct questions to security-cvd@anthropic.com and reference ANT-2026-REDYDMAE.


Reference: ANT-2026-REDYDMAE
Anthropic CVD Policy: https://www.anthropic.com/coordinated-vulnerability-disclosure

UPSTREAM FIX

The change that resolved this finding.

diff --git a/apps/studio/components/interfaces/Integrations/Vercel/VercelIntegration.utils.ts b/apps/studio/components/interfaces/Integrations/Vercel/VercelIntegration.utils.ts
index cf4a7f0c93bf5..3c55db6f9e6aa 100644
--- a/apps/studio/components/interfaces/Integrations/Vercel/VercelIntegration.utils.ts
+++ b/apps/studio/components/interfaces/Integrations/Vercel/VercelIntegration.utils.ts
@@ -1,6 +1,7 @@
 export function isVercelUrl(url: string): boolean {
   try {
-    return new URL(url).hostname === 'vercel.com'
+    const u = new URL(url)
+    return u.protocol === 'https:' && u.hostname === 'vercel.com'
   } catch {
     // If the URL is invalid, return false
     return false

https://github.com/supabase/supabase/commit/1097fcaa2f4e7954f7636eb8bbdc04c8060affc0

TIMELINE

Dates from discovery through public reveal.

  1. 2026-05-14 Reported to tracker
  2. 2026-05-14 Maintainer acknowledged
  3. 2026-05-15 Sent to maintainer
  4. 2026-05-20 Patch released
  5. 2026-08-18 Publicly revealed
PROVENANCE

SHA-3-512 hash:

eee969c5af0ed7459b1785ef07a4d522782ddf10821f03a6515b47586c9d3369bb3fa34dbdb84a165ff71a303e7a47b22386c6369e6e72b990cb54174b4672c9

Committed 2026-05-17 17:55 PT

Revealed 2026-08-18 07:11 PT

Verify (download preimage.json)

Show preimage JSON
{
  "ant_id": "ANT-2026-REDYDMAE",
  "bug_class": "xss",
  "claude_severity": "medium",
  "commit_sha": null,
  "created_at": "2026-05-14T22:03:38+00:00",
  "description": "The isVercelUrl() helper checks only that new URL(url).hostname === 'vercel.com' and never inspects the protocol. Because non-special schemes like javascript: parse an authority after //, a URL such as javascript://vercel.com/%0a<payload> yields hostname 'vercel.com' and passes validation. The attacker-controlled `next` query parameter flows through this check to window.location.href in three Vercel integration pages (choose-project.tsx:85/142 and new-project.tsx:246). Assigning a javascript: URL to location.href executes the decoded body after the %0a newline in the Studio origin, and the app's CSP permits 'unsafe-inline' so it does not block the navigation. An attacker who lures an authenticated admin to a crafted link and gets one click on the 'Skip' button gains arbitrary JS execution in the victim's dashboard session.",
  "discovered_at": "2026-05-10T00:00:00+00:00",
  "location": "apps/studio/components/interfaces/Integrations/Vercel/VercelIntegration.utils.ts:3",
  "poc_sha256": null,
  "preimage_version": 1,
  "project": "supabase/supabase",
  "reproduction": [
    "1. Craft a URL to /dashboard/integrations/vercel/<slug>/marketplace/choose-project with next=javascript%3A%2F%2Fvercel.com%2F%250a<js-payload>",
    "2. Deliver the link to an authenticated Studio user",
    "3. Victim loads the page; isVercelUrl() approves the next value because hostname === 'vercel.com'",
    "4. Victim clicks 'Skip', triggering window.location.href = next",
    "5. Browser executes the javascript: body after the %0a newline in the Studio origin"
  ],
  "technical_details": "Root cause is a protocol-blind URL allowlist: validating only `new URL(url).hostname` is insufficient because the WHATWG URL parser will populate hostname for non-special schemes when the input contains `//authority`. Thus `new URL('javascript://vercel.com/%0aalert(1)').hostname === 'vercel.com'` is true, the check passes, and the javascript: URL is later assigned to window.location.href, where the browser treats `//vercel.com/` as a JS line comment and executes everything after the %0a newline.",
  "title": "DOM-based XSS via javascript: scheme in URL hostname check",
  "vendor_severity": null
}