From 3f703632ee19da79d5ceab7b05e1d47776636073 Mon Sep 17 00:00:00 2001 From: Anthony Ronning <101225832+AnthonyRonning@users.noreply.github.com> Date: Tue, 14 Jul 2026 18:27:13 +0000 Subject: [PATCH] fix: reject malformed parent secret responses --- src/main.rs | 39 ++++++++++----- src/parent_secret_response_tests.rs | 76 +++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+), 12 deletions(-) create mode 100644 src/parent_secret_response_tests.rs diff --git a/src/main.rs b/src/main.rs index 86cc5ff6..aa77621b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -107,6 +107,8 @@ mod web; #[cfg(test)] mod aead_db_tamper_tests; +#[cfg(test)] +mod parent_secret_response_tests; use apple_signin::AppleJwtVerifier; use oauth::{AppleProvider, GithubProvider, GoogleProvider, OAuthManager}; @@ -2223,20 +2225,33 @@ async fn get_secret(key_name: &str) -> Result { crate::aws_credentials::MAX_VSOCK_RESPONSE_BYTES, )?; - let parent_response: ParentResponse = serde_json::from_str(&response)?; - if parent_response.response_type == "secret" { - let secret_json: Value = - serde_json::from_str(parent_response.response_value.as_str().unwrap())?; + parse_parent_secret_response(&response) +} - // Assuming the secret is always a JSON object with a single key-value pair - if let Some((_, value)) = secret_json.as_object().and_then(|obj| obj.iter().next()) { - Ok(value.as_str().unwrap_or_default().to_string()) - } else { - Err(Error::SecretParsingError) - } - } else { - Err(Error::AuthenticationError) +fn parse_parent_secret_response(response: &str) -> Result { + let parent_response: ParentResponse = serde_json::from_str(response)?; + if parent_response.response_type != "secret" { + return Err(Error::AuthenticationError); } + + let response_value = parent_response + .response_value + .as_str() + .ok_or(Error::SecretParsingError)?; + let secret_json: Value = serde_json::from_str(response_value)?; + + // The parent returns a JSON object containing exactly the requested secret value. + let secret_object = secret_json.as_object().ok_or(Error::SecretParsingError)?; + if secret_object.len() != 1 { + return Err(Error::SecretParsingError); + } + + secret_object + .values() + .next() + .and_then(Value::as_str) + .map(ToOwned::to_owned) + .ok_or(Error::SecretParsingError) } async fn get_or_create_enclave_key( diff --git a/src/parent_secret_response_tests.rs b/src/parent_secret_response_tests.rs new file mode 100644 index 00000000..a5d70cd7 --- /dev/null +++ b/src/parent_secret_response_tests.rs @@ -0,0 +1,76 @@ +use super::{parse_parent_secret_response, Error}; + +#[test] +fn parses_valid_parent_secret_response() { + let response = r#"{ + "response_type": "secret", + "response_value": "{\"database_url\":\"postgres://localhost/test\"}" + }"#; + + let secret = parse_parent_secret_response(response).expect("valid response should parse"); + + assert_eq!(secret, "postgres://localhost/test"); +} + +#[test] +fn rejects_non_string_response_value_without_panicking() { + let response = r#"{ + "response_type": "secret", + "response_value": {"database_url": "postgres://localhost/test"} + }"#; + + assert!(matches!( + parse_parent_secret_response(response), + Err(Error::SecretParsingError) + )); +} + +#[test] +fn rejects_truncated_parent_response() { + let response = r#"{"response_type":"secret","response_value":"{}""#; + + assert!(matches!( + parse_parent_secret_response(response), + Err(Error::JsonError(_)) + )); +} + +#[test] +fn rejects_unexpected_parent_response_type() { + let response = r#"{"response_type":"credentials","response_value":"{}"}"#; + + assert!(matches!( + parse_parent_secret_response(response), + Err(Error::AuthenticationError) + )); +} + +#[test] +fn rejects_malformed_encoded_secret() { + let response = r#"{"response_type":"secret","response_value":"not json"}"#; + + assert!(matches!( + parse_parent_secret_response(response), + Err(Error::JsonError(_)) + )); +} + +#[test] +fn rejects_invalid_secret_object_shapes() { + for response_value in [ + "{}", + r#"{"database_url":42}"#, + r#"{"database_url":"first","jwt_secret":"second"}"#, + ] { + let response = serde_json::json!({ + "response_type": "secret", + "response_value": response_value, + }) + .to_string(); + + assert!(matches!( + parse_parent_secret_response(&response), + Err(Error::SecretParsingError) + )); + } +}