From 81cd6d2ffaef5a63d7a65d2a80d54650c54628b2 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 31 Jul 2026 11:08:23 +0000 Subject: [PATCH 1/5] test(api): reproduce OpenAPI auth body leak Co-authored-by: Patrick Mill --- romm-api/src/client/openapi.rs | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/romm-api/src/client/openapi.rs b/romm-api/src/client/openapi.rs index 36c50e6..c38d111 100644 --- a/romm-api/src/client/openapi.rs +++ b/romm-api/src/client/openapi.rs @@ -194,4 +194,29 @@ mod tests { .expect("second URL should supply valid OpenAPI JSON"); assert!(got.contains("\"paths\"")); } + + #[tokio::test] + async fn fetch_openapi_json_failure_does_not_echo_auth_body() { + let server = MockServer::start().await; + let echoed_secret = "proxy echoed Authorization: Bearer secret"; + Mock::given(method("GET")) + .and(path("/openapi.json")) + .respond_with(ResponseTemplate::new(401).set_body_string(echoed_secret)) + .mount(&server) + .await; + Mock::given(method("GET")) + .and(path("/api/openapi.json")) + .respond_with(ResponseTemplate::new(401).set_body_string(echoed_secret)) + .mount(&server) + .await; + + let err = client_for(&server.uri()) + .fetch_openapi_json() + .await + .expect_err("failed OpenAPI downloads should report sanitized attempts"); + let msg = err.to_string(); + assert!(!msg.contains("Bearer secret"), "{msg}"); + assert!(!err.redacted_for_log().contains("Bearer secret")); + assert!(msg.contains("401")); + } } From 881c724afd5cd3d997a4e99ee757bd4507445d74 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 31 Jul 2026 11:09:09 +0000 Subject: [PATCH 2/5] fix(api): redact OpenAPI fallback errors Co-authored-by: Patrick Mill --- romm-api/src/client/openapi.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/romm-api/src/client/openapi.rs b/romm-api/src/client/openapi.rs index c38d111..8d62071 100644 --- a/romm-api/src/client/openapi.rs +++ b/romm-api/src/client/openapi.rs @@ -66,7 +66,11 @@ impl RommClient { for url in &urls { match self.fetch_openapi_json_once(url).await { Ok(body) => return Ok(body), - Err(e) => failures.push(format!("{url}: {e}")), + Err(e) => failures.push(format!( + "{}: {}", + crate::log_redact::redact_url_for_log(url), + e.redacted_for_log() + )), } } Err(ApiError::UnexpectedResponse(format!( From c1795150303cfacc9c623c5d137941980241aa3a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 31 Jul 2026 11:12:34 +0000 Subject: [PATCH 3/5] test(api): cover OpenAPI request URL redaction Co-authored-by: Patrick Mill --- romm-api/src/client/openapi.rs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/romm-api/src/client/openapi.rs b/romm-api/src/client/openapi.rs index 8d62071..1486dfe 100644 --- a/romm-api/src/client/openapi.rs +++ b/romm-api/src/client/openapi.rs @@ -223,4 +223,25 @@ mod tests { assert!(!err.redacted_for_log().contains("Bearer secret")); assert!(msg.contains("401")); } + + #[tokio::test] + async fn fetch_openapi_json_request_error_redacts_url_secrets() { + let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); + let port = listener.local_addr().unwrap().port(); + drop(listener); + let base_url = format!( + "http://user:url-secret@127.0.0.1:{port}?token=query-secret" + ); + + let err = client_for(&base_url) + .fetch_openapi_json() + .await + .expect_err("connection failures should report sanitized URLs"); + let msg = err.to_string(); + assert!(!msg.contains("url-secret"), "{msg}"); + assert!(!msg.contains("query-secret"), "{msg}"); + let log_msg = err.redacted_for_log(); + assert!(!log_msg.contains("url-secret"), "{log_msg}"); + assert!(!log_msg.contains("query-secret"), "{log_msg}"); + } } From ee2994885777972fac002f98e57753ea0fb72b1d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 31 Jul 2026 11:13:10 +0000 Subject: [PATCH 4/5] fix(api): redact OpenAPI request error URLs Co-authored-by: Patrick Mill --- romm-api/src/client/openapi.rs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/romm-api/src/client/openapi.rs b/romm-api/src/client/openapi.rs index 1486dfe..e349347 100644 --- a/romm-api/src/client/openapi.rs +++ b/romm-api/src/client/openapi.rs @@ -48,6 +48,17 @@ pub fn openapi_spec_urls(api_root: &str) -> Vec { urls } +fn openapi_failure_summary(url: &str, error: &ApiError) -> String { + let reason = match error { + ApiError::Request(e) => format!( + "ApiError: request failed: {}", + crate::log_redact::redact_url_for_log(&e.to_string()) + ), + _ => error.redacted_for_log(), + }; + format!("{}: {reason}", crate::log_redact::redact_url_for_log(url)) +} + impl RommClient { /// RomM application version from `GET /api/heartbeat` (`SYSTEM.VERSION`), if the endpoint succeeds. pub async fn rom_server_version_from_heartbeat(&self) -> Option { @@ -66,11 +77,7 @@ impl RommClient { for url in &urls { match self.fetch_openapi_json_once(url).await { Ok(body) => return Ok(body), - Err(e) => failures.push(format!( - "{}: {}", - crate::log_redact::redact_url_for_log(url), - e.redacted_for_log() - )), + Err(e) => failures.push(openapi_failure_summary(url, &e)), } } Err(ApiError::UnexpectedResponse(format!( From 0e96e102600ea89ca227e4d85ec2c612ae11ed8b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 31 Jul 2026 11:13:26 +0000 Subject: [PATCH 5/5] style(api): format OpenAPI redaction tests Co-authored-by: Patrick Mill --- romm-api/src/client/openapi.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/romm-api/src/client/openapi.rs b/romm-api/src/client/openapi.rs index e349347..4894ec3 100644 --- a/romm-api/src/client/openapi.rs +++ b/romm-api/src/client/openapi.rs @@ -236,9 +236,7 @@ mod tests { let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); let port = listener.local_addr().unwrap().port(); drop(listener); - let base_url = format!( - "http://user:url-secret@127.0.0.1:{port}?token=query-secret" - ); + let base_url = format!("http://user:url-secret@127.0.0.1:{port}?token=query-secret"); let err = client_for(&base_url) .fetch_openapi_json()