ANT-2026-125WT6QC · supabase/supabase
denial-of-service low
Severity Claude low · Security research firm - · Maintainer -
Discovered by Claude Mythos Preview
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
- Send POST /functions/v1/get-tshirt-competition with body {"email":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!","size":"M"}
- The regex
.test()call enters exponential backtracking and does not return within the timeout - 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
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
Dates from discovery through public reveal.
- 2026-05-14 Reported to tracker
- 2026-05-14 Maintainer acknowledged
- 2026-05-15 Sent to maintainer
- 2026-06-25 Patch released
- 2026-08-18 Publicly revealed
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
}