# How to Exploit the Discourse Scoped API Key Pre-Auth Bypass Vulnerability: A Technical Deep Dive

> Learn how to exploit the discourse-scoped-api-key-preauth-bypass vulnerability. Discover how to escalate read-only API keys to write access by bypassing HTTP verb validation. Get the technical details.

- Repository: [bikini/exploitarium](https://github.com/bikini/exploitarium)
- Tags: deep-dive
- Published: 2026-09-06

---

**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`](https://github.com/bikini/exploitarium/blob/main/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:

```bash
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:

```json
{
  "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

- The discourse-scoped-api-key-preauth-bypass vulnerability stems from `RouteMatcher` failing to consider HTTP verbs during pre-route authentication checks.
- Key affected components include [`lib/middleware/processing_request.rb`](https://github.com/bikini/exploitarium/blob/main/lib/middleware/processing_request.rb), [`lib/middleware/overload_protections.rb`](https://github.com/bikini/exploitarium/blob/main/lib/middleware/overload_protections.rb), [`lib/auth/default_current_user_provider.rb`](https://github.com/bikini/exploitarium/blob/main/lib/auth/default_current_user_provider.rb), [`lib/route_matcher.rb`](https://github.com/bikini/exploitarium/blob/main/lib/route_matcher.rb), and [`app/controllers/topics_controller.rb`](https://github.com/bikini/exploitarium/blob/main/app/controllers/topics_controller.rb).
- Attackers require only a valid read-only API key and the ability to send an `X-Request-Start` header to escalate privileges.
- The [`poc.py`](https://github.com/bikini/exploitarium/blob/main/poc.py) script 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`](https://github.com/bikini/exploitarium/blob/main/lib/middleware/processing_request.rb) parses the header, [`lib/middleware/overload_protections.rb`](https://github.com/bikini/exploitarium/blob/main/lib/middleware/overload_protections.rb) triggers the pre-auth flow, [`lib/auth/default_current_user_provider.rb`](https://github.com/bikini/exploitarium/blob/main/lib/auth/default_current_user_provider.rb) performs early authentication, [`lib/route_matcher.rb`](https://github.com/bikini/exploitarium/blob/main/lib/route_matcher.rb) fails to validate the HTTP method, and [`app/controllers/topics_controller.rb`](https://github.com/bikini/exploitarium/blob/main/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.