ekofyi
CVE-2026-56244: A Row-Level Security Gap in Capgo Exposed Every Webhook Secret
Security Research8 min read

CVE-2026-56244: A Row-Level Security Gap in Capgo Exposed Every Webhook Secret

Capgo before 12.128.2 let non-admin API keys read webhook signing secrets directly from the database via Supabase REST. Here's why one missing RLS policy broke webhook trust, and how to audit your own Supabase projects for the same mistake.

Look, if you run Supabase in production, you already know Row-Level Security isn't optional. You've been told a million times: RLS policies are your authorization layer. Skip one, and your database is an open book.

But CVE-2026-56244 is a sharp reminder that even security-conscious teams can slip. Capgo, an open-source live-update platform for Capacitor apps, built their entire webhook infrastructure on top of Supabase. They stored signing secrets in a webhooks table, protected those secrets behind API key authentication, and assumed everything was fine. It wasn't. A missing RLS policy meant any API key with minimal privileges could walk straight up to the database REST endpoint and read those secrets. Once you've got the secret, you can forge any webhook you want. Update a live app with malicious code? Push a fraudulent event that triggers internal workflows? It's all on the table.

Published on June 24, 2026, this vulnerability carries a CVSS 4.0 score of 7.1 (High). But scores don't capture the lesson here. The lesson is about the gap between what developers think Supabase protects and what it actually protects.

What Capgo is supposed to do (and why webhooks matter)

Capgo gives Capacitor app developers the ability to ship updates directly to users' devices, bypassing app store review. A developer uploads a new bundle, Capgo stores it, and devices fetch the latest version. To integrate with CI/CD, Capgo exposes webhooks—HTTP POST requests that fire when an update is uploaded, deployed, or failed. These webhooks travel to a URL the developer configures, carrying a JSON payload.

Now, anyone who's built webhook receivers knows the fundamental problem: how do you know the request really came from Capgo? The answer is a signing secret. Capgo generates a secret, stores it alongside the webhook configuration, and uses it to compute an HMAC signature. That signature lands in the X-Capgo-Signature header. The receiver recomputes the signature from the body and the shared secret; if they match, the request is authentic.

The secret is the only thing preventing an attacker from sending thousands of fake webhook events—triggering unnecessary deployments, poisoning metrics, or worse. If the secret leaks, the whole trust model crumbles.

The fault: Row-Level Security that simply wasn't there

Capgo uses Supabase as its backend. Supabase gives you a PostgreSQL database and an auto-generated REST API. You can define Row-Level Security policies on each table to control which rows a given API key can access. For example, you might write a policy that says "users can see only their own webhooks." If you leave a table without any policies, Supabase will—by default—block all access. But the moment you add a single policy, that becomes the filter. If that filter is too broad, you've opened a door.

In the affected versions (below 12.128.2), the webhooks table had policies that allowed authenticated API keys to read webhook rows. The intention was likely "any authenticated user can read the webhooks they own." But the implementation didn't restrict ownership. A non-admin API key associated with any user could query webhooks?select=* and retrieve every row—including the signing secret.

No special privileges needed. No service_role key. Just a legitimate, low-privilege API key that you'd issue to a team member or a CI script.

To make it concrete, imagine this request lands directly against the Supabase REST endpoint:

http
GET /rest/v1/webhooks?select=id,url,secret HTTP/1.1
Host: [project].supabase.co
Authorization: Bearer [non-admin-api-key]

If that key belongs to any authenticated user, the response would cheerfully return a JSON array of all webhook configurations, secrets included. No row-level filter stopped it.

Why this is worse than a typical data leak

Reading PII or internal email addresses is bad. Reading webhook signing secrets is catastrophic—because it's not just read, it's impersonation. Once I, as an attacker, have your secret, I can sign arbitrary webhook payloads and send them to your receiver.

Your receiver trusts anything with a valid X-Capgo-Signature. So I craft a fake event:

json
{
  "event": "update.deployed",
  "data": {
    "app_id": "your-live-app",
    "version": "1337",
    "url": "https://evil-cdn.example.com/malware.zip"
  }
}

I sign it with the stolen secret and POST it to the endpoint you configured. Your system believes it, triggers a release pipeline, and now that malware URL is pushed to users. Or, if you're not using Capgo for updates but for internal notifications, I might manipulate status transitions, spoof build approvals, or trigger phantom alerts that waste hours of on-call time.

The CVSS vector tells the story: AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:L/VA:N/SC:N/SI:N/SA:N. Network-accessible, low attack complexity, low privileges required, no user interaction. Confidentiality impact is high (secret disclosure), integrity impact is low on the vulnerable component itself, but the downstream integrity impact on the receiver is where the real damage happens. The score does not capture that blast radius.

The fix (and what it tells you about Supabase RLS)

Capgo patched this in version 12.128.2. The fix was straightforward: add an RLS policy that restricts reads on the webhooks table so that only the owner (or admin) can see the row. In Supabase, you'd typically write a policy like:

sql
CREATE POLICY "Users can only read their own webhooks"
ON public.webhooks
FOR SELECT
USING (auth.uid() = user_id);

But that assumes the table has a user_id column referencing the owning user. More commonly, the policy is tied to the authenticated role. For admin-only access, you might use:

sql
CREATE POLICY "Only admins can read webhooks"
ON public.webhooks
FOR SELECT
USING (auth.jwt() ->> 'role' = 'admin');

Which policy was actually deployed depends on Capgo's data model. But the takeaway is universal: if you leave a table without a restrictive policy, the Supabase REST API becomes a data exfiltration vector. It's not enough to check permissions at the application layer—because the REST endpoint bypasses that entirely. That's the whole point of Supabase's client-side libraries; they speak directly to the database, relying on RLS for authorization.

Audit your own Supabase projects—today

I've tested dozens of Supabase-backed applications, and I can tell you that missing RLS policies are disturbingly common. Teams create a table, add a few policies for INSERT and UPDATE (if they even bother), and forget about SELECT. Or they write USING (true) during development and never tighten it.

Here's what you should check immediately:

  • Every table that stores tokens, secrets, or PII must have a restrictive SELECT policy. If a table contains a secret or token column, treat it like a password—nobody except the owner should see it, and never in plaintext via the REST API unless absolutely necessary. Consider a separate secrets store with even stricter access.
  • Review all policies for `USING (true)`. That's a public policy. If you see it on a table with sensitive data, you've got a problem.
  • Test with a low-privilege API key. Create a test user, get their API key, and try to list all rows on every table you expose. You'll be surprised what comes back.
  • Use Supabase's dashboard to simulate RLS. The table editor lets you switch roles and see exactly which rows are visible. It's a quick gut check.

For Capgo users, the fix is simple: update to version 12.128.2 or later. But don't stop there—rotate any webhook signing secrets that might have been exposed. You don't know if an attacker already snatched them. Regenerate the secret, update your receivers, and move on.

The broader lesson: API keys are not authorization

Here's the thing: a non-admin API key in Capgo wasn't supposed to read secrets. The application's internal logic probably assumed that. But when the data sits behind an auto-generated REST API, your assumptions about application logic don't matter. The API key is the credential, and what it can access is defined solely by the database policies.

Too many developers treat the Supabase client as a magic box that "just knows" what the user should see. It doesn't. It enforces whatever you told it—nothing more, nothing less. If you told it "any authenticated user can SELECT from webhooks," then that's exactly what happens. No middleware intercepts it. No Node.js controller checks the user's role before returning the result. The database is the gatekeeper, and if the gate is open, everything spills out.

This is not a Supabase-specific issue. Firebase had similar problems with Firestore rules years ago. Any platform that combines a database with a client-facing API shifts the entire authorization burden onto declarative policies. Get those policies wrong, and you've eliminated the last layer of defense. You can have the most secure application code in the world; it won't matter because attackers won't even touch your application—they'll go straight to the REST endpoint.

One policy away from disaster

CVE-2026-56244 is small. One missing policy on one table. No complex exploit chain, no memory corruption, no zero-day. Just an oversight in a system where every oversight is magnified by the architecture.

That's what makes it worth writing about. It's the kind of bug that keeps me up at night—because I know my own projects probably have a table somewhere where I thought, "Oh, the frontend will never ask for that," and forgot to lock it down. But someone else's frontend doesn't have the same self-restraint. A curl command with the right bearer token is all it takes.

Go check your policies. Not next sprint. Now. If you find a table that trusts authentication over authorization, fix it. And if you use Capgo, update and rotate your secrets. Better to spend an hour rotating credentials than clean up after a forged webhook that nobody saw coming.

Related posts

Written by Eko

If you found this useful, follow @ekofyi on X for more notes like this — or get in touch if you have a problem to solve.