From bc4bd5e897ffa02bf21ae614ba4038cfa803e2b1 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 10 Sep 2025 11:18:13 +0200 Subject: [PATCH 1/3] sanitize user agent to prevent html injection --- Cargo.lock | 225 ++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + src/handlers/mod.rs | 3 +- 3 files changed, 228 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index f7809c1f..85ec79ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -67,6 +67,19 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "ammonia" +version = "4.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6b346764dd0814805de8abf899fe03065bcee69bb1a4771c785817e39f3978f" +dependencies = [ + "cssparser", + "html5ever", + "maplit", + "tendril", + "url", +] + [[package]] name = "anstream" version = "0.6.20" @@ -436,6 +449,29 @@ dependencies = [ "typenum", ] +[[package]] +name = "cssparser" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e901edd733a1472f944a45116df3f846f54d37e67e68640ac8bb69689aca2aa" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn", +] + [[package]] name = "ctr" version = "0.9.2" @@ -504,6 +540,7 @@ checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" name = "defguard-proxy" version = "1.5.0" dependencies = [ + "ammonia", "anyhow", "axum", "axum-client-ip", @@ -620,6 +657,21 @@ version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" +[[package]] +name = "dtoa" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6add3b8cff394282be81f3fc1a0605db594ed69890078ca6e2cab1c408bcf04" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + [[package]] name = "either" version = "1.15.0" @@ -701,6 +753,16 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + [[package]] name = "futures-channel" version = "0.3.31" @@ -926,6 +988,17 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "html5ever" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55d958c2f74b664487a2035fe1dadb032c48718a03b63f3ab0b8537db8549ed4" +dependencies = [ + "log", + "markup5ever", + "match_token", +] + [[package]] name = "http" version = "1.3.1" @@ -1286,6 +1359,40 @@ dependencies = [ "serde", ] +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + +[[package]] +name = "markup5ever" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "311fe69c934650f8f19652b3946075f0fc41ad8757dbb68f1ca14e7900ecc1c3" +dependencies = [ + "log", + "tendril", + "web_atoms", +] + +[[package]] +name = "match_token" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac84fd3f360fcc43dc5f5d186f02a94192761a080e8bc58621ad4d12296a58cf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "matchers" version = "0.2.0" @@ -1349,6 +1456,12 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + [[package]] name = "nonempty" version = "0.7.0" @@ -1469,6 +1582,58 @@ dependencies = [ "indexmap", ] +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand 0.8.5", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project" version = "1.1.10" @@ -1562,6 +1727,12 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + [[package]] name = "prettyplease" version = "0.2.37" @@ -2083,6 +2254,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + [[package]] name = "slab" version = "0.4.11" @@ -2120,6 +2297,31 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "string_cache" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + [[package]] name = "strsim" version = "0.11.1" @@ -2173,6 +2375,17 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -2832,6 +3045,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web_atoms" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57ffde1dc01240bdf9992e3205668b235e59421fd085e8a317ed98da0178d414" +dependencies = [ + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index 5fc1d88a..ca43aff0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,6 +51,7 @@ mime_guess = "2.0" base64 = "0.22" tower = "0.5" futures-util = "0.3" +ammonia = "4.1.1" [build-dependencies] tonic-prost-build = "0.14" diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index 97a2da74..67cb3f01 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -35,7 +35,8 @@ where let user_agent = TypedHeader::::from_request_parts(parts, state) .await .map(|v| v.to_string()) - .ok(); + .ok() + .filter(|agent| !ammonia::is_html(agent)); let ip_address = forwarded_for_ip .or(insecure_ip) From 1b8e89f0b93e537dfacd159abca06f91f4d45bb8 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 10 Sep 2025 11:44:52 +0200 Subject: [PATCH 2/3] add tests --- src/handlers/mod.rs | 103 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index 67cb3f01..b6816c40 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -36,6 +36,7 @@ where .await .map(|v| v.to_string()) .ok() + // sanitize user-agent .filter(|agent| !ammonia::is_html(agent)); let ip_address = forwarded_for_ip @@ -77,3 +78,105 @@ pub(crate) async fn get_core_response(rx: Receiver) -> ResultCLICK HERE", + "", + "

CLICK HERE", + ]; + + struct DummyState; + + #[tokio::test] + async fn test_user_agent_sanitization() { + let state = DummyState; + + // valid user agents + for agent in VALID_USER_AGENTS { + let req = Request::builder() + .header("User-Agent", *agent) + .header("X-Forwarded-For", "10.0.0.1") + .body(Body::empty()) + .unwrap(); + let (parts, _) = req.into_parts(); + let mut parts = parts; + + let device_info = DeviceInfo::from_request_parts(&mut parts, &state) + .await + .expect("should succeed"); + + assert_eq!(device_info.user_agent, Some(agent.to_string())); + } + + // invalid user agents + for agent in INVALID_USER_AGENTS { + let req = Request::builder() + .header("User-Agent", *agent) + .header("X-Forwarded-For", "10.0.0.1") + .body(Body::empty()) + .unwrap(); + let (parts, _) = req.into_parts(); + let mut parts = parts; + + let device_info = DeviceInfo::from_request_parts(&mut parts, &state) + .await + .expect("should succeed"); + + assert!(device_info.user_agent.is_none()); + } + + // no user agent + let req = Request::builder() + .header("X-Forwarded-For", "10.0.0.1") + .body(Body::empty()) + .unwrap(); + let (parts, _) = req.into_parts(); + let mut parts = parts; + + let device_info = DeviceInfo::from_request_parts(&mut parts, &state) + .await + .expect("should succeed"); + + assert!(device_info.user_agent.is_none()); + } +} From 5ba60cffb7a99c77b503f83175e64cd97f18d406 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 10 Sep 2025 12:05:38 +0200 Subject: [PATCH 3/3] rename test --- src/handlers/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index b6816c40..aef69aeb 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -128,7 +128,7 @@ mod tests { struct DummyState; #[tokio::test] - async fn test_user_agent_sanitization() { + async fn test_user_agent_sanitization_dg25_16() { let state = DummyState; // valid user agents