mirror of
https://github.com/nimbold/Firelink.git
synced 2026-09-02 05:57:59 +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()
|
.lines()
|
||||||
.filter(|line| {
|
.filter(|line| {
|
||||||
line.split_once(':')
|
line.split_once(':')
|
||||||
.map(|(name, _)| {
|
.map(|(name, _)| !crate::queue::header_name_has_credential_material(name))
|
||||||
!matches!(
|
.unwrap_or(false)
|
||||||
name.trim().to_ascii_lowercase().as_str(),
|
|
||||||
"authorization"
|
|
||||||
| "cookie"
|
|
||||||
| "cookie2"
|
|
||||||
| "proxy-authorization"
|
|
||||||
| "set-cookie"
|
|
||||||
| "set-cookie2"
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.unwrap_or(true)
|
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join("\n");
|
.join("\n");
|
||||||
@@ -919,7 +909,7 @@ mod tests {
|
|||||||
silent: false,
|
silent: false,
|
||||||
filename: None,
|
filename: None,
|
||||||
headers: Some(format!(
|
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)
|
"x".repeat(64 * 1024)
|
||||||
)),
|
)),
|
||||||
cookies: Some(format!("large={}", "x".repeat(64 * 1024))),
|
cookies: Some(format!("large={}", "x".repeat(64 * 1024))),
|
||||||
@@ -933,7 +923,10 @@ mod tests {
|
|||||||
|
|
||||||
assert!(download.media);
|
assert!(download.media);
|
||||||
assert!(download.cookies.is_none());
|
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]
|
#[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]
|
#[test]
|
||||||
fn torrent_handoff_accepts_magnets_and_preserves_the_intent() {
|
fn torrent_handoff_accepts_magnets_and_preserves_the_intent() {
|
||||||
let download = normalize_download(ExtensionRequest {
|
let download = normalize_download(ExtensionRequest {
|
||||||
|
|||||||
+30
-9
@@ -11408,27 +11408,25 @@ pub(crate) fn redact_sensitive_text(line: &str) -> String {
|
|||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
static SECRET: OnceLock<regex::Regex> = OnceLock::new();
|
static SECRET: OnceLock<regex::Regex> = OnceLock::new();
|
||||||
static QUOTED_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 QUERY: OnceLock<regex::Regex> = OnceLock::new();
|
||||||
static USERINFO: OnceLock<regex::Regex> = OnceLock::new();
|
static USERINFO: OnceLock<regex::Regex> = OnceLock::new();
|
||||||
static FRAGMENT: OnceLock<regex::Regex> = OnceLock::new();
|
static FRAGMENT: OnceLock<regex::Regex> = OnceLock::new();
|
||||||
let secret = SECRET.get_or_init(|| {
|
let secret = SECRET.get_or_init(|| {
|
||||||
regex::Regex::new(
|
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")
|
.expect("valid secret redaction regex")
|
||||||
});
|
});
|
||||||
let quoted_secret = QUOTED_SECRET.get_or_init(|| {
|
let quoted_secret = QUOTED_SECRET.get_or_init(|| {
|
||||||
regex::Regex::new(
|
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")
|
.expect("valid quoted secret redaction regex")
|
||||||
});
|
});
|
||||||
let header = HEADER.get_or_init(|| {
|
let cookie_header = COOKIE_HEADER.get_or_init(|| {
|
||||||
regex::Regex::new(
|
regex::Regex::new(r"(?i)((?:set-)?cookie2?)\s*[:=]\s*[^\r\n]+")
|
||||||
r"(?i)(authorization|proxy-authorization|cookie|set-cookie)\s*:\s*[^\r\n]+",
|
.expect("valid cookie header redaction regex")
|
||||||
)
|
|
||||||
.expect("valid sensitive header redaction regex")
|
|
||||||
});
|
});
|
||||||
let query = QUERY.get_or_init(|| {
|
let query = QUERY.get_or_init(|| {
|
||||||
regex::Regex::new(r#"([A-Za-z][A-Za-z0-9+.-]*://[^\s?\"'<>},\]]+)\?[^\s\"'<>},\]]+"#)
|
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 = query.replace_all(line, "$1?[redacted]");
|
||||||
let redacted = fragment.replace_all(&redacted, "$1#[redacted]");
|
let redacted = fragment.replace_all(&redacted, "$1#[redacted]");
|
||||||
let redacted = userinfo.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]");
|
let redacted = quoted_secret.replace_all(&redacted, "$1$2$3$4[redacted]");
|
||||||
secret
|
secret
|
||||||
.replace_all(&redacted, "$1=[redacted]")
|
.replace_all(&redacted, "$1=[redacted]")
|
||||||
@@ -14220,6 +14218,29 @@ mod tests {
|
|||||||
assert!(redacted.contains("[redacted]"));
|
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]
|
#[test]
|
||||||
fn preserves_compact_json_delimiters_while_redacting_url_queries() {
|
fn preserves_compact_json_delimiters_while_redacting_url_queries() {
|
||||||
let redacted = redact_log_line(
|
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))
|
.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();
|
let name = name.trim().to_ascii_lowercase();
|
||||||
matches!(
|
name.is_empty() || matches!(
|
||||||
name.as_str(),
|
name.as_str(),
|
||||||
"authorization"
|
"authorization"
|
||||||
| "cookie"
|
| "cookie"
|
||||||
| "cookie2"
|
| "cookie2"
|
||||||
| "proxy-authorization"
|
| "proxy-authorization"
|
||||||
| "set-cookie"
|
| "set-cookie"
|
||||||
|
| "set-cookie2"
|
||||||
| "x-api-key"
|
| "x-api-key"
|
||||||
| "x-auth-token"
|
| "x-auth-token"
|
||||||
| "x-access-token"
|
| "x-access-token"
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { open } from '@tauri-apps/plugin-dialog';
|
|||||||
import { writeText as writeClipboardText } from '@tauri-apps/plugin-clipboard-manager';
|
import { writeText as writeClipboardText } from '@tauri-apps/plugin-clipboard-manager';
|
||||||
import { invokeCommand as invoke } from '../ipc';
|
import { invokeCommand as invoke } from '../ipc';
|
||||||
import { DuplicateResolutionModal, DuplicateConflict } from './DuplicateResolutionModal';
|
import { DuplicateResolutionModal, DuplicateConflict } from './DuplicateResolutionModal';
|
||||||
import { canonicalizeDownloadFileName, categoryForFileName, downloadFileNameWithSuffix, downloadFileNamesMatch, downloadMediaKindsMatch, isValidTorrentExcludeTrackerList, isValidTorrentTrackerList, MAX_TORRENT_STOP_TIMEOUT, MAX_TORRENT_TRACKER_INTERVAL, MAX_TORRENT_TRACKER_TIMEOUT, normalizeSpeedLimitForBackend, normalizeTorrentWebSeedDrafts, normalizeTorrentTrackerInterval, normalizeTorrentTrackerTimeout, serializeTorrentPreviewPriority, TORRENT_ENCRYPTION_POLICY_DISABLED, TORRENT_ENCRYPTION_POLICY_FORCE_ENCRYPTION, TORRENT_ENCRYPTION_POLICY_REQUIRE_CRYPTO, type TorrentEncryptionPolicy, type TorrentFileAllocation } from '../utils/downloads';
|
import { canonicalizeDownloadFileName, categoryForFileName, downloadFileNameWithSuffix, downloadFileNamesMatch, downloadMediaKindsMatch, headerNameHasCredentialMaterial, isValidTorrentExcludeTrackerList, isValidTorrentTrackerList, MAX_TORRENT_STOP_TIMEOUT, MAX_TORRENT_TRACKER_INTERVAL, MAX_TORRENT_TRACKER_TIMEOUT, normalizeSpeedLimitForBackend, normalizeTorrentWebSeedDrafts, normalizeTorrentTrackerInterval, normalizeTorrentTrackerTimeout, serializeTorrentPreviewPriority, TORRENT_ENCRYPTION_POLICY_DISABLED, TORRENT_ENCRYPTION_POLICY_FORCE_ENCRYPTION, TORRENT_ENCRYPTION_POLICY_REQUIRE_CRYPTO, type TorrentEncryptionPolicy, type TorrentFileAllocation } from '../utils/downloads';
|
||||||
import { fetchMediaMetadataDeduped, fetchMediaPlaylistMetadataDeduped } from '../utils/mediaMetadata';
|
import { fetchMediaMetadataDeduped, fetchMediaPlaylistMetadataDeduped } from '../utils/mediaMetadata';
|
||||||
import {
|
import {
|
||||||
expandTilde,
|
expandTilde,
|
||||||
@@ -127,7 +127,7 @@ const extensionHeaders = (context: PendingAddRequestContext | undefined) => [
|
|||||||
.split(/\r?\n/)
|
.split(/\r?\n/)
|
||||||
.filter(line => {
|
.filter(line => {
|
||||||
const separator = line.indexOf(':');
|
const separator = line.indexOf(':');
|
||||||
return separator < 0 || line.slice(0, separator).trim().toLowerCase() !== 'cookie';
|
return separator > 0 && !headerNameHasCredentialMaterial(line.slice(0, separator));
|
||||||
})
|
})
|
||||||
.join('\n')
|
.join('\n')
|
||||||
: context?.headers
|
: context?.headers
|
||||||
|
|||||||
@@ -3643,7 +3643,7 @@ describe('useDownloadStore', () => {
|
|||||||
silent: false,
|
silent: false,
|
||||||
filename: null,
|
filename: null,
|
||||||
headers: null,
|
headers: null,
|
||||||
cookies: null,
|
cookies: 'shared=session',
|
||||||
cookie_scopes: null,
|
cookie_scopes: null,
|
||||||
media: false,
|
media: false,
|
||||||
torrent: false,
|
torrent: false,
|
||||||
@@ -3659,6 +3659,7 @@ describe('useDownloadStore', () => {
|
|||||||
'https://example.com/one.zip\nhttps://example.com/two.zip'
|
'https://example.com/one.zip\nhttps://example.com/two.zip'
|
||||||
);
|
);
|
||||||
expect(useDownloadStore.getState().pendingAddBatch).toBe(false);
|
expect(useDownloadStore.getState().pendingAddBatch).toBe(false);
|
||||||
|
expect(useDownloadStore.getState().pendingAddCookies).toBe('');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('keeps each extension handoff context attached to its own URL while the Add Modal is open', async () => {
|
it('keeps each extension handoff context attached to its own URL while the Add Modal is open', async () => {
|
||||||
@@ -3720,7 +3721,7 @@ describe('useDownloadStore', () => {
|
|||||||
referer: 'https://adult.example/watch/123',
|
referer: 'https://adult.example/watch/123',
|
||||||
silent: false,
|
silent: false,
|
||||||
filename: null,
|
filename: null,
|
||||||
headers: `Cookie: stale=${'x'.repeat(64 * 1024)}\nCookie2: stale=1\nAuthorization: Bearer stale\nProxy-Authorization: Basic stale\nSet-Cookie: stale=1\nSet-Cookie2: stale=1\nUser-Agent: Firefox Test`,
|
headers: `Cookie: stale=${'x'.repeat(64 * 1024)}\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-Request-Signature: stale\nX-Session: stale\nUser-Agent: Firefox Test`,
|
||||||
cookies: `oversized=${'x'.repeat(64 * 1024)}`,
|
cookies: `oversized=${'x'.repeat(64 * 1024)}`,
|
||||||
cookie_scopes: null,
|
cookie_scopes: null,
|
||||||
media: true,
|
media: true,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import type { ExtensionCookieScope } from '../bindings/ExtensionCookieScope';
|
|||||||
import type { Queue } from '../bindings/Queue';
|
import type { Queue } from '../bindings/Queue';
|
||||||
import { useSettingsStore } from './useSettingsStore';
|
import { useSettingsStore } from './useSettingsStore';
|
||||||
import { useDownloadProgressStore } from './downloadProgressStore';
|
import { useDownloadProgressStore } from './downloadProgressStore';
|
||||||
import { canonicalizeDownloadFileName, categoryForDownload, categoryForFileName, hasCredentialBearingHeaders, isActiveDownloadStatus, isTransferActiveStatus, isValidTorrentExcludeTrackerList, isValidTorrentTrackerList, MAX_TORRENT_STOP_TIMEOUT, normalizeSpeedLimitForBackend, normalizeTorrentEncryptionPolicy, normalizeTorrentFileAllocation, normalizeTorrentPrioritizePiece, normalizeTorrentTrackerInterval, normalizeTorrentTrackerTimeout, redactDownloadForPersistence, resolveDownloadConnections } from '../utils/downloads';
|
import { canonicalizeDownloadFileName, categoryForDownload, categoryForFileName, hasCredentialBearingHeaders, headerNameHasCredentialMaterial, isActiveDownloadStatus, isTransferActiveStatus, isValidTorrentExcludeTrackerList, isValidTorrentTrackerList, MAX_TORRENT_STOP_TIMEOUT, normalizeSpeedLimitForBackend, normalizeTorrentEncryptionPolicy, normalizeTorrentFileAllocation, normalizeTorrentPrioritizePiece, normalizeTorrentTrackerInterval, normalizeTorrentTrackerTimeout, redactDownloadForPersistence, resolveDownloadConnections } from '../utils/downloads';
|
||||||
import {
|
import {
|
||||||
resolveCategoryDestination
|
resolveCategoryDestination
|
||||||
} from '../utils/downloadLocations';
|
} from '../utils/downloadLocations';
|
||||||
@@ -300,16 +300,8 @@ const stripSensitiveMediaHeaders = (value: string | null | undefined): string =>
|
|||||||
.split(/\r?\n/)
|
.split(/\r?\n/)
|
||||||
.filter(line => {
|
.filter(line => {
|
||||||
const separator = line.indexOf(':');
|
const separator = line.indexOf(':');
|
||||||
if (separator < 0) return true;
|
if (separator <= 0) return false;
|
||||||
const name = line.slice(0, separator).trim().toLowerCase();
|
return !headerNameHasCredentialMaterial(line.slice(0, separator));
|
||||||
return ![
|
|
||||||
'authorization',
|
|
||||||
'cookie',
|
|
||||||
'cookie2',
|
|
||||||
'proxy-authorization',
|
|
||||||
'set-cookie',
|
|
||||||
'set-cookie2'
|
|
||||||
].includes(name);
|
|
||||||
})
|
})
|
||||||
.join('\n')
|
.join('\n')
|
||||||
.trim();
|
.trim();
|
||||||
@@ -1818,8 +1810,8 @@ export const useDownloadStore = create<DownloadState>((set, get) => {
|
|||||||
// Explicit media authentication belongs to yt-dlp's configured browser
|
// Explicit media authentication belongs to yt-dlp's configured browser
|
||||||
// cookie source. Keep this frontend guard for events from older desktop or
|
// cookie source. Keep this frontend guard for events from older desktop or
|
||||||
// extension builds; ordinary captured downloads retain their cookies.
|
// extension builds; ordinary captured downloads retain their cookies.
|
||||||
const cookies = request.media === true ? null : request.cookies;
|
const cookies = request.media === true || urls.length > 1 ? null : request.cookies;
|
||||||
const headers = request.media === true
|
const headers = request.media === true || urls.length > 1
|
||||||
? stripSensitiveMediaHeaders(request.headers) || null
|
? stripSensitiveMediaHeaders(request.headers) || null
|
||||||
: request.headers;
|
: request.headers;
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
torrentWebSeedDraftsFromSeeds,
|
torrentWebSeedDraftsFromSeeds,
|
||||||
normalizeTorrentTrackerInterval,
|
normalizeTorrentTrackerInterval,
|
||||||
normalizeTorrentTrackerTimeout,
|
normalizeTorrentTrackerTimeout,
|
||||||
|
headerNameHasCredentialMaterial,
|
||||||
redactDownloadForPersistence,
|
redactDownloadForPersistence,
|
||||||
resolveDownloadConnections
|
resolveDownloadConnections
|
||||||
} from './downloads';
|
} from './downloads';
|
||||||
@@ -178,6 +179,17 @@ describe('download persistence progress snapshots', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('credential-bearing extension header names', () => {
|
||||||
|
it('classifies named and marker-based credential headers while preserving browser context names', () => {
|
||||||
|
expect(headerNameHasCredentialMaterial('X-Api-Key')).toBe(true);
|
||||||
|
expect(headerNameHasCredentialMaterial('X-Request-Signature')).toBe(true);
|
||||||
|
expect(headerNameHasCredentialMaterial('X-Session')).toBe(true);
|
||||||
|
expect(headerNameHasCredentialMaterial('Set-Cookie2')).toBe(true);
|
||||||
|
expect(headerNameHasCredentialMaterial('User-Agent')).toBe(false);
|
||||||
|
expect(headerNameHasCredentialMaterial('X-Trace')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('allocation phase visibility', () => {
|
describe('allocation phase visibility', () => {
|
||||||
it('does not override paused or completed statuses', () => {
|
it('does not override paused or completed statuses', () => {
|
||||||
expect(isAllocationPhaseVisible(true, 'ready')).toBe(true);
|
expect(isAllocationPhaseVisible(true, 'ready')).toBe(true);
|
||||||
|
|||||||
@@ -640,6 +640,37 @@ const NON_CREDENTIAL_REQUEST_HEADERS = new Set([
|
|||||||
'via',
|
'via',
|
||||||
'warning',
|
'warning',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const CREDENTIAL_HEADER_NAMES = new Set([
|
||||||
|
'authorization',
|
||||||
|
'cookie',
|
||||||
|
'cookie2',
|
||||||
|
'proxy-authorization',
|
||||||
|
'set-cookie',
|
||||||
|
'set-cookie2',
|
||||||
|
'x-api-key',
|
||||||
|
'x-auth-token',
|
||||||
|
'x-access-token',
|
||||||
|
]);
|
||||||
|
const CREDENTIAL_HEADER_MARKERS = [
|
||||||
|
'auth',
|
||||||
|
'credential',
|
||||||
|
'key',
|
||||||
|
'password',
|
||||||
|
'passwd',
|
||||||
|
'secret',
|
||||||
|
'session',
|
||||||
|
'signature',
|
||||||
|
'token',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
/** Header-name classifier shared by extension handoff defenses. */
|
||||||
|
export const headerNameHasCredentialMaterial = (rawName: string): boolean => {
|
||||||
|
const name = rawName.trim().toLowerCase();
|
||||||
|
return name.length === 0
|
||||||
|
|| CREDENTIAL_HEADER_NAMES.has(name)
|
||||||
|
|| CREDENTIAL_HEADER_MARKERS.some(marker => name.includes(marker));
|
||||||
|
};
|
||||||
// Only stable request context is safe to carry into a later lifecycle. Range,
|
// Only stable request context is safe to carry into a later lifecycle. Range,
|
||||||
// conditional, hop-by-hop, and routing headers describe the old HTTP request
|
// conditional, hop-by-hop, and routing headers describe the old HTTP request
|
||||||
// and can conflict with Aria2's own resume negotiation.
|
// and can conflict with Aria2's own resume negotiation.
|
||||||
|
|||||||
@@ -46,6 +46,25 @@ describe('log entry streaming', () => {
|
|||||||
expect(liveLogEntry(3, 'Authorization: Bearer secret').message).not.toContain('secret');
|
expect(liveLogEntry(3, 'Authorization: Bearer secret').message).not.toContain('secret');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('redacts custom session and signature headers from live output', () => {
|
||||||
|
const redacted = redactLogText('X-Request-Signature: signature-secret X-Session: session-secret');
|
||||||
|
|
||||||
|
expect(redacted).not.toContain('signature-secret');
|
||||||
|
expect(redacted).not.toContain('session-secret');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('redacts legacy cookie headers and compound custom values', () => {
|
||||||
|
const redacted = redactLogText('Set-Cookie2: legacy-cookie X-Session: id=session-secret; key=compound-secret');
|
||||||
|
|
||||||
|
expect(redacted).not.toContain('legacy-cookie');
|
||||||
|
expect(redacted).not.toContain('session-secret');
|
||||||
|
expect(redacted).not.toContain('compound-secret');
|
||||||
|
|
||||||
|
const equalsRedacted = redactLogText('Cookie2=a=1; user_id=secret; state=xyz');
|
||||||
|
expect(equalsRedacted).not.toContain('user_id=secret');
|
||||||
|
expect(equalsRedacted).not.toContain('state=xyz');
|
||||||
|
});
|
||||||
|
|
||||||
it('redacts persisted content and quoted credential fields', () => {
|
it('redacts persisted content and quoted credential fields', () => {
|
||||||
const persisted = persistedLogEntry('{"api_key":"json-secret","path":"/Users/nima/file"}', '/Users/nima');
|
const persisted = persistedLogEntry('{"api_key":"json-secret","path":"/Users/nima/file"}', '/Users/nima');
|
||||||
|
|
||||||
|
|||||||
@@ -49,11 +49,15 @@ export const redactLogText = (message: string, homePath = ''): string => {
|
|||||||
'$1[redacted]@'
|
'$1[redacted]@'
|
||||||
);
|
);
|
||||||
redacted = redacted.replace(
|
redacted = redacted.replace(
|
||||||
/(["'])(authorization|proxy-authorization|cookie|set-cookie|password|token|secret|credential|pairing[-_ ]?token|api[-_ ]?key)(["'])(\s*[:=]\s*)["'][^"\r\n,;]*["']/gi,
|
/([A-Za-z0-9_-]*(?: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)[A-Za-z0-9_-]*)\s*[:=]\s*[^\r\n]*?(;?)(?=\s+(?:[A-Za-z][A-Za-z0-9+.-]*:\/\/|[A-Za-z0-9_-]*(?: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*[:=])|$)/gi,
|
||||||
|
'$1: [redacted]$2'
|
||||||
|
);
|
||||||
|
redacted = redacted.replace(
|
||||||
|
/(["'])(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,;]*["']/gi,
|
||||||
'$1$2$3$4[redacted]'
|
'$1$2$3$4[redacted]'
|
||||||
);
|
);
|
||||||
return redacted.replace(
|
return redacted.replace(
|
||||||
/(authorization|proxy-authorization|cookie|set-cookie|password|token|secret|credential|pairing[-_ ]?token|api[-_ ]?key)(\s*)([:=])(\s*)([^\r\n,;]+)/gi,
|
/(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,;]+)/gi,
|
||||||
'$1$2$3$4[redacted]'
|
'$1$2$3$4[redacted]'
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user