From 16b778a84314f29e36c247f1e4f976a01bd6681f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-X=2E=20T=2E?= Date: Thu, 22 Oct 2026 13:33:29 -0400 Subject: [PATCH] fix(labrinth): use constant time equality for key checks --- Cargo.lock | 1 + Cargo.toml | 1 + apps/labrinth/Cargo.toml | 1 + apps/labrinth/src/util/guards.rs | 40 +++++++++++++++++--------------- 4 files changed, 24 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index edc3baa6f69..b4d0e1f5bdd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5909,6 +5909,7 @@ dependencies = [ "sqlx", "sqlx-tracing", "strum", + "subtle", "thiserror 2.0.17", "tikv-jemalloc-ctl", "tikv-jemallocator", diff --git a/Cargo.toml b/Cargo.toml index 96d3a08e9bc..1c46de628c3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -210,6 +210,7 @@ spdx = "0.12.0" sqlx = { version = "0.8.6", default-features = false } sqlx-tracing = { path = "packages/sqlx-tracing" } strum = "0.27.2" +subtle = "2.6.1" syn = { version = "2.0" } sysinfo = { version = "0.37.2", default-features = false } tar = "0.4.44" diff --git a/apps/labrinth/Cargo.toml b/apps/labrinth/Cargo.toml index 49511942cca..7ac9eb0d4fb 100644 --- a/apps/labrinth/Cargo.toml +++ b/apps/labrinth/Cargo.toml @@ -121,6 +121,7 @@ sqlx = { workspace = true, features = [ ] } sqlx-tracing = { workspace = true, features = ["postgres"] } strum = { workspace = true, features = ["derive"] } +subtle = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true, features = ["rt-multi-thread", "sync"] } tokio-stream = { workspace = true } diff --git a/apps/labrinth/src/util/guards.rs b/apps/labrinth/src/util/guards.rs index f018438f63e..d204b6edc8b 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 {