Skip to content

fix: prevent API key auth bypass via double slashes#588

Merged
ErikBjare merged 1 commit intoActivityWatch:masterfrom
0xbrayo:fix-auth-bypass
Apr 22, 2026
Merged

fix: prevent API key auth bypass via double slashes#588
ErikBjare merged 1 commit intoActivityWatch:masterfrom
0xbrayo:fix-auth-bypass

Conversation

@0xbrayo
Copy link
Copy Markdown
Member

@0xbrayo 0xbrayo commented Apr 22, 2026

This commit fixes a vulnerability where API key authentication could be bypassed by sending a request with a double slash (e.g., //api/0/buckets/...). The ApiKeyCheck fairing relied on request.uri().path().as_str().starts_with("/api/"), which evaluated to false for paths starting with //, causing the middleware to skip the authentication check. Rocket would then internally normalize the path and route the request successfully, completely bypassing the security measure.

The fix normalizes leading slashes in ApiKeyCheck before checking the path prefix, ensuring all API endpoints are properly authenticated regardless of duplicate slashes.

Additionally, this commit updates aw-client-rust to prevent it from unintentionally generating requests with double slashes. Because reqwest::Url's string representation automatically includes a trailing slash, the format! strings have been adjusted from {}/api/... to {}api/....

This commit fixes a vulnerability where API key authentication could be bypassed by sending a request with a double slash (e.g., `//api/0/buckets/...`). The `ApiKeyCheck` fairing relied on `request.uri().path().as_str().starts_with("/api/")`, which evaluated to false for paths starting with `//`, causing the middleware to skip the authentication check. Rocket would then internally normalize the path and route the request successfully, completely bypassing the security measure.

The fix normalizes leading slashes in `ApiKeyCheck` before checking the path prefix, ensuring all API endpoints are properly authenticated regardless of duplicate slashes.

Additionally, this commit updates `aw-client-rust` to prevent it from unintentionally generating requests with double slashes. Because `reqwest::Url`'s string representation automatically includes a trailing slash, the `format!` strings have been adjusted from `{}/api/...` to `{}api/...`.
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 22, 2026

Codecov Report

❌ Patch coverage is 81.25000% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 68.41%. Comparing base (656f3c9) to head (00c6ed0).
⚠️ Report is 45 commits behind head on master.

Files with missing lines Patch % Lines
aw-client-rust/src/lib.rs 75.00% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #588      +/-   ##
==========================================
- Coverage   70.81%   68.41%   -2.41%     
==========================================
  Files          51       55       +4     
  Lines        2916     3239     +323     
==========================================
+ Hits         2065     2216     +151     
- Misses        851     1023     +172     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@0xbrayo 0xbrayo marked this pull request as ready for review April 22, 2026 14:47
@0xbrayo
Copy link
Copy Markdown
Member Author

0xbrayo commented Apr 22, 2026

Currently affects all rust watchers.

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 22, 2026

Greptile Summary

This PR closes an API key authentication bypass where requests with double-slash paths (e.g. //api/0/buckets/) evaded the ApiKeyCheck fairing because the raw path string didn't match starts_with(\"/api/\"), while Rocket's internal path normalization later routed the request successfully. The fix normalizes leading slashes before the prefix check in the fairing, and defensively updates aw-client-rust URL formatting to eliminate double-slash generation at the source (since reqwest::Url serializes authority-only URLs with a trailing slash).

Confidence Score: 5/5

Safe to merge — the vulnerability fix is correct, all existing tests pass, and the only finding is a minor missing test case.

The normalization logic (trim_start_matches('/') + prepend '/') correctly collapses any number of leading slashes. Constant-time comparison is preserved. The aw-client-rust URL changes are consistent across all call sites and correct given reqwest::Url's trailing-slash serialization. The one remaining finding is a P2 suggestion to add a complementary test for //api/0/info remaining public, which does not block merge.

No files require special attention.

Important Files Changed

Filename Overview
aw-server/src/endpoints/apikey.rs Fixes auth bypass by normalizing leading slashes before the path prefix check; existing tests extended with double-slash case; implementation is correct and constant-time comparison is preserved.
aw-client-rust/src/lib.rs Removes extra / from all URL format strings; correct because reqwest::Url serializes authority-only URLs (e.g. http://host:port) with a trailing slash, so the old {}/api/... pattern produced double-slash URLs.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Rocket
    participant ApiKeyFairing
    participant Handler

    Note over Client,Handler: Before fix — bypass path
    Client->>Rocket: GET //api/0/buckets/ (no Bearer token)
    Rocket->>ApiKeyFairing: on_request()
    ApiKeyFairing->>ApiKeyFairing: path = "//api/0/buckets/"
    ApiKeyFairing->>ApiKeyFairing: starts_with("/api/") → false ❌ (skips auth)
    ApiKeyFairing-->>Rocket: return (no block)
    Rocket->>Rocket: normalize path → /api/0/buckets/
    Rocket->>Handler: route matched
    Handler-->>Client: 200 OK (bypass!)

    Note over Client,Handler: After fix — bypass closed
    Client->>Rocket: GET //api/0/buckets/ (no Bearer token)
    Rocket->>ApiKeyFairing: on_request()
    ApiKeyFairing->>ApiKeyFairing: path = "//api/0/buckets/"
    ApiKeyFairing->>ApiKeyFairing: normalized = "/api/0/buckets/"
    ApiKeyFairing->>ApiKeyFairing: starts_with("/api/") → true ✅
    ApiKeyFairing->>ApiKeyFairing: Bearer token missing → invalid
    ApiKeyFairing->>Rocket: redirect to /apikey_fairing
    Rocket->>Handler: FairingErrorRoute
    Handler-->>Client: 401 Unauthorized
Loading

Reviews (1): Last reviewed commit: "fix: prevent API key auth bypass via dou..." | Re-trigger Greptile

Comment on lines +214 to 221
// Double slash should also require auth
let res = client
.get("//api/0/buckets/")
.header(ContentType::JSON)
.header(Header::new("Host", "localhost:5600"))
.dispatch();
assert_eq!(res.status(), Status::Unauthorized);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Missing test: //api/0/info should remain publicly accessible

The new test verifies that //api/0/buckets/ requires auth after normalization, but doesn't cover the complementary case: //api/0/info with a double-slash should still be allowed through without credentials (public path). Without this test, a future refactor that normalizes the path before the PUBLIC_PATHS check but in the wrong order could silently break the public endpoint for double-slash requests.

// Suggested addition inside test_api_key_required:
let res = client
    .get("//api/0/info")
    .header(ContentType::JSON)
    .header(Header::new("Host", "localhost:5600"))
    .dispatch();
assert_eq!(res.status(), Status::Ok);

@ErikBjare ErikBjare merged commit 4f6c565 into ActivityWatch:master Apr 22, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants