diff --git a/Cargo.lock b/Cargo.lock index 365cc0a37f..e276105c24 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6809,6 +6809,7 @@ dependencies = [ "sqlx", "sqlx-tracing", "strum 0.28.0", + "subtle", "thiserror 2.0.21", "tikv-jemalloc-ctl", "tikv-jemallocator", diff --git a/Cargo.toml b/Cargo.toml index a01116a0d6..3b6009e90b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -214,6 +214,7 @@ spdx = "0.13.6" sqlx = { version = "0.9.0", default-features = false } sqlx-tracing = { path = "packages/sqlx-tracing" } strum = "0.28.0" +subtle = "2.6.1" syn = { version = "3.0" } sysinfo = { version = "0.38.4", default-features = false } tar = "0.4.46" diff --git a/apps/labrinth/Cargo.toml b/apps/labrinth/Cargo.toml index 3597a506fb..f3f1b81a7a 100644 --- a/apps/labrinth/Cargo.toml +++ b/apps/labrinth/Cargo.toml @@ -129,6 +129,7 @@ sqlx = { workspace = true, features = [ ] } sqlx-tracing = { workspace = true, features = ["postgres"] } strum = { workspace = true, features = ["derive"] } +subtle = { workspace = true } thiserror = { workspace = true } toasty = { workspace = true, features = ["postgresql"] } tokio = { workspace = true, features = ["rt-multi-thread", "sync"] } diff --git a/apps/labrinth/src/util/guards.rs b/apps/labrinth/src/util/guards.rs index f018438f63..d204b6edc8 100644 --- a/apps/labrinth/src/util/guards.rs +++ b/apps/labrinth/src/util/guards.rs @@ -1,5 +1,6 @@ use actix_web::guard::GuardContext; use actix_web::http::header::X_FORWARDED_FOR; +use subtle::ConstantTimeEq; use crate::env::ENV; @@ -8,27 +9,29 @@ pub const MEDAL_KEY_HEADER: &str = "X-Medal-Access-Key"; pub const EXTERNAL_NOTIFICATION_KEY_HEADER: &str = "External-Notification-Key"; pub const SUBSCRIPTIONS_KEY_HEADER: &str = "Modrinth-Subscriptions-Key"; -pub fn admin_key_guard(ctx: &GuardContext) -> bool { +/// Checks that the header `name` matches `key`, comparing in constant time to +/// avoid leaking the key through timing side channels. +fn header_matches_key(ctx: &GuardContext, name: &str, key: &str) -> bool { ctx.head() .headers() - .get(ADMIN_KEY_HEADER) - .is_some_and(|it| it.as_bytes() == ENV.LABRINTH_ADMIN_KEY.as_bytes()) + .get(name) + .is_some_and(|it| it.as_bytes().ct_eq(key.as_bytes()).into()) +} + +pub fn admin_key_guard(ctx: &GuardContext) -> bool { + header_matches_key(ctx, ADMIN_KEY_HEADER, &ENV.LABRINTH_ADMIN_KEY) } pub fn medal_key_guard(ctx: &GuardContext) -> bool { - ctx.head() - .headers() - .get(MEDAL_KEY_HEADER) - .is_some_and(|it| it.as_bytes() == ENV.LABRINTH_MEDAL_KEY.as_bytes()) + header_matches_key(ctx, MEDAL_KEY_HEADER, &ENV.LABRINTH_MEDAL_KEY) } pub fn external_notification_key_guard(ctx: &GuardContext) -> bool { - ctx.head() - .headers() - .get(EXTERNAL_NOTIFICATION_KEY_HEADER) - .is_some_and(|it| { - it.as_bytes() == ENV.LABRINTH_EXTERNAL_NOTIFICATION_KEY.as_bytes() - }) + header_matches_key( + ctx, + EXTERNAL_NOTIFICATION_KEY_HEADER, + &ENV.LABRINTH_EXTERNAL_NOTIFICATION_KEY, + ) } pub fn subscriptions_key_guard(ctx: &GuardContext) -> bool { @@ -37,12 +40,11 @@ pub fn subscriptions_key_guard(ctx: &GuardContext) -> bool { return false; } - ctx.head() - .headers() - .get(SUBSCRIPTIONS_KEY_HEADER) - .is_some_and(|it| { - it.as_bytes() == ENV.LABRINTH_SUBSCRIPTIONS_KEY.as_bytes() - }) + header_matches_key( + ctx, + SUBSCRIPTIONS_KEY_HEADER, + &ENV.LABRINTH_SUBSCRIPTIONS_KEY, + ) } pub fn internal_network_guard(ctx: &GuardContext) -> bool {