How to Exploit the Discourse Scoped API Key Pre-Auth Bypass Vulnerability: A Technical Deep Dive
The discourse-scoped-api-key-preauth-bypass vulnerability allows attackers to escalate a read-only topics:read API key into write-capable access by manipulating the X-Request-Start header to force pre-routing authentication that bypasses HTTP verb validation.
The bikini/exploitarium repository provides a proof-of-concept demonstrating how Discourse's overload protection mechanism can be abused to circumvent granular API key scopes. This vulnerability exists in the interaction between middleware authentication and Rails routing, specifically affecting Discourse instances at commit 3dfcc8f884313da69711ed5f26f3749fb6516ef2 and similar versions.
How the Discourse Scoped API Key Pre-Auth Bypass Works
Granular API Key Scope Validation
Discourse implements fine-grained API keys restricted to specific permissions such as topics:read. Legitimate authorization occurs through ApiKeyScope#permits?, which validates whether the requested action falls within the key's defined scope. Under normal operation, a read-only key attempting to update a topic receives a 403 Forbidden response.
The Overload Protection Trigger
The vulnerability leverages Discourse's overload protection middleware. When a request includes an X-Request-Start header, the ProcessingRequest middleware parses the timestamp and calculates a queue time, storing it in the Rack environment. If this queue time exceeds reject_anonymous_min_queue_seconds, the OverloadProtections middleware flags the request as overloaded, triggering a special authentication path before standard routing occurs.
Pre-Route Authentication Flow
During overload handling, DefaultCurrentUserProvider authenticates the API credentials before Rails routing resolves the controller and action. This premature authentication stores the user object in the Rack environment under the key _DISCOURSE_CURRENT_USER, caching the security context for later use.
Route Matching Without HTTP Verb
The critical flaw resides in RouteMatcher, which calls Rails.application.routes.recognize_path(request.path_info) without passing the actual HTTP method. Consequently, a PUT request to /t/:id.json matches the read-only route (topics#show) rather than the write route, passing the scope validation. When Rails subsequently dispatches the request to TopicsController#update, it reuses the cached user from the pre-auth phase, granting unauthorized write access.
Exploitation Steps Using poc.py
The proof-of-concept script poc.py in the bikini/exploitarium repository implements a six-step attack flow:
- Establish Baseline: Read the current topic title using the scoped API key via
GET /t/:id.jsonto confirm initial state. - Control Test: Attempt a
PUTrequest without the malicious header; the script expects a 403 response, confirming the scope restriction functions normally. - Verify Control: Re-read the topic title to ensure it remains unchanged after the blocked write attempt.
- Trigger Exploit: Send the same
PUTrequest withX-Request-Start: t=0to trigger overload protection and force pre-route authentication. - Confirm Bypass: Verify the topic title has been updated to the attacker-supplied value, indicating successful write access.
- Generate Proof: Output a JSON object summarizing the bypass outcomes via the
build_proof()function.
Running the Proof of Concept
Execute the exploit against a local Discourse development instance:
python poc.py \
--base-url http://127.0.0.1:3000 \
--api-key '<topics-read-api-key>' \
--api-username admin \
--topic-id 8 \
--new-title 'Exploited title' \
--output proof.json
Successful exploitation produces a JSON proof object demonstrating the bypass:
{
"controlWithoutHeader": {
"blocked": true,
"httpStatus": 403,
"titleAfterRequest": "Original title",
"unchanged": true
},
"triggeredWithHeader": {
"changed": true,
"finalTitle": "Exploited title",
"httpStatus": 200,
"success": true
},
"ok": true
}
Mitigation Strategies
- Include HTTP Method in Route Recognition: Modify
RouteMatcherto pass the request method torecognize_path, ensuringPUTrequests resolve to write routes during scope validation. - Separate Authentication from Overload Handling: Prevent
DefaultCurrentUserProviderfrom caching authenticated users during overload protection for reuse by subsequent controller actions. - Sanitize Client Headers: Treat
X-Request-Startas untrusted input unless normalized by a trusted reverse proxy, or ignore client-provided timing headers entirely. - Implement Regression Tests: Add test cases that attempt write operations using read-only scoped keys combined with attacker-controlled timing headers.
Summary
- The discourse-scoped-api-key-preauth-bypass vulnerability stems from
RouteMatcherfailing to consider HTTP verbs during pre-route authentication checks. - Key affected components include
lib/middleware/processing_request.rb,lib/middleware/overload_protections.rb,lib/auth/default_current_user_provider.rb,lib/route_matcher.rb, andapp/controllers/topics_controller.rb. - Attackers require only a valid read-only API key and the ability to send an
X-Request-Startheader to escalate privileges. - The
poc.pyscript provides a complete, Python standard library-only implementation for testing vulnerable instances.
Frequently Asked Questions
What is the discourse-scoped-api-key-preauth-bypass vulnerability?
The discourse-scoped-api-key-preauth-bypass vulnerability is a security flaw in Discourse that allows an attacker with a read-only API key (such as topics:read scope) to perform write operations. It exploits the order of operations where authentication occurs during overload protection handling before the HTTP verb is validated against the route, caching the authenticated user for later unauthorized use.
Which Discourse source files are involved in this vulnerability?
The vulnerability spans multiple components: lib/middleware/processing_request.rb parses the header, lib/middleware/overload_protections.rb triggers the pre-auth flow, lib/auth/default_current_user_provider.rb performs early authentication, lib/route_matcher.rb fails to validate the HTTP method, and app/controllers/topics_controller.rb ultimately executes the write operation using the cached credentials.
How does the X-Request-Start header enable the bypass?
The X-Request-Start header triggers Discourse's overload protection mechanism when its value indicates excessive queue time. This forces the application to authenticate the API key early in the request lifecycle and cache the user object in _DISCOURSE_CURRENT_USER. Because the route matching for scope validation ignores the HTTP method, the write request appears as a read request during authorization, but executes as a write request in the controller.
What versions of Discourse are affected by this vulnerability?
According to the bikini/exploitarium repository analysis, the vulnerability was tested against and exists in Discourse at commit 3dfcc8f884313da69711ed5f26f3749fb6516ef2. Any version implementing the overload protection middleware with pre-route authentication and route matching without HTTP verb consideration is potentially vulnerable until patched.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →