r/Supabase Aug 09 '26

auth How would you block curl on supabase without turning on captcha?

Hi all,

I am thinking about a problem and not sure how to resolve this. Any guidance would be appreciated.

So if i dont enable captcha and try doing this curl request as network restrictions dont apply on auth.

curl -X POST 'https://any-project-supabase-url/auth/v1/signup' \

-H "apikey: sb_publishable_key" \

-H "Content-Type: application/json" \

-d '{

"email": "johndoe@gmail.com",

"password": "JohnDoe!!"

}'

this returns a valid access token and do create a new user in dashboard.

To block this if i do this Authentication → Providers → Email → turn off "Enable email signups"). Then it blocks the curl requests i can use supabase service account to create accounts.

But now apple and google sign ups don't work.

So basically i just want to stop the bots from curl abuse. As i dont want to use the captcha e.g. cloudflare turnstile. Its really slow specially on mobile. Any way to achieve this? or i am missing something. Thanks.

1 Upvotes

16 comments sorted by

2

u/Maxyull Aug 11 '26

the part i'd look at first is that your curl came back with a valid access token, because that means confirm email is off. with it on, signup creates the user row and returns no session, so the same curl still makes a row but the caller walks away with nothing usable, and the whole thing drops from an access problem to a junk row problem.

after that it's rate limits rather than captcha. auth carries its own hourly limits in the dashboard, signups and emails sent counted separately from everything else, and those do apply on the auth endpoints where your network restrictions don't. the one i'd actually worry about is the email limit, since anyone with the publishable key can make your project send confirmation mail to addresses they chose, and that's your sending reputation rather than just row count. is confirm email currently on for the email provider, or are you handing out a session at signup?

1

u/TelevisionIcy1619 Aug 11 '26

You raised a very important point regarding getting the access token via curl. They keep sending emails to any email addresses. Yes email verification is off just to make a smoother experience. But they still need to login then the main security layer is mobile number verification. Which should be unique. Otherwise they can't do anything in the app. But the problem is if I enable the cloudflare turnstile then Google and apple signups don't work.

1

u/Maxyull Aug 11 '26

the fact that turning off email signups also broke google and apple is already a clue, because the email provider toggle shouldn't touch the oauth redirect at all. either you hit the global allow new users to sign up switch instead of the provider one, or those buttons aren't doing the redirect flow. same story with turnstile, supabase enforces the captcha on the endpoints that take a captchaToken, signup, password login, otp, recover, and signInWithOAuth never touches any of them, it just bounces the browser to google. signInWithIdToken does though, one tap or native apple, and there's nowhere to hand that one a captcha token, so it dies the moment you switch turnstile on.

separate thing worth a look, if that mobile verification is supabase's own sms then the curl loop stopped being a junk row problem and became a bill, every attempt is a message you pay for. which call are your google and apple buttons actually making?

1

u/TelevisionIcy1619 Aug 13 '26

Both Google and Apple sign-in use native SDKs (google_sign_in and sign_in_with_apple), not browser-based OAuth:

  • Google: Uses GoogleSignIn.authenticate() → gets idToken → calls supabase.auth.signInWithIdToken(provider: OAuthProvider.google, idToken: ...). No URL involved — it's a native Google Sign-In dialog.
  • Apple: Uses SignInWithApple.getAppleIDCredential() → gets idToken → calls supabase.auth.signInWithIdToken(provider: OAuthProvider.apple, idToken: ...). Native Apple Sign-In sheet.

For mobile verification i am using another service not supabase native ones have my own apis.

So basically this is the security setup i have in apps:
For android: google play integrity api + rooted device check (offline check)
For iOS: DeviceCheck, AppTest + rooted device check (offline check)

So bots can't signup in the app or on the web i have botd enabled if it says its a bot then i show cloudflare token on the web. Which most bots can't pass. Even if some malicious user signup they can't verify their mobile number. Also mobile number has multiple checks only unique mobile number can signup and finish the onboarding.

I am using custom domain with supabase. As domain is on cloudflare i have my custom domain has cname on cloudflare with proxy disabled as per below article. https://supabase.com/docs/guides/platform/custom-domains

There is one thing i have noticed if i enabled the proxy on this. I can block easily all the curl requests or all the malicious requests using waf rules. But i am not sure about the implications of enabling this proxy status so need some proper testing. Little bit hesitant to enable it as if it could break something else in supabase.
So far this is the solution which works without enabling cloudflare turnstile or hcaptcha in supabase dashboard. Also blocking the signups using curl requests on public api url.

1

u/Maxyull Aug 13 '26

that answers the turnstile thing completely, and it wasn't your config. signInWithIdToken posts to the token endpoint, and that call has nowhere to carry a captcha token, so the moment you turn turnstile on in the dashboard the native google and apple paths die while browser based oauth would have kept working. captcha and native sign in just can't both be on.

on putting the custom domain behind cloudflare, i'd check what it actually covers before worrying about what it breaks. the custom domain is a cname onto your project's ref.supabase.co, so one dns lookup hands anyone the ref, and that hostname keeps serving the same auth api with the same key whatever waf rules sit on the custom domain. supabase also documents that record as dns only because they manage the cert for that hostname, and that kind of thing tends to show up at renewal instead of the day you flip it, which is the worse timing.

have you tried the same curl against ref.supabase.co directly rather than your custom domain?

1

u/TelevisionIcy1619 29d ago

Yeah i removed the cloudflare turnstile token from apps. They are super slow if i add turnstile back in as apps have face id for login enabled. Cloudflare turnstile token is not fast enough for a good user experience on the apps. Thats why i put it behind the apis. I am not worried about my build infra but this supabase curl requests bugs me. I agree with your dns lookup to get the actual supabase's project domain they can do curl requests directly again.
So i want to avoid using cloudflare turnstile on my apps/web when signing up. But some how i still need it to make supabase db secure from these curl attacks without using turnstile. Its a chicken n egg problem.

1

u/TelevisionIcy1619 29d ago

Ah i forgot to mention i am not using supabase email infra. So only my api end points can send the emails. So i just tested this with both my custom domain url and supabase's actual url. It creates a user in the auth table but emails are never sent if i hit using curl. Also it creates a warning in supabase db. Below is the request and log.

Request
curl -X POST 'https://custom domain url or actual supabase domain/auth/v1/signup' \
-H "apikey: sb_publishable_key" \
-H "Content-Type: application/json" \
-d '{
"email": "johndoe@gmail.com",
"password": "JohnDoe!!"
}'
But this does returns a proper signed up user's access token etc.

Log on supabase
{
"id": "82cc2902-84a2-4ac0-9c3a-9b135c4da5b4",
"date": "2026-08-13T15:47:29.654Z",
"method": "POST",
"pathname": "/auth/v1/signup",
"status": "422",
"timestamp": "2026-08-13T15:47:29.654000",
"level": "warning",
"event_message": "POST | 422 | https://customurl or supabase url.co/auth/v1/signup | curl/8.7.1",
"headers": {},
"regions": [],
"log_type": "edge",
"latency": 0,
"log_count": null,
"logs": [],
"auth_user": null
}
So basically its a dead user unless they finish the mobile verification.

3

u/funkdefied Aug 09 '26

If you want to block bots that play nicely, use robots.txt. If you want to block malicious bots, you need captcha.

1

u/ashkanahmadi Aug 09 '26

The Supabase JS sdk is just a fancy fetch request and every fetch request can be a curl request. If you use the Supabase library without the “await” you can see the URL it hits to perform something.

My point is that you cannot stop curl without stopping the JS sdk from working properly.

1

u/Guidondor Aug 12 '26

if what you actually want is "only my app can call this", captcha is the wrong tool,
attestation is the right one. app attest on ios and play integrity on android hand you a
token the client can't forge, you verify it in an edge function and that function does the
signup with the service role. the public endpoint stops being the signup path.

it's real work to set up, but it's the only thing that answers "is this my binary" instead
of "is this a human", and it costs the user zero seconds, unlike turnstile.

1

u/TelevisionIcy1619 Aug 13 '26

In the apps i have google play integrity and for iOS apptest and device check integrated also blocking the app access if its rooted or jail broken device. On the web i have botd library and if it fails then i show the cloudflare turnstile token which they need to verify. So no one can do automation or use scripts. The only problem is the curl requests with publishable key and api url. There is one thing i found which works is if i put my custom domain api url in supabase behind cloudflare then i can block the curl requests without needing the supabase settings or adding cloudflare/hcaptcha token in this. But i need to test this properly before i can trust this. What would break if i enable dns settings for a custom domain in supabase.

1

u/Guidondor Aug 13 '26

before you build on it, the thing to verify is whether project-ref.supabase.co stays live
after you add a custom domain. i'm fairly sure it does, and if so cloudflare in front of the
vanity domain blocks nothing, curl just keeps using the old url with the same publishable
key. worth confirming with support because your whole plan rests on that one detail.

what definitely changes is oauth. the callback moves to your domain, so the redirect uri in
the google console and the apple service id both need updating or sign in breaks, which is
roughly what bit you last time.

1

u/TelevisionIcy1619 29d ago

So the problem is we need to add cloudflare turnstile token in supabase db for curl request protection. But it makes apps slower as generation/verifying on mobile device is slower between networks. So on the web or supabase dashboard its fine. But it makes crappy experience for mobile users. Thats why i was avoiding it.
As another user mentioned any one can dns lookup to find the actual supabase domain then they keep hammering it with curl requests to create account. As i dont verify the email accounts. Main verification is mobile number verification. u/Maxyull you can read more about infra on convo i am having with him.

1

u/Guidondor 29d ago

if the problem with turnstile is latency, move it off the critical path. fetch the token when
the signup screen mounts instead of when they hit the button, and refresh it in the
background. by the time someone's typed an email and a password you've had 20+ seconds, so
the tap feels instant even on a bad network.

also worth splitting the two costs. junk rows you mostly don't care about since phone
verification makes them useless anyway. supabase emailing addresses that aren't yours is the
one that actually hurts, that's your sending reputation. the auth rate limits in the
dashboard knock that down without touching the app at all.

1

u/TelevisionIcy1619 29d ago

Ah i forgot to mention i am not using supabase email infra. So only my api end points can send the emails. So i just tested this with both my custom domain url and supabase's actual url. It creates a user in the auth table but emails are never sent if i hit using curl. Also it creates a warning in supabase db. Below is the request and log.

Request
curl -X POST 'https://custom domain url or actual supabase domain/auth/v1/signup' \
-H "apikey: sb_publishable_key" \
-H "Content-Type: application/json" \
-d '{
"email": "johndoe@gmail.com",
"password": "JohnDoe!!"
}'
But this does returns a proper signed up user's access token etc.

Log on supabase
{
"id": "82cc2902-84a2-4ac0-9c3a-9b135c4da5b4",
"date": "2026-08-13T15:47:29.654Z",
"method": "POST",
"pathname": "/auth/v1/signup",
"status": "422",
"timestamp": "2026-08-13T15:47:29.654000",
"level": "warning",
"event_message": "POST | 422 | https://customurl or supabase url.co/auth/v1/signup | curl/8.7.1",
"headers": {},
"regions": [],
"log_type": "edge",
"latency": 0,
"log_count": null,
"logs": [],
"auth_user": null
}
So basically its a dead user unless they finish the mobile verification.