@patchstack/connect 0.5.15 → 0.5.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENT-INSTALL.md +37 -11
- package/dist/cli.js +654 -174
- package/dist/cli.js.map +1 -1
- package/dist/protect/templates/demo-rules.json +2 -2
- package/dist/protect.cjs +1384 -747
- package/dist/protect.cjs.map +1 -1
- package/dist/protect.d.cts +110 -16
- package/dist/protect.d.ts +110 -16
- package/dist/protect.edge.js +1372 -735
- package/dist/protect.edge.js.map +3 -3
- package/dist/protect.js +1372 -735
- package/dist/protect.js.map +1 -1
- package/package.json +1 -1
package/dist/protect.cjs
CHANGED
|
@@ -2033,7 +2033,7 @@ async function scanLockfile(cwd) {
|
|
|
2033
2033
|
await detectLockfile(cwd);
|
|
2034
2034
|
}
|
|
2035
2035
|
const declared = await readDeclaredDependencyNames(cwd);
|
|
2036
|
-
const
|
|
2036
|
+
const warnings2 = [];
|
|
2037
2037
|
let firstParsed = null;
|
|
2038
2038
|
let walked = null;
|
|
2039
2039
|
let walkWasCandidate = false;
|
|
@@ -2047,7 +2047,7 @@ async function scanLockfile(cwd) {
|
|
|
2047
2047
|
continue;
|
|
2048
2048
|
}
|
|
2049
2049
|
const unreadable = unreadableWarning(candidate.filename, report);
|
|
2050
|
-
if (unreadable !== null)
|
|
2050
|
+
if (unreadable !== null) warnings2.push(unreadable);
|
|
2051
2051
|
if (candidate.strategy === "node-modules-walk") {
|
|
2052
2052
|
walked = packages;
|
|
2053
2053
|
walkWasCandidate = true;
|
|
@@ -2058,16 +2058,16 @@ async function scanLockfile(cwd) {
|
|
|
2058
2058
|
for (const source of parsedSources) {
|
|
2059
2059
|
const missing = missingDependencies(declared, source.packages);
|
|
2060
2060
|
if (missing.length > 0) {
|
|
2061
|
-
|
|
2061
|
+
warnings2.push(staleWarning(source.filename, missing));
|
|
2062
2062
|
continue;
|
|
2063
2063
|
}
|
|
2064
2064
|
const conflicts = otherCompleteSources(parsedSources, source, declared).flatMap(
|
|
2065
2065
|
(other) => disagreements(source.packages, other.packages).map((conflict) => ({ other: other.filename, conflict }))
|
|
2066
2066
|
);
|
|
2067
2067
|
if (conflicts.length === 0) {
|
|
2068
|
-
return manifestWith(source.packages,
|
|
2068
|
+
return manifestWith(source.packages, warnings2);
|
|
2069
2069
|
}
|
|
2070
|
-
|
|
2070
|
+
warnings2.push(conflictWarning(source.filename, conflicts));
|
|
2071
2071
|
break;
|
|
2072
2072
|
}
|
|
2073
2073
|
if (walked === null) {
|
|
@@ -2080,12 +2080,12 @@ async function scanLockfile(cwd) {
|
|
|
2080
2080
|
if (walked !== null) {
|
|
2081
2081
|
const missing = missingDependencies(declared, walked);
|
|
2082
2082
|
if (missing.length > 0) {
|
|
2083
|
-
|
|
2083
|
+
warnings2.push(staleWarning("node_modules/", missing));
|
|
2084
2084
|
}
|
|
2085
|
-
|
|
2085
|
+
warnings2.push(
|
|
2086
2086
|
walkWasCandidate ? "Checked the installed packages in node_modules/, which is what the build loads." : "Checked the installed packages in node_modules/ instead. Delete the out-of-date package list file to stop this warning."
|
|
2087
2087
|
);
|
|
2088
|
-
return manifestWith(walked,
|
|
2088
|
+
return manifestWith(walked, warnings2);
|
|
2089
2089
|
}
|
|
2090
2090
|
if (firstParsed === null) {
|
|
2091
2091
|
const expected = LOCKFILE_CANDIDATES.map((candidate) => candidate.filename).join(", ");
|
|
@@ -2094,13 +2094,13 @@ async function scanLockfile(cwd) {
|
|
|
2094
2094
|
"LOCKFILE_NOT_FOUND"
|
|
2095
2095
|
);
|
|
2096
2096
|
}
|
|
2097
|
-
|
|
2097
|
+
warnings2.push(
|
|
2098
2098
|
`No package list matches package.json; checked ${firstParsed.filename}, so some packages may be missing. Reinstall your packages to fix this.`
|
|
2099
2099
|
);
|
|
2100
|
-
return manifestWith(firstParsed.packages,
|
|
2100
|
+
return manifestWith(firstParsed.packages, warnings2);
|
|
2101
2101
|
}
|
|
2102
|
-
function manifestWith(packages,
|
|
2103
|
-
return
|
|
2102
|
+
function manifestWith(packages, warnings2) {
|
|
2103
|
+
return warnings2.length > 0 ? { ecosystem: "npm", packages, warnings: warnings2 } : { ecosystem: "npm", packages };
|
|
2104
2104
|
}
|
|
2105
2105
|
function otherCompleteSources(all, current, declared) {
|
|
2106
2106
|
return all.filter(
|
|
@@ -2725,18 +2725,124 @@ function appendOwn(target, key, value) {
|
|
|
2725
2725
|
return setOwn(target, key, Array.isArray(previous) ? [...previous, value] : [previous, value]);
|
|
2726
2726
|
}
|
|
2727
2727
|
|
|
2728
|
-
// src/protect/engine/
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
const
|
|
2734
|
-
|
|
2735
|
-
|
|
2728
|
+
// src/protect/engine/cookies.js
|
|
2729
|
+
function parseCookieHeader(header) {
|
|
2730
|
+
const cookies = {};
|
|
2731
|
+
if (typeof header !== "string" || header === "") return cookies;
|
|
2732
|
+
const repeated = /* @__PURE__ */ new Map();
|
|
2733
|
+
for (const pair of header.split(";")) {
|
|
2734
|
+
const eq = pair.indexOf("=");
|
|
2735
|
+
if (eq === -1) continue;
|
|
2736
|
+
const name = pair.slice(0, eq).trim();
|
|
2737
|
+
if (name === "") continue;
|
|
2738
|
+
let value = pair.slice(eq + 1).trim();
|
|
2739
|
+
if (value.length >= 2 && value[0] === '"' && value[value.length - 1] === '"') value = value.slice(1, -1);
|
|
2740
|
+
if (!Object.hasOwn(cookies, name)) {
|
|
2741
|
+
setOwn(cookies, name, value);
|
|
2742
|
+
} else {
|
|
2743
|
+
let values = repeated.get(name);
|
|
2744
|
+
if (values === void 0) {
|
|
2745
|
+
values = [cookies[name]];
|
|
2746
|
+
repeated.set(name, values);
|
|
2747
|
+
setOwn(cookies, name, values);
|
|
2748
|
+
}
|
|
2749
|
+
values.push(value);
|
|
2750
|
+
}
|
|
2736
2751
|
}
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2752
|
+
return cookies;
|
|
2753
|
+
}
|
|
2754
|
+
|
|
2755
|
+
// src/protect/engine/normalizer.js
|
|
2756
|
+
var HTML_ENTITIES = {
|
|
2757
|
+
"&": "&",
|
|
2758
|
+
"<": "<",
|
|
2759
|
+
">": ">",
|
|
2760
|
+
""": '"',
|
|
2761
|
+
"'": "'",
|
|
2762
|
+
"'": "'",
|
|
2763
|
+
" ": " ",
|
|
2764
|
+
"'": "'",
|
|
2765
|
+
""": '"',
|
|
2766
|
+
"<": "<",
|
|
2767
|
+
">": ">",
|
|
2768
|
+
"&": "&",
|
|
2769
|
+
""": '"',
|
|
2770
|
+
"<": "<",
|
|
2771
|
+
">": ">",
|
|
2772
|
+
"&": "&"
|
|
2773
|
+
};
|
|
2774
|
+
var MAX_DECODE_ITERATIONS = 5;
|
|
2775
|
+
function normalize(value, options = {}) {
|
|
2776
|
+
if (typeof value !== "string") {
|
|
2777
|
+
return value;
|
|
2778
|
+
}
|
|
2779
|
+
const opts = {
|
|
2780
|
+
urlDecode: true,
|
|
2781
|
+
htmlDecode: true,
|
|
2782
|
+
sqlComments: true,
|
|
2783
|
+
nullBytes: true,
|
|
2784
|
+
whitespace: true,
|
|
2785
|
+
...options
|
|
2786
|
+
};
|
|
2787
|
+
let result = value;
|
|
2788
|
+
if (opts.nullBytes) {
|
|
2789
|
+
result = removeNullBytes(result);
|
|
2790
|
+
}
|
|
2791
|
+
if (opts.urlDecode) {
|
|
2792
|
+
result = urlDecode(result);
|
|
2793
|
+
}
|
|
2794
|
+
if (opts.htmlDecode) {
|
|
2795
|
+
result = htmlEntityDecode(result);
|
|
2796
|
+
}
|
|
2797
|
+
if (opts.sqlComments) {
|
|
2798
|
+
result = removeSqlComments(result);
|
|
2799
|
+
}
|
|
2800
|
+
if (opts.whitespace) {
|
|
2801
|
+
result = normalizeWhitespace(result);
|
|
2802
|
+
}
|
|
2803
|
+
return result;
|
|
2804
|
+
}
|
|
2805
|
+
function urlDecode(value) {
|
|
2806
|
+
if (typeof value !== "string") {
|
|
2807
|
+
return value;
|
|
2808
|
+
}
|
|
2809
|
+
let result = value;
|
|
2810
|
+
let previous = "";
|
|
2811
|
+
let iterations = 0;
|
|
2812
|
+
while (result !== previous && iterations < MAX_DECODE_ITERATIONS) {
|
|
2813
|
+
previous = result;
|
|
2814
|
+
iterations++;
|
|
2815
|
+
result = safeUrlDecode(result);
|
|
2816
|
+
}
|
|
2817
|
+
return result;
|
|
2818
|
+
}
|
|
2819
|
+
var utf8 = new TextDecoder();
|
|
2820
|
+
function safeUrlDecode(value) {
|
|
2821
|
+
return value.replace(/(?:%[0-9A-Fa-f]{2})+/g, (run) => {
|
|
2822
|
+
try {
|
|
2823
|
+
return decodeURIComponent(run);
|
|
2824
|
+
} catch {
|
|
2825
|
+
const bytes = new Uint8Array(run.length / 3);
|
|
2826
|
+
for (let i = 0; i < bytes.length; i++) bytes[i] = parseInt(run.slice(i * 3 + 1, i * 3 + 3), 16);
|
|
2827
|
+
return utf8.decode(bytes);
|
|
2828
|
+
}
|
|
2829
|
+
});
|
|
2830
|
+
}
|
|
2831
|
+
function decodeQueryPlus(target) {
|
|
2832
|
+
if (typeof target !== "string") return target;
|
|
2833
|
+
const query = target.indexOf("?");
|
|
2834
|
+
if (query === -1) return target;
|
|
2835
|
+
return target.slice(0, query + 1) + target.slice(query + 1).replace(/\+/g, " ");
|
|
2836
|
+
}
|
|
2837
|
+
function htmlEntityDecode(value) {
|
|
2838
|
+
if (typeof value !== "string") {
|
|
2839
|
+
return value;
|
|
2840
|
+
}
|
|
2841
|
+
let result = value;
|
|
2842
|
+
for (const [entity, char] of Object.entries(HTML_ENTITIES)) {
|
|
2843
|
+
result = result.split(entity).join(char);
|
|
2844
|
+
}
|
|
2845
|
+
return decodeHtmlEntities(result);
|
|
2740
2846
|
}
|
|
2741
2847
|
var NAMED_ENTITIES = {
|
|
2742
2848
|
lt: "<",
|
|
@@ -2744,7 +2850,6 @@ var NAMED_ENTITIES = {
|
|
|
2744
2850
|
amp: "&",
|
|
2745
2851
|
quot: '"',
|
|
2746
2852
|
apos: "'",
|
|
2747
|
-
"#39": "'",
|
|
2748
2853
|
nbsp: "\xA0",
|
|
2749
2854
|
sol: "/",
|
|
2750
2855
|
bsol: "\\",
|
|
@@ -2763,7 +2868,7 @@ var NAMED_ENTITIES = {
|
|
|
2763
2868
|
ast: "*"
|
|
2764
2869
|
};
|
|
2765
2870
|
function decodeHtmlEntities(input) {
|
|
2766
|
-
return input.replace(/&(#[xX]
|
|
2871
|
+
return input.replace(/&(#[xX][0-9a-fA-F]+|#\d+|[A-Za-z][A-Za-z0-9]*);?/g, (whole, body) => {
|
|
2767
2872
|
if (body[0] === "#") {
|
|
2768
2873
|
const hex = body[1] === "x" || body[1] === "X";
|
|
2769
2874
|
const code = Number.parseInt(hex ? body.slice(2) : body.slice(1), hex ? 16 : 10);
|
|
@@ -2777,74 +2882,359 @@ function decodeHtmlEntities(input) {
|
|
|
2777
2882
|
return Object.prototype.hasOwnProperty.call(NAMED_ENTITIES, body) ? NAMED_ENTITIES[body] : whole;
|
|
2778
2883
|
});
|
|
2779
2884
|
}
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
constructor(req) {
|
|
2784
|
-
this.#req = req;
|
|
2785
|
-
this.#cookies = null;
|
|
2885
|
+
function removeSqlComments(value) {
|
|
2886
|
+
if (typeof value !== "string") {
|
|
2887
|
+
return value;
|
|
2786
2888
|
}
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2889
|
+
let result = value;
|
|
2890
|
+
result = result.replace(/\/\*[\s\S]*?\*\//g, " ");
|
|
2891
|
+
result = result.replace(/\/\*![\s\S]*?\*\//g, " ");
|
|
2892
|
+
return result;
|
|
2893
|
+
}
|
|
2894
|
+
function removeNullBytes(value) {
|
|
2895
|
+
if (typeof value !== "string") {
|
|
2896
|
+
return value;
|
|
2897
|
+
}
|
|
2898
|
+
let result = value.replace(/\x00/g, "");
|
|
2899
|
+
result = result.replace(/[\x01-\x08\x0B\x0C\x0E-\x1F]/g, "");
|
|
2900
|
+
return result;
|
|
2901
|
+
}
|
|
2902
|
+
function normalizeWhitespace(value) {
|
|
2903
|
+
if (typeof value !== "string") {
|
|
2904
|
+
return value;
|
|
2905
|
+
}
|
|
2906
|
+
let result = value.replace(/[\t\r\n\f\v]+/g, " ");
|
|
2907
|
+
result = result.replace(/ {2,}/g, " ");
|
|
2908
|
+
return result;
|
|
2909
|
+
}
|
|
2910
|
+
var MAX_RAW_CHARS = 1024 * 1024;
|
|
2911
|
+
var MAX_RAW_NODES = 25e3;
|
|
2912
|
+
var MAX_RAW_DEPTH = 200;
|
|
2913
|
+
var MAX_RAW_ENTRIES = 2e3;
|
|
2914
|
+
function serializeForRawDetection(body) {
|
|
2915
|
+
if (body === null || body === void 0) return "";
|
|
2916
|
+
if (typeof body === "string") return body.slice(0, MAX_RAW_CHARS);
|
|
2917
|
+
const visited = /* @__PURE__ */ new Set();
|
|
2918
|
+
const output = [];
|
|
2919
|
+
const stack = [{ kind: "value", value: body, depth: 0, root: true }];
|
|
2920
|
+
let chars = 0;
|
|
2921
|
+
let nodes = 0;
|
|
2922
|
+
const append = (text) => {
|
|
2923
|
+
if (chars >= MAX_RAW_CHARS) return;
|
|
2924
|
+
const piece = String(text).slice(0, MAX_RAW_CHARS - chars);
|
|
2925
|
+
output.push(piece);
|
|
2926
|
+
chars += piece.length;
|
|
2927
|
+
};
|
|
2928
|
+
while (stack.length > 0 && chars < MAX_RAW_CHARS) {
|
|
2929
|
+
const task = stack.pop();
|
|
2930
|
+
if (task.kind === "text") {
|
|
2931
|
+
append(task.text);
|
|
2932
|
+
continue;
|
|
2790
2933
|
}
|
|
2791
|
-
if (
|
|
2792
|
-
|
|
2934
|
+
if (task.kind === "key") {
|
|
2935
|
+
append(boundedJsonString(task.value));
|
|
2936
|
+
append(":");
|
|
2937
|
+
continue;
|
|
2793
2938
|
}
|
|
2794
|
-
|
|
2795
|
-
|
|
2939
|
+
nodes++;
|
|
2940
|
+
if (nodes > MAX_RAW_NODES) {
|
|
2941
|
+
append('"[NodeLimit]"');
|
|
2942
|
+
continue;
|
|
2796
2943
|
}
|
|
2797
|
-
|
|
2798
|
-
|
|
2944
|
+
const value = task.value;
|
|
2945
|
+
if (value === null || value === void 0) {
|
|
2946
|
+
append(task.root ? "" : "null");
|
|
2947
|
+
continue;
|
|
2799
2948
|
}
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2949
|
+
if (typeof value === "string") {
|
|
2950
|
+
append(task.root ? value.slice(0, MAX_RAW_CHARS) : boundedJsonString(value));
|
|
2951
|
+
continue;
|
|
2803
2952
|
}
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
case "get":
|
|
2808
|
-
return this.#resolveGet(key);
|
|
2809
|
-
case "post":
|
|
2810
|
-
return this.#resolvePost(key);
|
|
2811
|
-
case "request":
|
|
2812
|
-
return this.#resolveRequest(key);
|
|
2813
|
-
case "cookie":
|
|
2814
|
-
return this.#resolveCookie(key);
|
|
2815
|
-
case "server":
|
|
2816
|
-
return this.#resolveServer(key);
|
|
2817
|
-
case "files":
|
|
2818
|
-
return this.#resolveFiles(key);
|
|
2819
|
-
case "response":
|
|
2820
|
-
return this.#resolveResponse(key);
|
|
2821
|
-
case "egress":
|
|
2822
|
-
return this.#resolveEgress(key);
|
|
2823
|
-
default:
|
|
2824
|
-
return [];
|
|
2953
|
+
if (typeof value !== "object") {
|
|
2954
|
+
append(String(value).slice(0, MAX_RAW_CHARS));
|
|
2955
|
+
continue;
|
|
2825
2956
|
}
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
#resolveResponse(key) {
|
|
2830
|
-
const resp = this.#req._response;
|
|
2831
|
-
if (!resp) {
|
|
2832
|
-
return [];
|
|
2957
|
+
if (visited.has(value)) {
|
|
2958
|
+
append("[Circular]");
|
|
2959
|
+
continue;
|
|
2833
2960
|
}
|
|
2834
|
-
if (
|
|
2835
|
-
|
|
2961
|
+
if (task.depth >= MAX_RAW_DEPTH) {
|
|
2962
|
+
append('"[DepthLimit]"');
|
|
2963
|
+
continue;
|
|
2836
2964
|
}
|
|
2837
|
-
|
|
2838
|
-
|
|
2965
|
+
visited.add(value);
|
|
2966
|
+
if (Array.isArray(value)) {
|
|
2967
|
+
const length2 = Math.min(value.length, MAX_RAW_ENTRIES);
|
|
2968
|
+
const items = [];
|
|
2969
|
+
for (let i = 0; i < length2; i++) {
|
|
2970
|
+
if (i > 0) items.push({ kind: "text", text: "," });
|
|
2971
|
+
items.push({ kind: "value", value: readOwnValue(value, String(i)), depth: task.depth + 1, root: false });
|
|
2972
|
+
}
|
|
2973
|
+
if (value.length > length2) {
|
|
2974
|
+
if (length2 > 0) items.push({ kind: "text", text: "," });
|
|
2975
|
+
items.push({ kind: "text", text: '"[EntryLimit]"' });
|
|
2976
|
+
}
|
|
2977
|
+
stack.push({ kind: "text", text: "]" });
|
|
2978
|
+
for (let i = items.length - 1; i >= 0; i--) stack.push(items[i]);
|
|
2979
|
+
stack.push({ kind: "text", text: "[" });
|
|
2980
|
+
continue;
|
|
2839
2981
|
}
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2982
|
+
let keys;
|
|
2983
|
+
try {
|
|
2984
|
+
keys = Object.getOwnPropertyNames(value);
|
|
2985
|
+
} catch {
|
|
2986
|
+
append('"[Unreadable]"');
|
|
2987
|
+
continue;
|
|
2843
2988
|
}
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2989
|
+
const length = Math.min(keys.length, MAX_RAW_ENTRIES);
|
|
2990
|
+
const entries = [];
|
|
2991
|
+
for (let i = 0; i < length; i++) {
|
|
2992
|
+
const key = keys[i];
|
|
2993
|
+
if (i > 0) entries.push({ kind: "text", text: "," });
|
|
2994
|
+
entries.push({ kind: "key", value: key });
|
|
2995
|
+
entries.push({ kind: "value", value: readOwnValue(value, key), depth: task.depth + 1, root: false });
|
|
2996
|
+
}
|
|
2997
|
+
if (keys.length > length) {
|
|
2998
|
+
if (length > 0) entries.push({ kind: "text", text: "," });
|
|
2999
|
+
entries.push({ kind: "text", text: '"[EntryLimit]":null' });
|
|
3000
|
+
}
|
|
3001
|
+
stack.push({ kind: "text", text: "}" });
|
|
3002
|
+
for (let i = entries.length - 1; i >= 0; i--) stack.push(entries[i]);
|
|
3003
|
+
stack.push({ kind: "text", text: "{" });
|
|
3004
|
+
}
|
|
3005
|
+
return output.join("");
|
|
3006
|
+
}
|
|
3007
|
+
function boundedJsonString(value) {
|
|
3008
|
+
return JSON.stringify(String(value).slice(0, MAX_RAW_CHARS));
|
|
3009
|
+
}
|
|
3010
|
+
function readOwnValue(value, key) {
|
|
3011
|
+
try {
|
|
3012
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
3013
|
+
return descriptor && Object.hasOwn(descriptor, "value") ? descriptor.value : "[Accessor]";
|
|
3014
|
+
} catch {
|
|
3015
|
+
return "[Unreadable]";
|
|
3016
|
+
}
|
|
3017
|
+
}
|
|
3018
|
+
var INHERITED_ACCESSORS = /* @__PURE__ */ new Set(["headers", "query"]);
|
|
3019
|
+
function requestField(req, key) {
|
|
3020
|
+
if (req === null || typeof req !== "object") return void 0;
|
|
3021
|
+
if (Object.hasOwn(req, key)) return req[key];
|
|
3022
|
+
if (!INHERITED_ACCESSORS.has(key)) return void 0;
|
|
3023
|
+
let holder = Object.getPrototypeOf(req);
|
|
3024
|
+
while (holder !== null && !Object.hasOwn(holder, key)) holder = Object.getPrototypeOf(holder);
|
|
3025
|
+
if (holder === null || holder === Object.prototype) return void 0;
|
|
3026
|
+
const descriptor = Object.getOwnPropertyDescriptor(holder, key);
|
|
3027
|
+
if (typeof descriptor?.get !== "function") return void 0;
|
|
3028
|
+
return req[key];
|
|
3029
|
+
}
|
|
3030
|
+
function normalizeRequest(req, options = {}) {
|
|
3031
|
+
const ownRaw = Object.hasOwn(req ?? {}, "_rawBody") && typeof req._rawBody === "string";
|
|
3032
|
+
const body = requestField(req, "body");
|
|
3033
|
+
const url = requestField(req, "url");
|
|
3034
|
+
const rawBody = ownRaw ? req._rawBody : serializeForRawDetection(body ?? null);
|
|
3035
|
+
const headers = requestField(req, "headers") || {};
|
|
3036
|
+
return {
|
|
3037
|
+
query: normalizeObject(requestField(req, "query") || {}, options),
|
|
3038
|
+
body: normalizeObject(body || {}, options),
|
|
3039
|
+
headers: normalizeObject(headers, options),
|
|
3040
|
+
// Cookies are split into name/value pairs BEFORE their values are normalised, and normalised the
|
|
3041
|
+
// same way whether a framework parsed them or they come from the header — so a rule on a cookie
|
|
3042
|
+
// sees one value, independent of which cookie parser (if any) ran first.
|
|
3043
|
+
cookies: normalizeObject(cookieValues(requestField(req, "cookies"), headers), options),
|
|
3044
|
+
url: normalize(decodeQueryPlus(url || ""), options),
|
|
3045
|
+
originalUrl: normalize(decodeQueryPlus(requestField(req, "originalUrl") || url || ""), options),
|
|
3046
|
+
_rawBody: rawBody
|
|
3047
|
+
};
|
|
3048
|
+
}
|
|
3049
|
+
function cookieValues(parsed, headers) {
|
|
3050
|
+
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
|
|
3051
|
+
return parseCookieHeader(headers?.cookie);
|
|
3052
|
+
}
|
|
3053
|
+
var MAX_NORMALIZE_DEPTH = 1e3;
|
|
3054
|
+
function normalizeObject(value, options = {}, depth = 0) {
|
|
3055
|
+
if (typeof value === "string") {
|
|
3056
|
+
return normalize(value, options);
|
|
3057
|
+
}
|
|
3058
|
+
if (value === null || typeof value !== "object") {
|
|
3059
|
+
return value;
|
|
3060
|
+
}
|
|
3061
|
+
if (depth >= MAX_NORMALIZE_DEPTH) {
|
|
3062
|
+
options.onLimit?.();
|
|
3063
|
+
return value;
|
|
3064
|
+
}
|
|
3065
|
+
const copies = /* @__PURE__ */ new Map();
|
|
3066
|
+
const copyOf = (node) => {
|
|
3067
|
+
const copy = Array.isArray(node) ? new Array(node.length) : {};
|
|
3068
|
+
copies.set(node, copy);
|
|
3069
|
+
return copy;
|
|
3070
|
+
};
|
|
3071
|
+
const top = copyOf(value);
|
|
3072
|
+
const stack = [[value, top, depth]];
|
|
3073
|
+
while (stack.length > 0) {
|
|
3074
|
+
const [node, copy, level] = stack.pop();
|
|
3075
|
+
for (const key of Object.keys(node)) {
|
|
3076
|
+
const child = node[key];
|
|
3077
|
+
if (typeof child === "string") {
|
|
3078
|
+
setOwn(copy, key, normalize(child, options));
|
|
3079
|
+
} else if (child === null || typeof child !== "object") {
|
|
3080
|
+
setOwn(copy, key, child);
|
|
3081
|
+
} else if (copies.has(child)) {
|
|
3082
|
+
setOwn(copy, key, copies.get(child));
|
|
3083
|
+
} else if (level + 1 >= MAX_NORMALIZE_DEPTH) {
|
|
3084
|
+
setOwn(copy, key, child);
|
|
3085
|
+
options.onLimit?.();
|
|
3086
|
+
} else {
|
|
3087
|
+
const childCopy = copyOf(child);
|
|
3088
|
+
setOwn(copy, key, childCopy);
|
|
3089
|
+
stack.push([child, childCopy, level + 1]);
|
|
3090
|
+
}
|
|
3091
|
+
}
|
|
3092
|
+
}
|
|
3093
|
+
return top;
|
|
3094
|
+
}
|
|
3095
|
+
|
|
3096
|
+
// src/protect/engine/request.js
|
|
3097
|
+
var FILE_ATTRS = /* @__PURE__ */ new Set(["content", "filename", "type"]);
|
|
3098
|
+
var ownValue = (obj, key) => obj !== null && typeof obj === "object" && Object.prototype.hasOwnProperty.call(obj, key) ? obj[key] : void 0;
|
|
3099
|
+
function fieldSpellings(key) {
|
|
3100
|
+
const spellings = [{ key, nested: true }];
|
|
3101
|
+
if (key.includes("[")) {
|
|
3102
|
+
const dotted = key.replace(/\[\]$/, "").replace(/\[([^\][]*)\]/g, ".$1");
|
|
3103
|
+
if (dotted !== key && dotted !== "" && !dotted.includes("[") && !dotted.includes("]")) {
|
|
3104
|
+
spellings.push({ key: dotted, nested: true });
|
|
3105
|
+
}
|
|
3106
|
+
return spellings;
|
|
3107
|
+
}
|
|
3108
|
+
const [head, ...rest] = key.split(".");
|
|
3109
|
+
const bracketed = head + rest.map((part) => `[${part}]`).join("");
|
|
3110
|
+
if (bracketed !== key) spellings.push({ key: bracketed, nested: false });
|
|
3111
|
+
spellings.push({ key: `${bracketed}[]`, nested: false });
|
|
3112
|
+
return spellings;
|
|
3113
|
+
}
|
|
3114
|
+
var fileFilename = (f) => f && typeof f === "object" ? f.filename : f;
|
|
3115
|
+
var fileAttribute = (f, attr) => f && typeof f === "object" ? f[attr] : attr === "filename" ? f : void 0;
|
|
3116
|
+
function base64DecodeUtf8(value) {
|
|
3117
|
+
const str = String(value);
|
|
3118
|
+
if (typeof Buffer !== "undefined") {
|
|
3119
|
+
return Buffer.from(str, "base64").toString("utf-8");
|
|
3120
|
+
}
|
|
3121
|
+
const binary = atob(str);
|
|
3122
|
+
const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
|
|
3123
|
+
return new TextDecoder().decode(bytes);
|
|
3124
|
+
}
|
|
3125
|
+
var TEXT_MUTATIONS = /* @__PURE__ */ new Set(["base64_decode", "urldecode", "htmlentitydecode"]);
|
|
3126
|
+
var MAX_MAPPED_NODES = 2e4;
|
|
3127
|
+
var MAX_MAPPED_DEPTH = 1e3;
|
|
3128
|
+
function mapStringLeaves(root, fn, onLimit) {
|
|
3129
|
+
const copies = /* @__PURE__ */ new Map();
|
|
3130
|
+
const copyOf = (node) => {
|
|
3131
|
+
const copy = Array.isArray(node) ? [] : {};
|
|
3132
|
+
copies.set(node, copy);
|
|
3133
|
+
return copy;
|
|
3134
|
+
};
|
|
3135
|
+
const top = copyOf(root);
|
|
3136
|
+
const stack = [[root, top, 0]];
|
|
3137
|
+
let visited = 0;
|
|
3138
|
+
while (stack.length) {
|
|
3139
|
+
const [node, copy, depth] = stack.pop();
|
|
3140
|
+
visited++;
|
|
3141
|
+
for (const key of Object.keys(node)) {
|
|
3142
|
+
const child = node[key];
|
|
3143
|
+
if (typeof child === "string") {
|
|
3144
|
+
setOwn(copy, key, fn(child));
|
|
3145
|
+
} else if (child === null || typeof child !== "object") {
|
|
3146
|
+
setOwn(copy, key, child);
|
|
3147
|
+
} else if (copies.has(child)) {
|
|
3148
|
+
setOwn(copy, key, copies.get(child));
|
|
3149
|
+
} else if (depth + 1 >= MAX_MAPPED_DEPTH || visited + stack.length >= MAX_MAPPED_NODES) {
|
|
3150
|
+
setOwn(copy, key, child);
|
|
3151
|
+
onLimit();
|
|
3152
|
+
} else {
|
|
3153
|
+
const childCopy = copyOf(child);
|
|
3154
|
+
setOwn(copy, key, childCopy);
|
|
3155
|
+
stack.push([child, childCopy, depth + 1]);
|
|
3156
|
+
}
|
|
3157
|
+
}
|
|
3158
|
+
}
|
|
3159
|
+
return top;
|
|
3160
|
+
}
|
|
3161
|
+
var RequestResolver = class {
|
|
3162
|
+
#req;
|
|
3163
|
+
#cookies;
|
|
3164
|
+
#skips = /* @__PURE__ */ new Set();
|
|
3165
|
+
constructor(req) {
|
|
3166
|
+
this.#req = req;
|
|
3167
|
+
this.#cookies = null;
|
|
3168
|
+
}
|
|
3169
|
+
/** Record an inspection limit this evaluation reached (reported once per reason). */
|
|
3170
|
+
noteSkip(reason) {
|
|
3171
|
+
this.#skips.add(reason);
|
|
3172
|
+
}
|
|
3173
|
+
/** The inspection limits recorded so far, in the order first reached. */
|
|
3174
|
+
get skips() {
|
|
3175
|
+
return [...this.#skips];
|
|
3176
|
+
}
|
|
3177
|
+
resolve(parameter) {
|
|
3178
|
+
if (!parameter || parameter === "false") {
|
|
3179
|
+
return [null];
|
|
3180
|
+
}
|
|
3181
|
+
if (parameter === "rules") {
|
|
3182
|
+
return [null];
|
|
3183
|
+
}
|
|
3184
|
+
if (parameter === "raw") {
|
|
3185
|
+
return this.#resolveRaw();
|
|
3186
|
+
}
|
|
3187
|
+
if (parameter === "all") {
|
|
3188
|
+
return this.#resolveAll();
|
|
3189
|
+
}
|
|
3190
|
+
const dotIndex = parameter.indexOf(".");
|
|
3191
|
+
if (dotIndex === -1) {
|
|
3192
|
+
return [];
|
|
3193
|
+
}
|
|
3194
|
+
const source = parameter.substring(0, dotIndex);
|
|
3195
|
+
const key = parameter.substring(dotIndex + 1);
|
|
3196
|
+
switch (source) {
|
|
3197
|
+
case "get":
|
|
3198
|
+
return this.#resolveGet(key);
|
|
3199
|
+
case "post":
|
|
3200
|
+
return this.#resolvePost(key);
|
|
3201
|
+
case "request":
|
|
3202
|
+
return this.#resolveRequest(key);
|
|
3203
|
+
case "cookie":
|
|
3204
|
+
return this.#resolveCookie(key);
|
|
3205
|
+
case "server":
|
|
3206
|
+
return this.#resolveServer(key);
|
|
3207
|
+
case "files":
|
|
3208
|
+
return this.#resolveFiles(key);
|
|
3209
|
+
case "response":
|
|
3210
|
+
return this.#resolveResponse(key);
|
|
3211
|
+
case "egress":
|
|
3212
|
+
return this.#resolveEgress(key);
|
|
3213
|
+
default:
|
|
3214
|
+
return [];
|
|
3215
|
+
}
|
|
3216
|
+
}
|
|
3217
|
+
// Response-phase sources (req._response = { status, headers, body }). Lets rules inspect
|
|
3218
|
+
// what the app is about to SEND — e.g. a leaked secret in the body — regardless of route.
|
|
3219
|
+
#resolveResponse(key) {
|
|
3220
|
+
const resp = this.#req._response;
|
|
3221
|
+
if (!resp) {
|
|
3222
|
+
return [];
|
|
3223
|
+
}
|
|
3224
|
+
if (key === "status") {
|
|
3225
|
+
return resp.status !== void 0 ? [String(resp.status)] : [];
|
|
3226
|
+
}
|
|
3227
|
+
if (key === "body") {
|
|
3228
|
+
return resp.body != null && resp.body !== "" ? [resp.body] : [];
|
|
3229
|
+
}
|
|
3230
|
+
if (key === "headers") {
|
|
3231
|
+
const headers = resp.headers ?? {};
|
|
3232
|
+
return [Object.entries(headers).map(([k, v]) => `${k}: ${v}`).join("\n")];
|
|
3233
|
+
}
|
|
3234
|
+
if (key.startsWith("header.")) {
|
|
3235
|
+
const name = key.slice("header.".length).toLowerCase();
|
|
3236
|
+
const headers = resp.headers ?? {};
|
|
3237
|
+
return Object.hasOwn(headers, name) ? [headers[name]] : [];
|
|
2848
3238
|
}
|
|
2849
3239
|
return [];
|
|
2850
3240
|
}
|
|
@@ -2874,6 +3264,9 @@ var RequestResolver = class {
|
|
|
2874
3264
|
if (value === null || value === void 0) {
|
|
2875
3265
|
return value;
|
|
2876
3266
|
}
|
|
3267
|
+
if (typeof value === "object" && TEXT_MUTATIONS.has(mutation)) {
|
|
3268
|
+
return mapStringLeaves(value, (leaf) => this.#applyMutation(mutation, leaf), () => this.noteSkip("container-cap"));
|
|
3269
|
+
}
|
|
2877
3270
|
switch (mutation) {
|
|
2878
3271
|
case "base64_decode":
|
|
2879
3272
|
try {
|
|
@@ -2894,11 +3287,7 @@ var RequestResolver = class {
|
|
|
2894
3287
|
return value;
|
|
2895
3288
|
}
|
|
2896
3289
|
case "urldecode":
|
|
2897
|
-
|
|
2898
|
-
return decodeURIComponent(String(value));
|
|
2899
|
-
} catch {
|
|
2900
|
-
return value;
|
|
2901
|
-
}
|
|
3290
|
+
return safeUrlDecode(String(value).replace(/\+/g, " "));
|
|
2902
3291
|
case "htmlentitydecode":
|
|
2903
3292
|
return decodeHtmlEntities(String(value));
|
|
2904
3293
|
case "intval":
|
|
@@ -2917,16 +3306,30 @@ var RequestResolver = class {
|
|
|
2917
3306
|
if (key.endsWith("*")) {
|
|
2918
3307
|
return this.#resolveWildcard(query, key);
|
|
2919
3308
|
}
|
|
2920
|
-
|
|
2921
|
-
return value !== void 0 ? [value] : [];
|
|
3309
|
+
return this.#lookup(query, key);
|
|
2922
3310
|
}
|
|
2923
3311
|
#resolvePost(key) {
|
|
2924
3312
|
const body = this.#req.body ?? {};
|
|
2925
3313
|
if (key.endsWith("*")) {
|
|
2926
3314
|
return this.#resolveWildcard(body, key);
|
|
2927
3315
|
}
|
|
2928
|
-
|
|
2929
|
-
|
|
3316
|
+
return this.#lookup(body, key);
|
|
3317
|
+
}
|
|
3318
|
+
/**
|
|
3319
|
+
* Every value a field path names in `obj`, in either spelling a form or query field can take.
|
|
3320
|
+
*
|
|
3321
|
+
* A parser that expands brackets (`qs`, as Express uses) turns `user[name]=x` into `{ user: { name } }`
|
|
3322
|
+
* and `id[]=x` into `{ id: [x] }`; a flat parser (`URLSearchParams`, as the Fetch and Node adapters use)
|
|
3323
|
+
* keeps `user[name]` and `id[]` as literal keys. The rule names the field once, so both shapes answer
|
|
3324
|
+
* to both spellings: `user.name` and `user[name]`, `id` and `id[]`.
|
|
3325
|
+
*/
|
|
3326
|
+
#lookup(obj, key) {
|
|
3327
|
+
const values = [];
|
|
3328
|
+
for (const candidate of fieldSpellings(key)) {
|
|
3329
|
+
const value = candidate.nested ? this.#getNestedValue(obj, candidate.key) : ownValue(obj, candidate.key);
|
|
3330
|
+
if (value !== void 0) values.push(value);
|
|
3331
|
+
}
|
|
3332
|
+
return values;
|
|
2930
3333
|
}
|
|
2931
3334
|
#resolveRequest(key) {
|
|
2932
3335
|
const query = this.#req.query ?? {};
|
|
@@ -2939,8 +3342,11 @@ var RequestResolver = class {
|
|
|
2939
3342
|
...this.#resolveWildcard(cookies, key)
|
|
2940
3343
|
];
|
|
2941
3344
|
}
|
|
2942
|
-
const
|
|
2943
|
-
|
|
3345
|
+
const fromQuery = this.#lookup(query, key);
|
|
3346
|
+
if (fromQuery.length > 0) return fromQuery;
|
|
3347
|
+
const fromBody = this.#lookup(body, key);
|
|
3348
|
+
if (fromBody.length > 0) return fromBody;
|
|
3349
|
+
return Object.hasOwn(cookies, key) ? [cookies[key]] : [];
|
|
2944
3350
|
}
|
|
2945
3351
|
#resolveCookie(key) {
|
|
2946
3352
|
const cookies = this.#parseCookies();
|
|
@@ -3110,329 +3516,51 @@ var RequestResolver = class {
|
|
|
3110
3516
|
this.#cookies = this.#req.cookies;
|
|
3111
3517
|
return this.#cookies;
|
|
3112
3518
|
}
|
|
3113
|
-
|
|
3114
|
-
if (!header) {
|
|
3115
|
-
this.#cookies = {};
|
|
3116
|
-
return this.#cookies;
|
|
3117
|
-
}
|
|
3118
|
-
const cookies = {};
|
|
3119
|
-
for (const pair of header.split(";")) {
|
|
3120
|
-
const eqIndex = pair.indexOf("=");
|
|
3121
|
-
if (eqIndex === -1) {
|
|
3122
|
-
continue;
|
|
3123
|
-
}
|
|
3124
|
-
const name = pair.substring(0, eqIndex).trim();
|
|
3125
|
-
const value = pair.substring(eqIndex + 1).trim();
|
|
3126
|
-
setOwn(cookies, name, value);
|
|
3127
|
-
}
|
|
3128
|
-
this.#cookies = cookies;
|
|
3519
|
+
this.#cookies = parseCookieHeader(this.#req.headers?.cookie);
|
|
3129
3520
|
return this.#cookies;
|
|
3130
3521
|
}
|
|
3131
3522
|
};
|
|
3132
3523
|
|
|
3133
3524
|
// src/protect/notify.js
|
|
3134
|
-
var reported = /* @__PURE__ */ new
|
|
3525
|
+
var reported = /* @__PURE__ */ new WeakMap();
|
|
3526
|
+
var warnings = 0;
|
|
3527
|
+
var MAX_WARNINGS = 20;
|
|
3135
3528
|
function notify(fn, arg, label) {
|
|
3136
3529
|
if (typeof fn !== "function") return false;
|
|
3137
3530
|
try {
|
|
3138
3531
|
const result = fn(arg);
|
|
3139
3532
|
if (result !== null && typeof result === "object" && typeof result.then === "function") {
|
|
3140
3533
|
try {
|
|
3141
|
-
result.then(void 0, (err) => warnOnce2(label, err));
|
|
3534
|
+
result.then(void 0, (err) => warnOnce2(fn, label, err));
|
|
3142
3535
|
} catch {
|
|
3143
|
-
}
|
|
3144
|
-
}
|
|
3145
|
-
return true;
|
|
3146
|
-
} catch (err) {
|
|
3147
|
-
warnOnce2(label, err);
|
|
3148
|
-
return false;
|
|
3149
|
-
}
|
|
3150
|
-
}
|
|
3151
|
-
function warnOnce2(label, err) {
|
|
3152
|
-
if (reported.has(label)) return;
|
|
3153
|
-
reported.add(label);
|
|
3154
|
-
try {
|
|
3155
|
-
console.warn(
|
|
3156
|
-
`Patchstack: the ${label} callback passed to createProtection failed and was ignored. Protection is unaffected; this is reported once per process. Cause: ${err && err.message ? err.message : String(err)}`
|
|
3157
|
-
);
|
|
3158
|
-
} catch {
|
|
3159
|
-
}
|
|
3160
|
-
}
|
|
3161
|
-
|
|
3162
|
-
// src/protect/engine/normalizer.js
|
|
3163
|
-
var HTML_ENTITIES = {
|
|
3164
|
-
"&": "&",
|
|
3165
|
-
"<": "<",
|
|
3166
|
-
">": ">",
|
|
3167
|
-
""": '"',
|
|
3168
|
-
"'": "'",
|
|
3169
|
-
"'": "'",
|
|
3170
|
-
" ": " ",
|
|
3171
|
-
"'": "'",
|
|
3172
|
-
""": '"',
|
|
3173
|
-
"<": "<",
|
|
3174
|
-
">": ">",
|
|
3175
|
-
"&": "&",
|
|
3176
|
-
""": '"',
|
|
3177
|
-
"<": "<",
|
|
3178
|
-
">": ">",
|
|
3179
|
-
"&": "&"
|
|
3180
|
-
};
|
|
3181
|
-
var MAX_DECODE_ITERATIONS = 5;
|
|
3182
|
-
function normalize(value, options = {}) {
|
|
3183
|
-
if (typeof value !== "string") {
|
|
3184
|
-
return value;
|
|
3185
|
-
}
|
|
3186
|
-
const opts = {
|
|
3187
|
-
urlDecode: true,
|
|
3188
|
-
htmlDecode: true,
|
|
3189
|
-
sqlComments: true,
|
|
3190
|
-
nullBytes: true,
|
|
3191
|
-
whitespace: true,
|
|
3192
|
-
...options
|
|
3193
|
-
};
|
|
3194
|
-
let result = value;
|
|
3195
|
-
if (opts.nullBytes) {
|
|
3196
|
-
result = removeNullBytes(result);
|
|
3197
|
-
}
|
|
3198
|
-
if (opts.urlDecode) {
|
|
3199
|
-
result = urlDecode(result);
|
|
3200
|
-
}
|
|
3201
|
-
if (opts.htmlDecode) {
|
|
3202
|
-
result = htmlEntityDecode(result);
|
|
3203
|
-
}
|
|
3204
|
-
if (opts.sqlComments) {
|
|
3205
|
-
result = removeSqlComments(result);
|
|
3206
|
-
}
|
|
3207
|
-
if (opts.whitespace) {
|
|
3208
|
-
result = normalizeWhitespace(result);
|
|
3209
|
-
}
|
|
3210
|
-
return result;
|
|
3211
|
-
}
|
|
3212
|
-
function urlDecode(value) {
|
|
3213
|
-
if (typeof value !== "string") {
|
|
3214
|
-
return value;
|
|
3215
|
-
}
|
|
3216
|
-
let result = value;
|
|
3217
|
-
let previous = "";
|
|
3218
|
-
let iterations = 0;
|
|
3219
|
-
while (result !== previous && iterations < MAX_DECODE_ITERATIONS) {
|
|
3220
|
-
previous = result;
|
|
3221
|
-
iterations++;
|
|
3222
|
-
try {
|
|
3223
|
-
result = decodeURIComponent(result);
|
|
3224
|
-
} catch {
|
|
3225
|
-
result = safeUrlDecode(result);
|
|
3226
|
-
break;
|
|
3227
|
-
}
|
|
3228
|
-
}
|
|
3229
|
-
return result;
|
|
3230
|
-
}
|
|
3231
|
-
function safeUrlDecode(value) {
|
|
3232
|
-
return value.replace(/%([0-9A-Fa-f]{2})/g, (match, hex) => {
|
|
3233
|
-
try {
|
|
3234
|
-
return String.fromCharCode(parseInt(hex, 16));
|
|
3235
|
-
} catch {
|
|
3236
|
-
return match;
|
|
3237
|
-
}
|
|
3238
|
-
});
|
|
3239
|
-
}
|
|
3240
|
-
function htmlEntityDecode(value) {
|
|
3241
|
-
if (typeof value !== "string") {
|
|
3242
|
-
return value;
|
|
3243
|
-
}
|
|
3244
|
-
let result = value;
|
|
3245
|
-
for (const [entity, char] of Object.entries(HTML_ENTITIES)) {
|
|
3246
|
-
result = result.split(entity).join(char);
|
|
3247
|
-
}
|
|
3248
|
-
result = result.replace(/&#(\d+);/g, (match, code) => {
|
|
3249
|
-
const num = parseInt(code, 10);
|
|
3250
|
-
return num > 0 && num < 65536 ? String.fromCharCode(num) : match;
|
|
3251
|
-
});
|
|
3252
|
-
result = result.replace(/&#x([0-9A-Fa-f]+);/g, (match, hex) => {
|
|
3253
|
-
const num = parseInt(hex, 16);
|
|
3254
|
-
return num > 0 && num < 65536 ? String.fromCharCode(num) : match;
|
|
3255
|
-
});
|
|
3256
|
-
return result;
|
|
3257
|
-
}
|
|
3258
|
-
function removeSqlComments(value) {
|
|
3259
|
-
if (typeof value !== "string") {
|
|
3260
|
-
return value;
|
|
3261
|
-
}
|
|
3262
|
-
let result = value;
|
|
3263
|
-
result = result.replace(/\/\*[\s\S]*?\*\//g, " ");
|
|
3264
|
-
result = result.replace(/\/\*![\s\S]*?\*\//g, " ");
|
|
3265
|
-
return result;
|
|
3266
|
-
}
|
|
3267
|
-
function removeNullBytes(value) {
|
|
3268
|
-
if (typeof value !== "string") {
|
|
3269
|
-
return value;
|
|
3270
|
-
}
|
|
3271
|
-
let result = value.replace(/\x00/g, "");
|
|
3272
|
-
result = result.replace(/[\x01-\x08\x0B\x0C\x0E-\x1F]/g, "");
|
|
3273
|
-
return result;
|
|
3274
|
-
}
|
|
3275
|
-
function normalizeWhitespace(value) {
|
|
3276
|
-
if (typeof value !== "string") {
|
|
3277
|
-
return value;
|
|
3278
|
-
}
|
|
3279
|
-
let result = value.replace(/[\t\r\n\f\v]+/g, " ");
|
|
3280
|
-
result = result.replace(/ {2,}/g, " ");
|
|
3281
|
-
return result;
|
|
3282
|
-
}
|
|
3283
|
-
var MAX_RAW_CHARS = 1024 * 1024;
|
|
3284
|
-
var MAX_RAW_NODES = 25e3;
|
|
3285
|
-
var MAX_RAW_DEPTH = 200;
|
|
3286
|
-
var MAX_RAW_ENTRIES = 2e3;
|
|
3287
|
-
function serializeForRawDetection(body) {
|
|
3288
|
-
if (body === null || body === void 0) return "";
|
|
3289
|
-
if (typeof body === "string") return body.slice(0, MAX_RAW_CHARS);
|
|
3290
|
-
const visited = /* @__PURE__ */ new Set();
|
|
3291
|
-
const output = [];
|
|
3292
|
-
const stack = [{ kind: "value", value: body, depth: 0, root: true }];
|
|
3293
|
-
let chars = 0;
|
|
3294
|
-
let nodes = 0;
|
|
3295
|
-
const append = (text) => {
|
|
3296
|
-
if (chars >= MAX_RAW_CHARS) return;
|
|
3297
|
-
const piece = String(text).slice(0, MAX_RAW_CHARS - chars);
|
|
3298
|
-
output.push(piece);
|
|
3299
|
-
chars += piece.length;
|
|
3300
|
-
};
|
|
3301
|
-
while (stack.length > 0 && chars < MAX_RAW_CHARS) {
|
|
3302
|
-
const task = stack.pop();
|
|
3303
|
-
if (task.kind === "text") {
|
|
3304
|
-
append(task.text);
|
|
3305
|
-
continue;
|
|
3306
|
-
}
|
|
3307
|
-
if (task.kind === "key") {
|
|
3308
|
-
append(boundedJsonString(task.value));
|
|
3309
|
-
append(":");
|
|
3310
|
-
continue;
|
|
3311
|
-
}
|
|
3312
|
-
nodes++;
|
|
3313
|
-
if (nodes > MAX_RAW_NODES) {
|
|
3314
|
-
append('"[NodeLimit]"');
|
|
3315
|
-
continue;
|
|
3316
|
-
}
|
|
3317
|
-
const value = task.value;
|
|
3318
|
-
if (value === null || value === void 0) {
|
|
3319
|
-
append(task.root ? "" : "null");
|
|
3320
|
-
continue;
|
|
3321
|
-
}
|
|
3322
|
-
if (typeof value === "string") {
|
|
3323
|
-
append(task.root ? value.slice(0, MAX_RAW_CHARS) : boundedJsonString(value));
|
|
3324
|
-
continue;
|
|
3325
|
-
}
|
|
3326
|
-
if (typeof value !== "object") {
|
|
3327
|
-
append(String(value).slice(0, MAX_RAW_CHARS));
|
|
3328
|
-
continue;
|
|
3329
|
-
}
|
|
3330
|
-
if (visited.has(value)) {
|
|
3331
|
-
append("[Circular]");
|
|
3332
|
-
continue;
|
|
3333
|
-
}
|
|
3334
|
-
if (task.depth >= MAX_RAW_DEPTH) {
|
|
3335
|
-
append('"[DepthLimit]"');
|
|
3336
|
-
continue;
|
|
3337
|
-
}
|
|
3338
|
-
visited.add(value);
|
|
3339
|
-
if (Array.isArray(value)) {
|
|
3340
|
-
const length2 = Math.min(value.length, MAX_RAW_ENTRIES);
|
|
3341
|
-
const items = [];
|
|
3342
|
-
for (let i = 0; i < length2; i++) {
|
|
3343
|
-
if (i > 0) items.push({ kind: "text", text: "," });
|
|
3344
|
-
items.push({ kind: "value", value: readOwnValue(value, String(i)), depth: task.depth + 1, root: false });
|
|
3345
|
-
}
|
|
3346
|
-
if (value.length > length2) {
|
|
3347
|
-
if (length2 > 0) items.push({ kind: "text", text: "," });
|
|
3348
|
-
items.push({ kind: "text", text: '"[EntryLimit]"' });
|
|
3349
|
-
}
|
|
3350
|
-
stack.push({ kind: "text", text: "]" });
|
|
3351
|
-
for (let i = items.length - 1; i >= 0; i--) stack.push(items[i]);
|
|
3352
|
-
stack.push({ kind: "text", text: "[" });
|
|
3353
|
-
continue;
|
|
3354
|
-
}
|
|
3355
|
-
let keys;
|
|
3356
|
-
try {
|
|
3357
|
-
keys = Object.getOwnPropertyNames(value);
|
|
3358
|
-
} catch {
|
|
3359
|
-
append('"[Unreadable]"');
|
|
3360
|
-
continue;
|
|
3361
|
-
}
|
|
3362
|
-
const length = Math.min(keys.length, MAX_RAW_ENTRIES);
|
|
3363
|
-
const entries = [];
|
|
3364
|
-
for (let i = 0; i < length; i++) {
|
|
3365
|
-
const key = keys[i];
|
|
3366
|
-
if (i > 0) entries.push({ kind: "text", text: "," });
|
|
3367
|
-
entries.push({ kind: "key", value: key });
|
|
3368
|
-
entries.push({ kind: "value", value: readOwnValue(value, key), depth: task.depth + 1, root: false });
|
|
3369
|
-
}
|
|
3370
|
-
if (keys.length > length) {
|
|
3371
|
-
if (length > 0) entries.push({ kind: "text", text: "," });
|
|
3372
|
-
entries.push({ kind: "text", text: '"[EntryLimit]":null' });
|
|
3373
|
-
}
|
|
3374
|
-
stack.push({ kind: "text", text: "}" });
|
|
3375
|
-
for (let i = entries.length - 1; i >= 0; i--) stack.push(entries[i]);
|
|
3376
|
-
stack.push({ kind: "text", text: "{" });
|
|
3377
|
-
}
|
|
3378
|
-
return output.join("");
|
|
3379
|
-
}
|
|
3380
|
-
function boundedJsonString(value) {
|
|
3381
|
-
return JSON.stringify(String(value).slice(0, MAX_RAW_CHARS));
|
|
3382
|
-
}
|
|
3383
|
-
function readOwnValue(value, key) {
|
|
3384
|
-
try {
|
|
3385
|
-
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
3386
|
-
return descriptor && Object.hasOwn(descriptor, "value") ? descriptor.value : "[Accessor]";
|
|
3387
|
-
} catch {
|
|
3388
|
-
return "[Unreadable]";
|
|
3389
|
-
}
|
|
3390
|
-
}
|
|
3391
|
-
var INHERITED_ACCESSORS = /* @__PURE__ */ new Set(["headers", "query"]);
|
|
3392
|
-
function requestField(req, key) {
|
|
3393
|
-
if (req === null || typeof req !== "object") return void 0;
|
|
3394
|
-
if (Object.hasOwn(req, key)) return req[key];
|
|
3395
|
-
if (!INHERITED_ACCESSORS.has(key)) return void 0;
|
|
3396
|
-
let holder = Object.getPrototypeOf(req);
|
|
3397
|
-
while (holder !== null && !Object.hasOwn(holder, key)) holder = Object.getPrototypeOf(holder);
|
|
3398
|
-
if (holder === null || holder === Object.prototype) return void 0;
|
|
3399
|
-
const descriptor = Object.getOwnPropertyDescriptor(holder, key);
|
|
3400
|
-
if (typeof descriptor?.get !== "function") return void 0;
|
|
3401
|
-
return req[key];
|
|
3402
|
-
}
|
|
3403
|
-
function normalizeRequest(req, options = {}) {
|
|
3404
|
-
const ownRaw = Object.hasOwn(req ?? {}, "_rawBody") && typeof req._rawBody === "string";
|
|
3405
|
-
const body = requestField(req, "body");
|
|
3406
|
-
const url = requestField(req, "url");
|
|
3407
|
-
const rawBody = ownRaw ? req._rawBody : serializeForRawDetection(body ?? null);
|
|
3408
|
-
return {
|
|
3409
|
-
query: normalizeObject(requestField(req, "query") || {}, options),
|
|
3410
|
-
body: normalizeObject(body || {}, options),
|
|
3411
|
-
headers: normalizeObject(requestField(req, "headers") || {}, options),
|
|
3412
|
-
url: normalize(url || "", options),
|
|
3413
|
-
originalUrl: normalize(requestField(req, "originalUrl") || url || "", options),
|
|
3414
|
-
_rawBody: rawBody
|
|
3415
|
-
};
|
|
3416
|
-
}
|
|
3417
|
-
var MAX_NORMALIZE_DEPTH = 200;
|
|
3418
|
-
function normalizeObject(value, options = {}, depth = 0) {
|
|
3419
|
-
if (typeof value === "string") {
|
|
3420
|
-
return normalize(value, options);
|
|
3421
|
-
}
|
|
3422
|
-
if (depth >= MAX_NORMALIZE_DEPTH) {
|
|
3423
|
-
return value;
|
|
3536
|
+
}
|
|
3537
|
+
}
|
|
3538
|
+
return true;
|
|
3539
|
+
} catch (err) {
|
|
3540
|
+
warnOnce2(fn, label, err);
|
|
3541
|
+
return false;
|
|
3424
3542
|
}
|
|
3425
|
-
|
|
3426
|
-
|
|
3543
|
+
}
|
|
3544
|
+
function warnOnce2(fn, label, err) {
|
|
3545
|
+
let labels = reported.get(fn);
|
|
3546
|
+
if (labels?.has(label)) return;
|
|
3547
|
+
if (!labels) {
|
|
3548
|
+
labels = /* @__PURE__ */ new Set();
|
|
3549
|
+
reported.set(fn, labels);
|
|
3427
3550
|
}
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3551
|
+
labels.add(label);
|
|
3552
|
+
if (warnings > MAX_WARNINGS) return;
|
|
3553
|
+
warnings++;
|
|
3554
|
+
try {
|
|
3555
|
+
if (warnings > MAX_WARNINGS) {
|
|
3556
|
+
console.warn("Patchstack: further failing callbacks passed to createProtection are not reported in this process.");
|
|
3557
|
+
return;
|
|
3432
3558
|
}
|
|
3433
|
-
|
|
3559
|
+
console.warn(
|
|
3560
|
+
`Patchstack: the ${label} callback passed to createProtection failed and was ignored. Protection is unaffected; this callback's failures are reported once. Cause: ${err && err.message ? err.message : String(err)}`
|
|
3561
|
+
);
|
|
3562
|
+
} catch {
|
|
3434
3563
|
}
|
|
3435
|
-
return value;
|
|
3436
3564
|
}
|
|
3437
3565
|
|
|
3438
3566
|
// src/protect/engine/engine.js
|
|
@@ -3820,17 +3948,21 @@ function arrayKeyValue(value, matchObj) {
|
|
|
3820
3948
|
}
|
|
3821
3949
|
var WHOLE_VALUE_MATCH_TYPES = /* @__PURE__ */ new Set(["isset", "array_in_array", "array_key_value"]);
|
|
3822
3950
|
function collectLeafValues(root, nodeCap = 2e4, maxDepth = 1e3) {
|
|
3823
|
-
const
|
|
3951
|
+
const leaves = [];
|
|
3824
3952
|
const stack = [[root, 0]];
|
|
3825
3953
|
let visited = 0;
|
|
3954
|
+
let truncated = false;
|
|
3826
3955
|
while (stack.length) {
|
|
3827
3956
|
const [node, depth] = stack.pop();
|
|
3828
3957
|
if (node === null || node === void 0) continue;
|
|
3829
3958
|
if (typeof node !== "object") {
|
|
3830
|
-
|
|
3959
|
+
leaves.push(node);
|
|
3960
|
+
continue;
|
|
3961
|
+
}
|
|
3962
|
+
if (depth >= maxDepth || visited >= nodeCap) {
|
|
3963
|
+
truncated = true;
|
|
3831
3964
|
continue;
|
|
3832
3965
|
}
|
|
3833
|
-
if (depth >= maxDepth || visited >= nodeCap) continue;
|
|
3834
3966
|
visited++;
|
|
3835
3967
|
if (Array.isArray(node)) {
|
|
3836
3968
|
for (let i = node.length - 1; i >= 0; i--) stack.push([node[i], depth + 1]);
|
|
@@ -3838,7 +3970,18 @@ function collectLeafValues(root, nodeCap = 2e4, maxDepth = 1e3) {
|
|
|
3838
3970
|
for (const k of Object.keys(node)) stack.push([node[k], depth + 1]);
|
|
3839
3971
|
}
|
|
3840
3972
|
}
|
|
3841
|
-
return
|
|
3973
|
+
return { leaves, truncated };
|
|
3974
|
+
}
|
|
3975
|
+
function skipsOf(resolver) {
|
|
3976
|
+
const skips = resolver?.skips;
|
|
3977
|
+
return skips && skips.length > 0 ? { skips } : {};
|
|
3978
|
+
}
|
|
3979
|
+
function serialisedValue(value) {
|
|
3980
|
+
try {
|
|
3981
|
+
return JSON.stringify(value);
|
|
3982
|
+
} catch {
|
|
3983
|
+
return void 0;
|
|
3984
|
+
}
|
|
3842
3985
|
}
|
|
3843
3986
|
var warnedKeys = /* @__PURE__ */ new Set();
|
|
3844
3987
|
function warnOnce3(key, message) {
|
|
@@ -3856,12 +3999,22 @@ function warnUnsupportedMatchType(type) {
|
|
|
3856
3999
|
`[patchstack] Unsupported rule_v2 match type "${type}" \u2014 condition treated as no-match. Rules relying on it are not enforced by this engine.`
|
|
3857
4000
|
);
|
|
3858
4001
|
}
|
|
4002
|
+
var SPECIAL_SCHEMES = /* @__PURE__ */ new Set(["http", "https", "ws", "wss", "ftp", "file"]);
|
|
4003
|
+
function trimControls(text) {
|
|
4004
|
+
let start = 0;
|
|
4005
|
+
let end = text.length;
|
|
4006
|
+
while (start < end && text.charCodeAt(start) <= 32) start++;
|
|
4007
|
+
while (end > start && text.charCodeAt(end - 1) <= 32) end--;
|
|
4008
|
+
return text.slice(start, end);
|
|
4009
|
+
}
|
|
3859
4010
|
function hostFromValue(value) {
|
|
3860
|
-
const raw = String(value ?? "").trim();
|
|
4011
|
+
const raw = trimControls(String(value ?? "").replace(/[\t\n\r]/g, "")).trim();
|
|
3861
4012
|
if (raw === "") return "";
|
|
3862
|
-
|
|
4013
|
+
const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(raw)?.[1]?.toLowerCase();
|
|
4014
|
+
const relative = /^[\\/]{2}/.test(raw);
|
|
4015
|
+
if (relative || /^[a-z][a-z0-9+.-]*:\/\//i.test(raw) || scheme !== void 0 && SPECIAL_SCHEMES.has(scheme)) {
|
|
3863
4016
|
try {
|
|
3864
|
-
return new URL(
|
|
4017
|
+
return new URL(relative ? `http:${raw}` : raw).hostname;
|
|
3865
4018
|
} catch {
|
|
3866
4019
|
return raw;
|
|
3867
4020
|
}
|
|
@@ -3889,10 +4042,18 @@ function isInternalHost(hostname) {
|
|
|
3889
4042
|
const allZeroHi = g[0] === 0 && g[1] === 0 && g[2] === 0 && g[3] === 0 && g[4] === 0;
|
|
3890
4043
|
if (allZeroHi && g[5] === 0 && g[6] === 0 && (g[7] === 0 || g[7] === 1)) return true;
|
|
3891
4044
|
if ((g[0] & 65472) === 65152) return true;
|
|
4045
|
+
if ((g[0] & 65472) === 65216) return true;
|
|
3892
4046
|
if ((g[0] & 65024) === 64512) return true;
|
|
4047
|
+
if ((g[0] & 65280) === 65280) return true;
|
|
4048
|
+
const low32 = (g[6] << 16 >>> 0 | g[7]) >>> 0;
|
|
3893
4049
|
if (allZeroHi && (g[5] === 65535 || g[5] === 0)) {
|
|
3894
|
-
return isPrivateV4Int(
|
|
4050
|
+
return isPrivateV4Int(low32);
|
|
4051
|
+
}
|
|
4052
|
+
if (g[0] === 100 && g[1] === 65435 && g[2] === 0 && g[3] === 0 && g[4] === 0 && g[5] === 0) {
|
|
4053
|
+
return isPrivateV4Int(low32);
|
|
3895
4054
|
}
|
|
4055
|
+
if (g[0] === 100 && g[1] === 65435 && g[2] === 1) return true;
|
|
4056
|
+
if (g[0] === 8194) return isPrivateV4Int((g[1] << 16 >>> 0 | g[2]) >>> 0);
|
|
3896
4057
|
return false;
|
|
3897
4058
|
}
|
|
3898
4059
|
const v4 = parseIPv4ToInt(host);
|
|
@@ -3902,6 +4063,10 @@ function isInternalHost(hostname) {
|
|
|
3902
4063
|
function isPrivateV4Int(n) {
|
|
3903
4064
|
const a = n >>> 24 & 255;
|
|
3904
4065
|
const b = n >>> 16 & 255;
|
|
4066
|
+
const c = n >>> 8 & 255;
|
|
4067
|
+
if (a >= 224) return true;
|
|
4068
|
+
if (a === 192 && b === 0 && c === 0) return true;
|
|
4069
|
+
if (a === 198 && (b === 18 || b === 19)) return true;
|
|
3905
4070
|
if (a === 127 || a === 10 || a === 0) return true;
|
|
3906
4071
|
if (a === 169 && b === 254) return true;
|
|
3907
4072
|
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
@@ -4250,8 +4415,12 @@ var RuleEngine = class {
|
|
|
4250
4415
|
let normalizedReq;
|
|
4251
4416
|
let resolver;
|
|
4252
4417
|
try {
|
|
4253
|
-
|
|
4418
|
+
let limited = false;
|
|
4419
|
+
normalizedReq = { ...req, ...normalizeRequest(req, { onLimit: () => {
|
|
4420
|
+
limited = true;
|
|
4421
|
+
} }) };
|
|
4254
4422
|
resolver = new RequestResolver(normalizedReq);
|
|
4423
|
+
if (limited) resolver.noteSkip("container-cap");
|
|
4255
4424
|
} catch (err) {
|
|
4256
4425
|
this.#reportError(err);
|
|
4257
4426
|
return { blocked: false, rule: null, message: null };
|
|
@@ -4277,14 +4446,15 @@ var RuleEngine = class {
|
|
|
4277
4446
|
// that re-read the request instead would be reading it a second time: a getter, a stream or
|
|
4278
4447
|
// anything else that answers once can give a different value, and evidence that disagrees
|
|
4279
4448
|
// with the match it belongs to is worse than none.
|
|
4280
|
-
resolver
|
|
4449
|
+
resolver,
|
|
4450
|
+
...skipsOf(resolver)
|
|
4281
4451
|
};
|
|
4282
4452
|
}
|
|
4283
4453
|
} catch (err) {
|
|
4284
4454
|
this.#reportError(err);
|
|
4285
4455
|
}
|
|
4286
4456
|
}
|
|
4287
|
-
return { blocked: false, rule: null, message: null };
|
|
4457
|
+
return { blocked: false, rule: null, message: null, ...skipsOf(resolver) };
|
|
4288
4458
|
}
|
|
4289
4459
|
#evaluateRule(conditions, resolver) {
|
|
4290
4460
|
const inclusiveConditions = conditions.filter((c) => c.inclusive);
|
|
@@ -4340,9 +4510,15 @@ var RuleEngine = class {
|
|
|
4340
4510
|
if (WHOLE_VALUE_MATCH_TYPES.has(match.type)) {
|
|
4341
4511
|
if (matchValue(match.type, value, match.value, match)) return true;
|
|
4342
4512
|
} else {
|
|
4343
|
-
|
|
4513
|
+
const { leaves, truncated } = collectLeafValues(value);
|
|
4514
|
+
for (const leaf of leaves) {
|
|
4344
4515
|
if (matchValue(match.type, leaf, match.value, match)) return true;
|
|
4345
4516
|
}
|
|
4517
|
+
if (truncated) {
|
|
4518
|
+
resolver.noteSkip("container-cap");
|
|
4519
|
+
const text = serialisedValue(value);
|
|
4520
|
+
if (text !== void 0 && matchValue(match.type, text, match.value, match)) return true;
|
|
4521
|
+
}
|
|
4346
4522
|
}
|
|
4347
4523
|
continue;
|
|
4348
4524
|
}
|
|
@@ -4729,6 +4905,9 @@ function conditionShapeProblem(condition) {
|
|
|
4729
4905
|
if (nested && !named) {
|
|
4730
4906
|
return `condition carries nested rules but its parameter is ${JSON.stringify(condition.parameter)}; a group must be {"parameter":"${GROUP_PARAMETER}"}`;
|
|
4731
4907
|
}
|
|
4908
|
+
if (condition.inclusive !== void 0 && typeof condition.inclusive !== "boolean") {
|
|
4909
|
+
return "inclusive must be true or false";
|
|
4910
|
+
}
|
|
4732
4911
|
if (named && nested) {
|
|
4733
4912
|
if (condition.match !== void 0) return "a group carries no match of its own; the engine ignores it";
|
|
4734
4913
|
if (condition.mutations !== void 0) return "a group carries no mutations of its own; the engine ignores them";
|
|
@@ -5438,7 +5617,7 @@ async function fromFetchRequest(request, options = {}) {
|
|
|
5438
5617
|
// that guessed differently would attribute one request to two addresses.
|
|
5439
5618
|
ip: client.ip ?? "",
|
|
5440
5619
|
_clientIp: client,
|
|
5441
|
-
cookies:
|
|
5620
|
+
cookies: parseCookieHeader(headers.cookie),
|
|
5442
5621
|
// Verbatim body text: preserves literal keys (e.g. `__proto__`) that JSON.stringify
|
|
5443
5622
|
// drops, so prototype-pollution rules on `raw` are robust.
|
|
5444
5623
|
_rawBody: rawBody,
|
|
@@ -5592,22 +5771,53 @@ function decodeExtendedValue(value) {
|
|
|
5592
5771
|
return match[2];
|
|
5593
5772
|
}
|
|
5594
5773
|
}
|
|
5595
|
-
function parseCookies(header) {
|
|
5596
|
-
const cookies = {};
|
|
5597
|
-
if (!header) {
|
|
5598
|
-
return cookies;
|
|
5599
|
-
}
|
|
5600
|
-
for (const pair of header.split(";")) {
|
|
5601
|
-
const idx = pair.indexOf("=");
|
|
5602
|
-
if (idx === -1) {
|
|
5603
|
-
continue;
|
|
5604
|
-
}
|
|
5605
|
-
setOwn(cookies, pair.slice(0, idx).trim(), pair.slice(idx + 1).trim());
|
|
5606
|
-
}
|
|
5607
|
-
return cookies;
|
|
5608
|
-
}
|
|
5609
5774
|
|
|
5610
5775
|
// src/protect/engine/node.js
|
|
5776
|
+
function readBodyPrefix(req, maxBytes, done) {
|
|
5777
|
+
const chunks = [];
|
|
5778
|
+
let retained = 0;
|
|
5779
|
+
let size = 0;
|
|
5780
|
+
let failed = false;
|
|
5781
|
+
let finished = false;
|
|
5782
|
+
const finish = (error, read) => {
|
|
5783
|
+
if (finished) return;
|
|
5784
|
+
finished = true;
|
|
5785
|
+
done(error, read);
|
|
5786
|
+
};
|
|
5787
|
+
req.on("data", (chunk) => {
|
|
5788
|
+
if (failed) return;
|
|
5789
|
+
try {
|
|
5790
|
+
const bytes = typeof chunk === "string" ? Buffer.from(chunk, req.readableEncoding || "utf8") : chunk;
|
|
5791
|
+
size += bytes.length;
|
|
5792
|
+
const take = Math.min(bytes.length, maxBytes - retained);
|
|
5793
|
+
if (take <= 0) return;
|
|
5794
|
+
chunks.push(take === bytes.length ? bytes : bytes.subarray(0, take));
|
|
5795
|
+
retained += take;
|
|
5796
|
+
} catch {
|
|
5797
|
+
failed = true;
|
|
5798
|
+
chunks.length = 0;
|
|
5799
|
+
}
|
|
5800
|
+
});
|
|
5801
|
+
req.on("error", (error) => finish(error));
|
|
5802
|
+
req.on("end", () => {
|
|
5803
|
+
if (failed) {
|
|
5804
|
+
finish(null, { text: "", overflow: false, size, failed: true });
|
|
5805
|
+
return;
|
|
5806
|
+
}
|
|
5807
|
+
const prefix = Buffer.concat(chunks);
|
|
5808
|
+
const overflow = size > maxBytes;
|
|
5809
|
+
const text = (overflow ? prefix.subarray(0, completeUtf8Length(prefix)) : prefix).toString("utf8");
|
|
5810
|
+
finish(null, { text, overflow, size, failed: false });
|
|
5811
|
+
});
|
|
5812
|
+
}
|
|
5813
|
+
function completeUtf8Length(bytes) {
|
|
5814
|
+
let start = bytes.length - 1;
|
|
5815
|
+
while (start >= 0 && bytes.length - start <= 3 && (bytes[start] & 192) === 128) start--;
|
|
5816
|
+
if (start < 0) return bytes.length;
|
|
5817
|
+
const lead = bytes[start];
|
|
5818
|
+
const needed = lead >= 240 ? 4 : lead >= 224 ? 3 : lead >= 192 ? 2 : 1;
|
|
5819
|
+
return bytes.length - start < needed ? start : bytes.length;
|
|
5820
|
+
}
|
|
5611
5821
|
function fromNodeRequest(req, rawBody = "", options = {}) {
|
|
5612
5822
|
const method = (req.method || "GET").toUpperCase();
|
|
5613
5823
|
const headers = {};
|
|
@@ -5653,29 +5863,41 @@ function fromNodeRequest(req, rawBody = "", options = {}) {
|
|
|
5653
5863
|
headers,
|
|
5654
5864
|
ip: client.ip ?? "",
|
|
5655
5865
|
_clientIp: client,
|
|
5656
|
-
cookies:
|
|
5866
|
+
cookies: parseCookieHeader(headers.cookie),
|
|
5657
5867
|
// Verbatim body text: preserves literal keys (e.g. `__proto__`) that JSON.stringify drops.
|
|
5658
5868
|
_rawBody: rawBody
|
|
5659
5869
|
};
|
|
5660
5870
|
}
|
|
5661
|
-
|
|
5662
|
-
|
|
5663
|
-
|
|
5664
|
-
|
|
5871
|
+
|
|
5872
|
+
// src/protect/egress.js
|
|
5873
|
+
var REGISTRY = /* @__PURE__ */ Symbol.for("patchstack.connect.egress-guard");
|
|
5874
|
+
function egressRegistry() {
|
|
5875
|
+
const existing = globalThis[REGISTRY];
|
|
5876
|
+
if (existing && existing.screens instanceof Set && existing.surfaces instanceof Map) return existing;
|
|
5877
|
+
const created = { screens: /* @__PURE__ */ new Set(), surfaces: /* @__PURE__ */ new Map() };
|
|
5878
|
+
Object.defineProperty(globalThis, REGISTRY, { value: created, configurable: true, writable: true });
|
|
5879
|
+
return created;
|
|
5880
|
+
}
|
|
5881
|
+
var refusal = (host) => new Error(`Patchstack blocked an outbound request to a disallowed address: ${host}`);
|
|
5882
|
+
function readableDestination(input, init) {
|
|
5883
|
+
try {
|
|
5884
|
+
const raw = typeof input === "string" || input instanceof URL ? String(input) : input?.url ?? input?.href;
|
|
5885
|
+
if (typeof raw !== "string" && !(raw instanceof URL)) return null;
|
|
5886
|
+
const url = new URL(String(raw)).href;
|
|
5887
|
+
const method = String(init?.method ?? input?.method ?? "GET").toUpperCase();
|
|
5888
|
+
return { url, method };
|
|
5889
|
+
} catch {
|
|
5890
|
+
return null;
|
|
5665
5891
|
}
|
|
5666
|
-
|
|
5667
|
-
|
|
5668
|
-
|
|
5669
|
-
|
|
5670
|
-
|
|
5671
|
-
setOwn(cookies, pair.slice(0, idx).trim(), pair.slice(idx + 1).trim());
|
|
5892
|
+
}
|
|
5893
|
+
function anyRefuses(screens, url, host, method) {
|
|
5894
|
+
let refused = false;
|
|
5895
|
+
for (const screen of screens) {
|
|
5896
|
+
if (screen.block(url, host, method)) refused = true;
|
|
5672
5897
|
}
|
|
5673
|
-
return
|
|
5898
|
+
return refused;
|
|
5674
5899
|
}
|
|
5675
|
-
|
|
5676
|
-
// src/protect/egress.js
|
|
5677
5900
|
async function installEgressGuard({ shouldBlock, onBlock, onSkip, dnsScreen = true, lookup, allowHosts } = {}) {
|
|
5678
|
-
const restores = [];
|
|
5679
5901
|
if (typeof shouldBlock !== "function") return () => {
|
|
5680
5902
|
};
|
|
5681
5903
|
const exempt = new Set((allowHosts ?? []).map((h) => String(h).toLowerCase()));
|
|
@@ -5720,6 +5942,9 @@ async function installEgressGuard({ shouldBlock, onBlock, onSkip, dnsScreen = tr
|
|
|
5720
5942
|
resolve(false);
|
|
5721
5943
|
}
|
|
5722
5944
|
});
|
|
5945
|
+
const registry = egressRegistry();
|
|
5946
|
+
const own = { block, prescreen: resolvesToDisallowed, dns: screen, skip };
|
|
5947
|
+
registry.screens.add(own);
|
|
5723
5948
|
const originalFetch = globalThis.fetch;
|
|
5724
5949
|
if (typeof originalFetch === "function" && !originalFetch.__patchstackGuarded) {
|
|
5725
5950
|
const REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
|
|
@@ -5792,15 +6017,19 @@ async function installEgressGuard({ shouldBlock, onBlock, onSkip, dnsScreen = tr
|
|
|
5792
6017
|
} catch {
|
|
5793
6018
|
host = null;
|
|
5794
6019
|
}
|
|
5795
|
-
|
|
5796
|
-
|
|
5797
|
-
|
|
6020
|
+
const screens = [...registry.screens];
|
|
6021
|
+
if (anyRefuses(screens, u, host, method)) throw refusal(host ?? u);
|
|
6022
|
+
const resolved = await Promise.all(screens.map((each) => each.prescreen(u, host, method)));
|
|
6023
|
+
if (resolved.includes(true)) throw refusal(host ?? u);
|
|
5798
6024
|
};
|
|
5799
6025
|
const guarded = async (input, init) => {
|
|
5800
6026
|
let cur;
|
|
5801
6027
|
try {
|
|
5802
6028
|
cur = new Request(input, { ...init || {}, redirect: "manual" });
|
|
5803
6029
|
} catch {
|
|
6030
|
+
const destination = readableDestination(input, init);
|
|
6031
|
+
if (destination) await screenUrl(destination.url, destination.method);
|
|
6032
|
+
else for (const each of registry.screens) each.skip("unrecognised-request", {});
|
|
5804
6033
|
return originalFetch(input, init);
|
|
5805
6034
|
}
|
|
5806
6035
|
const callerRedirect = init && init.redirect || input && input.redirect || "follow";
|
|
@@ -5811,13 +6040,12 @@ async function installEgressGuard({ shouldBlock, onBlock, onSkip, dnsScreen = tr
|
|
|
5811
6040
|
const headers = new Headers(cur.headers);
|
|
5812
6041
|
const signal = cur.signal;
|
|
5813
6042
|
const replay = captureReplayBody(cur);
|
|
6043
|
+
const { body: _callerBody, ...callerOptions } = init || {};
|
|
5814
6044
|
let body;
|
|
5815
6045
|
for (let hop = 0; ; hop++) {
|
|
5816
6046
|
let resp;
|
|
5817
6047
|
try {
|
|
5818
|
-
resp = await originalFetch(
|
|
5819
|
-
hop === 0 ? cur : new Request(url, { method, headers, body, redirect: "manual", signal })
|
|
5820
|
-
);
|
|
6048
|
+
resp = hop === 0 ? await originalFetch(cur, { ...callerOptions, redirect: "manual" }) : await originalFetch(url, { ...callerOptions, method, headers, body, redirect: "manual", signal });
|
|
5821
6049
|
} catch (error) {
|
|
5822
6050
|
replay.cancel();
|
|
5823
6051
|
throw error;
|
|
@@ -5825,7 +6053,7 @@ async function installEgressGuard({ shouldBlock, onBlock, onSkip, dnsScreen = tr
|
|
|
5825
6053
|
const location = REDIRECT_STATUSES.has(resp.status) ? resp.headers.get("location") : null;
|
|
5826
6054
|
if (!location) {
|
|
5827
6055
|
replay.cancel();
|
|
5828
|
-
return resp;
|
|
6056
|
+
return hop === 0 ? resp : markRedirected(resp);
|
|
5829
6057
|
}
|
|
5830
6058
|
await discardResponseBody(resp);
|
|
5831
6059
|
if (hop >= MAX_REDIRECTS) {
|
|
@@ -5858,8 +6086,12 @@ async function installEgressGuard({ shouldBlock, onBlock, onSkip, dnsScreen = tr
|
|
|
5858
6086
|
};
|
|
5859
6087
|
guarded.__patchstackGuarded = true;
|
|
5860
6088
|
globalThis.fetch = guarded;
|
|
5861
|
-
|
|
5862
|
-
|
|
6089
|
+
registry.surfaces.set(guarded, {
|
|
6090
|
+
release() {
|
|
6091
|
+
if (globalThis.fetch !== guarded) return false;
|
|
6092
|
+
globalThis.fetch = originalFetch;
|
|
6093
|
+
return true;
|
|
6094
|
+
}
|
|
5863
6095
|
});
|
|
5864
6096
|
}
|
|
5865
6097
|
let syncBuiltins = () => {
|
|
@@ -5873,12 +6105,16 @@ async function installEgressGuard({ shouldBlock, onBlock, onSkip, dnsScreen = tr
|
|
|
5873
6105
|
for (const moduleName of ["node:http", "node:https"]) {
|
|
5874
6106
|
try {
|
|
5875
6107
|
const mod = await import(moduleName);
|
|
5876
|
-
|
|
5877
|
-
|
|
6108
|
+
if (registry.surfaces.has(moduleName)) continue;
|
|
6109
|
+
const release = patchHttpModule(mod.default ?? mod, registry);
|
|
6110
|
+
if (release) {
|
|
5878
6111
|
patchedAny = true;
|
|
5879
|
-
|
|
5880
|
-
|
|
5881
|
-
|
|
6112
|
+
registry.surfaces.set(moduleName, {
|
|
6113
|
+
release() {
|
|
6114
|
+
if (!release()) return false;
|
|
6115
|
+
syncBuiltins();
|
|
6116
|
+
return true;
|
|
6117
|
+
}
|
|
5882
6118
|
});
|
|
5883
6119
|
}
|
|
5884
6120
|
} catch {
|
|
@@ -5886,27 +6122,41 @@ async function installEgressGuard({ shouldBlock, onBlock, onSkip, dnsScreen = tr
|
|
|
5886
6122
|
}
|
|
5887
6123
|
if (patchedAny) syncBuiltins();
|
|
5888
6124
|
return () => {
|
|
5889
|
-
|
|
6125
|
+
registry.screens.delete(own);
|
|
6126
|
+
if (registry.screens.size > 0) return;
|
|
6127
|
+
for (const [name, surface] of registry.surfaces) {
|
|
5890
6128
|
try {
|
|
5891
|
-
|
|
6129
|
+
if (surface.release()) registry.surfaces.delete(name);
|
|
5892
6130
|
} catch {
|
|
5893
6131
|
}
|
|
5894
6132
|
}
|
|
5895
6133
|
};
|
|
5896
6134
|
}
|
|
5897
|
-
function
|
|
6135
|
+
function markRedirected(response) {
|
|
6136
|
+
try {
|
|
6137
|
+
Object.defineProperty(response, "redirected", { value: true, configurable: true });
|
|
6138
|
+
} catch {
|
|
6139
|
+
}
|
|
6140
|
+
return response;
|
|
6141
|
+
}
|
|
6142
|
+
function patchHttpModule(http, registry) {
|
|
5898
6143
|
if (!http || typeof http.request !== "function" || http.__patchstackGuarded) return null;
|
|
5899
6144
|
const originalRequest = http.request;
|
|
5900
6145
|
const originalGet = http.get;
|
|
5901
6146
|
const OriginalClientRequest = typeof http.ClientRequest === "function" ? http.ClientRequest : null;
|
|
5902
6147
|
const guardArgs = (args) => {
|
|
5903
6148
|
const target = extractHttpTarget(args);
|
|
5904
|
-
if (target
|
|
5905
|
-
|
|
5906
|
-
|
|
5907
|
-
|
|
6149
|
+
if (!target) return args;
|
|
6150
|
+
const screens = [...registry.screens];
|
|
6151
|
+
if (anyRefuses(screens, target.url, target.host, target.method)) throw refusal(target.host ?? target.url);
|
|
6152
|
+
const resolving = target.host ? screens.filter((each) => each.dns && each.dns.isIP(target.host) === 0 && !each.dns.isExempt(target.host)) : [];
|
|
6153
|
+
if (resolving.length > 0) {
|
|
6154
|
+
const block = (url, host, method) => anyRefuses(resolving, url, host, method);
|
|
6155
|
+
const skip = (reason, detail) => {
|
|
6156
|
+
for (const each of resolving) each.skip(reason, detail);
|
|
6157
|
+
};
|
|
5908
6158
|
try {
|
|
5909
|
-
return withScreeningLookup(args, target, block,
|
|
6159
|
+
return withScreeningLookup(args, target, block, resolving[0].dns.lookup, skip);
|
|
5910
6160
|
} catch {
|
|
5911
6161
|
}
|
|
5912
6162
|
}
|
|
@@ -5934,10 +6184,14 @@ function patchHttpModule(http, block, screen, skip) {
|
|
|
5934
6184
|
}
|
|
5935
6185
|
http.__patchstackGuarded = true;
|
|
5936
6186
|
return () => {
|
|
5937
|
-
if (http.request
|
|
5938
|
-
if (guardedGet && http.get
|
|
5939
|
-
if (GuardedClientRequest && http.ClientRequest
|
|
6187
|
+
if (http.request !== guardedRequest) return false;
|
|
6188
|
+
if (guardedGet && http.get !== guardedGet) return false;
|
|
6189
|
+
if (GuardedClientRequest && http.ClientRequest !== GuardedClientRequest) return false;
|
|
6190
|
+
http.request = originalRequest;
|
|
6191
|
+
if (guardedGet) http.get = originalGet;
|
|
6192
|
+
if (GuardedClientRequest) http.ClientRequest = OriginalClientRequest;
|
|
5940
6193
|
delete http.__patchstackGuarded;
|
|
6194
|
+
return true;
|
|
5941
6195
|
};
|
|
5942
6196
|
}
|
|
5943
6197
|
function extractHttpTarget(args) {
|
|
@@ -6838,7 +7092,7 @@ function makeRefreshHandler(tick, secret) {
|
|
|
6838
7092
|
return async (request) => {
|
|
6839
7093
|
if (!secret) return new Response("not found", { status: 404 });
|
|
6840
7094
|
const provided = request?.headers?.get?.("x-patchstack-refresh") ?? null;
|
|
6841
|
-
if (provided
|
|
7095
|
+
if (!await sameSecret(provided, secret)) return new Response("forbidden", { status: 403 });
|
|
6842
7096
|
let refreshed = true;
|
|
6843
7097
|
try {
|
|
6844
7098
|
const status = await runOnce();
|
|
@@ -6849,6 +7103,33 @@ function makeRefreshHandler(tick, secret) {
|
|
|
6849
7103
|
return new Response(JSON.stringify({ refreshed }), { status: 200, headers: { "content-type": "application/json" } });
|
|
6850
7104
|
};
|
|
6851
7105
|
}
|
|
7106
|
+
async function sameSecret(provided, secret) {
|
|
7107
|
+
if (typeof provided !== "string" || typeof secret !== "string") return false;
|
|
7108
|
+
const subtle = globalThis.crypto?.subtle;
|
|
7109
|
+
if (subtle && typeof TextEncoder === "function") {
|
|
7110
|
+
try {
|
|
7111
|
+
const encoder2 = new TextEncoder();
|
|
7112
|
+
const [a, b] = await Promise.all([
|
|
7113
|
+
subtle.digest("SHA-256", encoder2.encode(provided)),
|
|
7114
|
+
subtle.digest("SHA-256", encoder2.encode(secret))
|
|
7115
|
+
]);
|
|
7116
|
+
return equalBytes(new Uint8Array(a), new Uint8Array(b));
|
|
7117
|
+
} catch {
|
|
7118
|
+
}
|
|
7119
|
+
}
|
|
7120
|
+
let diff = provided.length ^ secret.length;
|
|
7121
|
+
const length = Math.max(provided.length, secret.length);
|
|
7122
|
+
for (let i = 0; i < length; i++) {
|
|
7123
|
+
diff |= (provided.charCodeAt(i) || 0) ^ (secret.charCodeAt(i) || 0);
|
|
7124
|
+
}
|
|
7125
|
+
return diff === 0;
|
|
7126
|
+
}
|
|
7127
|
+
function equalBytes(a, b) {
|
|
7128
|
+
if (a.length !== b.length) return false;
|
|
7129
|
+
let diff = 0;
|
|
7130
|
+
for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i];
|
|
7131
|
+
return diff === 0;
|
|
7132
|
+
}
|
|
6852
7133
|
|
|
6853
7134
|
// src/protect/detections.js
|
|
6854
7135
|
init_pulse_token();
|
|
@@ -7584,9 +7865,180 @@ function hardensWithoutBody(rule) {
|
|
|
7584
7865
|
if (!rule || !HEADER_ACTIONS.includes(rule.action)) return false;
|
|
7585
7866
|
const { parameters, complete } = readRuleParameters(rule);
|
|
7586
7867
|
if (!complete) return false;
|
|
7587
|
-
return parameters.every(
|
|
7588
|
-
|
|
7868
|
+
return parameters.every(bodyIndependent);
|
|
7869
|
+
}
|
|
7870
|
+
function readsOnlyResponseHeaders(rule) {
|
|
7871
|
+
if (!rule) return false;
|
|
7872
|
+
const { parameters, complete } = readRuleParameters(rule);
|
|
7873
|
+
if (!complete || !parameters.some(isHeaderParameter)) return false;
|
|
7874
|
+
return parameters.every(bodyIndependent);
|
|
7875
|
+
}
|
|
7876
|
+
function isHeaderParameter(parameter) {
|
|
7877
|
+
return parameter === "response.headers" || parameter.startsWith(BODY_INDEPENDENT_PREFIX);
|
|
7878
|
+
}
|
|
7879
|
+
function bodyIndependent(parameter) {
|
|
7880
|
+
return BODY_INDEPENDENT.includes(parameter) || parameter.startsWith(BODY_INDEPENDENT_PREFIX);
|
|
7881
|
+
}
|
|
7882
|
+
|
|
7883
|
+
// src/protect/supabase-guard.js
|
|
7884
|
+
var GUARD_PATH = "/_patchstack/guard";
|
|
7885
|
+
var DEFAULT_MAX_BODY_BYTES = 1024 * 1024;
|
|
7886
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
7887
|
+
var REQUEST_HEADER_DENYLIST = /* @__PURE__ */ new Set([
|
|
7888
|
+
"connection",
|
|
7889
|
+
"cookie",
|
|
7890
|
+
"cookie2",
|
|
7891
|
+
"forwarded",
|
|
7892
|
+
"host",
|
|
7893
|
+
"keep-alive",
|
|
7894
|
+
"proxy-authenticate",
|
|
7895
|
+
"proxy-authorization",
|
|
7896
|
+
"te",
|
|
7897
|
+
"trailer",
|
|
7898
|
+
"transfer-encoding",
|
|
7899
|
+
"upgrade",
|
|
7900
|
+
"x-forwarded-for",
|
|
7901
|
+
"x-forwarded-host",
|
|
7902
|
+
"x-forwarded-port",
|
|
7903
|
+
"x-forwarded-proto",
|
|
7904
|
+
"x-ps-target",
|
|
7905
|
+
"x-real-ip"
|
|
7906
|
+
]);
|
|
7907
|
+
var RESPONSE_HEADER_DENYLIST = /* @__PURE__ */ new Set([
|
|
7908
|
+
"connection",
|
|
7909
|
+
"content-encoding",
|
|
7910
|
+
"content-length",
|
|
7911
|
+
"keep-alive",
|
|
7912
|
+
"proxy-authenticate",
|
|
7913
|
+
"proxy-authorization",
|
|
7914
|
+
"set-cookie",
|
|
7915
|
+
"set-cookie2",
|
|
7916
|
+
"te",
|
|
7917
|
+
"trailer",
|
|
7918
|
+
"transfer-encoding",
|
|
7919
|
+
"upgrade"
|
|
7920
|
+
]);
|
|
7921
|
+
var BodyTooLargeError = class extends Error {
|
|
7922
|
+
};
|
|
7923
|
+
async function boundedBody(request, maxBytes) {
|
|
7924
|
+
const stated2 = Number(request.headers.get("content-length"));
|
|
7925
|
+
if (Number.isFinite(stated2) && stated2 > maxBytes) throw new BodyTooLargeError();
|
|
7926
|
+
if (!request.body) return void 0;
|
|
7927
|
+
const reader = request.body.getReader();
|
|
7928
|
+
const chunks = [];
|
|
7929
|
+
let total = 0;
|
|
7930
|
+
try {
|
|
7931
|
+
for (; ; ) {
|
|
7932
|
+
const { done, value } = await reader.read();
|
|
7933
|
+
if (done) break;
|
|
7934
|
+
total += value.byteLength;
|
|
7935
|
+
if (total > maxBytes) {
|
|
7936
|
+
void reader.cancel().catch(() => {
|
|
7937
|
+
});
|
|
7938
|
+
throw new BodyTooLargeError();
|
|
7939
|
+
}
|
|
7940
|
+
chunks.push(value);
|
|
7941
|
+
}
|
|
7942
|
+
} finally {
|
|
7943
|
+
reader.releaseLock();
|
|
7944
|
+
}
|
|
7945
|
+
const body = new Uint8Array(total);
|
|
7946
|
+
let offset = 0;
|
|
7947
|
+
for (const chunk of chunks) {
|
|
7948
|
+
body.set(chunk, offset);
|
|
7949
|
+
offset += chunk.byteLength;
|
|
7950
|
+
}
|
|
7951
|
+
return body;
|
|
7952
|
+
}
|
|
7953
|
+
function requestHeaders(input) {
|
|
7954
|
+
const output = new Headers();
|
|
7955
|
+
const connectionTokens = new Set(
|
|
7956
|
+
(input.get("connection") ?? "").split(",").map((value) => value.trim().toLowerCase()).filter(Boolean)
|
|
7589
7957
|
);
|
|
7958
|
+
input.forEach((value, key) => {
|
|
7959
|
+
const lower = key.toLowerCase();
|
|
7960
|
+
if (!REQUEST_HEADER_DENYLIST.has(lower) && !connectionTokens.has(lower)) output.append(key, value);
|
|
7961
|
+
});
|
|
7962
|
+
return output;
|
|
7963
|
+
}
|
|
7964
|
+
function responseHeaders(input) {
|
|
7965
|
+
const output = new Headers();
|
|
7966
|
+
const connectionTokens = new Set(
|
|
7967
|
+
(input.get("connection") ?? "").split(",").map((value) => value.trim().toLowerCase()).filter(Boolean)
|
|
7968
|
+
);
|
|
7969
|
+
input.forEach((value, key) => {
|
|
7970
|
+
const lower = key.toLowerCase();
|
|
7971
|
+
if (!RESPONSE_HEADER_DENYLIST.has(lower) && !connectionTokens.has(lower)) output.append(key, value);
|
|
7972
|
+
});
|
|
7973
|
+
return output;
|
|
7974
|
+
}
|
|
7975
|
+
var SOURCE_REQUEST = /* @__PURE__ */ Symbol.for("@patchstack/connect.sourceRequest");
|
|
7976
|
+
function createSupabaseGuard({
|
|
7977
|
+
protection,
|
|
7978
|
+
supabaseUrl,
|
|
7979
|
+
fetchImpl = fetch,
|
|
7980
|
+
maxBodyBytes = DEFAULT_MAX_BODY_BYTES,
|
|
7981
|
+
timeoutMs = DEFAULT_TIMEOUT_MS
|
|
7982
|
+
}) {
|
|
7983
|
+
const guard = protection.fetchGuard();
|
|
7984
|
+
const allowedOrigin = supabaseUrl ? new URL(supabaseUrl).origin : null;
|
|
7985
|
+
const bodyLimit = Number.isFinite(maxBodyBytes) && maxBodyBytes > 0 ? maxBodyBytes : DEFAULT_MAX_BODY_BYTES;
|
|
7986
|
+
const upstreamTimeout = Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : DEFAULT_TIMEOUT_MS;
|
|
7987
|
+
return async function handleGuardRequest(request, ...hostArgs) {
|
|
7988
|
+
const target = request.headers.get("x-ps-target");
|
|
7989
|
+
if (!target) return new Response("patchstack: missing x-ps-target", { status: 400 });
|
|
7990
|
+
let targetUrl;
|
|
7991
|
+
try {
|
|
7992
|
+
targetUrl = new URL(target);
|
|
7993
|
+
} catch {
|
|
7994
|
+
return new Response("patchstack: invalid target", { status: 400 });
|
|
7995
|
+
}
|
|
7996
|
+
if (!allowedOrigin || targetUrl.protocol !== "https:" || targetUrl.origin !== allowedOrigin) {
|
|
7997
|
+
return new Response("patchstack: target not allowed", { status: 403 });
|
|
7998
|
+
}
|
|
7999
|
+
const hasBody = request.method !== "GET" && request.method !== "HEAD";
|
|
8000
|
+
let body;
|
|
8001
|
+
try {
|
|
8002
|
+
body = hasBody ? await boundedBody(request, bodyLimit) : void 0;
|
|
8003
|
+
} catch (error) {
|
|
8004
|
+
if (error instanceof BodyTooLargeError) {
|
|
8005
|
+
return new Response("patchstack: request body too large", { status: 413 });
|
|
8006
|
+
}
|
|
8007
|
+
throw error;
|
|
8008
|
+
}
|
|
8009
|
+
const forwardHeaders = requestHeaders(request.headers);
|
|
8010
|
+
const evalReq = new Request(targetUrl.toString(), {
|
|
8011
|
+
method: request.method,
|
|
8012
|
+
// Inspect what the browser sent, including app-origin cookies; only the forwarded copy is
|
|
8013
|
+
// sanitized at the cross-origin boundary below.
|
|
8014
|
+
headers: request.headers,
|
|
8015
|
+
body
|
|
8016
|
+
});
|
|
8017
|
+
Object.defineProperty(evalReq, SOURCE_REQUEST, { value: request });
|
|
8018
|
+
const blocked = await guard(evalReq, ...hostArgs);
|
|
8019
|
+
if (blocked) return blocked;
|
|
8020
|
+
let upstream;
|
|
8021
|
+
try {
|
|
8022
|
+
upstream = await fetchImpl(targetUrl.toString(), {
|
|
8023
|
+
method: request.method,
|
|
8024
|
+
headers: forwardHeaders,
|
|
8025
|
+
body,
|
|
8026
|
+
redirect: "manual",
|
|
8027
|
+
signal: AbortSignal.timeout(upstreamTimeout)
|
|
8028
|
+
});
|
|
8029
|
+
} catch (error) {
|
|
8030
|
+
if (error?.name === "TimeoutError" || error?.name === "AbortError") {
|
|
8031
|
+
return new Response("patchstack: upstream timed out", { status: 504 });
|
|
8032
|
+
}
|
|
8033
|
+
throw error;
|
|
8034
|
+
}
|
|
8035
|
+
const forwarded = new Response(upstream.body, {
|
|
8036
|
+
status: upstream.status,
|
|
8037
|
+
statusText: upstream.statusText,
|
|
8038
|
+
headers: responseHeaders(upstream.headers)
|
|
8039
|
+
});
|
|
8040
|
+
return protection.screenResponse ? protection.screenResponse(forwarded, evalReq) : forwarded;
|
|
8041
|
+
};
|
|
7590
8042
|
}
|
|
7591
8043
|
|
|
7592
8044
|
// src/protect/firewall-log.js
|
|
@@ -7637,8 +8089,13 @@ function resolveApiBase(pulseOrManifestUrl) {
|
|
|
7637
8089
|
function createFirewallLogReporter(opts) {
|
|
7638
8090
|
const creds = parseApiKey(opts.apiKey);
|
|
7639
8091
|
if (!creds) {
|
|
7640
|
-
return {
|
|
7641
|
-
|
|
8092
|
+
return {
|
|
8093
|
+
record() {
|
|
8094
|
+
},
|
|
8095
|
+
flush: () => Promise.resolve(),
|
|
8096
|
+
stop: () => Promise.resolve(),
|
|
8097
|
+
health: () => ({ recorded: 0, delivered: 0, failed: 0, dropped: 0, queued: 0 })
|
|
8098
|
+
};
|
|
7642
8099
|
}
|
|
7643
8100
|
const apiBase = safeBaseUrl(opts.apiBase, DEFAULT_API_BASE, "block-log API").replace(/\/$/, "");
|
|
7644
8101
|
const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
@@ -7651,6 +8108,19 @@ function createFirewallLogReporter(opts) {
|
|
|
7651
8108
|
let drainPromise = null;
|
|
7652
8109
|
let activeController = null;
|
|
7653
8110
|
let ended = false;
|
|
8111
|
+
let recorded = 0;
|
|
8112
|
+
let delivered = 0;
|
|
8113
|
+
let failed = 0;
|
|
8114
|
+
let dropped = 0;
|
|
8115
|
+
let activeBatch = null;
|
|
8116
|
+
const settle = (sent, outcome) => {
|
|
8117
|
+
if (sent.counted) return;
|
|
8118
|
+
sent.counted = true;
|
|
8119
|
+
if (outcome === "delivered") delivered += sent.size;
|
|
8120
|
+
else if (outcome === "failed") failed += sent.size;
|
|
8121
|
+
else dropped += sent.size;
|
|
8122
|
+
if (activeBatch === sent) activeBatch = null;
|
|
8123
|
+
};
|
|
7654
8124
|
let cachedToken = null;
|
|
7655
8125
|
let tokenInflight = null;
|
|
7656
8126
|
const fetchAccessToken = async (signal) => {
|
|
@@ -7704,10 +8174,16 @@ function createFirewallLogReporter(opts) {
|
|
|
7704
8174
|
const attemptTimer = setTimeout(() => controller?.abort(), ATTEMPT_TIMEOUT_MS2);
|
|
7705
8175
|
attemptTimer?.unref?.();
|
|
7706
8176
|
let entry;
|
|
8177
|
+
const sent = { size: batch.length, counted: false };
|
|
8178
|
+
activeBatch = sent;
|
|
7707
8179
|
entry = (async () => {
|
|
7708
8180
|
try {
|
|
7709
8181
|
const token = await fetchAccessToken(controller?.signal);
|
|
7710
|
-
if (
|
|
8182
|
+
if (ended) return;
|
|
8183
|
+
if (!token) {
|
|
8184
|
+
settle(sent, "failed");
|
|
8185
|
+
return;
|
|
8186
|
+
}
|
|
7711
8187
|
const body = new URLSearchParams();
|
|
7712
8188
|
body.set("type", "firewall");
|
|
7713
8189
|
body.set("logs", JSON.stringify(batch));
|
|
@@ -7724,10 +8200,11 @@ function createFirewallLogReporter(opts) {
|
|
|
7724
8200
|
// Both phases carry the attempt controller, so slow transports cannot accumulate work.
|
|
7725
8201
|
...controller ? { signal: controller.signal } : {}
|
|
7726
8202
|
});
|
|
7727
|
-
|
|
7728
|
-
|
|
8203
|
+
const res = p && typeof p.then === "function" ? await p.catch(() => null) : p;
|
|
8204
|
+
settle(sent, res && res.ok ? "delivered" : "failed");
|
|
7729
8205
|
} catch {
|
|
7730
8206
|
} finally {
|
|
8207
|
+
settle(sent, "failed");
|
|
7731
8208
|
clearTimeout(attemptTimer);
|
|
7732
8209
|
if (activeController === controller) activeController = null;
|
|
7733
8210
|
if (inFlight === entry) inFlight = null;
|
|
@@ -7754,7 +8231,11 @@ function createFirewallLogReporter(opts) {
|
|
|
7754
8231
|
if (stopped) return;
|
|
7755
8232
|
const fid = event?.rule?.id;
|
|
7756
8233
|
if (fid === void 0 || fid === null || fid === "") return;
|
|
7757
|
-
if (queue.length >= MAX_QUEUE2)
|
|
8234
|
+
if (queue.length >= MAX_QUEUE2) {
|
|
8235
|
+
dropped++;
|
|
8236
|
+
return;
|
|
8237
|
+
}
|
|
8238
|
+
recorded++;
|
|
7758
8239
|
queue.push({
|
|
7759
8240
|
fid,
|
|
7760
8241
|
method: event.method ?? null,
|
|
@@ -7770,6 +8251,10 @@ function createFirewallLogReporter(opts) {
|
|
|
7770
8251
|
if (!timer) timer = setTimeout(flush, flushMs);
|
|
7771
8252
|
},
|
|
7772
8253
|
flush,
|
|
8254
|
+
/** Where the records went so far; see the counters above. `queued` is what is waiting now. */
|
|
8255
|
+
health() {
|
|
8256
|
+
return { recorded, delivered, failed, dropped, queued: queue.length };
|
|
8257
|
+
},
|
|
7773
8258
|
/**
|
|
7774
8259
|
* Stop, and hand back a wait for what was outstanding.
|
|
7775
8260
|
*
|
|
@@ -7788,6 +8273,8 @@ function createFirewallLogReporter(opts) {
|
|
|
7788
8273
|
ended = true;
|
|
7789
8274
|
activeController?.abort();
|
|
7790
8275
|
activeController = null;
|
|
8276
|
+
if (activeBatch) settle(activeBatch, "dropped");
|
|
8277
|
+
dropped += queue.length;
|
|
7791
8278
|
queue = [];
|
|
7792
8279
|
inFlight = null;
|
|
7793
8280
|
};
|
|
@@ -7824,165 +8311,6 @@ function telemetryEnabled() {
|
|
|
7824
8311
|
return !/^(0|false|off|no)$/i.test(String(v));
|
|
7825
8312
|
}
|
|
7826
8313
|
|
|
7827
|
-
// src/protect/supabase-guard.js
|
|
7828
|
-
var GUARD_PATH = "/_patchstack/guard";
|
|
7829
|
-
var DEFAULT_MAX_BODY_BYTES = 1024 * 1024;
|
|
7830
|
-
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
7831
|
-
var REQUEST_HEADER_DENYLIST = /* @__PURE__ */ new Set([
|
|
7832
|
-
"connection",
|
|
7833
|
-
"cookie",
|
|
7834
|
-
"cookie2",
|
|
7835
|
-
"forwarded",
|
|
7836
|
-
"host",
|
|
7837
|
-
"keep-alive",
|
|
7838
|
-
"proxy-authenticate",
|
|
7839
|
-
"proxy-authorization",
|
|
7840
|
-
"te",
|
|
7841
|
-
"trailer",
|
|
7842
|
-
"transfer-encoding",
|
|
7843
|
-
"upgrade",
|
|
7844
|
-
"x-forwarded-for",
|
|
7845
|
-
"x-forwarded-host",
|
|
7846
|
-
"x-forwarded-port",
|
|
7847
|
-
"x-forwarded-proto",
|
|
7848
|
-
"x-ps-target",
|
|
7849
|
-
"x-real-ip"
|
|
7850
|
-
]);
|
|
7851
|
-
var RESPONSE_HEADER_DENYLIST = /* @__PURE__ */ new Set([
|
|
7852
|
-
"connection",
|
|
7853
|
-
"content-encoding",
|
|
7854
|
-
"content-length",
|
|
7855
|
-
"keep-alive",
|
|
7856
|
-
"proxy-authenticate",
|
|
7857
|
-
"proxy-authorization",
|
|
7858
|
-
"set-cookie",
|
|
7859
|
-
"set-cookie2",
|
|
7860
|
-
"te",
|
|
7861
|
-
"trailer",
|
|
7862
|
-
"transfer-encoding",
|
|
7863
|
-
"upgrade"
|
|
7864
|
-
]);
|
|
7865
|
-
var BodyTooLargeError = class extends Error {
|
|
7866
|
-
};
|
|
7867
|
-
async function boundedBody(request, maxBytes) {
|
|
7868
|
-
const stated2 = Number(request.headers.get("content-length"));
|
|
7869
|
-
if (Number.isFinite(stated2) && stated2 > maxBytes) throw new BodyTooLargeError();
|
|
7870
|
-
if (!request.body) return void 0;
|
|
7871
|
-
const reader = request.body.getReader();
|
|
7872
|
-
const chunks = [];
|
|
7873
|
-
let total = 0;
|
|
7874
|
-
try {
|
|
7875
|
-
for (; ; ) {
|
|
7876
|
-
const { done, value } = await reader.read();
|
|
7877
|
-
if (done) break;
|
|
7878
|
-
total += value.byteLength;
|
|
7879
|
-
if (total > maxBytes) {
|
|
7880
|
-
void reader.cancel().catch(() => {
|
|
7881
|
-
});
|
|
7882
|
-
throw new BodyTooLargeError();
|
|
7883
|
-
}
|
|
7884
|
-
chunks.push(value);
|
|
7885
|
-
}
|
|
7886
|
-
} finally {
|
|
7887
|
-
reader.releaseLock();
|
|
7888
|
-
}
|
|
7889
|
-
const body = new Uint8Array(total);
|
|
7890
|
-
let offset = 0;
|
|
7891
|
-
for (const chunk of chunks) {
|
|
7892
|
-
body.set(chunk, offset);
|
|
7893
|
-
offset += chunk.byteLength;
|
|
7894
|
-
}
|
|
7895
|
-
return body;
|
|
7896
|
-
}
|
|
7897
|
-
function requestHeaders(input) {
|
|
7898
|
-
const output = new Headers();
|
|
7899
|
-
const connectionTokens = new Set(
|
|
7900
|
-
(input.get("connection") ?? "").split(",").map((value) => value.trim().toLowerCase()).filter(Boolean)
|
|
7901
|
-
);
|
|
7902
|
-
input.forEach((value, key) => {
|
|
7903
|
-
const lower = key.toLowerCase();
|
|
7904
|
-
if (!REQUEST_HEADER_DENYLIST.has(lower) && !connectionTokens.has(lower)) output.append(key, value);
|
|
7905
|
-
});
|
|
7906
|
-
return output;
|
|
7907
|
-
}
|
|
7908
|
-
function responseHeaders(input) {
|
|
7909
|
-
const output = new Headers();
|
|
7910
|
-
const connectionTokens = new Set(
|
|
7911
|
-
(input.get("connection") ?? "").split(",").map((value) => value.trim().toLowerCase()).filter(Boolean)
|
|
7912
|
-
);
|
|
7913
|
-
input.forEach((value, key) => {
|
|
7914
|
-
const lower = key.toLowerCase();
|
|
7915
|
-
if (!RESPONSE_HEADER_DENYLIST.has(lower) && !connectionTokens.has(lower)) output.append(key, value);
|
|
7916
|
-
});
|
|
7917
|
-
return output;
|
|
7918
|
-
}
|
|
7919
|
-
function createSupabaseGuard({
|
|
7920
|
-
protection,
|
|
7921
|
-
supabaseUrl,
|
|
7922
|
-
fetchImpl = fetch,
|
|
7923
|
-
maxBodyBytes = DEFAULT_MAX_BODY_BYTES,
|
|
7924
|
-
timeoutMs = DEFAULT_TIMEOUT_MS
|
|
7925
|
-
}) {
|
|
7926
|
-
const guard = protection.fetchGuard();
|
|
7927
|
-
const allowedOrigin = supabaseUrl ? new URL(supabaseUrl).origin : null;
|
|
7928
|
-
const bodyLimit = Number.isFinite(maxBodyBytes) && maxBodyBytes > 0 ? maxBodyBytes : DEFAULT_MAX_BODY_BYTES;
|
|
7929
|
-
const upstreamTimeout = Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : DEFAULT_TIMEOUT_MS;
|
|
7930
|
-
return async function handleGuardRequest(request) {
|
|
7931
|
-
const target = request.headers.get("x-ps-target");
|
|
7932
|
-
if (!target) return new Response("patchstack: missing x-ps-target", { status: 400 });
|
|
7933
|
-
let targetUrl;
|
|
7934
|
-
try {
|
|
7935
|
-
targetUrl = new URL(target);
|
|
7936
|
-
} catch {
|
|
7937
|
-
return new Response("patchstack: invalid target", { status: 400 });
|
|
7938
|
-
}
|
|
7939
|
-
if (!allowedOrigin || targetUrl.protocol !== "https:" || targetUrl.origin !== allowedOrigin) {
|
|
7940
|
-
return new Response("patchstack: target not allowed", { status: 403 });
|
|
7941
|
-
}
|
|
7942
|
-
const hasBody = request.method !== "GET" && request.method !== "HEAD";
|
|
7943
|
-
let body;
|
|
7944
|
-
try {
|
|
7945
|
-
body = hasBody ? await boundedBody(request, bodyLimit) : void 0;
|
|
7946
|
-
} catch (error) {
|
|
7947
|
-
if (error instanceof BodyTooLargeError) {
|
|
7948
|
-
return new Response("patchstack: request body too large", { status: 413 });
|
|
7949
|
-
}
|
|
7950
|
-
throw error;
|
|
7951
|
-
}
|
|
7952
|
-
const forwardHeaders = requestHeaders(request.headers);
|
|
7953
|
-
const evalReq = new Request(targetUrl.toString(), {
|
|
7954
|
-
method: request.method,
|
|
7955
|
-
// Inspect what the browser sent, including app-origin cookies; only the forwarded copy is
|
|
7956
|
-
// sanitized at the cross-origin boundary below.
|
|
7957
|
-
headers: request.headers,
|
|
7958
|
-
body
|
|
7959
|
-
});
|
|
7960
|
-
const blocked = await guard(evalReq);
|
|
7961
|
-
if (blocked) return blocked;
|
|
7962
|
-
let upstream;
|
|
7963
|
-
try {
|
|
7964
|
-
upstream = await fetchImpl(targetUrl.toString(), {
|
|
7965
|
-
method: request.method,
|
|
7966
|
-
headers: forwardHeaders,
|
|
7967
|
-
body,
|
|
7968
|
-
redirect: "manual",
|
|
7969
|
-
signal: AbortSignal.timeout(upstreamTimeout)
|
|
7970
|
-
});
|
|
7971
|
-
} catch (error) {
|
|
7972
|
-
if (error?.name === "TimeoutError" || error?.name === "AbortError") {
|
|
7973
|
-
return new Response("patchstack: upstream timed out", { status: 504 });
|
|
7974
|
-
}
|
|
7975
|
-
throw error;
|
|
7976
|
-
}
|
|
7977
|
-
const forwarded = new Response(upstream.body, {
|
|
7978
|
-
status: upstream.status,
|
|
7979
|
-
statusText: upstream.statusText,
|
|
7980
|
-
headers: responseHeaders(upstream.headers)
|
|
7981
|
-
});
|
|
7982
|
-
return protection.screenResponse ? protection.screenResponse(forwarded, evalReq) : forwarded;
|
|
7983
|
-
};
|
|
7984
|
-
}
|
|
7985
|
-
|
|
7986
8314
|
// src/protect/verify-sentinel.js
|
|
7987
8315
|
var VERIFY_HEADER = "x-patchstack-verify";
|
|
7988
8316
|
var CHALLENGE = "PATCHSTACK_VERIFY_CHALLENGE";
|
|
@@ -8169,20 +8497,47 @@ async function createProtection(options = {}) {
|
|
|
8169
8497
|
whitelist_keys: delivered.whitelist_keys,
|
|
8170
8498
|
onError
|
|
8171
8499
|
});
|
|
8172
|
-
responseRuleSet = responseRules.map((rule) =>
|
|
8173
|
-
|
|
8174
|
-
|
|
8175
|
-
|
|
8176
|
-
|
|
8177
|
-
|
|
8178
|
-
|
|
8179
|
-
|
|
8180
|
-
|
|
8181
|
-
|
|
8182
|
-
|
|
8183
|
-
|
|
8184
|
-
|
|
8185
|
-
|
|
8500
|
+
responseRuleSet = responseRules.map((rule) => {
|
|
8501
|
+
const unscoped = [];
|
|
8502
|
+
const rewrites = rule.action === "redact" || rule.action === "encode";
|
|
8503
|
+
let derived;
|
|
8504
|
+
try {
|
|
8505
|
+
const redactors = rewrites ? extractRedactors(rule, (parameter) => unscoped.push(parameter)) : null;
|
|
8506
|
+
const mutatedSpan = rewrites && hasSpanMutations(rule);
|
|
8507
|
+
derived = {
|
|
8508
|
+
redactors,
|
|
8509
|
+
mutatedSpan,
|
|
8510
|
+
// A redaction that reads response headers only, and masks spans in them, and an explicit block
|
|
8511
|
+
// that reads response headers only, are decided and carried out without the body — so they
|
|
8512
|
+
// still apply to a response whose body was not screened.
|
|
8513
|
+
redactsHeaders: rule.action === "redact" && !mutatedSpan && (redactors ?? []).some((r) => !r.jsonPath) && readsOnlyResponseHeaders(rule),
|
|
8514
|
+
blocksOnHeaders: rule.action === "block" && readsOnlyResponseHeaders(rule)
|
|
8515
|
+
};
|
|
8516
|
+
} catch (err) {
|
|
8517
|
+
notify(onError, err, "onError");
|
|
8518
|
+
derived = { redactors: null, mutatedSpan: false, redactsHeaders: false, blocksOnHeaders: false };
|
|
8519
|
+
}
|
|
8520
|
+
if (unscoped.length) {
|
|
8521
|
+
notify(
|
|
8522
|
+
onError,
|
|
8523
|
+
new Error(
|
|
8524
|
+
`Patchstack: response rule ${JSON.stringify(rule.id ?? null)} has a condition whose parameter names no place in the response to mask (${unscoped.map((p) => JSON.stringify(p ?? null)).join(", ")}); its matches are not masked`
|
|
8525
|
+
),
|
|
8526
|
+
"onError"
|
|
8527
|
+
);
|
|
8528
|
+
}
|
|
8529
|
+
return {
|
|
8530
|
+
rule,
|
|
8531
|
+
engine: new RuleEngine({ firewall: [rule], onError }),
|
|
8532
|
+
...derived,
|
|
8533
|
+
// Optional cheap pre-filter: literal anchor(s) that MUST appear for the (expensive) regex to
|
|
8534
|
+
// have any chance of matching. Lets screenText skip the full scan on responses with no candidate
|
|
8535
|
+
// — the common case — cutting CPU/latency and shrinking the regex/ReDoS surface.
|
|
8536
|
+
// Case-insensitive, and checked against the header values as well as the body, since a rule may
|
|
8537
|
+
// read either.
|
|
8538
|
+
prefilter: Array.isArray(rule.prefilter) && rule.prefilter.length ? rule.prefilter.map((s) => String(s).toLowerCase()) : null
|
|
8539
|
+
};
|
|
8540
|
+
});
|
|
8186
8541
|
egressRuleSet = egressRules.map((rule) => ({
|
|
8187
8542
|
rule,
|
|
8188
8543
|
engine: new RuleEngine({ firewall: [rule], onError })
|
|
@@ -8196,6 +8551,9 @@ async function createProtection(options = {}) {
|
|
|
8196
8551
|
skipCounts[key] = (skipCounts[key] ?? 0) + 1;
|
|
8197
8552
|
notify(onSkip, { phase, reason, detail, count: skipCounts[key] }, "onSkip");
|
|
8198
8553
|
};
|
|
8554
|
+
const recordEvaluationSkips = (result) => {
|
|
8555
|
+
for (const reason of result?.skips ?? []) recordSkip("request", reason);
|
|
8556
|
+
};
|
|
8199
8557
|
const maskFn = typeof options.maskWith === "function" ? options.maskWith : () => typeof options.maskWith === "string" ? options.maskWith : "[REDACTED]";
|
|
8200
8558
|
const ruleMode = (rule) => rule?.enforcement === "dry-run" ? "dry-run" : mode;
|
|
8201
8559
|
const planCache = createPlanCache();
|
|
@@ -8238,15 +8596,19 @@ async function createProtection(options = {}) {
|
|
|
8238
8596
|
});
|
|
8239
8597
|
return effectiveMode === "block" ? block() : allow2();
|
|
8240
8598
|
};
|
|
8599
|
+
const decidedByHeaders = (_rule, entry) => Boolean(entry?.redactsHeaders || entry?.blocksOnHeaders);
|
|
8600
|
+
const decidedWithoutBody = (rule, entry) => hardensWithoutBody(rule) || decidedByHeaders(rule, entry);
|
|
8241
8601
|
const screenText = (text, meta, reqCtx, only) => {
|
|
8242
8602
|
let blockRule = null;
|
|
8243
8603
|
const redactions = [];
|
|
8244
8604
|
const headerMutations = [];
|
|
8245
8605
|
let lowerText = null;
|
|
8246
|
-
|
|
8247
|
-
|
|
8606
|
+
const responseSkips = /* @__PURE__ */ new Set();
|
|
8607
|
+
for (const entry of responseRuleSet) {
|
|
8608
|
+
const { rule, engine: re, redactors, prefilter, mutatedSpan } = entry;
|
|
8609
|
+
if (only && !only(rule, entry)) continue;
|
|
8248
8610
|
if (prefilter) {
|
|
8249
|
-
if (lowerText === null) lowerText = text.
|
|
8611
|
+
if (lowerText === null) lowerText = prefilterText(text, meta.headers);
|
|
8250
8612
|
if (!prefilter.some((p) => lowerText.includes(p))) continue;
|
|
8251
8613
|
}
|
|
8252
8614
|
let result;
|
|
@@ -8256,6 +8618,11 @@ async function createProtection(options = {}) {
|
|
|
8256
8618
|
notify(onError, err, "onError");
|
|
8257
8619
|
continue;
|
|
8258
8620
|
}
|
|
8621
|
+
for (const reason of result.skips ?? []) {
|
|
8622
|
+
if (responseSkips.has(reason)) continue;
|
|
8623
|
+
responseSkips.add(reason);
|
|
8624
|
+
recordSkip("response", reason);
|
|
8625
|
+
}
|
|
8259
8626
|
if (!result.blocked) continue;
|
|
8260
8627
|
const responseMode = ruleMode(rule);
|
|
8261
8628
|
onDetect({
|
|
@@ -8285,6 +8652,8 @@ async function createProtection(options = {}) {
|
|
|
8285
8652
|
if (blockRule) return { verdict: "block" };
|
|
8286
8653
|
let body = text;
|
|
8287
8654
|
const headers = { ...meta.headers || {} };
|
|
8655
|
+
let responseIsJson;
|
|
8656
|
+
let structure;
|
|
8288
8657
|
for (const { rule, redactors } of redactions) {
|
|
8289
8658
|
const mask = maskFn(rule.category);
|
|
8290
8659
|
const transform = rule.action === "encode" ? htmlEscape : null;
|
|
@@ -8293,23 +8662,32 @@ async function createProtection(options = {}) {
|
|
|
8293
8662
|
if (pathRedactors.length) body = applyPathRedactors(body, pathRedactors, mask, screenCap, transform);
|
|
8294
8663
|
if (!spanRedactors.length) continue;
|
|
8295
8664
|
const beforeSpan = body;
|
|
8296
|
-
|
|
8297
|
-
if (body
|
|
8665
|
+
const bodySpans = spanRedactors.filter((r) => r.scope.body === true);
|
|
8666
|
+
if (bodySpans.length) body = applyRedactors(body, bodySpans, mask, transform);
|
|
8667
|
+
if (body !== beforeSpan) {
|
|
8668
|
+
responseIsJson ??= isJson(text);
|
|
8669
|
+
if (responseIsJson) {
|
|
8670
|
+
if (structure?.text !== beforeSpan) structure = { text: beforeSpan, tokens: lexJson(beforeSpan) };
|
|
8671
|
+
const tokens = lexJson(body);
|
|
8672
|
+
if (!sameJsonStructure(structure.text, structure.tokens, body, tokens)) return { verdict: "block" };
|
|
8673
|
+
structure = { text: body, tokens };
|
|
8674
|
+
}
|
|
8675
|
+
}
|
|
8298
8676
|
if (transform) continue;
|
|
8299
8677
|
for (const name of Object.keys(headers)) {
|
|
8300
8678
|
const value = headers[name];
|
|
8679
|
+
const headerSpans = spanRedactors.filter((r) => masksHeader(r, name));
|
|
8301
8680
|
if (typeof value === "string") {
|
|
8302
|
-
setOwn(headers, name, applyRedactors(value,
|
|
8681
|
+
setOwn(headers, name, applyRedactors(value, headerSpans, mask));
|
|
8303
8682
|
} else if (Array.isArray(value)) {
|
|
8304
8683
|
setOwn(
|
|
8305
8684
|
headers,
|
|
8306
8685
|
name,
|
|
8307
|
-
value.map((item) => typeof item === "string" ? applyRedactors(item,
|
|
8686
|
+
value.map((item) => typeof item === "string" ? applyRedactors(item, headerSpans, mask) : item)
|
|
8308
8687
|
);
|
|
8309
8688
|
}
|
|
8310
8689
|
}
|
|
8311
8690
|
}
|
|
8312
|
-
if (body !== text && isJson(text) && !isJson(body)) return { verdict: "block" };
|
|
8313
8691
|
for (const rule of headerMutations) applyHeaderMutation(headers, rule);
|
|
8314
8692
|
return { verdict: "redact", body, headers };
|
|
8315
8693
|
};
|
|
@@ -8352,18 +8730,53 @@ async function createProtection(options = {}) {
|
|
|
8352
8730
|
}
|
|
8353
8731
|
};
|
|
8354
8732
|
};
|
|
8355
|
-
const
|
|
8733
|
+
const peerOf = (request, hostArgs) => {
|
|
8734
|
+
if (typeof options.peerAddress !== "function") return void 0;
|
|
8735
|
+
try {
|
|
8736
|
+
return options.peerAddress(request?.[SOURCE_REQUEST] ?? request, ...hostArgs);
|
|
8737
|
+
} catch (err) {
|
|
8738
|
+
notify(onError, err, "onError");
|
|
8739
|
+
return void 0;
|
|
8740
|
+
}
|
|
8741
|
+
};
|
|
8742
|
+
let warnedNoPeer = false;
|
|
8743
|
+
const warnNoPeer = () => {
|
|
8744
|
+
if (warnedNoPeer) return;
|
|
8745
|
+
warnedNoPeer = true;
|
|
8746
|
+
const message = "[patchstack] trustedProxy is set, but this Fetch runtime supplied no peer address, so forwarded headers are not used and client addresses read as unavailable. Pass { peerAddress } to supply one.";
|
|
8747
|
+
if (typeof onError === "function") notify(onError, new Error(message), "onError");
|
|
8748
|
+
else console.warn(message);
|
|
8749
|
+
};
|
|
8750
|
+
const clientByRequest = /* @__PURE__ */ new WeakMap();
|
|
8751
|
+
const clientForResponse = (request, hostArgs) => {
|
|
8752
|
+
const known = clientByRequest.get(request);
|
|
8753
|
+
if (known) return known;
|
|
8754
|
+
const client = resolveClientIp({
|
|
8755
|
+
peer: peerOf(request, hostArgs),
|
|
8756
|
+
headers: headerObject(request.headers),
|
|
8757
|
+
trustedProxy: options.trustedProxy
|
|
8758
|
+
});
|
|
8759
|
+
if (options.trustedProxy !== void 0 && client.source === "unavailable") warnNoPeer();
|
|
8760
|
+
return client;
|
|
8761
|
+
};
|
|
8762
|
+
const screenFetchRequest = async (request, hostArgs = []) => {
|
|
8356
8763
|
let result;
|
|
8357
8764
|
let shaped;
|
|
8358
8765
|
try {
|
|
8359
8766
|
shaped = await fromFetchRequest(request, {
|
|
8767
|
+
peer: peerOf(request, hostArgs),
|
|
8360
8768
|
trustedProxy: options.trustedProxy,
|
|
8361
8769
|
maxBodyBytes: options.maxBodyBytes
|
|
8362
8770
|
});
|
|
8771
|
+
if (shaped?._clientIp) {
|
|
8772
|
+
clientByRequest.set(request, shaped._clientIp);
|
|
8773
|
+
if (options.trustedProxy !== void 0 && shaped._clientIp.source === "unavailable") warnNoPeer();
|
|
8774
|
+
}
|
|
8363
8775
|
if (shaped?._bodyInspectionSkip) {
|
|
8364
8776
|
recordSkip("request", shaped._bodyInspectionSkip, { limit: options.maxBodyBytes ?? 1024 * 1024 });
|
|
8365
8777
|
}
|
|
8366
8778
|
result = engine.evaluate(shaped);
|
|
8779
|
+
recordEvaluationSkips(result);
|
|
8367
8780
|
} catch (err) {
|
|
8368
8781
|
notify(onError, err, "onError");
|
|
8369
8782
|
return { blocked: null, client: void 0 };
|
|
@@ -8413,15 +8826,19 @@ async function createProtection(options = {}) {
|
|
|
8413
8826
|
const read = await readTextResponse(response, screenCap);
|
|
8414
8827
|
if (read.skip) {
|
|
8415
8828
|
if (read.skip !== "not-a-response") {
|
|
8416
|
-
recordSkip("response", read.skip, {
|
|
8829
|
+
recordSkip("response", read.skip, {
|
|
8830
|
+
status: response?.status,
|
|
8831
|
+
...read.encoding ? { encoding: read.encoding } : {},
|
|
8832
|
+
...read.charset ? { charset: read.charset } : {}
|
|
8833
|
+
});
|
|
8417
8834
|
}
|
|
8418
8835
|
if (read.skip === "not-a-response") return response;
|
|
8419
8836
|
return hardenHeadersOnly(response, reqCtx);
|
|
8420
8837
|
}
|
|
8421
|
-
const text = read.text;
|
|
8838
|
+
const text = readJsonEscapes(read.text);
|
|
8422
8839
|
const r = screenText(text, { status: response.status, headers: headerObject(response.headers) }, reqCtx);
|
|
8423
|
-
if (r.verdict === "block") return leakResponse();
|
|
8424
|
-
if (r.verdict === "redact") return rebuildResponse(response, r.body, r.headers);
|
|
8840
|
+
if (r.verdict === "block" || r.verdict === "redact" && outgrowsCharset(read.ascii, r.body)) return leakResponse();
|
|
8841
|
+
if (r.verdict === "redact") return rebuildResponse(response, withBom(read.bom, r.body), r.headers);
|
|
8425
8842
|
return response;
|
|
8426
8843
|
};
|
|
8427
8844
|
const headerValueChanged = (was, value) => {
|
|
@@ -8483,7 +8900,8 @@ async function createProtection(options = {}) {
|
|
|
8483
8900
|
const hardenHeadersOnly = (response, reqCtx) => {
|
|
8484
8901
|
try {
|
|
8485
8902
|
const meta = { status: response.status, headers: headerObject(response.headers) };
|
|
8486
|
-
const r = screenText("", meta, reqCtx,
|
|
8903
|
+
const r = screenText("", meta, reqCtx, decidedWithoutBody);
|
|
8904
|
+
if (r.verdict === "block") return leakResponse();
|
|
8487
8905
|
if (r.verdict !== "redact" || !r.headers) return response;
|
|
8488
8906
|
if (!headersChanged(meta.headers, r.headers)) return response;
|
|
8489
8907
|
return rebuildResponse(response, response.body, r.headers);
|
|
@@ -8503,6 +8921,8 @@ async function createProtection(options = {}) {
|
|
|
8503
8921
|
const chunks = [];
|
|
8504
8922
|
let size = 0;
|
|
8505
8923
|
let overflow = false;
|
|
8924
|
+
let withheld = false;
|
|
8925
|
+
let headScreened = false;
|
|
8506
8926
|
const MAX = screenCap;
|
|
8507
8927
|
let hardened = false;
|
|
8508
8928
|
let answeredWithoutBody = false;
|
|
@@ -8534,6 +8954,7 @@ async function createProtection(options = {}) {
|
|
|
8534
8954
|
}
|
|
8535
8955
|
};
|
|
8536
8956
|
const stillToAnswer = (rule) => !hardensWithoutBody(rule);
|
|
8957
|
+
const notYetAnswered = (rule, entry) => (!answeredWithoutBody || stillToAnswer(rule)) && !(headScreened && decidedByHeaders(rule, entry));
|
|
8537
8958
|
let pendingHead = null;
|
|
8538
8959
|
let sending = false;
|
|
8539
8960
|
const origWriteHead = typeof res.writeHead === "function" ? res.writeHead.bind(res) : null;
|
|
@@ -8595,7 +9016,12 @@ async function createProtection(options = {}) {
|
|
|
8595
9016
|
if (typeof res.flushHeaders === "function") {
|
|
8596
9017
|
const origFlushHeaders = res.flushHeaders.bind(res);
|
|
8597
9018
|
res.flushHeaders = function() {
|
|
8598
|
-
|
|
9019
|
+
const unread = screenUnreadHead();
|
|
9020
|
+
if (unread?.withhold) {
|
|
9021
|
+
sendWithheld();
|
|
9022
|
+
return void 0;
|
|
9023
|
+
}
|
|
9024
|
+
sendHead(unread);
|
|
8599
9025
|
return origFlushHeaders();
|
|
8600
9026
|
};
|
|
8601
9027
|
}
|
|
@@ -8630,6 +9056,32 @@ async function createProtection(options = {}) {
|
|
|
8630
9056
|
headers: at === -1 ? set2 : { ...set2, ...writeHeadHeaderObject(pendingHead[at]) }
|
|
8631
9057
|
};
|
|
8632
9058
|
};
|
|
9059
|
+
const screenUnreadHead = () => {
|
|
9060
|
+
if (headScreened) return void 0;
|
|
9061
|
+
headScreened = true;
|
|
9062
|
+
try {
|
|
9063
|
+
const head = effectiveHead();
|
|
9064
|
+
const r = screenText("", head, reqCtx, decidedByHeaders);
|
|
9065
|
+
if (r.verdict === "block") {
|
|
9066
|
+
if (!reallySent()) return { withhold: true };
|
|
9067
|
+
recordSkip("response", "headers-sent", { action: "block" });
|
|
9068
|
+
return void 0;
|
|
9069
|
+
}
|
|
9070
|
+
if (r.verdict !== "redact" || !r.headers) return void 0;
|
|
9071
|
+
const changed = /* @__PURE__ */ new Map();
|
|
9072
|
+
for (const [name, value] of Object.entries(r.headers)) {
|
|
9073
|
+
if (headerValueChanged(head.headers[name], value)) changed.set(name.toLowerCase(), value);
|
|
9074
|
+
}
|
|
9075
|
+
if (changed.size && reallySent()) {
|
|
9076
|
+
recordSkip("response", "headers-sent", { headers: [...changed.keys()] });
|
|
9077
|
+
return void 0;
|
|
9078
|
+
}
|
|
9079
|
+
return changed.size ? { headers: changed } : void 0;
|
|
9080
|
+
} catch (err) {
|
|
9081
|
+
notify(onError, err, "onError");
|
|
9082
|
+
return void 0;
|
|
9083
|
+
}
|
|
9084
|
+
};
|
|
8633
9085
|
const sendHead = (changes) => {
|
|
8634
9086
|
if (sending) return;
|
|
8635
9087
|
const hardened2 = hardenOnce();
|
|
@@ -8651,6 +9103,17 @@ async function createProtection(options = {}) {
|
|
|
8651
9103
|
applyToResponse({ headers: merged });
|
|
8652
9104
|
origWriteHead(...args);
|
|
8653
9105
|
};
|
|
9106
|
+
const sendWithheld = (cb) => {
|
|
9107
|
+
const body = JSON.stringify({ error: "Response withheld by Patchstack (sensitive data detected)" });
|
|
9108
|
+
const headers = /* @__PURE__ */ new Map();
|
|
9109
|
+
for (const name of Object.keys(effectiveHead().headers)) headers.set(name.toLowerCase(), null);
|
|
9110
|
+
headers.set("content-type", "application/json");
|
|
9111
|
+
headers.set("content-length", String(Buffer.byteLength(body)));
|
|
9112
|
+
headers.set("transfer-encoding", null);
|
|
9113
|
+
withheld = true;
|
|
9114
|
+
sendHead({ status: 500, headers });
|
|
9115
|
+
return origEnd(body, cb);
|
|
9116
|
+
};
|
|
8654
9117
|
const applyToResponse = (changes) => {
|
|
8655
9118
|
if (changes.status !== void 0) res.statusCode = changes.status;
|
|
8656
9119
|
for (const [name, value] of changes.headers ?? []) {
|
|
@@ -8666,7 +9129,12 @@ async function createProtection(options = {}) {
|
|
|
8666
9129
|
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, typeof enc === "string" ? enc : "utf8");
|
|
8667
9130
|
size += buf.length;
|
|
8668
9131
|
if (size > MAX) {
|
|
8669
|
-
|
|
9132
|
+
const unread = screenUnreadHead();
|
|
9133
|
+
if (unread?.withhold) {
|
|
9134
|
+
sendWithheld();
|
|
9135
|
+
return;
|
|
9136
|
+
}
|
|
9137
|
+
sendHead(unread);
|
|
8670
9138
|
for (const c of chunks) origWrite(c);
|
|
8671
9139
|
chunks.length = 0;
|
|
8672
9140
|
origWrite(buf);
|
|
@@ -8677,6 +9145,11 @@ async function createProtection(options = {}) {
|
|
|
8677
9145
|
chunks.push(buf);
|
|
8678
9146
|
};
|
|
8679
9147
|
res.write = function(chunk, enc, cb) {
|
|
9148
|
+
if (withheld) {
|
|
9149
|
+
if (typeof enc === "function") enc();
|
|
9150
|
+
else if (typeof cb === "function") cb();
|
|
9151
|
+
return true;
|
|
9152
|
+
}
|
|
8680
9153
|
if (overflow) return origWrite(chunk, enc, cb);
|
|
8681
9154
|
collect(chunk, enc);
|
|
8682
9155
|
if (typeof enc === "function") enc();
|
|
@@ -8692,15 +9165,24 @@ async function createProtection(options = {}) {
|
|
|
8692
9165
|
cb = enc;
|
|
8693
9166
|
enc = void 0;
|
|
8694
9167
|
}
|
|
9168
|
+
if (withheld) {
|
|
9169
|
+
if (typeof cb === "function") cb();
|
|
9170
|
+
return res;
|
|
9171
|
+
}
|
|
8695
9172
|
if (overflow) {
|
|
8696
9173
|
if (chunk != null) origWrite(chunk, enc);
|
|
8697
9174
|
return origEnd(cb);
|
|
8698
9175
|
}
|
|
8699
9176
|
collect(chunk, enc);
|
|
9177
|
+
if (withheld) {
|
|
9178
|
+
if (typeof cb === "function") cb();
|
|
9179
|
+
return res;
|
|
9180
|
+
}
|
|
8700
9181
|
if (overflow) return origEnd(cb);
|
|
8701
9182
|
hardenOnce();
|
|
8702
|
-
const passThrough = () => {
|
|
8703
|
-
|
|
9183
|
+
const passThrough = (changes) => {
|
|
9184
|
+
if (changes?.withhold) return sendWithheld(cb);
|
|
9185
|
+
sendHead(changes);
|
|
8704
9186
|
for (const c of chunks) origWrite(c);
|
|
8705
9187
|
return origEnd(cb);
|
|
8706
9188
|
};
|
|
@@ -8711,21 +9193,28 @@ async function createProtection(options = {}) {
|
|
|
8711
9193
|
const kind = screenableContentType(ct);
|
|
8712
9194
|
if (kind === "skip" || kind === "sniff" && looksBinary(buffer)) {
|
|
8713
9195
|
recordSkip("response", kind === "skip" ? baseContentType(ct) === "text/event-stream" ? "live-stream" : "non-text-content-type" : "binary-body");
|
|
8714
|
-
return passThrough();
|
|
9196
|
+
return passThrough(screenUnreadHead());
|
|
8715
9197
|
}
|
|
8716
9198
|
const codings = declaredCodings(head.headers["content-encoding"]);
|
|
8717
9199
|
if (codings.length > 0 && stillEncoded(buffer)) {
|
|
8718
9200
|
recordSkip("response", "encoded-body", { encoding: codings.join(", ") });
|
|
8719
|
-
return passThrough();
|
|
9201
|
+
return passThrough(screenUnreadHead());
|
|
9202
|
+
}
|
|
9203
|
+
const reading = charsetReading(ct, buffer);
|
|
9204
|
+
if (reading.skip) {
|
|
9205
|
+
recordSkip("response", "unsupported-charset", { charset: reading.skip });
|
|
9206
|
+
return passThrough(screenUnreadHead());
|
|
8720
9207
|
}
|
|
8721
|
-
const text = buffer.toString("utf8");
|
|
9208
|
+
const text = readJsonEscapes(buffer.toString("utf8", reading.bom ? 3 : 0));
|
|
8722
9209
|
let r;
|
|
8723
9210
|
try {
|
|
8724
|
-
r = screenText(text, head, reqCtx,
|
|
9211
|
+
r = screenText(text, head, reqCtx, notYetAnswered);
|
|
8725
9212
|
} catch (err) {
|
|
8726
9213
|
notify(onError, err, "onError");
|
|
8727
9214
|
return passThrough();
|
|
8728
9215
|
}
|
|
9216
|
+
if (r.verdict === "redact" && outgrowsCharset(reading.ascii, r.body)) r = { verdict: "block" };
|
|
9217
|
+
if (r.verdict === "redact") r = { ...r, body: withBom(reading.bom, r.body) };
|
|
8729
9218
|
if (r.verdict !== "block" && r.verdict !== "redact") return passThrough();
|
|
8730
9219
|
if (reallySent() && res.chunkedEncoding !== true) {
|
|
8731
9220
|
notify(onError, new Error("Patchstack: response withheld \u2014 its headers were sent before the body could be screened"), "onError");
|
|
@@ -8733,20 +9222,11 @@ async function createProtection(options = {}) {
|
|
|
8733
9222
|
return res;
|
|
8734
9223
|
}
|
|
8735
9224
|
if (r.verdict === "block") {
|
|
8736
|
-
const body = JSON.stringify({ error: "Response withheld by Patchstack (sensitive data detected)" });
|
|
8737
9225
|
if (reallySent()) {
|
|
8738
9226
|
res.destroy?.();
|
|
8739
9227
|
return res;
|
|
8740
9228
|
}
|
|
8741
|
-
|
|
8742
|
-
status: 500,
|
|
8743
|
-
headers: /* @__PURE__ */ new Map([
|
|
8744
|
-
["content-type", "application/json"],
|
|
8745
|
-
["content-length", String(Buffer.byteLength(body))],
|
|
8746
|
-
["transfer-encoding", null]
|
|
8747
|
-
])
|
|
8748
|
-
});
|
|
8749
|
-
return origEnd(body, cb);
|
|
9229
|
+
return sendWithheld(cb);
|
|
8750
9230
|
}
|
|
8751
9231
|
const changed = /* @__PURE__ */ new Map();
|
|
8752
9232
|
if (r.headers) {
|
|
@@ -8842,21 +9322,15 @@ async function createProtection(options = {}) {
|
|
|
8842
9322
|
// .fetch(), and by the Supabase guard on its forwarded upstream response.
|
|
8843
9323
|
// A standalone response screen with no request phase of its own — the Supabase guard's forwarded
|
|
8844
9324
|
// upstream response. It resolves once here, which is the only resolution for this call.
|
|
8845
|
-
screenResponse: (response, request) => screenResp(
|
|
8846
|
-
response,
|
|
8847
|
-
request ? reqContextFromFetch(
|
|
8848
|
-
request,
|
|
8849
|
-
resolveClientIp({ headers: headerObject(request.headers), trustedProxy: options.trustedProxy })
|
|
8850
|
-
) : void 0
|
|
8851
|
-
),
|
|
9325
|
+
screenResponse: (response, request, ...hostArgs) => screenResp(response, request ? reqContextFromFetch(request, clientForResponse(request, hostArgs)) : void 0),
|
|
8852
9326
|
// (request) => Response | null (null = allow, caller proceeds). Request phase only.
|
|
8853
9327
|
fetchGuard() {
|
|
8854
|
-
return async (request) => (await screenFetchRequest(request)).blocked;
|
|
9328
|
+
return async (request, ...hostArgs) => (await screenFetchRequest(request, hostArgs)).blocked;
|
|
8855
9329
|
},
|
|
8856
9330
|
// Wrap a fetch handler: screens the request, then the response (redact/block).
|
|
8857
9331
|
fetch(handler) {
|
|
8858
9332
|
return async (request, ...rest) => {
|
|
8859
|
-
const { blocked, client } = await screenFetchRequest(request);
|
|
9333
|
+
const { blocked, client } = await screenFetchRequest(request, rest);
|
|
8860
9334
|
if (blocked) return blocked;
|
|
8861
9335
|
const response = await handler(request, ...rest);
|
|
8862
9336
|
return screenResp(response, reqContextFromFetch(request, client));
|
|
@@ -8870,6 +9344,7 @@ async function createProtection(options = {}) {
|
|
|
8870
9344
|
const { shaped, client } = shapeExpressRequest(req);
|
|
8871
9345
|
try {
|
|
8872
9346
|
result = engine.evaluate(shaped);
|
|
9347
|
+
recordEvaluationSkips(result);
|
|
8873
9348
|
} catch (err) {
|
|
8874
9349
|
notify(onError, err, "onError");
|
|
8875
9350
|
if (exprOptions.screenResponses) wrapNodeResponse(res, reqContextFromNode(req, client));
|
|
@@ -8904,33 +9379,25 @@ async function createProtection(options = {}) {
|
|
|
8904
9379
|
screenNodeRequest(req, res, next, "", req.body);
|
|
8905
9380
|
return;
|
|
8906
9381
|
}
|
|
8907
|
-
|
|
8908
|
-
|
|
8909
|
-
|
|
8910
|
-
|
|
8911
|
-
size += chunk.length;
|
|
8912
|
-
if (size > maxBytes) {
|
|
8913
|
-
overflow = true;
|
|
9382
|
+
readBodyPrefix(req, maxBytes, (err, read) => {
|
|
9383
|
+
if (err) {
|
|
9384
|
+
notify(onError, err, "onError");
|
|
9385
|
+
next();
|
|
8914
9386
|
return;
|
|
8915
9387
|
}
|
|
8916
|
-
|
|
8917
|
-
|
|
8918
|
-
|
|
8919
|
-
notify(onError, err, "onError");
|
|
8920
|
-
next();
|
|
8921
|
-
});
|
|
8922
|
-
req.on("end", () => {
|
|
8923
|
-
if (overflow) recordSkip("request", "body-cap", { bytes: size, limit: maxBytes });
|
|
8924
|
-
screenNodeRequest(req, res, next, overflow ? "" : Buffer.concat(chunks).toString("utf8"));
|
|
9388
|
+
if (read.overflow) recordSkip("request", "body-cap", { bytes: read.size, limit: maxBytes });
|
|
9389
|
+
if (read.failed) recordSkip("request", "read-failed", { bytes: read.size });
|
|
9390
|
+
screenNodeRequest(req, res, next, read.text, void 0, read.overflow || read.failed);
|
|
8925
9391
|
});
|
|
8926
9392
|
};
|
|
8927
|
-
function screenNodeRequest(req, res, next, rawBody, parsedBody) {
|
|
9393
|
+
function screenNodeRequest(req, res, next, rawBody, parsedBody, truncated = false) {
|
|
8928
9394
|
let shaped;
|
|
8929
9395
|
let result;
|
|
8930
9396
|
try {
|
|
8931
9397
|
shaped = fromNodeRequest(req, rawBody, { trustedProxy: options.trustedProxy });
|
|
8932
9398
|
if (parsedBody !== void 0 && parsedBody !== null) shaped.body = parsedBody;
|
|
8933
9399
|
result = engine.evaluate(shaped);
|
|
9400
|
+
recordEvaluationSkips(result);
|
|
8934
9401
|
} catch (err) {
|
|
8935
9402
|
notify(onError, err, "onError");
|
|
8936
9403
|
return next();
|
|
@@ -8949,7 +9416,7 @@ async function createProtection(options = {}) {
|
|
|
8949
9416
|
}
|
|
8950
9417
|
},
|
|
8951
9418
|
() => {
|
|
8952
|
-
if (req.body === void 0) req.body = shaped.body;
|
|
9419
|
+
if (req.body === void 0 && !truncated) req.body = shaped.body;
|
|
8953
9420
|
if (nodeOptions.screenResponses) wrapNodeResponse(res, reqContextFromNode(req, shaped?._clientIp));
|
|
8954
9421
|
next();
|
|
8955
9422
|
},
|
|
@@ -9015,12 +9482,17 @@ async function createProtection(options = {}) {
|
|
|
9015
9482
|
protection.stop = () => {
|
|
9016
9483
|
loop?.stop();
|
|
9017
9484
|
recovery?.stop();
|
|
9485
|
+
protection.uninstallEgress?.();
|
|
9018
9486
|
const outstanding = [firewallLog?.stop(), detections?.stop()].filter(
|
|
9019
9487
|
(wait) => wait && typeof wait.then === "function"
|
|
9020
9488
|
);
|
|
9021
9489
|
return Promise.all(outstanding).then(() => void 0);
|
|
9022
9490
|
};
|
|
9023
|
-
protection.stopRefresh =
|
|
9491
|
+
protection.stopRefresh = () => {
|
|
9492
|
+
loop?.stop();
|
|
9493
|
+
recovery?.stop();
|
|
9494
|
+
return Promise.resolve();
|
|
9495
|
+
};
|
|
9024
9496
|
Object.defineProperty(protection, "ruleSource", {
|
|
9025
9497
|
get: () => ({ ...ruleSource }),
|
|
9026
9498
|
enumerable: true
|
|
@@ -9033,6 +9505,10 @@ async function createProtection(options = {}) {
|
|
|
9033
9505
|
get: () => detections ? () => detections.health() : void 0,
|
|
9034
9506
|
enumerable: true
|
|
9035
9507
|
});
|
|
9508
|
+
Object.defineProperty(protection, "blockLogHealth", {
|
|
9509
|
+
get: () => firewallLog ? () => firewallLog.health() : void 0,
|
|
9510
|
+
enumerable: true
|
|
9511
|
+
});
|
|
9036
9512
|
return protection;
|
|
9037
9513
|
}
|
|
9038
9514
|
function accessorOf(obj, name) {
|
|
@@ -9107,6 +9583,14 @@ function screenableContentType(ct) {
|
|
|
9107
9583
|
if (base === "application/octet-stream") return "sniff";
|
|
9108
9584
|
return "skip";
|
|
9109
9585
|
}
|
|
9586
|
+
function prefilterText(body, headers) {
|
|
9587
|
+
const values = [];
|
|
9588
|
+
for (const value of Object.values(headers ?? {})) {
|
|
9589
|
+
if (Array.isArray(value)) values.push(...value.map(String));
|
|
9590
|
+
else if (value !== void 0 && value !== null) values.push(String(value));
|
|
9591
|
+
}
|
|
9592
|
+
return [body, ...values].join("\n").toLowerCase();
|
|
9593
|
+
}
|
|
9110
9594
|
function looksBinary(bytes) {
|
|
9111
9595
|
const n = Math.min(bytes.length, 512);
|
|
9112
9596
|
let ctrl = 0;
|
|
@@ -9130,6 +9614,52 @@ function stillEncoded(bytes) {
|
|
|
9130
9614
|
return true;
|
|
9131
9615
|
}
|
|
9132
9616
|
}
|
|
9617
|
+
var UTF8_CHARSETS = /* @__PURE__ */ new Set(["utf-8", "utf8", "unicode-1-1-utf-8", "us-ascii", "ascii"]);
|
|
9618
|
+
var WIDE_CHARSET = /^(?:utf-?(?:16|32)|ucs-?[24]|unicode|iso-10646)/;
|
|
9619
|
+
function declaredCharset(ct) {
|
|
9620
|
+
const m = /;\s*charset\s*=\s*(?:"([^"]*)"|([^;\s]*))/i.exec(String(ct ?? ""));
|
|
9621
|
+
const label = m ? (m[1] ?? m[2] ?? "").trim().toLowerCase() : "";
|
|
9622
|
+
return label || null;
|
|
9623
|
+
}
|
|
9624
|
+
function charsetReading(ct, bytes) {
|
|
9625
|
+
if (bytes.length >= 3 && bytes[0] === 239 && bytes[1] === 187 && bytes[2] === 191) return { ascii: false, bom: true };
|
|
9626
|
+
if (bytes.length >= 2 && bytes[0] === 255 && bytes[1] === 254) return { skip: "utf-16le" };
|
|
9627
|
+
if (bytes.length >= 2 && bytes[0] === 254 && bytes[1] === 255) return { skip: "utf-16be" };
|
|
9628
|
+
const charset = declaredCharset(ct);
|
|
9629
|
+
if (!charset || UTF8_CHARSETS.has(charset)) return { ascii: false };
|
|
9630
|
+
if (WIDE_CHARSET.test(charset)) return { skip: charset };
|
|
9631
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
9632
|
+
const b = bytes[i];
|
|
9633
|
+
if (b >= 128 || b === 0 || b === 27) return { skip: charset };
|
|
9634
|
+
}
|
|
9635
|
+
return { ascii: true };
|
|
9636
|
+
}
|
|
9637
|
+
function decodeForScreening(ct, bytes) {
|
|
9638
|
+
const reading = charsetReading(ct, bytes);
|
|
9639
|
+
if (reading.skip) return { skip: "unsupported-charset", charset: reading.skip };
|
|
9640
|
+
return { text: new TextDecoder().decode(bytes), ascii: reading.ascii, bom: reading.bom === true };
|
|
9641
|
+
}
|
|
9642
|
+
function withBom(bom, body) {
|
|
9643
|
+
return bom ? "\uFEFF" + body : body;
|
|
9644
|
+
}
|
|
9645
|
+
function outgrowsCharset(ascii, body) {
|
|
9646
|
+
return ascii === true && /[^\x00-\x7f]/.test(body);
|
|
9647
|
+
}
|
|
9648
|
+
var KEPT_ESCAPES = /* @__PURE__ */ new Set([34, 92, 60, 62, 38, 39]);
|
|
9649
|
+
function readJsonEscapes(text) {
|
|
9650
|
+
if (!text.includes("\\u") && !text.includes("\\/")) return text;
|
|
9651
|
+
try {
|
|
9652
|
+
JSON.parse(text);
|
|
9653
|
+
} catch {
|
|
9654
|
+
return text;
|
|
9655
|
+
}
|
|
9656
|
+
return text.replace(/\\(?:u([0-9a-fA-F]{4})|([\s\S]))/g, (whole, hex, ch, offset) => {
|
|
9657
|
+
if (hex === void 0) return ch === "/" && text[offset - 1] !== "<" ? "/" : whole;
|
|
9658
|
+
const code = parseInt(hex, 16);
|
|
9659
|
+
if (code < 32 || code > 126 || KEPT_ESCAPES.has(code)) return whole;
|
|
9660
|
+
return String.fromCharCode(code);
|
|
9661
|
+
});
|
|
9662
|
+
}
|
|
9133
9663
|
async function readTextResponse(response, cap = DEFAULT_SCREEN_CAP) {
|
|
9134
9664
|
if (!response || typeof response.clone !== "function") return { skip: "not-a-response" };
|
|
9135
9665
|
const ct = response.headers?.get?.("content-type") || "";
|
|
@@ -9183,17 +9713,18 @@ async function readTextResponse(response, cap = DEFAULT_SCREEN_CAP) {
|
|
|
9183
9713
|
const bytes = concatBytes(chunks, size);
|
|
9184
9714
|
if (codings.length > 0 && stillEncoded(bytes)) return encodedSkip();
|
|
9185
9715
|
try {
|
|
9186
|
-
return
|
|
9716
|
+
return decodeForScreening(ct, bytes);
|
|
9187
9717
|
} catch {
|
|
9188
9718
|
return { skip: "decode-failed" };
|
|
9189
9719
|
}
|
|
9190
9720
|
}
|
|
9191
9721
|
try {
|
|
9192
|
-
if (codings.length > 0) {
|
|
9722
|
+
if (codings.length > 0 || typeof clone.arrayBuffer === "function") {
|
|
9193
9723
|
const bytes = new Uint8Array(await clone.arrayBuffer());
|
|
9194
9724
|
if (bytes.byteLength > cap) return { skip: "body-cap" };
|
|
9195
|
-
if (stillEncoded(bytes)) return encodedSkip();
|
|
9196
|
-
|
|
9725
|
+
if (codings.length > 0 && stillEncoded(bytes)) return encodedSkip();
|
|
9726
|
+
if (codings.length === 0 && sniff && looksBinary(bytes)) return { skip: "binary-body" };
|
|
9727
|
+
return decodeForScreening(ct, bytes);
|
|
9197
9728
|
}
|
|
9198
9729
|
const text = await clone.text();
|
|
9199
9730
|
if (text.length > cap) return { skip: "body-cap" };
|
|
@@ -9224,8 +9755,9 @@ function headerObject(headers) {
|
|
|
9224
9755
|
function hasSpanMutations(rule) {
|
|
9225
9756
|
let found = false;
|
|
9226
9757
|
const walk2 = (conds) => {
|
|
9227
|
-
for (const c of conds
|
|
9758
|
+
for (const c of Array.isArray(conds) ? conds : []) {
|
|
9228
9759
|
if (found) return;
|
|
9760
|
+
if (!c || typeof c !== "object") continue;
|
|
9229
9761
|
if (Array.isArray(c.rules)) walk2(c.rules);
|
|
9230
9762
|
const isSpan = c.match && (c.match.type === "regex" || c.match.type === "contains" || c.match.type === "stripos");
|
|
9231
9763
|
if (isSpan && Array.isArray(c.mutations) && c.mutations.length) found = true;
|
|
@@ -9234,10 +9766,34 @@ function hasSpanMutations(rule) {
|
|
|
9234
9766
|
walk2(rule.rule_v2);
|
|
9235
9767
|
return found;
|
|
9236
9768
|
}
|
|
9237
|
-
function
|
|
9769
|
+
function redactorScope(parameter) {
|
|
9770
|
+
if (parameter === void 0 || parameter === null || parameterProblem(parameter) !== null) return null;
|
|
9771
|
+
const scope = { body: false, allHeaders: false, headers: /* @__PURE__ */ new Set() };
|
|
9772
|
+
for (const name of Array.isArray(parameter) ? parameter : [parameter]) {
|
|
9773
|
+
if (typeof name !== "string" || name === "") return null;
|
|
9774
|
+
if (name.startsWith("response.header.")) scope.headers.add(name.slice("response.header.".length).toLowerCase());
|
|
9775
|
+
else if (name === "response.headers") scope.allHeaders = true;
|
|
9776
|
+
else if (name === "response.body" || name === "response.status" || BROAD_SCOPE_SOURCES.has(name.split(".")[0])) {
|
|
9777
|
+
scope.body = true;
|
|
9778
|
+
scope.allHeaders = true;
|
|
9779
|
+
} else return null;
|
|
9780
|
+
}
|
|
9781
|
+
return scope;
|
|
9782
|
+
}
|
|
9783
|
+
var BROAD_SCOPE_SOURCES = /* @__PURE__ */ new Set(["get", "post", "request", "cookie", "files", "server", "raw", "all"]);
|
|
9784
|
+
function masksHeader(redactor, name) {
|
|
9785
|
+
return redactor.scope.allHeaders || redactor.scope.headers.has(name);
|
|
9786
|
+
}
|
|
9787
|
+
function extractRedactors(rule, onUnscoped) {
|
|
9238
9788
|
const out = [];
|
|
9789
|
+
const scoped = (c) => {
|
|
9790
|
+
const scope = redactorScope(c.parameter);
|
|
9791
|
+
if (scope === null) onUnscoped?.(c.parameter);
|
|
9792
|
+
return scope;
|
|
9793
|
+
};
|
|
9239
9794
|
const walk2 = (conds) => {
|
|
9240
|
-
for (const c of conds
|
|
9795
|
+
for (const c of Array.isArray(conds) ? conds : []) {
|
|
9796
|
+
if (!c || typeof c !== "object") continue;
|
|
9241
9797
|
if (Array.isArray(c.rules)) walk2(c.rules);
|
|
9242
9798
|
const m = c.match;
|
|
9243
9799
|
if (!m) continue;
|
|
@@ -9245,16 +9801,21 @@ function extractRedactors(rule) {
|
|
|
9245
9801
|
const safe = safeRegExp(m.value);
|
|
9246
9802
|
if (safe) {
|
|
9247
9803
|
const flags = safe.flags.includes("g") ? safe.flags : safe.flags + "g";
|
|
9804
|
+
const scope = scoped(c);
|
|
9248
9805
|
try {
|
|
9249
|
-
out.push({ re: new RegExp(safe.source, flags) });
|
|
9806
|
+
if (scope) out.push({ re: new RegExp(safe.source, flags), scope });
|
|
9250
9807
|
} catch {
|
|
9251
9808
|
}
|
|
9252
9809
|
}
|
|
9253
9810
|
} else if ((m.type === "contains" || m.type === "stripos") && m.value != null) {
|
|
9254
|
-
|
|
9811
|
+
const scope = scoped(c);
|
|
9812
|
+
if (scope) out.push({ literal: String(m.value), scope });
|
|
9255
9813
|
} else if (m.type === "jwt_claim_equals" && typeof m.claim === "string") {
|
|
9256
|
-
|
|
9257
|
-
|
|
9814
|
+
const scope = scoped(c);
|
|
9815
|
+
if (scope) out.push({ jwtClaim: { claim: m.claim, value: String(m.value ?? "") }, scope });
|
|
9816
|
+
} else if (m.type === "array_key_value" && m.match && Array.isArray(c.parameter) && !readsBody(c.parameter) && redactorScope(c.parameter) === null) {
|
|
9817
|
+
onUnscoped?.(c.parameter);
|
|
9818
|
+
} else if (m.type === "array_key_value" && m.match && readsBody(c.parameter)) {
|
|
9258
9819
|
const keys = Array.isArray(m.key) ? m.key : [m.key];
|
|
9259
9820
|
for (const key of keys) {
|
|
9260
9821
|
out.push({ jsonPath: String(key).split("."), condition: m.match });
|
|
@@ -9276,30 +9837,101 @@ function isJson(text) {
|
|
|
9276
9837
|
return false;
|
|
9277
9838
|
}
|
|
9278
9839
|
}
|
|
9279
|
-
|
|
9280
|
-
|
|
9281
|
-
|
|
9282
|
-
|
|
9283
|
-
|
|
9284
|
-
|
|
9285
|
-
const left = expected.next();
|
|
9286
|
-
const right = actual.next();
|
|
9287
|
-
if (left.done || right.done) return left.done === right.done;
|
|
9288
|
-
if (left.value !== right.value) return false;
|
|
9289
|
-
}
|
|
9840
|
+
var JSON_STRING = 1;
|
|
9841
|
+
var JSON_NUMBER = 2;
|
|
9842
|
+
var JSON_LITERAL = 3;
|
|
9843
|
+
var JSON_NUMBER_TOKEN = /-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/y;
|
|
9844
|
+
function isHexCode(c) {
|
|
9845
|
+
return c >= 48 && c <= 57 || c >= 65 && c <= 70 || c >= 97 && c <= 102;
|
|
9290
9846
|
}
|
|
9291
|
-
|
|
9292
|
-
|
|
9293
|
-
|
|
9294
|
-
|
|
9295
|
-
|
|
9296
|
-
|
|
9847
|
+
var PLAIN_STRING_SEGMENT = /^[^"\\\u0000-\u001f]*$/;
|
|
9848
|
+
function lexJson(text) {
|
|
9849
|
+
let kinds = new Int32Array(1024);
|
|
9850
|
+
let spans = new Int32Array(2048);
|
|
9851
|
+
let count = 0;
|
|
9852
|
+
const n = text.length;
|
|
9853
|
+
let i = 0;
|
|
9854
|
+
while (i < n) {
|
|
9855
|
+
const c = text.charCodeAt(i);
|
|
9856
|
+
if (c === 32 || c === 9 || c === 10 || c === 13) {
|
|
9857
|
+
i++;
|
|
9297
9858
|
continue;
|
|
9298
9859
|
}
|
|
9299
|
-
|
|
9300
|
-
|
|
9301
|
-
|
|
9860
|
+
const start = i;
|
|
9861
|
+
let kind;
|
|
9862
|
+
if (c === 34) {
|
|
9863
|
+
i++;
|
|
9864
|
+
const close = text.indexOf('"', i);
|
|
9865
|
+
if (close === -1) return null;
|
|
9866
|
+
if (PLAIN_STRING_SEGMENT.test(text.slice(i, close))) {
|
|
9867
|
+
i = close;
|
|
9868
|
+
} else {
|
|
9869
|
+
for (; ; ) {
|
|
9870
|
+
if (i >= n) return null;
|
|
9871
|
+
const d = text.charCodeAt(i);
|
|
9872
|
+
if (d === 34) break;
|
|
9873
|
+
if (d < 32) return null;
|
|
9874
|
+
if (d !== 92) {
|
|
9875
|
+
i++;
|
|
9876
|
+
continue;
|
|
9877
|
+
}
|
|
9878
|
+
const e = text.charCodeAt(i + 1);
|
|
9879
|
+
if (e === 117) {
|
|
9880
|
+
for (let k = 2; k < 6; k++) if (!isHexCode(text.charCodeAt(i + k))) return null;
|
|
9881
|
+
i += 6;
|
|
9882
|
+
} else if (e === 34 || e === 92 || e === 47 || e === 98 || e === 102 || e === 110 || e === 114 || e === 116) {
|
|
9883
|
+
i += 2;
|
|
9884
|
+
} else {
|
|
9885
|
+
return null;
|
|
9886
|
+
}
|
|
9887
|
+
}
|
|
9888
|
+
}
|
|
9889
|
+
i++;
|
|
9890
|
+
kind = JSON_STRING;
|
|
9891
|
+
} else if (c === 123 || c === 125 || c === 91 || c === 93 || c === 58 || c === 44) {
|
|
9892
|
+
i++;
|
|
9893
|
+
kind = c;
|
|
9894
|
+
} else if (text.startsWith("true", i) || text.startsWith("null", i)) {
|
|
9895
|
+
i += 4;
|
|
9896
|
+
kind = JSON_LITERAL;
|
|
9897
|
+
} else if (text.startsWith("false", i)) {
|
|
9898
|
+
i += 5;
|
|
9899
|
+
kind = JSON_LITERAL;
|
|
9900
|
+
} else {
|
|
9901
|
+
JSON_NUMBER_TOKEN.lastIndex = i;
|
|
9902
|
+
if (!JSON_NUMBER_TOKEN.test(text)) return null;
|
|
9903
|
+
i = JSON_NUMBER_TOKEN.lastIndex;
|
|
9904
|
+
kind = JSON_NUMBER;
|
|
9905
|
+
}
|
|
9906
|
+
if (count === kinds.length) {
|
|
9907
|
+
const grownKinds = new Int32Array(count * 2);
|
|
9908
|
+
grownKinds.set(kinds);
|
|
9909
|
+
kinds = grownKinds;
|
|
9910
|
+
const grownSpans = new Int32Array(count * 4);
|
|
9911
|
+
grownSpans.set(spans);
|
|
9912
|
+
spans = grownSpans;
|
|
9913
|
+
}
|
|
9914
|
+
kinds[count] = kind;
|
|
9915
|
+
spans[2 * count] = start;
|
|
9916
|
+
spans[2 * count + 1] = i;
|
|
9917
|
+
count++;
|
|
9918
|
+
}
|
|
9919
|
+
return { kinds, spans, count };
|
|
9920
|
+
}
|
|
9921
|
+
function sameJsonStructure(beforeText, before, afterText, after) {
|
|
9922
|
+
if (!before || !after || before.count !== after.count) return false;
|
|
9923
|
+
const { kinds, spans, count } = before;
|
|
9924
|
+
for (let k = 0; k < count; k++) {
|
|
9925
|
+
const kind = kinds[k];
|
|
9926
|
+
if (after.kinds[k] !== kind) return false;
|
|
9927
|
+
if (kind > JSON_LITERAL) continue;
|
|
9928
|
+
if (kind === JSON_STRING && kinds[k + 1] !== 58) continue;
|
|
9929
|
+
const was = beforeText.slice(spans[2 * k], spans[2 * k + 1]);
|
|
9930
|
+
const now = afterText.slice(after.spans[2 * k], after.spans[2 * k + 1]);
|
|
9931
|
+
if (was === now) continue;
|
|
9932
|
+
if (kind !== JSON_STRING || JSON.parse(was) !== JSON.parse(now)) return false;
|
|
9302
9933
|
}
|
|
9934
|
+
return true;
|
|
9303
9935
|
}
|
|
9304
9936
|
function applyRedactors(body, redactors, mask, transform) {
|
|
9305
9937
|
let out = body;
|
|
@@ -9316,6 +9948,11 @@ function applyRedactors(body, redactors, mask, transform) {
|
|
|
9316
9948
|
}
|
|
9317
9949
|
return out;
|
|
9318
9950
|
}
|
|
9951
|
+
function readsBody(parameter) {
|
|
9952
|
+
if (!Array.isArray(parameter)) return isBodyParam(parameter);
|
|
9953
|
+
const names = parameter.every((member) => typeof member === "string" && member !== "");
|
|
9954
|
+
return names && parameterProblem(parameter) === null && parameter.some((member) => isBodyParam(member));
|
|
9955
|
+
}
|
|
9319
9956
|
function isBodyParam(parameter) {
|
|
9320
9957
|
return parameter == null || parameter === "response.body" || parameter === "raw" || parameter === "response.raw";
|
|
9321
9958
|
}
|
|
@@ -9347,7 +9984,7 @@ function applyPathRedactors(text, pathRedactors, mask, cap, transform) {
|
|
|
9347
9984
|
let obj;
|
|
9348
9985
|
try {
|
|
9349
9986
|
JSON.parse(text);
|
|
9350
|
-
preserved =
|
|
9987
|
+
preserved = preserveNumbers(text, mask);
|
|
9351
9988
|
obj = JSON.parse(preserved.text);
|
|
9352
9989
|
} catch {
|
|
9353
9990
|
return text;
|
|
@@ -9366,22 +10003,22 @@ function applyPathRedactors(text, pathRedactors, mask, cap, transform) {
|
|
|
9366
10003
|
}
|
|
9367
10004
|
});
|
|
9368
10005
|
}
|
|
9369
|
-
return changed ?
|
|
10006
|
+
return changed ? restoreNumbers(JSON.stringify(obj), preserved.numbers) : text;
|
|
9370
10007
|
}
|
|
9371
|
-
function
|
|
10008
|
+
function preserveNumbers(text, mask) {
|
|
9372
10009
|
const occupied = /* @__PURE__ */ new Set([mask]);
|
|
9373
|
-
const
|
|
10010
|
+
const found = [];
|
|
9374
10011
|
const tokens = /"(?:[^"\\]|\\[\s\S])*"|-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/g;
|
|
9375
10012
|
for (const match of text.matchAll(tokens)) {
|
|
9376
10013
|
const token = match[0];
|
|
9377
10014
|
if (token[0] === '"') occupied.add(JSON.parse(token));
|
|
9378
|
-
else
|
|
10015
|
+
else found.push({ start: match.index, token });
|
|
9379
10016
|
}
|
|
9380
10017
|
const numbers = /* @__PURE__ */ new Map();
|
|
9381
10018
|
const chunks = [];
|
|
9382
10019
|
let offset = 0;
|
|
9383
10020
|
let index = 0;
|
|
9384
|
-
for (const { start, token } of
|
|
10021
|
+
for (const { start, token } of found) {
|
|
9385
10022
|
let marker;
|
|
9386
10023
|
do {
|
|
9387
10024
|
marker = "__PSNUMBER_" + index++ + "__";
|
|
@@ -9393,7 +10030,7 @@ function preserveBigInts(text, mask) {
|
|
|
9393
10030
|
chunks.push(text.slice(offset));
|
|
9394
10031
|
return { text: chunks.join(""), numbers };
|
|
9395
10032
|
}
|
|
9396
|
-
function
|
|
10033
|
+
function restoreNumbers(text, numbers) {
|
|
9397
10034
|
return text.replace(/"(__PSNUMBER_\d+__)"/g, (token, marker) => numbers.get(marker) ?? token);
|
|
9398
10035
|
}
|
|
9399
10036
|
function isHeaderMutation(action) {
|