@patchstack/connect 0.5.15 → 0.5.17
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 +42 -14
- package/README.md +5 -5
- package/dist/cli.js +695 -183
- package/dist/cli.js.map +1 -1
- package/dist/protect/templates/demo-rules.json +2 -2
- package/dist/protect.cjs +1388 -748
- 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 +1376 -736
- package/dist/protect.edge.js.map +3 -3
- package/dist/protect.js +1376 -736
- 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) {
|
|
@@ -6206,7 +6460,10 @@ var DEFAULT_RESPONSE_RULES = [
|
|
|
6206
6460
|
// the constraint name, the column and the offending value discloses the schema anyway.
|
|
6207
6461
|
action: "block",
|
|
6208
6462
|
prefilter: ["SQLSTATE", "Sequelize", "ER_", "ORA-", "PG::", "SQLITE_ERROR", "SQL syntax"],
|
|
6209
|
-
|
|
6463
|
+
// Case-sensitive, because every signature is emitted in one fixed case. `ER_` is the one that needs
|
|
6464
|
+
// it: MySQL error codes are whole upper-case words (`ER_DUP_ENTRY`), and matched without case or a
|
|
6465
|
+
// word boundary it finds the end of ordinary identifiers such as a `cover__background` class name.
|
|
6466
|
+
rule_v2: [{ parameter: "response.body", match: { type: "regex", value: "/(SQLSTATE\\[[0-9A-Z]+\\]|SequelizeDatabaseError|\\bER_[A-Z][A-Z0-9_]*|ORA-\\d{5}|PG::[A-Za-z]+Error|SQLITE_ERROR|You have an error in your SQL syntax)/" } }]
|
|
6210
6467
|
},
|
|
6211
6468
|
{
|
|
6212
6469
|
id: "resp-exception-trace",
|
|
@@ -6838,7 +7095,7 @@ function makeRefreshHandler(tick, secret) {
|
|
|
6838
7095
|
return async (request) => {
|
|
6839
7096
|
if (!secret) return new Response("not found", { status: 404 });
|
|
6840
7097
|
const provided = request?.headers?.get?.("x-patchstack-refresh") ?? null;
|
|
6841
|
-
if (provided
|
|
7098
|
+
if (!await sameSecret(provided, secret)) return new Response("forbidden", { status: 403 });
|
|
6842
7099
|
let refreshed = true;
|
|
6843
7100
|
try {
|
|
6844
7101
|
const status = await runOnce();
|
|
@@ -6849,6 +7106,33 @@ function makeRefreshHandler(tick, secret) {
|
|
|
6849
7106
|
return new Response(JSON.stringify({ refreshed }), { status: 200, headers: { "content-type": "application/json" } });
|
|
6850
7107
|
};
|
|
6851
7108
|
}
|
|
7109
|
+
async function sameSecret(provided, secret) {
|
|
7110
|
+
if (typeof provided !== "string" || typeof secret !== "string") return false;
|
|
7111
|
+
const subtle = globalThis.crypto?.subtle;
|
|
7112
|
+
if (subtle && typeof TextEncoder === "function") {
|
|
7113
|
+
try {
|
|
7114
|
+
const encoder2 = new TextEncoder();
|
|
7115
|
+
const [a, b] = await Promise.all([
|
|
7116
|
+
subtle.digest("SHA-256", encoder2.encode(provided)),
|
|
7117
|
+
subtle.digest("SHA-256", encoder2.encode(secret))
|
|
7118
|
+
]);
|
|
7119
|
+
return equalBytes(new Uint8Array(a), new Uint8Array(b));
|
|
7120
|
+
} catch {
|
|
7121
|
+
}
|
|
7122
|
+
}
|
|
7123
|
+
let diff = provided.length ^ secret.length;
|
|
7124
|
+
const length = Math.max(provided.length, secret.length);
|
|
7125
|
+
for (let i = 0; i < length; i++) {
|
|
7126
|
+
diff |= (provided.charCodeAt(i) || 0) ^ (secret.charCodeAt(i) || 0);
|
|
7127
|
+
}
|
|
7128
|
+
return diff === 0;
|
|
7129
|
+
}
|
|
7130
|
+
function equalBytes(a, b) {
|
|
7131
|
+
if (a.length !== b.length) return false;
|
|
7132
|
+
let diff = 0;
|
|
7133
|
+
for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i];
|
|
7134
|
+
return diff === 0;
|
|
7135
|
+
}
|
|
6852
7136
|
|
|
6853
7137
|
// src/protect/detections.js
|
|
6854
7138
|
init_pulse_token();
|
|
@@ -7584,9 +7868,180 @@ function hardensWithoutBody(rule) {
|
|
|
7584
7868
|
if (!rule || !HEADER_ACTIONS.includes(rule.action)) return false;
|
|
7585
7869
|
const { parameters, complete } = readRuleParameters(rule);
|
|
7586
7870
|
if (!complete) return false;
|
|
7587
|
-
return parameters.every(
|
|
7588
|
-
|
|
7871
|
+
return parameters.every(bodyIndependent);
|
|
7872
|
+
}
|
|
7873
|
+
function readsOnlyResponseHeaders(rule) {
|
|
7874
|
+
if (!rule) return false;
|
|
7875
|
+
const { parameters, complete } = readRuleParameters(rule);
|
|
7876
|
+
if (!complete || !parameters.some(isHeaderParameter)) return false;
|
|
7877
|
+
return parameters.every(bodyIndependent);
|
|
7878
|
+
}
|
|
7879
|
+
function isHeaderParameter(parameter) {
|
|
7880
|
+
return parameter === "response.headers" || parameter.startsWith(BODY_INDEPENDENT_PREFIX);
|
|
7881
|
+
}
|
|
7882
|
+
function bodyIndependent(parameter) {
|
|
7883
|
+
return BODY_INDEPENDENT.includes(parameter) || parameter.startsWith(BODY_INDEPENDENT_PREFIX);
|
|
7884
|
+
}
|
|
7885
|
+
|
|
7886
|
+
// src/protect/supabase-guard.js
|
|
7887
|
+
var GUARD_PATH = "/_patchstack/guard";
|
|
7888
|
+
var DEFAULT_MAX_BODY_BYTES = 1024 * 1024;
|
|
7889
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
7890
|
+
var REQUEST_HEADER_DENYLIST = /* @__PURE__ */ new Set([
|
|
7891
|
+
"connection",
|
|
7892
|
+
"cookie",
|
|
7893
|
+
"cookie2",
|
|
7894
|
+
"forwarded",
|
|
7895
|
+
"host",
|
|
7896
|
+
"keep-alive",
|
|
7897
|
+
"proxy-authenticate",
|
|
7898
|
+
"proxy-authorization",
|
|
7899
|
+
"te",
|
|
7900
|
+
"trailer",
|
|
7901
|
+
"transfer-encoding",
|
|
7902
|
+
"upgrade",
|
|
7903
|
+
"x-forwarded-for",
|
|
7904
|
+
"x-forwarded-host",
|
|
7905
|
+
"x-forwarded-port",
|
|
7906
|
+
"x-forwarded-proto",
|
|
7907
|
+
"x-ps-target",
|
|
7908
|
+
"x-real-ip"
|
|
7909
|
+
]);
|
|
7910
|
+
var RESPONSE_HEADER_DENYLIST = /* @__PURE__ */ new Set([
|
|
7911
|
+
"connection",
|
|
7912
|
+
"content-encoding",
|
|
7913
|
+
"content-length",
|
|
7914
|
+
"keep-alive",
|
|
7915
|
+
"proxy-authenticate",
|
|
7916
|
+
"proxy-authorization",
|
|
7917
|
+
"set-cookie",
|
|
7918
|
+
"set-cookie2",
|
|
7919
|
+
"te",
|
|
7920
|
+
"trailer",
|
|
7921
|
+
"transfer-encoding",
|
|
7922
|
+
"upgrade"
|
|
7923
|
+
]);
|
|
7924
|
+
var BodyTooLargeError = class extends Error {
|
|
7925
|
+
};
|
|
7926
|
+
async function boundedBody(request, maxBytes) {
|
|
7927
|
+
const stated2 = Number(request.headers.get("content-length"));
|
|
7928
|
+
if (Number.isFinite(stated2) && stated2 > maxBytes) throw new BodyTooLargeError();
|
|
7929
|
+
if (!request.body) return void 0;
|
|
7930
|
+
const reader = request.body.getReader();
|
|
7931
|
+
const chunks = [];
|
|
7932
|
+
let total = 0;
|
|
7933
|
+
try {
|
|
7934
|
+
for (; ; ) {
|
|
7935
|
+
const { done, value } = await reader.read();
|
|
7936
|
+
if (done) break;
|
|
7937
|
+
total += value.byteLength;
|
|
7938
|
+
if (total > maxBytes) {
|
|
7939
|
+
void reader.cancel().catch(() => {
|
|
7940
|
+
});
|
|
7941
|
+
throw new BodyTooLargeError();
|
|
7942
|
+
}
|
|
7943
|
+
chunks.push(value);
|
|
7944
|
+
}
|
|
7945
|
+
} finally {
|
|
7946
|
+
reader.releaseLock();
|
|
7947
|
+
}
|
|
7948
|
+
const body = new Uint8Array(total);
|
|
7949
|
+
let offset = 0;
|
|
7950
|
+
for (const chunk of chunks) {
|
|
7951
|
+
body.set(chunk, offset);
|
|
7952
|
+
offset += chunk.byteLength;
|
|
7953
|
+
}
|
|
7954
|
+
return body;
|
|
7955
|
+
}
|
|
7956
|
+
function requestHeaders(input) {
|
|
7957
|
+
const output = new Headers();
|
|
7958
|
+
const connectionTokens = new Set(
|
|
7959
|
+
(input.get("connection") ?? "").split(",").map((value) => value.trim().toLowerCase()).filter(Boolean)
|
|
7589
7960
|
);
|
|
7961
|
+
input.forEach((value, key) => {
|
|
7962
|
+
const lower = key.toLowerCase();
|
|
7963
|
+
if (!REQUEST_HEADER_DENYLIST.has(lower) && !connectionTokens.has(lower)) output.append(key, value);
|
|
7964
|
+
});
|
|
7965
|
+
return output;
|
|
7966
|
+
}
|
|
7967
|
+
function responseHeaders(input) {
|
|
7968
|
+
const output = new Headers();
|
|
7969
|
+
const connectionTokens = new Set(
|
|
7970
|
+
(input.get("connection") ?? "").split(",").map((value) => value.trim().toLowerCase()).filter(Boolean)
|
|
7971
|
+
);
|
|
7972
|
+
input.forEach((value, key) => {
|
|
7973
|
+
const lower = key.toLowerCase();
|
|
7974
|
+
if (!RESPONSE_HEADER_DENYLIST.has(lower) && !connectionTokens.has(lower)) output.append(key, value);
|
|
7975
|
+
});
|
|
7976
|
+
return output;
|
|
7977
|
+
}
|
|
7978
|
+
var SOURCE_REQUEST = /* @__PURE__ */ Symbol.for("@patchstack/connect.sourceRequest");
|
|
7979
|
+
function createSupabaseGuard({
|
|
7980
|
+
protection,
|
|
7981
|
+
supabaseUrl,
|
|
7982
|
+
fetchImpl = fetch,
|
|
7983
|
+
maxBodyBytes = DEFAULT_MAX_BODY_BYTES,
|
|
7984
|
+
timeoutMs = DEFAULT_TIMEOUT_MS
|
|
7985
|
+
}) {
|
|
7986
|
+
const guard = protection.fetchGuard();
|
|
7987
|
+
const allowedOrigin = supabaseUrl ? new URL(supabaseUrl).origin : null;
|
|
7988
|
+
const bodyLimit = Number.isFinite(maxBodyBytes) && maxBodyBytes > 0 ? maxBodyBytes : DEFAULT_MAX_BODY_BYTES;
|
|
7989
|
+
const upstreamTimeout = Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : DEFAULT_TIMEOUT_MS;
|
|
7990
|
+
return async function handleGuardRequest(request, ...hostArgs) {
|
|
7991
|
+
const target = request.headers.get("x-ps-target");
|
|
7992
|
+
if (!target) return new Response("patchstack: missing x-ps-target", { status: 400 });
|
|
7993
|
+
let targetUrl;
|
|
7994
|
+
try {
|
|
7995
|
+
targetUrl = new URL(target);
|
|
7996
|
+
} catch {
|
|
7997
|
+
return new Response("patchstack: invalid target", { status: 400 });
|
|
7998
|
+
}
|
|
7999
|
+
if (!allowedOrigin || targetUrl.protocol !== "https:" || targetUrl.origin !== allowedOrigin) {
|
|
8000
|
+
return new Response("patchstack: target not allowed", { status: 403 });
|
|
8001
|
+
}
|
|
8002
|
+
const hasBody = request.method !== "GET" && request.method !== "HEAD";
|
|
8003
|
+
let body;
|
|
8004
|
+
try {
|
|
8005
|
+
body = hasBody ? await boundedBody(request, bodyLimit) : void 0;
|
|
8006
|
+
} catch (error) {
|
|
8007
|
+
if (error instanceof BodyTooLargeError) {
|
|
8008
|
+
return new Response("patchstack: request body too large", { status: 413 });
|
|
8009
|
+
}
|
|
8010
|
+
throw error;
|
|
8011
|
+
}
|
|
8012
|
+
const forwardHeaders = requestHeaders(request.headers);
|
|
8013
|
+
const evalReq = new Request(targetUrl.toString(), {
|
|
8014
|
+
method: request.method,
|
|
8015
|
+
// Inspect what the browser sent, including app-origin cookies; only the forwarded copy is
|
|
8016
|
+
// sanitized at the cross-origin boundary below.
|
|
8017
|
+
headers: request.headers,
|
|
8018
|
+
body
|
|
8019
|
+
});
|
|
8020
|
+
Object.defineProperty(evalReq, SOURCE_REQUEST, { value: request });
|
|
8021
|
+
const blocked = await guard(evalReq, ...hostArgs);
|
|
8022
|
+
if (blocked) return blocked;
|
|
8023
|
+
let upstream;
|
|
8024
|
+
try {
|
|
8025
|
+
upstream = await fetchImpl(targetUrl.toString(), {
|
|
8026
|
+
method: request.method,
|
|
8027
|
+
headers: forwardHeaders,
|
|
8028
|
+
body,
|
|
8029
|
+
redirect: "manual",
|
|
8030
|
+
signal: AbortSignal.timeout(upstreamTimeout)
|
|
8031
|
+
});
|
|
8032
|
+
} catch (error) {
|
|
8033
|
+
if (error?.name === "TimeoutError" || error?.name === "AbortError") {
|
|
8034
|
+
return new Response("patchstack: upstream timed out", { status: 504 });
|
|
8035
|
+
}
|
|
8036
|
+
throw error;
|
|
8037
|
+
}
|
|
8038
|
+
const forwarded = new Response(upstream.body, {
|
|
8039
|
+
status: upstream.status,
|
|
8040
|
+
statusText: upstream.statusText,
|
|
8041
|
+
headers: responseHeaders(upstream.headers)
|
|
8042
|
+
});
|
|
8043
|
+
return protection.screenResponse ? protection.screenResponse(forwarded, evalReq) : forwarded;
|
|
8044
|
+
};
|
|
7590
8045
|
}
|
|
7591
8046
|
|
|
7592
8047
|
// src/protect/firewall-log.js
|
|
@@ -7637,8 +8092,13 @@ function resolveApiBase(pulseOrManifestUrl) {
|
|
|
7637
8092
|
function createFirewallLogReporter(opts) {
|
|
7638
8093
|
const creds = parseApiKey(opts.apiKey);
|
|
7639
8094
|
if (!creds) {
|
|
7640
|
-
return {
|
|
7641
|
-
|
|
8095
|
+
return {
|
|
8096
|
+
record() {
|
|
8097
|
+
},
|
|
8098
|
+
flush: () => Promise.resolve(),
|
|
8099
|
+
stop: () => Promise.resolve(),
|
|
8100
|
+
health: () => ({ recorded: 0, delivered: 0, failed: 0, dropped: 0, queued: 0 })
|
|
8101
|
+
};
|
|
7642
8102
|
}
|
|
7643
8103
|
const apiBase = safeBaseUrl(opts.apiBase, DEFAULT_API_BASE, "block-log API").replace(/\/$/, "");
|
|
7644
8104
|
const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
@@ -7651,6 +8111,19 @@ function createFirewallLogReporter(opts) {
|
|
|
7651
8111
|
let drainPromise = null;
|
|
7652
8112
|
let activeController = null;
|
|
7653
8113
|
let ended = false;
|
|
8114
|
+
let recorded = 0;
|
|
8115
|
+
let delivered = 0;
|
|
8116
|
+
let failed = 0;
|
|
8117
|
+
let dropped = 0;
|
|
8118
|
+
let activeBatch = null;
|
|
8119
|
+
const settle = (sent, outcome) => {
|
|
8120
|
+
if (sent.counted) return;
|
|
8121
|
+
sent.counted = true;
|
|
8122
|
+
if (outcome === "delivered") delivered += sent.size;
|
|
8123
|
+
else if (outcome === "failed") failed += sent.size;
|
|
8124
|
+
else dropped += sent.size;
|
|
8125
|
+
if (activeBatch === sent) activeBatch = null;
|
|
8126
|
+
};
|
|
7654
8127
|
let cachedToken = null;
|
|
7655
8128
|
let tokenInflight = null;
|
|
7656
8129
|
const fetchAccessToken = async (signal) => {
|
|
@@ -7704,10 +8177,16 @@ function createFirewallLogReporter(opts) {
|
|
|
7704
8177
|
const attemptTimer = setTimeout(() => controller?.abort(), ATTEMPT_TIMEOUT_MS2);
|
|
7705
8178
|
attemptTimer?.unref?.();
|
|
7706
8179
|
let entry;
|
|
8180
|
+
const sent = { size: batch.length, counted: false };
|
|
8181
|
+
activeBatch = sent;
|
|
7707
8182
|
entry = (async () => {
|
|
7708
8183
|
try {
|
|
7709
8184
|
const token = await fetchAccessToken(controller?.signal);
|
|
7710
|
-
if (
|
|
8185
|
+
if (ended) return;
|
|
8186
|
+
if (!token) {
|
|
8187
|
+
settle(sent, "failed");
|
|
8188
|
+
return;
|
|
8189
|
+
}
|
|
7711
8190
|
const body = new URLSearchParams();
|
|
7712
8191
|
body.set("type", "firewall");
|
|
7713
8192
|
body.set("logs", JSON.stringify(batch));
|
|
@@ -7724,10 +8203,11 @@ function createFirewallLogReporter(opts) {
|
|
|
7724
8203
|
// Both phases carry the attempt controller, so slow transports cannot accumulate work.
|
|
7725
8204
|
...controller ? { signal: controller.signal } : {}
|
|
7726
8205
|
});
|
|
7727
|
-
|
|
7728
|
-
|
|
8206
|
+
const res = p && typeof p.then === "function" ? await p.catch(() => null) : p;
|
|
8207
|
+
settle(sent, res && res.ok ? "delivered" : "failed");
|
|
7729
8208
|
} catch {
|
|
7730
8209
|
} finally {
|
|
8210
|
+
settle(sent, "failed");
|
|
7731
8211
|
clearTimeout(attemptTimer);
|
|
7732
8212
|
if (activeController === controller) activeController = null;
|
|
7733
8213
|
if (inFlight === entry) inFlight = null;
|
|
@@ -7754,7 +8234,11 @@ function createFirewallLogReporter(opts) {
|
|
|
7754
8234
|
if (stopped) return;
|
|
7755
8235
|
const fid = event?.rule?.id;
|
|
7756
8236
|
if (fid === void 0 || fid === null || fid === "") return;
|
|
7757
|
-
if (queue.length >= MAX_QUEUE2)
|
|
8237
|
+
if (queue.length >= MAX_QUEUE2) {
|
|
8238
|
+
dropped++;
|
|
8239
|
+
return;
|
|
8240
|
+
}
|
|
8241
|
+
recorded++;
|
|
7758
8242
|
queue.push({
|
|
7759
8243
|
fid,
|
|
7760
8244
|
method: event.method ?? null,
|
|
@@ -7770,6 +8254,10 @@ function createFirewallLogReporter(opts) {
|
|
|
7770
8254
|
if (!timer) timer = setTimeout(flush, flushMs);
|
|
7771
8255
|
},
|
|
7772
8256
|
flush,
|
|
8257
|
+
/** Where the records went so far; see the counters above. `queued` is what is waiting now. */
|
|
8258
|
+
health() {
|
|
8259
|
+
return { recorded, delivered, failed, dropped, queued: queue.length };
|
|
8260
|
+
},
|
|
7773
8261
|
/**
|
|
7774
8262
|
* Stop, and hand back a wait for what was outstanding.
|
|
7775
8263
|
*
|
|
@@ -7788,6 +8276,8 @@ function createFirewallLogReporter(opts) {
|
|
|
7788
8276
|
ended = true;
|
|
7789
8277
|
activeController?.abort();
|
|
7790
8278
|
activeController = null;
|
|
8279
|
+
if (activeBatch) settle(activeBatch, "dropped");
|
|
8280
|
+
dropped += queue.length;
|
|
7791
8281
|
queue = [];
|
|
7792
8282
|
inFlight = null;
|
|
7793
8283
|
};
|
|
@@ -7824,165 +8314,6 @@ function telemetryEnabled() {
|
|
|
7824
8314
|
return !/^(0|false|off|no)$/i.test(String(v));
|
|
7825
8315
|
}
|
|
7826
8316
|
|
|
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
8317
|
// src/protect/verify-sentinel.js
|
|
7987
8318
|
var VERIFY_HEADER = "x-patchstack-verify";
|
|
7988
8319
|
var CHALLENGE = "PATCHSTACK_VERIFY_CHALLENGE";
|
|
@@ -8169,20 +8500,47 @@ async function createProtection(options = {}) {
|
|
|
8169
8500
|
whitelist_keys: delivered.whitelist_keys,
|
|
8170
8501
|
onError
|
|
8171
8502
|
});
|
|
8172
|
-
responseRuleSet = responseRules.map((rule) =>
|
|
8173
|
-
|
|
8174
|
-
|
|
8175
|
-
|
|
8176
|
-
|
|
8177
|
-
|
|
8178
|
-
|
|
8179
|
-
|
|
8180
|
-
|
|
8181
|
-
|
|
8182
|
-
|
|
8183
|
-
|
|
8184
|
-
|
|
8185
|
-
|
|
8503
|
+
responseRuleSet = responseRules.map((rule) => {
|
|
8504
|
+
const unscoped = [];
|
|
8505
|
+
const rewrites = rule.action === "redact" || rule.action === "encode";
|
|
8506
|
+
let derived;
|
|
8507
|
+
try {
|
|
8508
|
+
const redactors = rewrites ? extractRedactors(rule, (parameter) => unscoped.push(parameter)) : null;
|
|
8509
|
+
const mutatedSpan = rewrites && hasSpanMutations(rule);
|
|
8510
|
+
derived = {
|
|
8511
|
+
redactors,
|
|
8512
|
+
mutatedSpan,
|
|
8513
|
+
// A redaction that reads response headers only, and masks spans in them, and an explicit block
|
|
8514
|
+
// that reads response headers only, are decided and carried out without the body — so they
|
|
8515
|
+
// still apply to a response whose body was not screened.
|
|
8516
|
+
redactsHeaders: rule.action === "redact" && !mutatedSpan && (redactors ?? []).some((r) => !r.jsonPath) && readsOnlyResponseHeaders(rule),
|
|
8517
|
+
blocksOnHeaders: rule.action === "block" && readsOnlyResponseHeaders(rule)
|
|
8518
|
+
};
|
|
8519
|
+
} catch (err) {
|
|
8520
|
+
notify(onError, err, "onError");
|
|
8521
|
+
derived = { redactors: null, mutatedSpan: false, redactsHeaders: false, blocksOnHeaders: false };
|
|
8522
|
+
}
|
|
8523
|
+
if (unscoped.length) {
|
|
8524
|
+
notify(
|
|
8525
|
+
onError,
|
|
8526
|
+
new Error(
|
|
8527
|
+
`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`
|
|
8528
|
+
),
|
|
8529
|
+
"onError"
|
|
8530
|
+
);
|
|
8531
|
+
}
|
|
8532
|
+
return {
|
|
8533
|
+
rule,
|
|
8534
|
+
engine: new RuleEngine({ firewall: [rule], onError }),
|
|
8535
|
+
...derived,
|
|
8536
|
+
// Optional cheap pre-filter: literal anchor(s) that MUST appear for the (expensive) regex to
|
|
8537
|
+
// have any chance of matching. Lets screenText skip the full scan on responses with no candidate
|
|
8538
|
+
// — the common case — cutting CPU/latency and shrinking the regex/ReDoS surface.
|
|
8539
|
+
// Case-insensitive, and checked against the header values as well as the body, since a rule may
|
|
8540
|
+
// read either.
|
|
8541
|
+
prefilter: Array.isArray(rule.prefilter) && rule.prefilter.length ? rule.prefilter.map((s) => String(s).toLowerCase()) : null
|
|
8542
|
+
};
|
|
8543
|
+
});
|
|
8186
8544
|
egressRuleSet = egressRules.map((rule) => ({
|
|
8187
8545
|
rule,
|
|
8188
8546
|
engine: new RuleEngine({ firewall: [rule], onError })
|
|
@@ -8196,6 +8554,9 @@ async function createProtection(options = {}) {
|
|
|
8196
8554
|
skipCounts[key] = (skipCounts[key] ?? 0) + 1;
|
|
8197
8555
|
notify(onSkip, { phase, reason, detail, count: skipCounts[key] }, "onSkip");
|
|
8198
8556
|
};
|
|
8557
|
+
const recordEvaluationSkips = (result) => {
|
|
8558
|
+
for (const reason of result?.skips ?? []) recordSkip("request", reason);
|
|
8559
|
+
};
|
|
8199
8560
|
const maskFn = typeof options.maskWith === "function" ? options.maskWith : () => typeof options.maskWith === "string" ? options.maskWith : "[REDACTED]";
|
|
8200
8561
|
const ruleMode = (rule) => rule?.enforcement === "dry-run" ? "dry-run" : mode;
|
|
8201
8562
|
const planCache = createPlanCache();
|
|
@@ -8238,15 +8599,19 @@ async function createProtection(options = {}) {
|
|
|
8238
8599
|
});
|
|
8239
8600
|
return effectiveMode === "block" ? block() : allow2();
|
|
8240
8601
|
};
|
|
8602
|
+
const decidedByHeaders = (_rule, entry) => Boolean(entry?.redactsHeaders || entry?.blocksOnHeaders);
|
|
8603
|
+
const decidedWithoutBody = (rule, entry) => hardensWithoutBody(rule) || decidedByHeaders(rule, entry);
|
|
8241
8604
|
const screenText = (text, meta, reqCtx, only) => {
|
|
8242
8605
|
let blockRule = null;
|
|
8243
8606
|
const redactions = [];
|
|
8244
8607
|
const headerMutations = [];
|
|
8245
8608
|
let lowerText = null;
|
|
8246
|
-
|
|
8247
|
-
|
|
8609
|
+
const responseSkips = /* @__PURE__ */ new Set();
|
|
8610
|
+
for (const entry of responseRuleSet) {
|
|
8611
|
+
const { rule, engine: re, redactors, prefilter, mutatedSpan } = entry;
|
|
8612
|
+
if (only && !only(rule, entry)) continue;
|
|
8248
8613
|
if (prefilter) {
|
|
8249
|
-
if (lowerText === null) lowerText = text.
|
|
8614
|
+
if (lowerText === null) lowerText = prefilterText(text, meta.headers);
|
|
8250
8615
|
if (!prefilter.some((p) => lowerText.includes(p))) continue;
|
|
8251
8616
|
}
|
|
8252
8617
|
let result;
|
|
@@ -8256,6 +8621,11 @@ async function createProtection(options = {}) {
|
|
|
8256
8621
|
notify(onError, err, "onError");
|
|
8257
8622
|
continue;
|
|
8258
8623
|
}
|
|
8624
|
+
for (const reason of result.skips ?? []) {
|
|
8625
|
+
if (responseSkips.has(reason)) continue;
|
|
8626
|
+
responseSkips.add(reason);
|
|
8627
|
+
recordSkip("response", reason);
|
|
8628
|
+
}
|
|
8259
8629
|
if (!result.blocked) continue;
|
|
8260
8630
|
const responseMode = ruleMode(rule);
|
|
8261
8631
|
onDetect({
|
|
@@ -8285,6 +8655,8 @@ async function createProtection(options = {}) {
|
|
|
8285
8655
|
if (blockRule) return { verdict: "block" };
|
|
8286
8656
|
let body = text;
|
|
8287
8657
|
const headers = { ...meta.headers || {} };
|
|
8658
|
+
let responseIsJson;
|
|
8659
|
+
let structure;
|
|
8288
8660
|
for (const { rule, redactors } of redactions) {
|
|
8289
8661
|
const mask = maskFn(rule.category);
|
|
8290
8662
|
const transform = rule.action === "encode" ? htmlEscape : null;
|
|
@@ -8293,23 +8665,32 @@ async function createProtection(options = {}) {
|
|
|
8293
8665
|
if (pathRedactors.length) body = applyPathRedactors(body, pathRedactors, mask, screenCap, transform);
|
|
8294
8666
|
if (!spanRedactors.length) continue;
|
|
8295
8667
|
const beforeSpan = body;
|
|
8296
|
-
|
|
8297
|
-
if (body
|
|
8668
|
+
const bodySpans = spanRedactors.filter((r) => r.scope.body === true);
|
|
8669
|
+
if (bodySpans.length) body = applyRedactors(body, bodySpans, mask, transform);
|
|
8670
|
+
if (body !== beforeSpan) {
|
|
8671
|
+
responseIsJson ??= isJson(text);
|
|
8672
|
+
if (responseIsJson) {
|
|
8673
|
+
if (structure?.text !== beforeSpan) structure = { text: beforeSpan, tokens: lexJson(beforeSpan) };
|
|
8674
|
+
const tokens = lexJson(body);
|
|
8675
|
+
if (!sameJsonStructure(structure.text, structure.tokens, body, tokens)) return { verdict: "block" };
|
|
8676
|
+
structure = { text: body, tokens };
|
|
8677
|
+
}
|
|
8678
|
+
}
|
|
8298
8679
|
if (transform) continue;
|
|
8299
8680
|
for (const name of Object.keys(headers)) {
|
|
8300
8681
|
const value = headers[name];
|
|
8682
|
+
const headerSpans = spanRedactors.filter((r) => masksHeader(r, name));
|
|
8301
8683
|
if (typeof value === "string") {
|
|
8302
|
-
setOwn(headers, name, applyRedactors(value,
|
|
8684
|
+
setOwn(headers, name, applyRedactors(value, headerSpans, mask));
|
|
8303
8685
|
} else if (Array.isArray(value)) {
|
|
8304
8686
|
setOwn(
|
|
8305
8687
|
headers,
|
|
8306
8688
|
name,
|
|
8307
|
-
value.map((item) => typeof item === "string" ? applyRedactors(item,
|
|
8689
|
+
value.map((item) => typeof item === "string" ? applyRedactors(item, headerSpans, mask) : item)
|
|
8308
8690
|
);
|
|
8309
8691
|
}
|
|
8310
8692
|
}
|
|
8311
8693
|
}
|
|
8312
|
-
if (body !== text && isJson(text) && !isJson(body)) return { verdict: "block" };
|
|
8313
8694
|
for (const rule of headerMutations) applyHeaderMutation(headers, rule);
|
|
8314
8695
|
return { verdict: "redact", body, headers };
|
|
8315
8696
|
};
|
|
@@ -8352,18 +8733,53 @@ async function createProtection(options = {}) {
|
|
|
8352
8733
|
}
|
|
8353
8734
|
};
|
|
8354
8735
|
};
|
|
8355
|
-
const
|
|
8736
|
+
const peerOf = (request, hostArgs) => {
|
|
8737
|
+
if (typeof options.peerAddress !== "function") return void 0;
|
|
8738
|
+
try {
|
|
8739
|
+
return options.peerAddress(request?.[SOURCE_REQUEST] ?? request, ...hostArgs);
|
|
8740
|
+
} catch (err) {
|
|
8741
|
+
notify(onError, err, "onError");
|
|
8742
|
+
return void 0;
|
|
8743
|
+
}
|
|
8744
|
+
};
|
|
8745
|
+
let warnedNoPeer = false;
|
|
8746
|
+
const warnNoPeer = () => {
|
|
8747
|
+
if (warnedNoPeer) return;
|
|
8748
|
+
warnedNoPeer = true;
|
|
8749
|
+
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.";
|
|
8750
|
+
if (typeof onError === "function") notify(onError, new Error(message), "onError");
|
|
8751
|
+
else console.warn(message);
|
|
8752
|
+
};
|
|
8753
|
+
const clientByRequest = /* @__PURE__ */ new WeakMap();
|
|
8754
|
+
const clientForResponse = (request, hostArgs) => {
|
|
8755
|
+
const known = clientByRequest.get(request);
|
|
8756
|
+
if (known) return known;
|
|
8757
|
+
const client = resolveClientIp({
|
|
8758
|
+
peer: peerOf(request, hostArgs),
|
|
8759
|
+
headers: headerObject(request.headers),
|
|
8760
|
+
trustedProxy: options.trustedProxy
|
|
8761
|
+
});
|
|
8762
|
+
if (options.trustedProxy !== void 0 && client.source === "unavailable") warnNoPeer();
|
|
8763
|
+
return client;
|
|
8764
|
+
};
|
|
8765
|
+
const screenFetchRequest = async (request, hostArgs = []) => {
|
|
8356
8766
|
let result;
|
|
8357
8767
|
let shaped;
|
|
8358
8768
|
try {
|
|
8359
8769
|
shaped = await fromFetchRequest(request, {
|
|
8770
|
+
peer: peerOf(request, hostArgs),
|
|
8360
8771
|
trustedProxy: options.trustedProxy,
|
|
8361
8772
|
maxBodyBytes: options.maxBodyBytes
|
|
8362
8773
|
});
|
|
8774
|
+
if (shaped?._clientIp) {
|
|
8775
|
+
clientByRequest.set(request, shaped._clientIp);
|
|
8776
|
+
if (options.trustedProxy !== void 0 && shaped._clientIp.source === "unavailable") warnNoPeer();
|
|
8777
|
+
}
|
|
8363
8778
|
if (shaped?._bodyInspectionSkip) {
|
|
8364
8779
|
recordSkip("request", shaped._bodyInspectionSkip, { limit: options.maxBodyBytes ?? 1024 * 1024 });
|
|
8365
8780
|
}
|
|
8366
8781
|
result = engine.evaluate(shaped);
|
|
8782
|
+
recordEvaluationSkips(result);
|
|
8367
8783
|
} catch (err) {
|
|
8368
8784
|
notify(onError, err, "onError");
|
|
8369
8785
|
return { blocked: null, client: void 0 };
|
|
@@ -8413,15 +8829,19 @@ async function createProtection(options = {}) {
|
|
|
8413
8829
|
const read = await readTextResponse(response, screenCap);
|
|
8414
8830
|
if (read.skip) {
|
|
8415
8831
|
if (read.skip !== "not-a-response") {
|
|
8416
|
-
recordSkip("response", read.skip, {
|
|
8832
|
+
recordSkip("response", read.skip, {
|
|
8833
|
+
status: response?.status,
|
|
8834
|
+
...read.encoding ? { encoding: read.encoding } : {},
|
|
8835
|
+
...read.charset ? { charset: read.charset } : {}
|
|
8836
|
+
});
|
|
8417
8837
|
}
|
|
8418
8838
|
if (read.skip === "not-a-response") return response;
|
|
8419
8839
|
return hardenHeadersOnly(response, reqCtx);
|
|
8420
8840
|
}
|
|
8421
|
-
const text = read.text;
|
|
8841
|
+
const text = readJsonEscapes(read.text);
|
|
8422
8842
|
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);
|
|
8843
|
+
if (r.verdict === "block" || r.verdict === "redact" && outgrowsCharset(read.ascii, r.body)) return leakResponse();
|
|
8844
|
+
if (r.verdict === "redact") return rebuildResponse(response, withBom(read.bom, r.body), r.headers);
|
|
8425
8845
|
return response;
|
|
8426
8846
|
};
|
|
8427
8847
|
const headerValueChanged = (was, value) => {
|
|
@@ -8483,7 +8903,8 @@ async function createProtection(options = {}) {
|
|
|
8483
8903
|
const hardenHeadersOnly = (response, reqCtx) => {
|
|
8484
8904
|
try {
|
|
8485
8905
|
const meta = { status: response.status, headers: headerObject(response.headers) };
|
|
8486
|
-
const r = screenText("", meta, reqCtx,
|
|
8906
|
+
const r = screenText("", meta, reqCtx, decidedWithoutBody);
|
|
8907
|
+
if (r.verdict === "block") return leakResponse();
|
|
8487
8908
|
if (r.verdict !== "redact" || !r.headers) return response;
|
|
8488
8909
|
if (!headersChanged(meta.headers, r.headers)) return response;
|
|
8489
8910
|
return rebuildResponse(response, response.body, r.headers);
|
|
@@ -8503,6 +8924,8 @@ async function createProtection(options = {}) {
|
|
|
8503
8924
|
const chunks = [];
|
|
8504
8925
|
let size = 0;
|
|
8505
8926
|
let overflow = false;
|
|
8927
|
+
let withheld = false;
|
|
8928
|
+
let headScreened = false;
|
|
8506
8929
|
const MAX = screenCap;
|
|
8507
8930
|
let hardened = false;
|
|
8508
8931
|
let answeredWithoutBody = false;
|
|
@@ -8534,6 +8957,7 @@ async function createProtection(options = {}) {
|
|
|
8534
8957
|
}
|
|
8535
8958
|
};
|
|
8536
8959
|
const stillToAnswer = (rule) => !hardensWithoutBody(rule);
|
|
8960
|
+
const notYetAnswered = (rule, entry) => (!answeredWithoutBody || stillToAnswer(rule)) && !(headScreened && decidedByHeaders(rule, entry));
|
|
8537
8961
|
let pendingHead = null;
|
|
8538
8962
|
let sending = false;
|
|
8539
8963
|
const origWriteHead = typeof res.writeHead === "function" ? res.writeHead.bind(res) : null;
|
|
@@ -8595,7 +9019,12 @@ async function createProtection(options = {}) {
|
|
|
8595
9019
|
if (typeof res.flushHeaders === "function") {
|
|
8596
9020
|
const origFlushHeaders = res.flushHeaders.bind(res);
|
|
8597
9021
|
res.flushHeaders = function() {
|
|
8598
|
-
|
|
9022
|
+
const unread = screenUnreadHead();
|
|
9023
|
+
if (unread?.withhold) {
|
|
9024
|
+
sendWithheld();
|
|
9025
|
+
return void 0;
|
|
9026
|
+
}
|
|
9027
|
+
sendHead(unread);
|
|
8599
9028
|
return origFlushHeaders();
|
|
8600
9029
|
};
|
|
8601
9030
|
}
|
|
@@ -8630,6 +9059,32 @@ async function createProtection(options = {}) {
|
|
|
8630
9059
|
headers: at === -1 ? set2 : { ...set2, ...writeHeadHeaderObject(pendingHead[at]) }
|
|
8631
9060
|
};
|
|
8632
9061
|
};
|
|
9062
|
+
const screenUnreadHead = () => {
|
|
9063
|
+
if (headScreened) return void 0;
|
|
9064
|
+
headScreened = true;
|
|
9065
|
+
try {
|
|
9066
|
+
const head = effectiveHead();
|
|
9067
|
+
const r = screenText("", head, reqCtx, decidedByHeaders);
|
|
9068
|
+
if (r.verdict === "block") {
|
|
9069
|
+
if (!reallySent()) return { withhold: true };
|
|
9070
|
+
recordSkip("response", "headers-sent", { action: "block" });
|
|
9071
|
+
return void 0;
|
|
9072
|
+
}
|
|
9073
|
+
if (r.verdict !== "redact" || !r.headers) return void 0;
|
|
9074
|
+
const changed = /* @__PURE__ */ new Map();
|
|
9075
|
+
for (const [name, value] of Object.entries(r.headers)) {
|
|
9076
|
+
if (headerValueChanged(head.headers[name], value)) changed.set(name.toLowerCase(), value);
|
|
9077
|
+
}
|
|
9078
|
+
if (changed.size && reallySent()) {
|
|
9079
|
+
recordSkip("response", "headers-sent", { headers: [...changed.keys()] });
|
|
9080
|
+
return void 0;
|
|
9081
|
+
}
|
|
9082
|
+
return changed.size ? { headers: changed } : void 0;
|
|
9083
|
+
} catch (err) {
|
|
9084
|
+
notify(onError, err, "onError");
|
|
9085
|
+
return void 0;
|
|
9086
|
+
}
|
|
9087
|
+
};
|
|
8633
9088
|
const sendHead = (changes) => {
|
|
8634
9089
|
if (sending) return;
|
|
8635
9090
|
const hardened2 = hardenOnce();
|
|
@@ -8651,6 +9106,17 @@ async function createProtection(options = {}) {
|
|
|
8651
9106
|
applyToResponse({ headers: merged });
|
|
8652
9107
|
origWriteHead(...args);
|
|
8653
9108
|
};
|
|
9109
|
+
const sendWithheld = (cb) => {
|
|
9110
|
+
const body = JSON.stringify({ error: "Response withheld by Patchstack (sensitive data detected)" });
|
|
9111
|
+
const headers = /* @__PURE__ */ new Map();
|
|
9112
|
+
for (const name of Object.keys(effectiveHead().headers)) headers.set(name.toLowerCase(), null);
|
|
9113
|
+
headers.set("content-type", "application/json");
|
|
9114
|
+
headers.set("content-length", String(Buffer.byteLength(body)));
|
|
9115
|
+
headers.set("transfer-encoding", null);
|
|
9116
|
+
withheld = true;
|
|
9117
|
+
sendHead({ status: 500, headers });
|
|
9118
|
+
return origEnd(body, cb);
|
|
9119
|
+
};
|
|
8654
9120
|
const applyToResponse = (changes) => {
|
|
8655
9121
|
if (changes.status !== void 0) res.statusCode = changes.status;
|
|
8656
9122
|
for (const [name, value] of changes.headers ?? []) {
|
|
@@ -8666,7 +9132,12 @@ async function createProtection(options = {}) {
|
|
|
8666
9132
|
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, typeof enc === "string" ? enc : "utf8");
|
|
8667
9133
|
size += buf.length;
|
|
8668
9134
|
if (size > MAX) {
|
|
8669
|
-
|
|
9135
|
+
const unread = screenUnreadHead();
|
|
9136
|
+
if (unread?.withhold) {
|
|
9137
|
+
sendWithheld();
|
|
9138
|
+
return;
|
|
9139
|
+
}
|
|
9140
|
+
sendHead(unread);
|
|
8670
9141
|
for (const c of chunks) origWrite(c);
|
|
8671
9142
|
chunks.length = 0;
|
|
8672
9143
|
origWrite(buf);
|
|
@@ -8677,6 +9148,11 @@ async function createProtection(options = {}) {
|
|
|
8677
9148
|
chunks.push(buf);
|
|
8678
9149
|
};
|
|
8679
9150
|
res.write = function(chunk, enc, cb) {
|
|
9151
|
+
if (withheld) {
|
|
9152
|
+
if (typeof enc === "function") enc();
|
|
9153
|
+
else if (typeof cb === "function") cb();
|
|
9154
|
+
return true;
|
|
9155
|
+
}
|
|
8680
9156
|
if (overflow) return origWrite(chunk, enc, cb);
|
|
8681
9157
|
collect(chunk, enc);
|
|
8682
9158
|
if (typeof enc === "function") enc();
|
|
@@ -8692,15 +9168,24 @@ async function createProtection(options = {}) {
|
|
|
8692
9168
|
cb = enc;
|
|
8693
9169
|
enc = void 0;
|
|
8694
9170
|
}
|
|
9171
|
+
if (withheld) {
|
|
9172
|
+
if (typeof cb === "function") cb();
|
|
9173
|
+
return res;
|
|
9174
|
+
}
|
|
8695
9175
|
if (overflow) {
|
|
8696
9176
|
if (chunk != null) origWrite(chunk, enc);
|
|
8697
9177
|
return origEnd(cb);
|
|
8698
9178
|
}
|
|
8699
9179
|
collect(chunk, enc);
|
|
9180
|
+
if (withheld) {
|
|
9181
|
+
if (typeof cb === "function") cb();
|
|
9182
|
+
return res;
|
|
9183
|
+
}
|
|
8700
9184
|
if (overflow) return origEnd(cb);
|
|
8701
9185
|
hardenOnce();
|
|
8702
|
-
const passThrough = () => {
|
|
8703
|
-
|
|
9186
|
+
const passThrough = (changes) => {
|
|
9187
|
+
if (changes?.withhold) return sendWithheld(cb);
|
|
9188
|
+
sendHead(changes);
|
|
8704
9189
|
for (const c of chunks) origWrite(c);
|
|
8705
9190
|
return origEnd(cb);
|
|
8706
9191
|
};
|
|
@@ -8711,21 +9196,28 @@ async function createProtection(options = {}) {
|
|
|
8711
9196
|
const kind = screenableContentType(ct);
|
|
8712
9197
|
if (kind === "skip" || kind === "sniff" && looksBinary(buffer)) {
|
|
8713
9198
|
recordSkip("response", kind === "skip" ? baseContentType(ct) === "text/event-stream" ? "live-stream" : "non-text-content-type" : "binary-body");
|
|
8714
|
-
return passThrough();
|
|
9199
|
+
return passThrough(screenUnreadHead());
|
|
8715
9200
|
}
|
|
8716
9201
|
const codings = declaredCodings(head.headers["content-encoding"]);
|
|
8717
9202
|
if (codings.length > 0 && stillEncoded(buffer)) {
|
|
8718
9203
|
recordSkip("response", "encoded-body", { encoding: codings.join(", ") });
|
|
8719
|
-
return passThrough();
|
|
9204
|
+
return passThrough(screenUnreadHead());
|
|
9205
|
+
}
|
|
9206
|
+
const reading = charsetReading(ct, buffer);
|
|
9207
|
+
if (reading.skip) {
|
|
9208
|
+
recordSkip("response", "unsupported-charset", { charset: reading.skip });
|
|
9209
|
+
return passThrough(screenUnreadHead());
|
|
8720
9210
|
}
|
|
8721
|
-
const text = buffer.toString("utf8");
|
|
9211
|
+
const text = readJsonEscapes(buffer.toString("utf8", reading.bom ? 3 : 0));
|
|
8722
9212
|
let r;
|
|
8723
9213
|
try {
|
|
8724
|
-
r = screenText(text, head, reqCtx,
|
|
9214
|
+
r = screenText(text, head, reqCtx, notYetAnswered);
|
|
8725
9215
|
} catch (err) {
|
|
8726
9216
|
notify(onError, err, "onError");
|
|
8727
9217
|
return passThrough();
|
|
8728
9218
|
}
|
|
9219
|
+
if (r.verdict === "redact" && outgrowsCharset(reading.ascii, r.body)) r = { verdict: "block" };
|
|
9220
|
+
if (r.verdict === "redact") r = { ...r, body: withBom(reading.bom, r.body) };
|
|
8729
9221
|
if (r.verdict !== "block" && r.verdict !== "redact") return passThrough();
|
|
8730
9222
|
if (reallySent() && res.chunkedEncoding !== true) {
|
|
8731
9223
|
notify(onError, new Error("Patchstack: response withheld \u2014 its headers were sent before the body could be screened"), "onError");
|
|
@@ -8733,20 +9225,11 @@ async function createProtection(options = {}) {
|
|
|
8733
9225
|
return res;
|
|
8734
9226
|
}
|
|
8735
9227
|
if (r.verdict === "block") {
|
|
8736
|
-
const body = JSON.stringify({ error: "Response withheld by Patchstack (sensitive data detected)" });
|
|
8737
9228
|
if (reallySent()) {
|
|
8738
9229
|
res.destroy?.();
|
|
8739
9230
|
return res;
|
|
8740
9231
|
}
|
|
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);
|
|
9232
|
+
return sendWithheld(cb);
|
|
8750
9233
|
}
|
|
8751
9234
|
const changed = /* @__PURE__ */ new Map();
|
|
8752
9235
|
if (r.headers) {
|
|
@@ -8842,21 +9325,15 @@ async function createProtection(options = {}) {
|
|
|
8842
9325
|
// .fetch(), and by the Supabase guard on its forwarded upstream response.
|
|
8843
9326
|
// A standalone response screen with no request phase of its own — the Supabase guard's forwarded
|
|
8844
9327
|
// 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
|
-
),
|
|
9328
|
+
screenResponse: (response, request, ...hostArgs) => screenResp(response, request ? reqContextFromFetch(request, clientForResponse(request, hostArgs)) : void 0),
|
|
8852
9329
|
// (request) => Response | null (null = allow, caller proceeds). Request phase only.
|
|
8853
9330
|
fetchGuard() {
|
|
8854
|
-
return async (request) => (await screenFetchRequest(request)).blocked;
|
|
9331
|
+
return async (request, ...hostArgs) => (await screenFetchRequest(request, hostArgs)).blocked;
|
|
8855
9332
|
},
|
|
8856
9333
|
// Wrap a fetch handler: screens the request, then the response (redact/block).
|
|
8857
9334
|
fetch(handler) {
|
|
8858
9335
|
return async (request, ...rest) => {
|
|
8859
|
-
const { blocked, client } = await screenFetchRequest(request);
|
|
9336
|
+
const { blocked, client } = await screenFetchRequest(request, rest);
|
|
8860
9337
|
if (blocked) return blocked;
|
|
8861
9338
|
const response = await handler(request, ...rest);
|
|
8862
9339
|
return screenResp(response, reqContextFromFetch(request, client));
|
|
@@ -8870,6 +9347,7 @@ async function createProtection(options = {}) {
|
|
|
8870
9347
|
const { shaped, client } = shapeExpressRequest(req);
|
|
8871
9348
|
try {
|
|
8872
9349
|
result = engine.evaluate(shaped);
|
|
9350
|
+
recordEvaluationSkips(result);
|
|
8873
9351
|
} catch (err) {
|
|
8874
9352
|
notify(onError, err, "onError");
|
|
8875
9353
|
if (exprOptions.screenResponses) wrapNodeResponse(res, reqContextFromNode(req, client));
|
|
@@ -8904,33 +9382,25 @@ async function createProtection(options = {}) {
|
|
|
8904
9382
|
screenNodeRequest(req, res, next, "", req.body);
|
|
8905
9383
|
return;
|
|
8906
9384
|
}
|
|
8907
|
-
|
|
8908
|
-
|
|
8909
|
-
|
|
8910
|
-
|
|
8911
|
-
size += chunk.length;
|
|
8912
|
-
if (size > maxBytes) {
|
|
8913
|
-
overflow = true;
|
|
9385
|
+
readBodyPrefix(req, maxBytes, (err, read) => {
|
|
9386
|
+
if (err) {
|
|
9387
|
+
notify(onError, err, "onError");
|
|
9388
|
+
next();
|
|
8914
9389
|
return;
|
|
8915
9390
|
}
|
|
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"));
|
|
9391
|
+
if (read.overflow) recordSkip("request", "body-cap", { bytes: read.size, limit: maxBytes });
|
|
9392
|
+
if (read.failed) recordSkip("request", "read-failed", { bytes: read.size });
|
|
9393
|
+
screenNodeRequest(req, res, next, read.text, void 0, read.overflow || read.failed);
|
|
8925
9394
|
});
|
|
8926
9395
|
};
|
|
8927
|
-
function screenNodeRequest(req, res, next, rawBody, parsedBody) {
|
|
9396
|
+
function screenNodeRequest(req, res, next, rawBody, parsedBody, truncated = false) {
|
|
8928
9397
|
let shaped;
|
|
8929
9398
|
let result;
|
|
8930
9399
|
try {
|
|
8931
9400
|
shaped = fromNodeRequest(req, rawBody, { trustedProxy: options.trustedProxy });
|
|
8932
9401
|
if (parsedBody !== void 0 && parsedBody !== null) shaped.body = parsedBody;
|
|
8933
9402
|
result = engine.evaluate(shaped);
|
|
9403
|
+
recordEvaluationSkips(result);
|
|
8934
9404
|
} catch (err) {
|
|
8935
9405
|
notify(onError, err, "onError");
|
|
8936
9406
|
return next();
|
|
@@ -8949,7 +9419,7 @@ async function createProtection(options = {}) {
|
|
|
8949
9419
|
}
|
|
8950
9420
|
},
|
|
8951
9421
|
() => {
|
|
8952
|
-
if (req.body === void 0) req.body = shaped.body;
|
|
9422
|
+
if (req.body === void 0 && !truncated) req.body = shaped.body;
|
|
8953
9423
|
if (nodeOptions.screenResponses) wrapNodeResponse(res, reqContextFromNode(req, shaped?._clientIp));
|
|
8954
9424
|
next();
|
|
8955
9425
|
},
|
|
@@ -9015,12 +9485,17 @@ async function createProtection(options = {}) {
|
|
|
9015
9485
|
protection.stop = () => {
|
|
9016
9486
|
loop?.stop();
|
|
9017
9487
|
recovery?.stop();
|
|
9488
|
+
protection.uninstallEgress?.();
|
|
9018
9489
|
const outstanding = [firewallLog?.stop(), detections?.stop()].filter(
|
|
9019
9490
|
(wait) => wait && typeof wait.then === "function"
|
|
9020
9491
|
);
|
|
9021
9492
|
return Promise.all(outstanding).then(() => void 0);
|
|
9022
9493
|
};
|
|
9023
|
-
protection.stopRefresh =
|
|
9494
|
+
protection.stopRefresh = () => {
|
|
9495
|
+
loop?.stop();
|
|
9496
|
+
recovery?.stop();
|
|
9497
|
+
return Promise.resolve();
|
|
9498
|
+
};
|
|
9024
9499
|
Object.defineProperty(protection, "ruleSource", {
|
|
9025
9500
|
get: () => ({ ...ruleSource }),
|
|
9026
9501
|
enumerable: true
|
|
@@ -9033,6 +9508,10 @@ async function createProtection(options = {}) {
|
|
|
9033
9508
|
get: () => detections ? () => detections.health() : void 0,
|
|
9034
9509
|
enumerable: true
|
|
9035
9510
|
});
|
|
9511
|
+
Object.defineProperty(protection, "blockLogHealth", {
|
|
9512
|
+
get: () => firewallLog ? () => firewallLog.health() : void 0,
|
|
9513
|
+
enumerable: true
|
|
9514
|
+
});
|
|
9036
9515
|
return protection;
|
|
9037
9516
|
}
|
|
9038
9517
|
function accessorOf(obj, name) {
|
|
@@ -9107,6 +9586,14 @@ function screenableContentType(ct) {
|
|
|
9107
9586
|
if (base === "application/octet-stream") return "sniff";
|
|
9108
9587
|
return "skip";
|
|
9109
9588
|
}
|
|
9589
|
+
function prefilterText(body, headers) {
|
|
9590
|
+
const values = [];
|
|
9591
|
+
for (const value of Object.values(headers ?? {})) {
|
|
9592
|
+
if (Array.isArray(value)) values.push(...value.map(String));
|
|
9593
|
+
else if (value !== void 0 && value !== null) values.push(String(value));
|
|
9594
|
+
}
|
|
9595
|
+
return [body, ...values].join("\n").toLowerCase();
|
|
9596
|
+
}
|
|
9110
9597
|
function looksBinary(bytes) {
|
|
9111
9598
|
const n = Math.min(bytes.length, 512);
|
|
9112
9599
|
let ctrl = 0;
|
|
@@ -9130,6 +9617,52 @@ function stillEncoded(bytes) {
|
|
|
9130
9617
|
return true;
|
|
9131
9618
|
}
|
|
9132
9619
|
}
|
|
9620
|
+
var UTF8_CHARSETS = /* @__PURE__ */ new Set(["utf-8", "utf8", "unicode-1-1-utf-8", "us-ascii", "ascii"]);
|
|
9621
|
+
var WIDE_CHARSET = /^(?:utf-?(?:16|32)|ucs-?[24]|unicode|iso-10646)/;
|
|
9622
|
+
function declaredCharset(ct) {
|
|
9623
|
+
const m = /;\s*charset\s*=\s*(?:"([^"]*)"|([^;\s]*))/i.exec(String(ct ?? ""));
|
|
9624
|
+
const label = m ? (m[1] ?? m[2] ?? "").trim().toLowerCase() : "";
|
|
9625
|
+
return label || null;
|
|
9626
|
+
}
|
|
9627
|
+
function charsetReading(ct, bytes) {
|
|
9628
|
+
if (bytes.length >= 3 && bytes[0] === 239 && bytes[1] === 187 && bytes[2] === 191) return { ascii: false, bom: true };
|
|
9629
|
+
if (bytes.length >= 2 && bytes[0] === 255 && bytes[1] === 254) return { skip: "utf-16le" };
|
|
9630
|
+
if (bytes.length >= 2 && bytes[0] === 254 && bytes[1] === 255) return { skip: "utf-16be" };
|
|
9631
|
+
const charset = declaredCharset(ct);
|
|
9632
|
+
if (!charset || UTF8_CHARSETS.has(charset)) return { ascii: false };
|
|
9633
|
+
if (WIDE_CHARSET.test(charset)) return { skip: charset };
|
|
9634
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
9635
|
+
const b = bytes[i];
|
|
9636
|
+
if (b >= 128 || b === 0 || b === 27) return { skip: charset };
|
|
9637
|
+
}
|
|
9638
|
+
return { ascii: true };
|
|
9639
|
+
}
|
|
9640
|
+
function decodeForScreening(ct, bytes) {
|
|
9641
|
+
const reading = charsetReading(ct, bytes);
|
|
9642
|
+
if (reading.skip) return { skip: "unsupported-charset", charset: reading.skip };
|
|
9643
|
+
return { text: new TextDecoder().decode(bytes), ascii: reading.ascii, bom: reading.bom === true };
|
|
9644
|
+
}
|
|
9645
|
+
function withBom(bom, body) {
|
|
9646
|
+
return bom ? "\uFEFF" + body : body;
|
|
9647
|
+
}
|
|
9648
|
+
function outgrowsCharset(ascii, body) {
|
|
9649
|
+
return ascii === true && /[^\x00-\x7f]/.test(body);
|
|
9650
|
+
}
|
|
9651
|
+
var KEPT_ESCAPES = /* @__PURE__ */ new Set([34, 92, 60, 62, 38, 39]);
|
|
9652
|
+
function readJsonEscapes(text) {
|
|
9653
|
+
if (!text.includes("\\u") && !text.includes("\\/")) return text;
|
|
9654
|
+
try {
|
|
9655
|
+
JSON.parse(text);
|
|
9656
|
+
} catch {
|
|
9657
|
+
return text;
|
|
9658
|
+
}
|
|
9659
|
+
return text.replace(/\\(?:u([0-9a-fA-F]{4})|([\s\S]))/g, (whole, hex, ch, offset) => {
|
|
9660
|
+
if (hex === void 0) return ch === "/" && text[offset - 1] !== "<" ? "/" : whole;
|
|
9661
|
+
const code = parseInt(hex, 16);
|
|
9662
|
+
if (code < 32 || code > 126 || KEPT_ESCAPES.has(code)) return whole;
|
|
9663
|
+
return String.fromCharCode(code);
|
|
9664
|
+
});
|
|
9665
|
+
}
|
|
9133
9666
|
async function readTextResponse(response, cap = DEFAULT_SCREEN_CAP) {
|
|
9134
9667
|
if (!response || typeof response.clone !== "function") return { skip: "not-a-response" };
|
|
9135
9668
|
const ct = response.headers?.get?.("content-type") || "";
|
|
@@ -9183,17 +9716,18 @@ async function readTextResponse(response, cap = DEFAULT_SCREEN_CAP) {
|
|
|
9183
9716
|
const bytes = concatBytes(chunks, size);
|
|
9184
9717
|
if (codings.length > 0 && stillEncoded(bytes)) return encodedSkip();
|
|
9185
9718
|
try {
|
|
9186
|
-
return
|
|
9719
|
+
return decodeForScreening(ct, bytes);
|
|
9187
9720
|
} catch {
|
|
9188
9721
|
return { skip: "decode-failed" };
|
|
9189
9722
|
}
|
|
9190
9723
|
}
|
|
9191
9724
|
try {
|
|
9192
|
-
if (codings.length > 0) {
|
|
9725
|
+
if (codings.length > 0 || typeof clone.arrayBuffer === "function") {
|
|
9193
9726
|
const bytes = new Uint8Array(await clone.arrayBuffer());
|
|
9194
9727
|
if (bytes.byteLength > cap) return { skip: "body-cap" };
|
|
9195
|
-
if (stillEncoded(bytes)) return encodedSkip();
|
|
9196
|
-
|
|
9728
|
+
if (codings.length > 0 && stillEncoded(bytes)) return encodedSkip();
|
|
9729
|
+
if (codings.length === 0 && sniff && looksBinary(bytes)) return { skip: "binary-body" };
|
|
9730
|
+
return decodeForScreening(ct, bytes);
|
|
9197
9731
|
}
|
|
9198
9732
|
const text = await clone.text();
|
|
9199
9733
|
if (text.length > cap) return { skip: "body-cap" };
|
|
@@ -9224,8 +9758,9 @@ function headerObject(headers) {
|
|
|
9224
9758
|
function hasSpanMutations(rule) {
|
|
9225
9759
|
let found = false;
|
|
9226
9760
|
const walk2 = (conds) => {
|
|
9227
|
-
for (const c of conds
|
|
9761
|
+
for (const c of Array.isArray(conds) ? conds : []) {
|
|
9228
9762
|
if (found) return;
|
|
9763
|
+
if (!c || typeof c !== "object") continue;
|
|
9229
9764
|
if (Array.isArray(c.rules)) walk2(c.rules);
|
|
9230
9765
|
const isSpan = c.match && (c.match.type === "regex" || c.match.type === "contains" || c.match.type === "stripos");
|
|
9231
9766
|
if (isSpan && Array.isArray(c.mutations) && c.mutations.length) found = true;
|
|
@@ -9234,10 +9769,34 @@ function hasSpanMutations(rule) {
|
|
|
9234
9769
|
walk2(rule.rule_v2);
|
|
9235
9770
|
return found;
|
|
9236
9771
|
}
|
|
9237
|
-
function
|
|
9772
|
+
function redactorScope(parameter) {
|
|
9773
|
+
if (parameter === void 0 || parameter === null || parameterProblem(parameter) !== null) return null;
|
|
9774
|
+
const scope = { body: false, allHeaders: false, headers: /* @__PURE__ */ new Set() };
|
|
9775
|
+
for (const name of Array.isArray(parameter) ? parameter : [parameter]) {
|
|
9776
|
+
if (typeof name !== "string" || name === "") return null;
|
|
9777
|
+
if (name.startsWith("response.header.")) scope.headers.add(name.slice("response.header.".length).toLowerCase());
|
|
9778
|
+
else if (name === "response.headers") scope.allHeaders = true;
|
|
9779
|
+
else if (name === "response.body" || name === "response.status" || BROAD_SCOPE_SOURCES.has(name.split(".")[0])) {
|
|
9780
|
+
scope.body = true;
|
|
9781
|
+
scope.allHeaders = true;
|
|
9782
|
+
} else return null;
|
|
9783
|
+
}
|
|
9784
|
+
return scope;
|
|
9785
|
+
}
|
|
9786
|
+
var BROAD_SCOPE_SOURCES = /* @__PURE__ */ new Set(["get", "post", "request", "cookie", "files", "server", "raw", "all"]);
|
|
9787
|
+
function masksHeader(redactor, name) {
|
|
9788
|
+
return redactor.scope.allHeaders || redactor.scope.headers.has(name);
|
|
9789
|
+
}
|
|
9790
|
+
function extractRedactors(rule, onUnscoped) {
|
|
9238
9791
|
const out = [];
|
|
9792
|
+
const scoped = (c) => {
|
|
9793
|
+
const scope = redactorScope(c.parameter);
|
|
9794
|
+
if (scope === null) onUnscoped?.(c.parameter);
|
|
9795
|
+
return scope;
|
|
9796
|
+
};
|
|
9239
9797
|
const walk2 = (conds) => {
|
|
9240
|
-
for (const c of conds
|
|
9798
|
+
for (const c of Array.isArray(conds) ? conds : []) {
|
|
9799
|
+
if (!c || typeof c !== "object") continue;
|
|
9241
9800
|
if (Array.isArray(c.rules)) walk2(c.rules);
|
|
9242
9801
|
const m = c.match;
|
|
9243
9802
|
if (!m) continue;
|
|
@@ -9245,16 +9804,21 @@ function extractRedactors(rule) {
|
|
|
9245
9804
|
const safe = safeRegExp(m.value);
|
|
9246
9805
|
if (safe) {
|
|
9247
9806
|
const flags = safe.flags.includes("g") ? safe.flags : safe.flags + "g";
|
|
9807
|
+
const scope = scoped(c);
|
|
9248
9808
|
try {
|
|
9249
|
-
out.push({ re: new RegExp(safe.source, flags) });
|
|
9809
|
+
if (scope) out.push({ re: new RegExp(safe.source, flags), scope });
|
|
9250
9810
|
} catch {
|
|
9251
9811
|
}
|
|
9252
9812
|
}
|
|
9253
9813
|
} else if ((m.type === "contains" || m.type === "stripos") && m.value != null) {
|
|
9254
|
-
|
|
9814
|
+
const scope = scoped(c);
|
|
9815
|
+
if (scope) out.push({ literal: String(m.value), scope });
|
|
9255
9816
|
} else if (m.type === "jwt_claim_equals" && typeof m.claim === "string") {
|
|
9256
|
-
|
|
9257
|
-
|
|
9817
|
+
const scope = scoped(c);
|
|
9818
|
+
if (scope) out.push({ jwtClaim: { claim: m.claim, value: String(m.value ?? "") }, scope });
|
|
9819
|
+
} else if (m.type === "array_key_value" && m.match && Array.isArray(c.parameter) && !readsBody(c.parameter) && redactorScope(c.parameter) === null) {
|
|
9820
|
+
onUnscoped?.(c.parameter);
|
|
9821
|
+
} else if (m.type === "array_key_value" && m.match && readsBody(c.parameter)) {
|
|
9258
9822
|
const keys = Array.isArray(m.key) ? m.key : [m.key];
|
|
9259
9823
|
for (const key of keys) {
|
|
9260
9824
|
out.push({ jsonPath: String(key).split("."), condition: m.match });
|
|
@@ -9276,30 +9840,101 @@ function isJson(text) {
|
|
|
9276
9840
|
return false;
|
|
9277
9841
|
}
|
|
9278
9842
|
}
|
|
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
|
-
}
|
|
9843
|
+
var JSON_STRING = 1;
|
|
9844
|
+
var JSON_NUMBER = 2;
|
|
9845
|
+
var JSON_LITERAL = 3;
|
|
9846
|
+
var JSON_NUMBER_TOKEN = /-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/y;
|
|
9847
|
+
function isHexCode(c) {
|
|
9848
|
+
return c >= 48 && c <= 57 || c >= 65 && c <= 70 || c >= 97 && c <= 102;
|
|
9290
9849
|
}
|
|
9291
|
-
|
|
9292
|
-
|
|
9293
|
-
|
|
9294
|
-
|
|
9295
|
-
|
|
9296
|
-
|
|
9850
|
+
var PLAIN_STRING_SEGMENT = /^[^"\\\u0000-\u001f]*$/;
|
|
9851
|
+
function lexJson(text) {
|
|
9852
|
+
let kinds = new Int32Array(1024);
|
|
9853
|
+
let spans = new Int32Array(2048);
|
|
9854
|
+
let count = 0;
|
|
9855
|
+
const n = text.length;
|
|
9856
|
+
let i = 0;
|
|
9857
|
+
while (i < n) {
|
|
9858
|
+
const c = text.charCodeAt(i);
|
|
9859
|
+
if (c === 32 || c === 9 || c === 10 || c === 13) {
|
|
9860
|
+
i++;
|
|
9297
9861
|
continue;
|
|
9298
9862
|
}
|
|
9299
|
-
|
|
9300
|
-
|
|
9301
|
-
|
|
9863
|
+
const start = i;
|
|
9864
|
+
let kind;
|
|
9865
|
+
if (c === 34) {
|
|
9866
|
+
i++;
|
|
9867
|
+
const close = text.indexOf('"', i);
|
|
9868
|
+
if (close === -1) return null;
|
|
9869
|
+
if (PLAIN_STRING_SEGMENT.test(text.slice(i, close))) {
|
|
9870
|
+
i = close;
|
|
9871
|
+
} else {
|
|
9872
|
+
for (; ; ) {
|
|
9873
|
+
if (i >= n) return null;
|
|
9874
|
+
const d = text.charCodeAt(i);
|
|
9875
|
+
if (d === 34) break;
|
|
9876
|
+
if (d < 32) return null;
|
|
9877
|
+
if (d !== 92) {
|
|
9878
|
+
i++;
|
|
9879
|
+
continue;
|
|
9880
|
+
}
|
|
9881
|
+
const e = text.charCodeAt(i + 1);
|
|
9882
|
+
if (e === 117) {
|
|
9883
|
+
for (let k = 2; k < 6; k++) if (!isHexCode(text.charCodeAt(i + k))) return null;
|
|
9884
|
+
i += 6;
|
|
9885
|
+
} else if (e === 34 || e === 92 || e === 47 || e === 98 || e === 102 || e === 110 || e === 114 || e === 116) {
|
|
9886
|
+
i += 2;
|
|
9887
|
+
} else {
|
|
9888
|
+
return null;
|
|
9889
|
+
}
|
|
9890
|
+
}
|
|
9891
|
+
}
|
|
9892
|
+
i++;
|
|
9893
|
+
kind = JSON_STRING;
|
|
9894
|
+
} else if (c === 123 || c === 125 || c === 91 || c === 93 || c === 58 || c === 44) {
|
|
9895
|
+
i++;
|
|
9896
|
+
kind = c;
|
|
9897
|
+
} else if (text.startsWith("true", i) || text.startsWith("null", i)) {
|
|
9898
|
+
i += 4;
|
|
9899
|
+
kind = JSON_LITERAL;
|
|
9900
|
+
} else if (text.startsWith("false", i)) {
|
|
9901
|
+
i += 5;
|
|
9902
|
+
kind = JSON_LITERAL;
|
|
9903
|
+
} else {
|
|
9904
|
+
JSON_NUMBER_TOKEN.lastIndex = i;
|
|
9905
|
+
if (!JSON_NUMBER_TOKEN.test(text)) return null;
|
|
9906
|
+
i = JSON_NUMBER_TOKEN.lastIndex;
|
|
9907
|
+
kind = JSON_NUMBER;
|
|
9908
|
+
}
|
|
9909
|
+
if (count === kinds.length) {
|
|
9910
|
+
const grownKinds = new Int32Array(count * 2);
|
|
9911
|
+
grownKinds.set(kinds);
|
|
9912
|
+
kinds = grownKinds;
|
|
9913
|
+
const grownSpans = new Int32Array(count * 4);
|
|
9914
|
+
grownSpans.set(spans);
|
|
9915
|
+
spans = grownSpans;
|
|
9916
|
+
}
|
|
9917
|
+
kinds[count] = kind;
|
|
9918
|
+
spans[2 * count] = start;
|
|
9919
|
+
spans[2 * count + 1] = i;
|
|
9920
|
+
count++;
|
|
9921
|
+
}
|
|
9922
|
+
return { kinds, spans, count };
|
|
9923
|
+
}
|
|
9924
|
+
function sameJsonStructure(beforeText, before, afterText, after) {
|
|
9925
|
+
if (!before || !after || before.count !== after.count) return false;
|
|
9926
|
+
const { kinds, spans, count } = before;
|
|
9927
|
+
for (let k = 0; k < count; k++) {
|
|
9928
|
+
const kind = kinds[k];
|
|
9929
|
+
if (after.kinds[k] !== kind) return false;
|
|
9930
|
+
if (kind > JSON_LITERAL) continue;
|
|
9931
|
+
if (kind === JSON_STRING && kinds[k + 1] !== 58) continue;
|
|
9932
|
+
const was = beforeText.slice(spans[2 * k], spans[2 * k + 1]);
|
|
9933
|
+
const now = afterText.slice(after.spans[2 * k], after.spans[2 * k + 1]);
|
|
9934
|
+
if (was === now) continue;
|
|
9935
|
+
if (kind !== JSON_STRING || JSON.parse(was) !== JSON.parse(now)) return false;
|
|
9302
9936
|
}
|
|
9937
|
+
return true;
|
|
9303
9938
|
}
|
|
9304
9939
|
function applyRedactors(body, redactors, mask, transform) {
|
|
9305
9940
|
let out = body;
|
|
@@ -9316,6 +9951,11 @@ function applyRedactors(body, redactors, mask, transform) {
|
|
|
9316
9951
|
}
|
|
9317
9952
|
return out;
|
|
9318
9953
|
}
|
|
9954
|
+
function readsBody(parameter) {
|
|
9955
|
+
if (!Array.isArray(parameter)) return isBodyParam(parameter);
|
|
9956
|
+
const names = parameter.every((member) => typeof member === "string" && member !== "");
|
|
9957
|
+
return names && parameterProblem(parameter) === null && parameter.some((member) => isBodyParam(member));
|
|
9958
|
+
}
|
|
9319
9959
|
function isBodyParam(parameter) {
|
|
9320
9960
|
return parameter == null || parameter === "response.body" || parameter === "raw" || parameter === "response.raw";
|
|
9321
9961
|
}
|
|
@@ -9347,7 +9987,7 @@ function applyPathRedactors(text, pathRedactors, mask, cap, transform) {
|
|
|
9347
9987
|
let obj;
|
|
9348
9988
|
try {
|
|
9349
9989
|
JSON.parse(text);
|
|
9350
|
-
preserved =
|
|
9990
|
+
preserved = preserveNumbers(text, mask);
|
|
9351
9991
|
obj = JSON.parse(preserved.text);
|
|
9352
9992
|
} catch {
|
|
9353
9993
|
return text;
|
|
@@ -9366,22 +10006,22 @@ function applyPathRedactors(text, pathRedactors, mask, cap, transform) {
|
|
|
9366
10006
|
}
|
|
9367
10007
|
});
|
|
9368
10008
|
}
|
|
9369
|
-
return changed ?
|
|
10009
|
+
return changed ? restoreNumbers(JSON.stringify(obj), preserved.numbers) : text;
|
|
9370
10010
|
}
|
|
9371
|
-
function
|
|
10011
|
+
function preserveNumbers(text, mask) {
|
|
9372
10012
|
const occupied = /* @__PURE__ */ new Set([mask]);
|
|
9373
|
-
const
|
|
10013
|
+
const found = [];
|
|
9374
10014
|
const tokens = /"(?:[^"\\]|\\[\s\S])*"|-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/g;
|
|
9375
10015
|
for (const match of text.matchAll(tokens)) {
|
|
9376
10016
|
const token = match[0];
|
|
9377
10017
|
if (token[0] === '"') occupied.add(JSON.parse(token));
|
|
9378
|
-
else
|
|
10018
|
+
else found.push({ start: match.index, token });
|
|
9379
10019
|
}
|
|
9380
10020
|
const numbers = /* @__PURE__ */ new Map();
|
|
9381
10021
|
const chunks = [];
|
|
9382
10022
|
let offset = 0;
|
|
9383
10023
|
let index = 0;
|
|
9384
|
-
for (const { start, token } of
|
|
10024
|
+
for (const { start, token } of found) {
|
|
9385
10025
|
let marker;
|
|
9386
10026
|
do {
|
|
9387
10027
|
marker = "__PSNUMBER_" + index++ + "__";
|
|
@@ -9393,7 +10033,7 @@ function preserveBigInts(text, mask) {
|
|
|
9393
10033
|
chunks.push(text.slice(offset));
|
|
9394
10034
|
return { text: chunks.join(""), numbers };
|
|
9395
10035
|
}
|
|
9396
|
-
function
|
|
10036
|
+
function restoreNumbers(text, numbers) {
|
|
9397
10037
|
return text.replace(/"(__PSNUMBER_\d+__)"/g, (token, marker) => numbers.get(marker) ?? token);
|
|
9398
10038
|
}
|
|
9399
10039
|
function isHeaderMutation(action) {
|