fix: restore security hardening compatibility and validation

Restore authorized rule reveal, explicit full HTTP capture and retention, video task business fields, and valid payment URLs. Add opt-in credential preservation for trusted recovery, fix frontend type contracts and async races, and eliminate PostgreSQL test fixture resource leaks. Document audit coverage and successful fmt and CI-scoped Clippy checks.
This commit is contained in:
elky
2026-09-07 21:14:27 +08:00
parent a5c3699ae9
commit a90d564931
191 changed files with 6785 additions and 1643 deletions
@@ -268,7 +268,7 @@ pub fn strip_deprecated_usage_display_fields(mut usage: UpsertUsageRecord) -> Up
usage
}
pub fn sanitize_usage_for_persistence(mut usage: UpsertUsageRecord) -> UpsertUsageRecord {
fn sanitize_usage_record_metadata(mut usage: UpsertUsageRecord) -> UpsertUsageRecord {
usage = strip_deprecated_usage_display_fields(usage);
sanitize_usage_routing_fields(&mut usage, None);
usage.error_message = None;
@@ -280,6 +280,11 @@ pub fn sanitize_usage_for_persistence(mut usage: UpsertUsageRecord) -> UpsertUsa
.map(str::to_string);
}
usage.request_metadata = super::sanitize_usage_request_metadata(usage.request_metadata);
usage
}
pub fn sanitize_usage_for_persistence(usage: UpsertUsageRecord) -> UpsertUsageRecord {
let mut usage = sanitize_usage_record_metadata(usage);
usage.request_headers = None;
usage.request_body = None;
usage.request_body_ref = None;
@@ -299,40 +304,96 @@ pub fn sanitize_usage_for_persistence(mut usage: UpsertUsageRecord) -> UpsertUsa
usage
}
/// Project an event onto the non-content controls accepted by auxiliary usage storage.
///
/// Explicit `none` states are retained only as tombstones for removing historical captures.
/// Every header, body, reference, and non-clear capture state is discarded.
pub fn sanitize_usage_capture_controls_for_persistence(
mut usage: UpsertUsageRecord,
) -> UpsertUsageRecord {
// Routing facts are allowed in the transient event metadata for compatibility with older
// writers. Project only the known scalar fields into typed slots before the general metadata
// sanitizer drops unknown keys. This keeps snapshots useful without re-persisting arbitrary
// metadata (or any body/header material).
let metadata = usage
.request_metadata
.as_ref()
.and_then(Value::as_object)
.cloned();
sanitize_usage_routing_fields(&mut usage, metadata.as_ref());
let clear_request_body = usage.request_body_state == Some(super::UsageBodyCaptureState::None);
let clear_provider_request_body =
usage.provider_request_body_state == Some(super::UsageBodyCaptureState::None);
let clear_response_body = usage.response_body_state == Some(super::UsageBodyCaptureState::None);
let clear_client_response_body =
usage.client_response_body_state == Some(super::UsageBodyCaptureState::None);
let mut usage = sanitize_usage_for_persistence(usage);
usage.request_body_state = clear_request_body.then_some(super::UsageBodyCaptureState::None);
usage.provider_request_body_state =
clear_provider_request_body.then_some(super::UsageBodyCaptureState::None);
usage.response_body_state = clear_response_body.then_some(super::UsageBodyCaptureState::None);
usage.client_response_body_state =
clear_client_response_body.then_some(super::UsageBodyCaptureState::None);
let mut usage = sanitize_usage_record_metadata(usage);
for headers in [
&mut usage.request_headers,
&mut usage.provider_request_headers,
&mut usage.response_headers,
&mut usage.client_response_headers,
] {
*headers = sanitize_usage_headers_for_persistence(headers.take());
}
for (field, body, body_ref, state) in [
(
super::UsageBodyField::RequestBody,
&mut usage.request_body,
&mut usage.request_body_ref,
usage.request_body_state,
),
(
super::UsageBodyField::ProviderRequestBody,
&mut usage.provider_request_body,
&mut usage.provider_request_body_ref,
usage.provider_request_body_state,
),
(
super::UsageBodyField::ResponseBody,
&mut usage.response_body,
&mut usage.response_body_ref,
usage.response_body_state,
),
(
super::UsageBodyField::ClientResponseBody,
&mut usage.client_response_body,
&mut usage.client_response_body_ref,
usage.client_response_body_state,
),
] {
if matches!(
state,
Some(
super::UsageBodyCaptureState::None
| super::UsageBodyCaptureState::Disabled
| super::UsageBodyCaptureState::Unavailable
)
) {
*body = None;
*body_ref = None;
} else {
*body_ref = body_ref.as_deref().and_then(|value| {
super::canonical_usage_body_ref_for(value, &usage.request_id, field)
});
}
}
usage
}
pub fn usage_header_value_is_sensitive(name: &str) -> bool {
![
"accept",
"accept-encoding",
"content-encoding",
"content-length",
"content-type",
"transfer-encoding",
"x-request-id",
"x-trace-id",
]
.iter()
.any(|candidate| name.trim().eq_ignore_ascii_case(candidate))
}
pub fn sanitize_usage_headers_for_persistence(value: Option<Value>) -> Option<Value> {
let Value::Object(mut headers) = value? else {
return None;
};
for (name, value) in &mut headers {
if usage_header_value_is_sensitive(name) && !value.is_null() {
*value = Value::String("[redacted]".to_string());
}
}
Some(Value::Object(headers))
}
fn sanitize_usage_routing_fields(
usage: &mut UpsertUsageRecord,
metadata: Option<&Map<String, Value>>,
@@ -773,20 +834,79 @@ mod tests {
}
#[test]
fn auxiliary_capture_projection_keeps_only_explicit_clear_tombstones() {
fn auxiliary_capture_projection_preserves_captures_and_honors_disabled_states() {
let mut input = usage_with_http_capture();
input.request_body_state = Some(UsageBodyCaptureState::None);
input.response_body_state = Some(UsageBodyCaptureState::Disabled);
let usage = sanitize_usage_capture_controls_for_persistence(input);
assert!(usage.request_headers.is_none());
assert_eq!(
usage.request_headers,
Some(json!({"authorization": "[redacted]"}))
);
assert!(usage.request_body.is_none());
assert!(usage.request_body_ref.is_none());
assert_eq!(usage.request_body_state, Some(UsageBodyCaptureState::None));
assert!(usage.provider_request_body_state.is_none());
assert!(usage.response_body_state.is_none());
assert!(usage.client_response_body_state.is_none());
assert_eq!(
usage.provider_request_body,
Some(json!({"prompt": "private"}))
);
assert_eq!(
usage.provider_request_body_state,
Some(UsageBodyCaptureState::Inline)
);
assert!(usage.response_body.is_none());
assert!(usage.response_body_ref.is_none());
assert_eq!(
usage.response_body_state,
Some(UsageBodyCaptureState::Disabled)
);
assert!(usage.client_response_body.is_none());
assert_eq!(
usage.client_response_body_state,
Some(UsageBodyCaptureState::Disabled)
);
}
#[test]
fn auxiliary_capture_projection_preserves_all_body_directions_and_scopes_references() {
let mut input = usage_with_http_capture();
input.request_headers =
Some(json!({"Content-Type": "application/json", "Authorization": "Bearer secret"}));
input.request_body_state = Some(UsageBodyCaptureState::Inline);
input.client_response_body_state = Some(UsageBodyCaptureState::Inline);
input.request_body_ref = Some(super::super::usage_body_ref(
&input.request_id,
super::super::UsageBodyField::RequestBody,
));
input.provider_request_body_ref = input.request_body_ref.clone();
input.response_body_ref = Some(super::super::usage_body_ref(
"another-request",
super::super::UsageBodyField::ResponseBody,
));
let captured = sanitize_usage_capture_controls_for_persistence(input.clone());
assert_eq!(captured.request_body, input.request_body);
assert_eq!(captured.provider_request_body, input.provider_request_body);
assert_eq!(captured.response_body, input.response_body);
assert_eq!(captured.client_response_body, input.client_response_body);
assert_eq!(captured.request_body_ref, input.request_body_ref);
assert!(captured.provider_request_body_ref.is_none());
assert!(captured.response_body_ref.is_none());
assert!(captured.client_response_body_ref.is_none());
assert_eq!(
captured.request_headers,
Some(json!({"Content-Type": "application/json", "Authorization": "[redacted]"}))
);
assert_eq!(
captured.provider_request_headers,
Some(json!({"x-api-key": "[redacted]"}))
);
assert_eq!(
captured.response_headers,
Some(json!({"set-cookie": "[redacted]"}))
);
assert!(captured.error_message.is_none());
}
#[test]
@@ -1,8 +1,6 @@
use async_trait::async_trait;
use serde_json::Value;
const SAFE_VIDEO_URL_QUERY_KEYS: &[(&str, &str)] = &[("alt", "media")];
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
)]
@@ -221,16 +219,11 @@ impl StoredVideoTask {
}
fn sanitize_persisted_diagnostics(&mut self) {
self.prompt = None;
self.original_request_body = None;
self.progress_message = None;
self.error_code = sanitize_video_task_error_code(self.error_code.take());
self.error_message = None;
self.video_url = sanitize_video_task_url(
self.client_api_format.as_deref(),
self.provider_api_format.as_deref(),
self.video_url.take(),
);
self.video_url = sanitize_video_task_url(self.video_url.take());
self.request_metadata = None;
}
@@ -339,18 +332,11 @@ pub struct UpsertVideoTask {
impl UpsertVideoTask {
pub fn sanitize_for_persistence(&mut self) {
self.username = None;
self.api_key_name = None;
self.prompt = None;
self.original_request_body = None;
self.progress_message = None;
self.error_code = sanitize_video_task_error_code(self.error_code.take());
self.error_message = None;
self.video_url = sanitize_video_task_url(
self.client_api_format.as_deref(),
self.provider_api_format.as_deref(),
self.video_url.take(),
);
self.video_url = sanitize_video_task_url(self.video_url.take());
self.request_metadata = None;
}
@@ -421,16 +407,7 @@ fn sanitize_video_task_error_code(value: Option<String>) -> Option<String> {
})
}
fn sanitize_video_task_url(
client_api_format: Option<&str>,
provider_api_format: Option<&str>,
value: Option<String>,
) -> Option<String> {
if effective_video_task_api_format(client_api_format, provider_api_format)
!= Some("gemini:video")
{
return None;
}
fn sanitize_video_task_url(value: Option<String>) -> Option<String> {
let mut url = url::Url::parse(value?.trim()).ok()?;
if !matches!(url.scheme(), "http" | "https")
|| url.host_str().is_none()
@@ -440,15 +417,6 @@ fn sanitize_video_task_url(
return None;
}
let query = url
.query_pairs()
.filter(|(key, value)| SAFE_VIDEO_URL_QUERY_KEYS.contains(&(key.as_ref(), value.as_ref())))
.map(|(key, value)| (key.into_owned(), value.into_owned()))
.collect::<Vec<_>>();
url.set_query(None);
if !query.is_empty() {
url.query_pairs_mut().extend_pairs(query);
}
url.set_fragment(None);
Some(url.into())
}
@@ -940,20 +908,23 @@ mod tests {
assert_eq!(task.user_id.as_deref(), Some("user-1"));
assert_eq!(task.api_key_id.as_deref(), Some("api-key-1"));
assert_eq!(task.username, None);
assert_eq!(task.api_key_name, None);
assert_eq!(task.username.as_deref(), Some("private-user-name"));
assert_eq!(task.api_key_name.as_deref(), Some("private-key-name"));
assert_eq!(task.original_request_body, None);
assert_eq!(task.progress_message, None);
assert_eq!(task.error_message, None);
assert_eq!(task.error_code.as_deref(), Some("provider_error"));
assert_eq!(task.video_url, None);
assert_eq!(
task.video_url.as_deref(),
Some("https://cdn.example.test/video.mp4?token=secret")
);
assert_eq!(task.request_metadata, None);
assert_eq!(task.prompt, None);
assert_eq!(task.prompt.as_deref(), Some("prompt"));
assert_eq!(task.duration_seconds, Some(4));
}
#[test]
fn upsert_sanitization_keeps_only_noncredential_video_urls() {
fn stored_task_preserves_prompt_and_signed_download_url() {
let mut args = base_new_args();
args.12 = Some("gemini:video".to_string());
args.15 = Some("private prompt".to_string());
@@ -968,15 +939,15 @@ mod tests {
args.28, args.29, args.30, args.31, args.32, args.33, args.34, args.35, args.36,
)
.expect("stored task should build");
assert_eq!(task.prompt, None);
assert_eq!(task.prompt.as_deref(), Some("private prompt"));
assert_eq!(
task.video_url.as_deref(),
Some("https://cdn.example.test/video.mp4?alt=media")
Some("https://cdn.example.test/video.mp4?key=secret&alt=media&signature=private")
);
}
#[test]
fn upsert_sanitization_uses_client_format_when_legacy_provider_format_is_blank() {
fn stored_task_preserves_download_url_when_legacy_provider_format_is_blank() {
let mut args = base_new_args();
args.11 = Some("gemini:video".to_string());
args.12 = Some(" ".to_string());
@@ -992,7 +963,32 @@ mod tests {
assert_eq!(
task.video_url.as_deref(),
Some("https://cdn.example.test/video.mp4?alt=media")
Some("https://cdn.example.test/video.mp4?key=secret&alt=media")
);
}
#[test]
fn video_url_sanitization_preserves_signed_query_encoding_and_order() {
let video_url =
"https://cdn.example.test/video.mp4?signature=a%2Fb%2Bc%3D&part=2&part=1&name=a%20b";
assert_eq!(
super::sanitize_video_task_url(Some(format!("{video_url}#fragment"))).as_deref(),
Some(video_url)
);
}
#[test]
fn video_url_sanitization_rejects_invalid_schemes_and_embedded_credentials() {
for video_url in [
"file:///etc/passwd",
"javascript:alert(1)",
"data:video/mp4;base64,AAAA",
"https://user:password@cdn.example.test/video.mp4",
"https://user@cdn.example.test/video.mp4",
"/relative/video.mp4",
"not a url",
] {
assert!(super::sanitize_video_task_url(Some(video_url.to_string())).is_none());
}
}
}