diff --git a/Cargo.lock b/Cargo.lock
index f4c4ff4e17..574a08e36d 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -89,6 +89,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 = "android_system_properties"
version = "0.1.5"
@@ -921,6 +934,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"
@@ -1044,6 +1080,7 @@ dependencies = [
name = "defguard_core"
version = "1.5.0"
dependencies = [
+ "ammonia",
"anyhow",
"argon2",
"axum",
@@ -1358,6 +1395,21 @@ dependencies = [
"zeroize",
]
+[[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 = "dyn-clone"
version = "1.0.20"
@@ -1622,6 +1674,16 @@ version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
+[[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"
version = "0.3.31"
@@ -1968,6 +2030,17 @@ dependencies = [
"windows-link 0.1.3",
]
+[[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"
@@ -2642,6 +2715,40 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
+[[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"
@@ -2761,6 +2868,12 @@ dependencies = [
"tempfile",
]
+[[package]]
+name = "new_debug_unreachable"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
+
[[package]]
name = "nom"
version = "7.1.3"
@@ -3338,6 +3451,7 @@ version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078"
dependencies = [
+ "phf_macros",
"phf_shared",
]
@@ -3361,6 +3475,19 @@ dependencies = [
"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"
@@ -3478,6 +3605,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"
@@ -4846,6 +4979,31 @@ dependencies = [
"windows-sys 0.59.0",
]
+[[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 = "stringprep"
version = "0.1.5"
@@ -4981,6 +5139,17 @@ dependencies = [
"windows-sys 0.61.0",
]
+[[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 = "tera"
version = "1.20.0"
@@ -5627,6 +5796,12 @@ dependencies = [
"serde",
]
+[[package]]
+name = "utf-8"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
+
[[package]]
name = "utf8_iter"
version = "1.0.4"
@@ -5912,6 +6087,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 = "webauthn-attestation-ca"
version = "0.5.2"
diff --git a/crates/defguard_core/Cargo.toml b/crates/defguard_core/Cargo.toml
index 10083cf94f..3ee6c10de5 100644
--- a/crates/defguard_core/Cargo.toml
+++ b/crates/defguard_core/Cargo.toml
@@ -85,6 +85,7 @@ bytes = { workspace = true }
ed25519-dalek = { version = "2.2", features = ["rand_core"] }
tower = "0.5"
regex = "1.10"
+ammonia = "4.1.1"
[dev-dependencies]
bytes = "1.6"
diff --git a/crates/defguard_core/src/handlers/openid_clients.rs b/crates/defguard_core/src/handlers/openid_clients.rs
index cc6a0bd52e..e0a911590d 100644
--- a/crates/defguard_core/src/handlers/openid_clients.rs
+++ b/crates/defguard_core/src/handlers/openid_clients.rs
@@ -26,6 +26,16 @@ pub async fn add_openid_client(
"User {} adding OpenID client {}",
session.user.username, data.name
);
+ if ammonia::is_html(&data.name) {
+ warn!(
+ "User {} attempted to create openid client with name containing HTML: {}",
+ session.user.username, data.name
+ );
+ return Ok(ApiResponse {
+ json: json!({"msg": "invalid name"}),
+ status: StatusCode::BAD_REQUEST,
+ });
+ }
let client = OAuth2Client::from_new(data).save(&appstate.pool).await?;
info!(
"User {} added OpenID client {}",
@@ -89,6 +99,16 @@ pub async fn change_openid_client(
"User {} updating OpenID client {client_id}...",
session.user.username
);
+ if ammonia::is_html(&data.name) {
+ warn!(
+ "User {} attempted to edit openid client with name containing HTML: {}",
+ session.user.username, data.name
+ );
+ return Ok(ApiResponse {
+ json: json!({"msg": "invalid name"}),
+ status: StatusCode::BAD_REQUEST,
+ });
+ }
let mut transaction = appstate.pool.begin().await?;
let status = match OAuth2Client::find_by_client_id(&mut *transaction, &client_id).await? {
Some(mut client) => {
diff --git a/crates/defguard_core/tests/integration/api/openid.rs b/crates/defguard_core/tests/integration/api/openid.rs
index 7b701bba14..c1803111e3 100644
--- a/crates/defguard_core/tests/integration/api/openid.rs
+++ b/crates/defguard_core/tests/integration/api/openid.rs
@@ -943,6 +943,74 @@ async fn dg25_22_test_respect_openid_scope_in_userinfo(
assert!(claims.phone_number().is_none());
}
+#[sqlx::test]
+async fn dg25_21_test_openid_html_injection(_: PgPoolOptions, options: PgConnectOptions) {
+ let pool = setup_pool(options).await;
+
+ let client = make_client(pool).await;
+ let auth = Auth::new("admin", "pass123");
+ let response = client.post("/api/v1/auth").json(&auth).send().await;
+ assert_eq!(response.status(), StatusCode::OK);
+
+ let invalid_names = &[
+ "Test Click",
+ "Test Click",
+ "Test ",
+ "Test