Skip to content
This repository was archived by the owner on Jan 27, 2026. It is now read-only.

Commit dd84f2f

Browse files
authored
fix(auth): use subtle for constant time key comparison, fix lower case key issue (#526)
* use subtle for constant time key comparison, fix lower case key issue
1 parent f4f1bfd commit dd84f2f

File tree

3 files changed

+153
-6
lines changed

3 files changed

+153
-6
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/shared/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,4 @@ async-trait = "0.1.88"
3535
regex = "1.11.1"
3636
iroh = { workspace = true }
3737
rand_v8 = { workspace = true }
38+
subtle = "2.6.1"

crates/shared/src/security/api_key_middleware.rs

Lines changed: 151 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use actix_web::{
55
Error,
66
};
77
use futures_util::future::{ready, LocalBoxFuture, Ready};
8+
use subtle::ConstantTimeEq;
89

910
pub struct ApiKeyMiddleware {
1011
api_key: String,
@@ -56,16 +57,160 @@ where
5657
fn call(&self, req: ServiceRequest) -> Self::Future {
5758
if let Some(auth_header) = req.headers().get(AUTHORIZATION) {
5859
if let Ok(auth_str) = auth_header.to_str() {
59-
if auth_str.to_lowercase() == format!("bearer {}", self.api_key) {
60-
let fut = self.service.call(req);
61-
return Box::pin(async move {
62-
let res = fut.await?;
63-
Ok(res)
64-
});
60+
if auth_str.len() > 7 {
61+
let (scheme, key) = auth_str.split_at(7);
62+
if scheme.eq_ignore_ascii_case("Bearer ") {
63+
let provided_key_bytes = key.as_bytes();
64+
let expected_key_bytes = self.api_key.as_bytes();
65+
66+
if provided_key_bytes.len() == expected_key_bytes.len()
67+
&& provided_key_bytes.ct_eq(expected_key_bytes).into()
68+
{
69+
let fut = self.service.call(req);
70+
return Box::pin(async move {
71+
let res = fut.await?;
72+
Ok(res)
73+
});
74+
}
75+
}
6576
}
6677
}
6778
}
6879

6980
Box::pin(async move { Err(ErrorUnauthorized("Invalid API key")) })
7081
}
7182
}
83+
84+
#[cfg(test)]
85+
mod tests {
86+
use super::*;
87+
use actix_web::{test, web, App, HttpResponse};
88+
89+
async fn test_handler() -> HttpResponse {
90+
HttpResponse::Ok().body("Success")
91+
}
92+
93+
#[actix_web::test]
94+
async fn test_valid_api_key() {
95+
let api_key = "test-api-key";
96+
let app = test::init_service(
97+
App::new()
98+
.wrap(ApiKeyMiddleware::new(api_key.to_string()))
99+
.route("/", web::get().to(test_handler)),
100+
)
101+
.await;
102+
103+
let req = test::TestRequest::get()
104+
.uri("/")
105+
.insert_header(("Authorization", "Bearer test-api-key"))
106+
.to_request();
107+
108+
let resp = test::call_service(&app, req).await;
109+
assert!(resp.status().is_success());
110+
}
111+
112+
#[actix_web::test]
113+
async fn test_invalid_api_key() {
114+
let api_key = "test-api-key";
115+
let app = test::init_service(
116+
App::new()
117+
.wrap(ApiKeyMiddleware::new(api_key.to_string()))
118+
.route("/", web::get().to(test_handler)),
119+
)
120+
.await;
121+
122+
let req = test::TestRequest::get()
123+
.uri("/")
124+
.insert_header(("Authorization", "Bearer wrong-key"))
125+
.to_request();
126+
127+
let resp = app.call(req).await;
128+
assert!(resp.is_err());
129+
assert_eq!(resp.unwrap_err().to_string(), "Invalid API key");
130+
}
131+
132+
#[actix_web::test]
133+
async fn test_missing_auth_header() {
134+
let api_key = "test-api-key";
135+
let app = test::init_service(
136+
App::new()
137+
.wrap(ApiKeyMiddleware::new(api_key.to_string()))
138+
.route("/", web::get().to(test_handler)),
139+
)
140+
.await;
141+
142+
let req = test::TestRequest::get().uri("/").to_request();
143+
144+
let resp = app.call(req).await;
145+
assert!(resp.is_err());
146+
assert_eq!(resp.unwrap_err().to_string(), "Invalid API key");
147+
}
148+
149+
#[actix_web::test]
150+
async fn test_lowercase_bearer_accepted() {
151+
let api_key = "test-API-key";
152+
let app = test::init_service(
153+
App::new()
154+
.wrap(ApiKeyMiddleware::new(api_key.to_string()))
155+
.route("/", web::get().to(test_handler)),
156+
)
157+
.await;
158+
159+
let req = test::TestRequest::get()
160+
.uri("/")
161+
.insert_header(("Authorization", "bearer test-API-key"))
162+
.to_request();
163+
164+
let resp = test::call_service(&app, req).await;
165+
assert!(resp.status().is_success());
166+
167+
let req = test::TestRequest::get()
168+
.uri("/")
169+
.insert_header(("Authorization", "bearer test-api-key"))
170+
.to_request();
171+
172+
let resp = app.call(req).await;
173+
assert!(resp.is_err());
174+
assert_eq!(resp.unwrap_err().to_string(), "Invalid API key");
175+
}
176+
177+
#[actix_web::test]
178+
async fn test_mixed_case_api_key_rejected() {
179+
let api_key = "test-api-key";
180+
let app = test::init_service(
181+
App::new()
182+
.wrap(ApiKeyMiddleware::new(api_key.to_string()))
183+
.route("/", web::get().to(test_handler)),
184+
)
185+
.await;
186+
187+
let req = test::TestRequest::get()
188+
.uri("/")
189+
.insert_header(("Authorization", "BeArEr test-API-key"))
190+
.to_request();
191+
192+
let resp = app.call(req).await;
193+
assert!(resp.is_err());
194+
assert_eq!(resp.unwrap_err().to_string(), "Invalid API key");
195+
}
196+
197+
#[actix_web::test]
198+
async fn test_malformed_auth_header() {
199+
let api_key = "test-api-key";
200+
let app = test::init_service(
201+
App::new()
202+
.wrap(ApiKeyMiddleware::new(api_key.to_string()))
203+
.route("/", web::get().to(test_handler)),
204+
)
205+
.await;
206+
207+
let req = test::TestRequest::get()
208+
.uri("/")
209+
.insert_header(("Authorization", "InvalidFormat"))
210+
.to_request();
211+
212+
let resp = app.call(req).await;
213+
assert!(resp.is_err());
214+
assert_eq!(resp.unwrap_err().to_string(), "Invalid API key");
215+
}
216+
}

0 commit comments

Comments
 (0)