Discourse Scoped API Key Pre-Route Authorization Bypass
by bikini (@ashdfrkl) — original discovery; mirrored via exploitarium · 2026-07-03
- Severity
- High
- CVE
- None assigned as of 2026-07-03
- Category
- web
- Affected product
- Discourse (forum platform)
- Affected versions
- Commit 3dfcc8f884313da69711ed5f26f3749fb6516ef2 (verified against docker.io/discourse/discourse_dev:20260609-1222)
- Disclosed
- 2026-07-03
- Patch status
- unpatched
Tags
Archive entry
intelseclab/poc-archiveMetadata
| Field | Value |
|---|---|
| Date Added | 2026-07-03 |
| Last Updated | 2026-06 |
| Author / Researcher | bikini (@ashdfrkl) — original discovery; mirrored via exploitarium |
| CVE / Advisory | None assigned as of 2026-07-03 |
| Category | web |
| Severity | High |
| CVSS Score | Not yet scored (no CVE/CVSS assigned) |
| Status | PoC |
| Tags | discourse, authorization-bypass, api-key-scope, rails, middleware, privilege-escalation, route-confusion |
| Related | N/A |
Affected Target
| Field | Value |
|---|---|
| Software / System | Discourse (forum platform) |
| Versions Affected | Commit 3dfcc8f884313da69711ed5f26f3749fb6516ef2 (verified against docker.io/discourse/discourse_dev:20260609-1222) |
| Language / Platform | Ruby on Rails (target), Python 3.10+ (PoC driver using stdlib HTTP only) |
| Authentication Required | Yes (attacker needs a valid granular all-users API key scoped only to topics:read) |
| Network Access Required | Yes (HTTP access to the Discourse instance) |
Summary
Discourse’s overload-protection middleware authenticates API requests before Rails routing has resolved the actual HTTP verb, and its scoped API key matcher (lib/route_matcher.rb) calls Rails.application.routes.recognize_path(request.path_info) without passing the real request method. By sending a caller-controlled X-Request-Start header that makes the request appear queued long enough to trigger the overload-protection authentication path, an attacker holding a read-only scoped API key (topics:read) can have that key’s permission check resolve against the GET route for a topic path even though the actual request is a PUT /t/:topic_id.json. The middleware then caches the authenticated API user in the Rack environment, and the later TopicsController#update action executes using that cached user, effectively turning a read-scoped API key into a write-capable request. In the researcher’s validated run, a control PUT without the header was correctly rejected (403) while the same request with X-Request-Start: t=0 succeeded (200) and changed the topic title. This PoC was published by a pseudonymous independent researcher (bikini/ashdfrkl) as part of the uncoordinated “exploitarium” vulnerability dump; it has not been vendor-confirmed.
Vulnerability Details
Root Cause
lib/route_matcher.rb’s pre-route scope check resolves the request path via recognize_path without the actual HTTP method, so a PUT request can be matched against a GET-only route (topics#show) for scope-permission purposes; combined with the overload-protection middleware caching the resulting authenticated user in the Rack environment, this pre-route authorization decision is later reused by the real controller action regardless of the request’s true verb.
Attack Vector
- Attacker obtains or is issued a granular all-users Discourse API key scoped only to
topics:read, plus a privileged API username able to edit the target topic. - Attacker sends
PUT /t/:topic_id.jsonwith the scoped API key and an attacker-controlledX-Request-Startheader set to a value that makesOverloadProtectionstreat the request as queued/overloaded. - The overload middleware invokes the current-user provider, which calls
api_key.request_allowed?→ApiKeyScope#permits?→RouteMatcher#path_params_from_request, resolving the path astopics#show(atopics:read-permitted route) without checking the realPUTverb. - The middleware caches the resolved API user into
_DISCOURSE_CURRENT_USERin the Rack environment. - Rails then routes the request normally to
TopicsController#update, which readscurrent_userfrom the same Rack environment (the cached API user) and performs the update, includingguardian.ensure_can_edit!, which passes because the cached user has edit rights.
Impact
A read-scoped (topics:read) API key can be used to perform write operations (topic updates) as the API key’s associated username, effectively bypassing granular API key scope restrictions for any writable route whose path shape overlaps a read-permitted route.
Environment / Lab Setup
Target: Discourse @ 3dfcc8f884313da69711ed5f26f3749fb6516ef2 (docker.io/discourse/discourse_dev:20260609-1222)
Attacker: Python 3.10+ (stdlib only), a topics:read-scoped all-users API key, one visible target topic
Proof of Concept
PoC Script
See
poc.pyin this folder.
| |
The script reads the target topic’s current title, sends a control PUT /t/:topic_id.json without X-Request-Start and confirms it is rejected (403) and the title unchanged, then sends the same request with X-Request-Start: t=0 and confirms it succeeds (200) and the title is changed — writing a JSON proof object with both results.
Detection & Indicators of Compromise
Signs of compromise:
- Topic or other resource updates attributed to API usernames whose associated key is scoped read-only
- Inbound requests with client-supplied
X-Request-Startheaders not stripped/overwritten by the reverse proxy - Audit log entries for writes performed via API keys that should be incapable of writes per their granular scope
Remediation
| Action | Detail |
|---|---|
| Primary fix | No vendor patch confirmed as of 2026-07-03 — monitor for advisory from Discourse |
| Interim mitigation | Ensure the reverse proxy strips/overwrites any client-supplied X-Request-Start header before it reaches Discourse; resolve routes with the real HTTP method during pre-route scope checks; avoid caching an authenticated user during overload handling for later reuse by unrelated controller actions |
References
Notes
Mirrored from https://github.com/bikini/exploitarium (folder: discourse-scoped-api-key-preauth-bypass) on 2026-07-03. No CVE has been assigned as of ingestion — this is an uncoordinated disclosure by a pseudonymous researcher; treat with appropriate caution pending vendor confirmation.
| |