@@ -18,40 +18,112 @@ export const DEFAULT_SENSITIVE_QUERY_PARAM_NAMES = [
1818 "csrf" ,
1919 "nonce" ,
2020] ;
21+ const DEFAULT_REDACTION = "[REDACTED]" ;
2122const DEFAULT_SET = new Set ( DEFAULT_SENSITIVE_QUERY_PARAM_NAMES . map ( ( n ) => n . toLowerCase ( ) ) ) ;
23+ function normalizeKey ( k ) {
24+ return k . toLowerCase ( ) . replace ( / [ - _ ] / g, "" ) ;
25+ }
26+ function buildPartialFragments ( sensitive ) {
27+ const out = [ ] ;
28+ for ( const s of sensitive ) {
29+ const n = normalizeKey ( s ) ;
30+ if ( n !== "" )
31+ out . push ( n ) ;
32+ }
33+ return out ;
34+ }
35+ const DEFAULT_PARTIAL_FRAGMENTS = buildPartialFragments ( DEFAULT_SET ) ;
36+ /**
37+ * Path- or query-shaped strings that are safe to resolve with a dummy base.
38+ * Avoids turning arbitrary tokens like `not-a-url` into `http://localhost/not-a-url`.
39+ */
40+ function mayBeRelativeRequestUrl ( url ) {
41+ return ( url . startsWith ( "/" ) ||
42+ url . startsWith ( "./" ) ||
43+ url . startsWith ( "../" ) ||
44+ url . startsWith ( "?" ) ||
45+ url . includes ( "/" ) ||
46+ url . includes ( "?" ) ) ;
47+ }
48+ function parseUrlForRedaction ( url ) {
49+ try {
50+ const u = new URL ( url ) ;
51+ return { u, relativeInput : false , queryOnlyInput : false } ;
52+ }
53+ catch {
54+ if ( ! mayBeRelativeRequestUrl ( url ) )
55+ return null ;
56+ try {
57+ const u = new URL ( url , "http://localhost" ) ;
58+ return {
59+ u,
60+ relativeInput : true ,
61+ queryOnlyInput : url . startsWith ( "?" ) ,
62+ } ;
63+ }
64+ catch {
65+ return null ;
66+ }
67+ }
68+ }
69+ function isSensitiveName ( nameLower , sensitive , partialFragments ) {
70+ if ( sensitive . has ( nameLower ) )
71+ return true ;
72+ const kn = normalizeKey ( nameLower ) ;
73+ if ( kn === "" )
74+ return false ;
75+ for ( const frag of partialFragments ) {
76+ if ( frag !== "" && kn . includes ( frag ) )
77+ return true ;
78+ }
79+ return false ;
80+ }
81+ function serializeAfterRedaction ( parsed ) {
82+ const { u, relativeInput, queryOnlyInput } = parsed ;
83+ if ( ! relativeInput )
84+ return u . toString ( ) ;
85+ if ( queryOnlyInput )
86+ return `${ u . search } ${ u . hash } ` ;
87+ return `${ u . pathname } ${ u . search } ${ u . hash } ` ;
88+ }
2289/**
2390 * Replaces values of sensitive query parameters for safe logging or serialization.
24- * Invalid or non-absolute URLs are returned unchanged.
91+ * Supports absolute URLs and common relative forms (`/path?…`, `?only=query`, `api/x?…`).
92+ * Strings that are not valid URLs and do not look like path/query requests are returned unchanged.
2593 */
2694export function redactSensitiveUrlQuery ( url , options ) {
2795 if ( options ?. enabled === false || url === "" )
2896 return url ;
2997 const extra = options ?. paramNames ?? [ ] ;
3098 const sensitive = extra . length === 0
3199 ? DEFAULT_SET
32- : new Set ( [
33- ...DEFAULT_SET ,
34- ...extra . map ( ( n ) => n . toLowerCase ( ) ) ,
35- ] ) ;
36- try {
37- const u = new URL ( url ) ;
38- if ( u . search === "" )
39- return url ;
40- const params = u . searchParams ;
41- let changed = false ;
42- const names = new Set ( ) ;
43- params . forEach ( ( _v , name ) => {
44- names . add ( name ) ;
45- } ) ;
46- for ( const name of names ) {
47- if ( sensitive . has ( name . toLowerCase ( ) ) ) {
48- params . set ( name , "[REDACTED]" ) ;
49- changed = true ;
50- }
51- }
52- return changed ? u . toString ( ) : url ;
53- }
54- catch {
100+ : new Set ( [ ...DEFAULT_SET , ...extra . map ( ( n ) => n . toLowerCase ( ) ) ] ) ;
101+ const partialFragments = extra . length === 0
102+ ? DEFAULT_PARTIAL_FRAGMENTS
103+ : buildPartialFragments ( sensitive ) ;
104+ const replacement = options ?. replacement ?? DEFAULT_REDACTION ;
105+ const parsed = parseUrlForRedaction ( url ) ;
106+ if ( parsed === null )
107+ return url ;
108+ const { u } = parsed ;
109+ if ( u . search === "" )
55110 return url ;
111+ const params = u . searchParams ;
112+ let changed = false ;
113+ const names = new Set ( ) ;
114+ params . forEach ( ( _v , name ) => {
115+ names . add ( name ) ;
116+ } ) ;
117+ for ( const name of names ) {
118+ const lower = name . toLowerCase ( ) ;
119+ if ( ! isSensitiveName ( lower , sensitive , partialFragments ) )
120+ continue ;
121+ const n = params . getAll ( name ) . length ;
122+ params . delete ( name ) ;
123+ for ( let i = 0 ; i < n ; i ++ ) {
124+ params . append ( name , replacement ) ;
125+ }
126+ changed = true ;
56127 }
128+ return changed ? serializeAfterRedaction ( parsed ) : url ;
57129}
0 commit comments