mirror of
https://github.com/nimbold/Firelink.git
synced 2026-09-01 21:48:00 +00:00
fix(handoff): close browser credential boundaries
- Filter custom credential headers and cookies at restricted handoff consumers. - Preserve ordinary single-file capture credentials for Add-window review. - Extend native and renderer redaction coverage with focused regressions.
This commit is contained in:
@@ -566,18 +566,8 @@ fn normalize_headers(headers: Option<String>, media: bool) -> Option<String> {
|
||||
.lines()
|
||||
.filter(|line| {
|
||||
line.split_once(':')
|
||||
.map(|(name, _)| {
|
||||
!matches!(
|
||||
name.trim().to_ascii_lowercase().as_str(),
|
||||
"authorization"
|
||||
| "cookie"
|
||||
| "cookie2"
|
||||
| "proxy-authorization"
|
||||
| "set-cookie"
|
||||
| "set-cookie2"
|
||||
)
|
||||
})
|
||||
.unwrap_or(true)
|
||||
.map(|(name, _)| !crate::queue::header_name_has_credential_material(name))
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
@@ -919,7 +909,7 @@ mod tests {
|
||||
silent: false,
|
||||
filename: None,
|
||||
headers: Some(format!(
|
||||
"Cookie: stale={};\nCookie2: stale=1\nAuthorization: Bearer stale\nProxy-Authorization: Basic stale\nSet-Cookie: stale=1\nSet-Cookie2: stale=1\nUser-Agent: Firefox",
|
||||
"Cookie: stale={};\nCookie2: stale=1\nAuthorization: Bearer stale\nProxy-Authorization: Basic stale\nSet-Cookie: stale=1\nSet-Cookie2: stale=1\nX-Api-Key: stale\nX-Auth-Token: stale\nX-Access-Token: stale\nX-Request-Signature: stale\nX-Session: stale\n: malformed\nUser-Agent: Firefox\nX-Trace: safe",
|
||||
"x".repeat(64 * 1024)
|
||||
)),
|
||||
cookies: Some(format!("large={}", "x".repeat(64 * 1024))),
|
||||
@@ -933,7 +923,10 @@ mod tests {
|
||||
|
||||
assert!(download.media);
|
||||
assert!(download.cookies.is_none());
|
||||
assert_eq!(download.headers.as_deref(), Some("User-Agent: Firefox"));
|
||||
assert_eq!(
|
||||
download.headers.as_deref(),
|
||||
Some("User-Agent: Firefox\nX-Trace: safe")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -960,6 +953,37 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_url_capture_drops_shared_credentials_but_keeps_safe_headers() {
|
||||
let download = normalize_download(ExtensionRequest {
|
||||
urls: vec![
|
||||
"https://one.example/file.zip".to_string(),
|
||||
"https://two.example/file.zip".to_string(),
|
||||
],
|
||||
referer: None,
|
||||
silent: false,
|
||||
filename: None,
|
||||
headers: Some(
|
||||
"X-Api-Key: shared-secret\nX-Request-Signature: signature-secret\n: malformed\nUser-Agent: Firefox\nX-Trace: safe"
|
||||
.to_string(),
|
||||
),
|
||||
cookies: Some("session=must-not-cross-hosts".to_string()),
|
||||
cookie_scopes: None,
|
||||
media: false,
|
||||
torrent: false,
|
||||
batch: true,
|
||||
batch_name: Some("batch".to_string()),
|
||||
})
|
||||
.expect("valid multi-url handoff");
|
||||
|
||||
assert!(download.batch);
|
||||
assert!(download.cookies.is_none());
|
||||
assert_eq!(
|
||||
download.headers.as_deref(),
|
||||
Some("User-Agent: Firefox\nX-Trace: safe")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn torrent_handoff_accepts_magnets_and_preserves_the_intent() {
|
||||
let download = normalize_download(ExtensionRequest {
|
||||
|
||||
+30
-9
@@ -11408,27 +11408,25 @@ pub(crate) fn redact_sensitive_text(line: &str) -> String {
|
||||
use std::sync::OnceLock;
|
||||
static SECRET: OnceLock<regex::Regex> = OnceLock::new();
|
||||
static QUOTED_SECRET: OnceLock<regex::Regex> = OnceLock::new();
|
||||
static HEADER: OnceLock<regex::Regex> = OnceLock::new();
|
||||
static COOKIE_HEADER: OnceLock<regex::Regex> = OnceLock::new();
|
||||
static QUERY: OnceLock<regex::Regex> = OnceLock::new();
|
||||
static USERINFO: OnceLock<regex::Regex> = OnceLock::new();
|
||||
static FRAGMENT: OnceLock<regex::Regex> = OnceLock::new();
|
||||
let secret = SECRET.get_or_init(|| {
|
||||
regex::Regex::new(
|
||||
r"(?i)(authorization|proxy-authorization|cookie|set-cookie|password|token|secret|credential|pairing[-_ ]?token|api[-_ ]?key)\s*[:=]\s*([^\r\n,;]+)",
|
||||
r"(?i)(authorization|proxy-authorization|cookie2|cookie|set-cookie2|set-cookie|password|passwd|auth|token|secret|credential|key|pairing[-_ ]?token|api[-_ ]?key|access[-_ ]?token|auth[-_ ]?token|signature|session)\s*[:=]\s*([^\r\n,;]+)",
|
||||
)
|
||||
.expect("valid secret redaction regex")
|
||||
});
|
||||
let quoted_secret = QUOTED_SECRET.get_or_init(|| {
|
||||
regex::Regex::new(
|
||||
r#"(?i)(["'])(authorization|proxy-authorization|cookie|set-cookie|password|token|secret|credential|pairing[-_ ]?token|api[-_ ]?key)(["'])(\s*[:=]\s*)["'][^"\r\n,;]*["']"#,
|
||||
r#"(?i)(["'])(authorization|proxy-authorization|cookie2|cookie|set-cookie2|set-cookie|password|passwd|auth|token|secret|credential|key|pairing[-_ ]?token|api[-_ ]?key|access[-_ ]?token|auth[-_ ]?token|signature|session)(["'])(\s*[:=]\s*)["'][^"\r\n,;]*["']"#,
|
||||
)
|
||||
.expect("valid quoted secret redaction regex")
|
||||
});
|
||||
let header = HEADER.get_or_init(|| {
|
||||
regex::Regex::new(
|
||||
r"(?i)(authorization|proxy-authorization|cookie|set-cookie)\s*:\s*[^\r\n]+",
|
||||
)
|
||||
.expect("valid sensitive header redaction regex")
|
||||
let cookie_header = COOKIE_HEADER.get_or_init(|| {
|
||||
regex::Regex::new(r"(?i)((?:set-)?cookie2?)\s*[:=]\s*[^\r\n]+")
|
||||
.expect("valid cookie header redaction regex")
|
||||
});
|
||||
let query = QUERY.get_or_init(|| {
|
||||
regex::Regex::new(r#"([A-Za-z][A-Za-z0-9+.-]*://[^\s?\"'<>},\]]+)\?[^\s\"'<>},\]]+"#)
|
||||
@@ -11445,7 +11443,7 @@ pub(crate) fn redact_sensitive_text(line: &str) -> String {
|
||||
let redacted = query.replace_all(line, "$1?[redacted]");
|
||||
let redacted = fragment.replace_all(&redacted, "$1#[redacted]");
|
||||
let redacted = userinfo.replace_all(&redacted, "$1[redacted]@");
|
||||
let redacted = header.replace_all(&redacted, "$1: [redacted]");
|
||||
let redacted = cookie_header.replace_all(&redacted, "$1: [redacted]");
|
||||
let redacted = quoted_secret.replace_all(&redacted, "$1$2$3$4[redacted]");
|
||||
secret
|
||||
.replace_all(&redacted, "$1=[redacted]")
|
||||
@@ -14220,6 +14218,29 @@ mod tests {
|
||||
assert!(redacted.contains("[redacted]"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn redacts_legacy_cookie_and_compound_custom_headers() {
|
||||
let line = "Set-Cookie2: legacy-cookie X-Session: id=session-secret; key=compound-secret";
|
||||
let redacted = redact_log_line(line);
|
||||
assert!(!redacted.contains("legacy-cookie"));
|
||||
assert!(!redacted.contains("session-secret"));
|
||||
assert!(!redacted.contains("compound-secret"));
|
||||
assert!(redacted.contains("[redacted]"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn redacts_all_pairs_in_cookie_headers() {
|
||||
let redacted = redact_log_line("Cookie: a=1; user_id=secret; state=xyz");
|
||||
assert!(!redacted.contains("a=1"));
|
||||
assert!(!redacted.contains("user_id=secret"));
|
||||
assert!(!redacted.contains("state=xyz"));
|
||||
assert!(redacted.contains("[redacted]"));
|
||||
|
||||
let redacted = redact_log_line("Cookie2=a=1; user_id=secret; state=xyz");
|
||||
assert!(!redacted.contains("user_id=secret"));
|
||||
assert!(!redacted.contains("state=xyz"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preserves_compact_json_delimiters_while_redacting_url_queries() {
|
||||
let redacted = redact_log_line(
|
||||
|
||||
@@ -6086,15 +6086,16 @@ fn payload_has_credential_material(payload: &SpawnPayload) -> bool {
|
||||
.any(|name| header_name_has_credential_material(&name))
|
||||
}
|
||||
|
||||
fn header_name_has_credential_material(name: &str) -> bool {
|
||||
pub(crate) fn header_name_has_credential_material(name: &str) -> bool {
|
||||
let name = name.trim().to_ascii_lowercase();
|
||||
matches!(
|
||||
name.is_empty() || matches!(
|
||||
name.as_str(),
|
||||
"authorization"
|
||||
| "cookie"
|
||||
| "cookie2"
|
||||
| "proxy-authorization"
|
||||
| "set-cookie"
|
||||
| "set-cookie2"
|
||||
| "x-api-key"
|
||||
| "x-auth-token"
|
||||
| "x-access-token"
|
||||
|
||||
Reference in New Issue
Block a user