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:

  1. Establish Baseline: Read the current topic title using the scoped API key via GET /t/:id.json to confirm initial state.
  2. Control Test: Attempt a PUT request without the malicious header; the script expects a 403 response, confirming the scope restriction functions normally.
  3. Verify Control: Re-read the topic title to ensure it remains unchanged after the blocked write attempt.
  4. Trigger Exploit: Send the same PUT request with X-Request-Start: t=0 to trigger overload protection and force pre-route authentication.
  5. Confirm Bypass: Verify the topic title has been updated to the attacker-supplied value, indicating successful write access.
  6. 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 RouteMatcher to pass the request method to recognize_path, ensuring PUT requests resolve to write routes during scope validation.
  • Separate Authentication from Overload Handling: Prevent DefaultCurrentUserProvider from caching authenticated users during overload protection for reuse by subsequent controller actions.
  • Sanitize Client Headers: Treat X-Request-Start as 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

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →