Skip to content

Commit a5cfbba

Browse files
feat/url-query-redaction-hardening
Feat/url query redaction hardening
2 parents e406e65 + c6a2eb2 commit a5cfbba

249 files changed

Lines changed: 275 additions & 17800 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

dist/core/error.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ export type OpenFetchErrorToShapeOptions = {
3030
redactSensitiveUrlQuery?: boolean;
3131
/** Extra query parameter names to redact (case-insensitive); merged with the built-in list. */
3232
sensitiveQueryParamNames?: string[];
33+
/** Replacement string for redacted query values (default `"[REDACTED]"`). */
34+
sensitiveQueryParamReplacement?: string;
3335
};
3436
export declare class OpenFetchError<T = unknown> extends Error {
3537
config?: OpenFetchConfig;

dist/core/error.d.ts.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/core/error.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export class OpenFetchError extends Error {
3939
const redactOpts = {
4040
enabled: options?.redactSensitiveUrlQuery !== false,
4141
paramNames: options?.sensitiveQueryParamNames,
42+
replacement: options?.sensitiveQueryParamReplacement,
4243
};
4344
url = redactSensitiveUrlQuery(url, redactOpts);
4445
const method = (this.config?.method ?? "GET").toUpperCase();

dist/helpers/redactUrlQuery.d.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,13 @@ export type RedactUrlQueryOptions = {
55
paramNames?: string[];
66
/** When false, returns `url` unchanged. Default true. */
77
enabled?: boolean;
8+
/** Value substituted for sensitive query parameters (default `"[REDACTED]"`). */
9+
replacement?: string;
810
};
911
/**
1012
* Replaces values of sensitive query parameters for safe logging or serialization.
11-
* Invalid or non-absolute URLs are returned unchanged.
13+
* Supports absolute URLs and common relative forms (`/path?…`, `?only=query`, `api/x?…`).
14+
* Strings that are not valid URLs and do not look like path/query requests are returned unchanged.
1215
*/
1316
export declare function redactSensitiveUrlQuery(url: string, options?: RedactUrlQueryOptions): string;
1417
//# sourceMappingURL=redactUrlQuery.d.ts.map

dist/helpers/redactUrlQuery.d.ts.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/helpers/redactUrlQuery.js

Lines changed: 96 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -18,40 +18,112 @@ export const DEFAULT_SENSITIVE_QUERY_PARAM_NAMES = [
1818
"csrf",
1919
"nonce",
2020
];
21+
const DEFAULT_REDACTION = "[REDACTED]";
2122
const 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
*/
2694
export 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
}

dist/plugins/debug.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ export type DebugPluginOptions = {
3636
maskUrlQuery?: boolean;
3737
/** Extra query parameter names to redact in the logged URL (case-insensitive). */
3838
sensitiveQueryParamNames?: string[];
39+
/** Replacement string for redacted query values in the logged URL (default `"[REDACTED]"`). */
40+
sensitiveQueryParamReplacement?: string;
3941
};
4042
/**
4143
* Development-oriented logging middleware. Omit from production bundles if unused.

dist/plugins/debug.d.ts.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/plugins/debug.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export function debug(options = {}) {
2828
const includeReqH = options.includeRequestHeaders === true;
2929
const maskUrlQ = options.maskUrlQuery !== false;
3030
const sensitiveQueryParams = options.sensitiveQueryParamNames;
31+
const sensitiveQueryReplacement = options.sensitiveQueryParamReplacement;
3132
const log = options.log ??
3233
((phase, p) => {
3334
if (typeof console !== "undefined" && console.debug) {
@@ -46,6 +47,7 @@ export function debug(options = {}) {
4647
? redactSensitiveUrlQuery(rawUrl, {
4748
enabled: true,
4849
paramNames: sensitiveQueryParams,
50+
replacement: sensitiveQueryReplacement,
4951
})
5052
: rawUrl;
5153
const reqPayload = { method, url };

frontEnd/openfetch/.gitignore

Lines changed: 0 additions & 24 deletions
This file was deleted.

0 commit comments

Comments
 (0)