ANT-2026-125WT6QC · supabase/supabase

denial-of-service low

Severity Claude low · Security research firm - · Maintainer -

Discovered by Claude Mythos Preview

REPORT

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

ANT-2026-125WT6QC: Catastrophic backtracking in t-shirt competition email regex

The get-tshirt-competition example edge function validates the email body field with the pattern /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/. The sub-pattern ([\.-]?\w+)* nests a quantified group inside *, so a long run of word characters with no @ (e.g. 40+ as followed by !) forces V8's regex engine into exponential backtracking. The function is deployed with --no-verify-jwt, so an unauthenticated attacker can POST such a body and pin the isolate at 100% CPU until the platform kills it; repeated requests exhaust the function's concurrency. Impact is limited because this is example code that a user must explicitly deploy.

Target

Project: supabase/supabase
Location: examples/edge-functions/supabase/functions/get-tshirt-competition/index.ts:42
Discovery: static analysis — not yet dynamically reproduced

Technical Details

The root cause is the nested quantifier ([\.-]?\w+)* applied after ^\w+: because [\.-]? is optional, the engine can partition a string of word characters across the outer \w+ and arbitrarily many inner \w+ groups in exponentially many ways before failing at the mandatory @. No input-length cap is applied before .test() is called.

Reproduction

  1. Send POST /functions/v1/get-tshirt-competition with body {"email":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!","size":"M"}
  2. The regex .test() call enters exponential backtracking and does not return within the timeout
  3. Repeat concurrently to exhaust the function's available concurrency

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

Suggested Fix

Replace the pattern with a linear-time email check (e.g. a bounded-length .+@.+\..+ test or a known-safe regex) and enforce a maximum length on email before matching.

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-125WT6QC.


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

UPSTREAM FIX

The change that resolved this finding.

diff --git a/examples/ai/aws_bedrock_image_gen/supabase/config.toml b/examples/ai/aws_bedrock_image_gen/supabase/config.toml
index de3c5ea0f71a7..5d8dd5e76b507 100644
--- a/examples/ai/aws_bedrock_image_gen/supabase/config.toml
+++ b/examples/ai/aws_bedrock_image_gen/supabase/config.toml
@@ -13,7 +13,7 @@ file_size_limit = "50MiB"
 
 [functions.image_gen]
 enabled = true
-verify_jwt = true
+verify_jwt = false
 # import_map = "./functions/image_gen/deno.json"
 # Uncomment to specify a custom file path to the entrypoint.
 # Supported file extensions are: .ts, .js, .mjs, .jsx, .tsx
diff --git a/examples/ai/aws_bedrock_image_gen/supabase/functions/image_gen/index.ts b/examples/ai/aws_bedrock_image_gen/supabase/functions/image_gen/index.ts
index e20b971cfd5b3..b3b596daa1ed7 100644
--- a/examples/ai/aws_bedrock_image_gen/supabase/functions/image_gen/index.ts
+++ b/examples/ai/aws_bedrock_image_gen/supabase/functions/image_gen/index.ts
@@ -1,82 +1,77 @@
 // AWS SDK issue: https://github.com/aws/aws-sdk-js-v3/issues/6134
 // We need to mock the file system for the AWS SDK to work.
 import { prepareVirtualFile } from 'https://deno.land/x/mock_file@v1.1.2/mod.ts'
-
-import { BedrockRuntimeClient, InvokeModelCommand } from 'npm:@aws-sdk/client-bedrock-runtime'
-import { createClient } from 'npm:@supabase/supabase-js'
-import { decode } from 'npm:base64-arraybuffer'
+import { BedrockRuntimeClient, InvokeModelCommand } from 'npm:@aws-sdk/client-bedrock-runtime@^3'
+import { withSupabase } from 'npm:@supabase/server@^1'
+import { decode } from 'npm:base64-arraybuffer@^1'
 
 console.log('Hello from Amazon Bedrock!')
 
-Deno.serve(async (req) => {
-  prepareVirtualFile('./aws/config')
-  prepareVirtualFile('./aws/credentials')
-
-  const client = new BedrockRuntimeClient({
-    region: 'us-west-2',
-    credentials: {
-      accessKeyId: Deno.env.get('AWS_ACCESS_KEY_ID') ?? '',
-      secretAccessKey: Deno.env.get('AWS_SECRET_ACCESS_KEY') ?? '',
-      sessionToken: Deno.env.get('AWS_SESSION_TOKEN') ?? '',
-    },
-  })
+// Called with a publishable key on the `apikey` header. Deploy with verify_jwt = false.
+export default {
+  fetch: withSupabase({ auth: 'publishable' }, async (req, ctx) => {
+    prepareVirtualFile('./aws/config')
+    prepareVirtualFile('./aws/credentials')
 
-  const { prompt, seed } = await req.json()
-  console.log(prompt)
-  const input = {
-    contentType: 'application/json',
-    accept: '*/*',
-    modelId: 'amazon.titan-image-generator-v1',
-    body: JSON.stringify({
-      taskType: 'TEXT_IMAGE',
-      textToImageParams: { text: prompt },
-      imageGenerationConfig: {
-        numberOfImages: 1,
-        quality: 'standard',
-        cfgScale: 8.0,
-        height: 512,
-        width: 512,
-        seed: seed ?? 0,
+    const client = new BedrockRuntimeClient({
+      region: 'us-west-2',
+      credentials: {
+        accessKeyId: Deno.env.get('AWS_ACCESS_KEY_ID') ?? '',
+        secretAccessKey: Deno.env.get('AWS_SECRET_ACCESS_KEY') ?? '',
+        sessionToken: Deno.env.get('AWS_SESSION_TOKEN') ?? '',
       },
-    }),
-  }
+    })
+
+    const { prompt, seed } = await req.json()
+    console.log(prompt)
+    const input = {
+      contentType: 'application/json',
+      accept: '*/*',
+      modelId: 'amazon.titan-image-generator-v1',
+      body: JSON.stringify({
+        taskType: 'TEXT_IMAGE',
+        textToImageParams: { text: prompt },
+        imageGenerationConfig: {
+          numberOfImages: 1,
+          quality: 'standard',
+          cfgScale: 8.0,
+          height: 512,
+          width: 512,
+          seed: seed ?? 0,
+        },
+      }),
+    }
 
-  const command = new InvokeModelCommand(input)
-  const response = await client.send(command)
-  console.log(response)
+    const command = new InvokeModelCommand(input)
+    const response = await client.send(command)
+    console.log(response)
 
-  if (response.$metadata.httpStatusCode === 200) {
-    const { body, $metadata } = response
+    if (response.$metadata.httpStatusCode === 200) {
+      const { body, $metadata } = response
 
-    const textDecoder = new TextDecoder('utf-8')
-    const jsonString = textDecoder.decode(body.buffer)
-    const parsedData = JSON.parse(jsonString)
-    console.log(parsedData)
-    const image = parsedData.images[0]
-    const SUPABASE_SECRET_KEYS = JSON.parse(Deno.env.get('SUPABASE_SECRET_KEYS')!)
-    const supabaseClient = createClient(
-      // Supabase API URL - env var exported by default.
-      Deno.env.get('SUPABASE_URL')!,
-      // Supabase API SECRET KEY - env var exported by default.
-      SUPABASE_SECRET_KEYS['default']!
-    )
+      const textDecoder = new TextDecoder('utf-8')
+      const jsonString = textDecoder.decode(body.buffer)
+      const parsedData = JSON.parse(jsonString)
+      console.log(parsedData)
+      const image = parsedData.images[0]
 
-    const { data: upload, error: uploadError } = await supabaseClient.storage
-      .from('images')
-      .upload(`${$metadata.requestId ?? ''}.png`, decode(image), {
-        contentType: 'image/png',
-        cacheControl: '3600',
-        upsert: false,
-      })
-    if (!upload) {
-      return Response.json(uploadError)
+      const { data: upload, error: uploadError } = await ctx.supabaseAdmin.storage
+        .from('images')
+        .upload(`${$metadata.requestId ?? ''}.png`, decode(image), {
+          contentType: 'image/png',
+          cacheControl: '3600',
+          upsert: false,
+        })
+      if (!upload) {
+        return Response.json(uploadError)
+      }
+      const { data } = ctx.supabaseAdmin.storage.from('images').getPublicUrl(upload.path!)
+      return Response.json(data)
     }
-    const { data } = supabaseClient.storage.from('images').getPublicUrl(upload.path!)
-    return Response.json(data)
-  }
 
-  return Response.json(response)
-})
+    return Response.json(response)
+  }),
+}
 
 /* To invoke locally:
 
@@ -85,7 +80,7 @@ Deno.serve(async (req) => {
   3. Make an HTTP request:
 
   curl -i --location --request POST 'http://127.0.0.1:54321/functions/v1/image_gen' \
-    --header 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0' \
+    --header 'apikey: <SUPABASE_PUBLISHABLE_KEY>' \
     --header 'Content-Type: application/json' \
     --data '{"prompt":"A beautiful picture of a bird"}'
 */
diff --git a/examples/ai/edge-functions/README.md b/examples/ai/edge-functions/README.md
index 64800461b9b10..7d8665bd0e775 100644
--- a/examples/ai/edge-functions/README.md
+++ b/examples/ai/edge-functions/README.md
@@ -7,7 +7,7 @@ Since Supabase Edge Runtime [v1.36.0](https://github.com/supabase/edge-runtime/r
 This demo consists of three parts:
 
 1. A [`generate-embedding`](./supabase/functions/generate-embedding/index.ts) database webhook edge function which generates embeddings when a content row is added (or updated) in the [`public.embeddings`](./supabase/migrations/20240408072601_embeddings.sql) table.
-2. A [`query_embeddings` Postgres function](./supabase/migrations/20240410031515_vector-search.sql) which allows us to perform similarity search from an egde function via [Remote Procedure Call (RPC)](https://supabase.com/docs/guides/database/functions?language=js).
+2. A [`query_embeddings` Postgres function](./supabase/migrations/20240410031515_vector-search.sql) which allows us to perform similarity search from an edge function via [Remote Procedure Call (RPC)](https://supabase.com/docs/guides/database/functions?language=js).
 3. A [`search` edge function](./supabase/functions/search/index.ts) which generates the embedding for the search term, performs the similarity search via RPC function call, and returns the result.
 
 ## Deploy
@@ -24,7 +24,7 @@ Run a search via curl POST request:
 
 ```bash
 curl -i --location --request POST 'https://<PROJECT-REF>.supabase.co/functions/v1/search' \
-    --header 'apikey: <SUPABASE_PUBLISHABLE_KEY>' \
+    --header 'apikey: <SUPABASE_SECRET_KEY>' \
     --header 'Content-Type: application/json' \
     --data '{"search":"vehicles"}'
 ```
dif
… (truncated)

https://github.com/supabase/supabase/commit/d5fde192d573fe27a9e11122d5e7a2d0d0fd160a

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-06-25 Patch released
  5. 2026-08-18 Publicly revealed
PROVENANCE

SHA-3-512 hash:

10ef0e5a2036a83573e96213e51ad270b15750309a10d75977cba90c272568d4923589945b324cf27d85048f2181a7963fc4deb91ee2f13e75ccf9878f523e38

Committed 2026-05-17 17:54 PT

Revealed 2026-08-18 07:12 PT

Verify (download preimage.json)

Show preimage JSON
{
  "ant_id": "ANT-2026-125WT6QC",
  "bug_class": "denial-of-service",
  "claude_severity": "low",
  "commit_sha": null,
  "created_at": "2026-05-14T22:04:31+00:00",
  "description": "The `get-tshirt-competition` example edge function validates the `email` body field with the pattern `/^\\w+([\\.-]?\\w+)*@\\w+([\\.-]?\\w+)*(\\.\\w{2,3})+$/`. The sub-pattern `([\\.-]?\\w+)*` nests a quantified group inside `*`, so a long run of word characters with no `@` (e.g. 40+ `a`s followed by `!`) forces V8's regex engine into exponential backtracking. The function is deployed with `--no-verify-jwt`, so an unauthenticated attacker can POST such a body and pin the isolate at 100% CPU until the platform kills it; repeated requests exhaust the function's concurrency. Impact is limited because this is example code that a user must explicitly deploy.",
  "discovered_at": "2026-05-10T00:00:00+00:00",
  "location": "examples/edge-functions/supabase/functions/get-tshirt-competition/index.ts:42",
  "poc_sha256": null,
  "preimage_version": 1,
  "project": "supabase/supabase",
  "reproduction": [
    "1. Send POST /functions/v1/get-tshirt-competition with body {\"email\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!\",\"size\":\"M\"}",
    "2. The regex `.test()` call enters exponential backtracking and does not return within the timeout",
    "3. Repeat concurrently to exhaust the function's available concurrency"
  ],
  "technical_details": "The root cause is the nested quantifier `([\\.-]?\\w+)*` applied after `^\\w+`: because `[\\.-]?` is optional, the engine can partition a string of word characters across the outer `\\w+` and arbitrarily many inner `\\w+` groups in exponentially many ways before failing at the mandatory `@`. No input-length cap is applied before `.test()` is called.",
  "title": "Catastrophic backtracking in t-shirt competition email regex",
  "vendor_severity": null
}