mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-16 16:07:45 +08:00
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:
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user