@patchstack/connect 0.5.12 → 0.5.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENT-INSTALL.md +60 -32
- package/README.md +19 -19
- package/dist/cli.js +1275 -642
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +16 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.js +16 -9
- package/dist/index.js.map +1 -1
- package/dist/protect/templates/demo-rules.json +2 -2
- package/dist/protect.cjs +2132 -915
- package/dist/protect.cjs.map +1 -1
- package/dist/protect.d.cts +135 -17
- package/dist/protect.d.ts +135 -17
- package/dist/protect.edge.js +2195 -985
- package/dist/protect.edge.js.map +3 -3
- package/dist/protect.js +2198 -988
- package/dist/protect.js.map +1 -1
- package/dist/{refresh-manifest-47HJRCDX.js → refresh-manifest-DZQOEO3Q.js} +17 -10
- package/dist/refresh-manifest-DZQOEO3Q.js.map +1 -0
- package/package.json +1 -1
- package/dist/refresh-manifest-47HJRCDX.js.map +0 -1
package/dist/protect.cjs
CHANGED
|
@@ -397,13 +397,13 @@ function buildEndpointUrl(base, siteUuid) {
|
|
|
397
397
|
function authFailureMessage(status, config) {
|
|
398
398
|
const hasCredential = typeof config.pulseAuth === "string" && config.pulseAuth.length > 0;
|
|
399
399
|
if (status === 401 && !hasCredential) {
|
|
400
|
-
return "Patchstack
|
|
400
|
+
return "Patchstack needs this project's API key, and none is set here. Run npx @patchstack/connect login, or set PATCHSTACK_API_KEY.";
|
|
401
401
|
}
|
|
402
402
|
if (status === 401) {
|
|
403
|
-
return "Patchstack
|
|
403
|
+
return "Patchstack did not accept this project's API key: it may have expired or been revoked, or the project may no longer exist. Run npx @patchstack/connect login to get a new one.";
|
|
404
404
|
}
|
|
405
405
|
if (status === 403) {
|
|
406
|
-
return "This API
|
|
406
|
+
return "This API key belongs to a different project. Check that siteUuid in .patchstackrc.json is the project the key was made for.";
|
|
407
407
|
}
|
|
408
408
|
return null;
|
|
409
409
|
}
|
|
@@ -1145,7 +1145,7 @@ function unreadableWarning(filename, report) {
|
|
|
1145
1145
|
if (report.unreadable === 0) return null;
|
|
1146
1146
|
const shown = report.samples.join(", ");
|
|
1147
1147
|
const one = report.unreadable === 1;
|
|
1148
|
-
return `${filename}: ${report.unreadable} entr${one ? "y
|
|
1148
|
+
return `${filename}: ${report.unreadable} entr${one ? "y" : "ies"} could not be read (${shown}${report.unreadable > report.samples.length ? ", \u2026" : ""}), so ${one ? "that package is" : "those packages are"} not being checked.`;
|
|
1149
1149
|
}
|
|
1150
1150
|
var MAX_SAMPLES;
|
|
1151
1151
|
var init_report = __esm({
|
|
@@ -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
|
-
|
|
2086
|
-
walkWasCandidate ? "
|
|
2085
|
+
warnings2.push(
|
|
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
|
-
|
|
2098
|
-
`No
|
|
2097
|
+
warnings2.push(
|
|
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(
|
|
@@ -2109,12 +2109,12 @@ function otherCompleteSources(all, current, declared) {
|
|
|
2109
2109
|
}
|
|
2110
2110
|
function conflictWarning(filename, conflicts) {
|
|
2111
2111
|
const shown = conflicts.slice(0, 3).map(({ other, conflict }) => `${conflict.name} ${conflict.version} vs ${conflict.otherVersion} in ${other}`).join("; ");
|
|
2112
|
-
return `${filename} and another
|
|
2112
|
+
return `${filename} and another package list file give different versions (${shown}${conflicts.length > 3 ? `, +${conflicts.length - 3} more` : ""}). Checked node_modules/ instead. Delete the file your package manager does not use.`;
|
|
2113
2113
|
}
|
|
2114
2114
|
function staleWarning(source, missing) {
|
|
2115
2115
|
const sample = missing.slice(0, 3).join(", ");
|
|
2116
2116
|
const suffix = missing.length > 3 ? `, +${missing.length - 3} more` : "";
|
|
2117
|
-
return `${source}
|
|
2117
|
+
return `${source} is out of date: it is missing ${missing.length} package${missing.length === 1 ? "" : "s"} listed in package.json (${sample}${suffix}). Run ${INSTALL_BY_SOURCE[source] ?? "your install command"} to update it.`;
|
|
2118
2118
|
}
|
|
2119
2119
|
async function presentLockfiles(cwd) {
|
|
2120
2120
|
const probed = await Promise.all(
|
|
@@ -2149,7 +2149,7 @@ async function exists(filePath) {
|
|
|
2149
2149
|
return false;
|
|
2150
2150
|
}
|
|
2151
2151
|
}
|
|
2152
|
-
var import_promises9, import_node_path8, LOCKFILE_CANDIDATES;
|
|
2152
|
+
var import_promises9, import_node_path8, LOCKFILE_CANDIDATES, INSTALL_BY_SOURCE;
|
|
2153
2153
|
var init_parsers = __esm({
|
|
2154
2154
|
"src/parsers/index.ts"() {
|
|
2155
2155
|
"use strict";
|
|
@@ -2173,6 +2173,13 @@ var init_parsers = __esm({
|
|
|
2173
2173
|
{ filename: "pnpm-lock.yaml", strategy: "pnpm-lockfile" },
|
|
2174
2174
|
{ filename: "yarn.lock", strategy: "yarn-lockfile" }
|
|
2175
2175
|
];
|
|
2176
|
+
INSTALL_BY_SOURCE = {
|
|
2177
|
+
"package-lock.json": "npm install",
|
|
2178
|
+
"bun.lock": "bun install",
|
|
2179
|
+
"bun.lockb": "bun install",
|
|
2180
|
+
"pnpm-lock.yaml": "pnpm install",
|
|
2181
|
+
"yarn.lock": "yarn install"
|
|
2182
|
+
};
|
|
2176
2183
|
}
|
|
2177
2184
|
});
|
|
2178
2185
|
|
|
@@ -2718,18 +2725,124 @@ function appendOwn(target, key, value) {
|
|
|
2718
2725
|
return setOwn(target, key, Array.isArray(previous) ? [...previous, value] : [previous, value]);
|
|
2719
2726
|
}
|
|
2720
2727
|
|
|
2721
|
-
// src/protect/engine/
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
const
|
|
2727
|
-
|
|
2728
|
-
|
|
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
|
+
}
|
|
2729
2751
|
}
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
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);
|
|
2733
2846
|
}
|
|
2734
2847
|
var NAMED_ENTITIES = {
|
|
2735
2848
|
lt: "<",
|
|
@@ -2737,7 +2850,6 @@ var NAMED_ENTITIES = {
|
|
|
2737
2850
|
amp: "&",
|
|
2738
2851
|
quot: '"',
|
|
2739
2852
|
apos: "'",
|
|
2740
|
-
"#39": "'",
|
|
2741
2853
|
nbsp: "\xA0",
|
|
2742
2854
|
sol: "/",
|
|
2743
2855
|
bsol: "\\",
|
|
@@ -2756,7 +2868,7 @@ var NAMED_ENTITIES = {
|
|
|
2756
2868
|
ast: "*"
|
|
2757
2869
|
};
|
|
2758
2870
|
function decodeHtmlEntities(input) {
|
|
2759
|
-
return input.replace(/&(#[xX]
|
|
2871
|
+
return input.replace(/&(#[xX][0-9a-fA-F]+|#\d+|[A-Za-z][A-Za-z0-9]*);?/g, (whole, body) => {
|
|
2760
2872
|
if (body[0] === "#") {
|
|
2761
2873
|
const hex = body[1] === "x" || body[1] === "X";
|
|
2762
2874
|
const code = Number.parseInt(hex ? body.slice(2) : body.slice(1), hex ? 16 : 10);
|
|
@@ -2770,80 +2882,365 @@ function decodeHtmlEntities(input) {
|
|
|
2770
2882
|
return Object.prototype.hasOwnProperty.call(NAMED_ENTITIES, body) ? NAMED_ENTITIES[body] : whole;
|
|
2771
2883
|
});
|
|
2772
2884
|
}
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
constructor(req) {
|
|
2777
|
-
this.#req = req;
|
|
2778
|
-
this.#cookies = null;
|
|
2885
|
+
function removeSqlComments(value) {
|
|
2886
|
+
if (typeof value !== "string") {
|
|
2887
|
+
return value;
|
|
2779
2888
|
}
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
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;
|
|
2786
2933
|
}
|
|
2787
|
-
if (
|
|
2788
|
-
|
|
2934
|
+
if (task.kind === "key") {
|
|
2935
|
+
append(boundedJsonString(task.value));
|
|
2936
|
+
append(":");
|
|
2937
|
+
continue;
|
|
2789
2938
|
}
|
|
2790
|
-
|
|
2791
|
-
|
|
2939
|
+
nodes++;
|
|
2940
|
+
if (nodes > MAX_RAW_NODES) {
|
|
2941
|
+
append('"[NodeLimit]"');
|
|
2942
|
+
continue;
|
|
2792
2943
|
}
|
|
2793
|
-
const
|
|
2794
|
-
if (
|
|
2795
|
-
|
|
2944
|
+
const value = task.value;
|
|
2945
|
+
if (value === null || value === void 0) {
|
|
2946
|
+
append(task.root ? "" : "null");
|
|
2947
|
+
continue;
|
|
2796
2948
|
}
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
case "get":
|
|
2801
|
-
return this.#resolveGet(key);
|
|
2802
|
-
case "post":
|
|
2803
|
-
return this.#resolvePost(key);
|
|
2804
|
-
case "request":
|
|
2805
|
-
return this.#resolveRequest(key);
|
|
2806
|
-
case "cookie":
|
|
2807
|
-
return this.#resolveCookie(key);
|
|
2808
|
-
case "server":
|
|
2809
|
-
return this.#resolveServer(key);
|
|
2810
|
-
case "files":
|
|
2811
|
-
return this.#resolveFiles(key);
|
|
2812
|
-
case "response":
|
|
2813
|
-
return this.#resolveResponse(key);
|
|
2814
|
-
case "egress":
|
|
2815
|
-
return this.#resolveEgress(key);
|
|
2816
|
-
default:
|
|
2817
|
-
return [];
|
|
2949
|
+
if (typeof value === "string") {
|
|
2950
|
+
append(task.root ? value.slice(0, MAX_RAW_CHARS) : boundedJsonString(value));
|
|
2951
|
+
continue;
|
|
2818
2952
|
}
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
#resolveResponse(key) {
|
|
2823
|
-
const resp = this.#req._response;
|
|
2824
|
-
if (!resp) {
|
|
2825
|
-
return [];
|
|
2953
|
+
if (typeof value !== "object") {
|
|
2954
|
+
append(String(value).slice(0, MAX_RAW_CHARS));
|
|
2955
|
+
continue;
|
|
2826
2956
|
}
|
|
2827
|
-
if (
|
|
2828
|
-
|
|
2957
|
+
if (visited.has(value)) {
|
|
2958
|
+
append("[Circular]");
|
|
2959
|
+
continue;
|
|
2829
2960
|
}
|
|
2830
|
-
if (
|
|
2831
|
-
|
|
2961
|
+
if (task.depth >= MAX_RAW_DEPTH) {
|
|
2962
|
+
append('"[DepthLimit]"');
|
|
2963
|
+
continue;
|
|
2832
2964
|
}
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
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;
|
|
2836
2981
|
}
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2982
|
+
let keys;
|
|
2983
|
+
try {
|
|
2984
|
+
keys = Object.getOwnPropertyNames(value);
|
|
2985
|
+
} catch {
|
|
2986
|
+
append('"[Unreadable]"');
|
|
2987
|
+
continue;
|
|
2841
2988
|
}
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
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]] : [];
|
|
3238
|
+
}
|
|
3239
|
+
return [];
|
|
3240
|
+
}
|
|
3241
|
+
// Egress-phase sources (req._egress = { url, host, method }). Lets rules inspect an
|
|
3242
|
+
// OUTBOUND request the app is about to make — SSRF at the egress boundary.
|
|
3243
|
+
#resolveEgress(key) {
|
|
2847
3244
|
const eg = this.#req._egress;
|
|
2848
3245
|
if (!eg) {
|
|
2849
3246
|
return [];
|
|
@@ -2867,6 +3264,9 @@ var RequestResolver = class {
|
|
|
2867
3264
|
if (value === null || value === void 0) {
|
|
2868
3265
|
return value;
|
|
2869
3266
|
}
|
|
3267
|
+
if (typeof value === "object" && TEXT_MUTATIONS.has(mutation)) {
|
|
3268
|
+
return mapStringLeaves(value, (leaf) => this.#applyMutation(mutation, leaf), () => this.noteSkip("container-cap"));
|
|
3269
|
+
}
|
|
2870
3270
|
switch (mutation) {
|
|
2871
3271
|
case "base64_decode":
|
|
2872
3272
|
try {
|
|
@@ -2887,11 +3287,7 @@ var RequestResolver = class {
|
|
|
2887
3287
|
return value;
|
|
2888
3288
|
}
|
|
2889
3289
|
case "urldecode":
|
|
2890
|
-
|
|
2891
|
-
return decodeURIComponent(String(value));
|
|
2892
|
-
} catch {
|
|
2893
|
-
return value;
|
|
2894
|
-
}
|
|
3290
|
+
return safeUrlDecode(String(value).replace(/\+/g, " "));
|
|
2895
3291
|
case "htmlentitydecode":
|
|
2896
3292
|
return decodeHtmlEntities(String(value));
|
|
2897
3293
|
case "intval":
|
|
@@ -2910,16 +3306,30 @@ var RequestResolver = class {
|
|
|
2910
3306
|
if (key.endsWith("*")) {
|
|
2911
3307
|
return this.#resolveWildcard(query, key);
|
|
2912
3308
|
}
|
|
2913
|
-
|
|
2914
|
-
return value !== void 0 ? [value] : [];
|
|
3309
|
+
return this.#lookup(query, key);
|
|
2915
3310
|
}
|
|
2916
3311
|
#resolvePost(key) {
|
|
2917
3312
|
const body = this.#req.body ?? {};
|
|
2918
3313
|
if (key.endsWith("*")) {
|
|
2919
3314
|
return this.#resolveWildcard(body, key);
|
|
2920
3315
|
}
|
|
2921
|
-
|
|
2922
|
-
|
|
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;
|
|
2923
3333
|
}
|
|
2924
3334
|
#resolveRequest(key) {
|
|
2925
3335
|
const query = this.#req.query ?? {};
|
|
@@ -2932,8 +3342,11 @@ var RequestResolver = class {
|
|
|
2932
3342
|
...this.#resolveWildcard(cookies, key)
|
|
2933
3343
|
];
|
|
2934
3344
|
}
|
|
2935
|
-
const
|
|
2936
|
-
|
|
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]] : [];
|
|
2937
3350
|
}
|
|
2938
3351
|
#resolveCookie(key) {
|
|
2939
3352
|
const cookies = this.#parseCookies();
|
|
@@ -3103,331 +3516,53 @@ var RequestResolver = class {
|
|
|
3103
3516
|
this.#cookies = this.#req.cookies;
|
|
3104
3517
|
return this.#cookies;
|
|
3105
3518
|
}
|
|
3106
|
-
|
|
3107
|
-
if (!header) {
|
|
3108
|
-
this.#cookies = {};
|
|
3109
|
-
return this.#cookies;
|
|
3110
|
-
}
|
|
3111
|
-
const cookies = {};
|
|
3112
|
-
for (const pair of header.split(";")) {
|
|
3113
|
-
const eqIndex = pair.indexOf("=");
|
|
3114
|
-
if (eqIndex === -1) {
|
|
3115
|
-
continue;
|
|
3116
|
-
}
|
|
3117
|
-
const name = pair.substring(0, eqIndex).trim();
|
|
3118
|
-
const value = pair.substring(eqIndex + 1).trim();
|
|
3119
|
-
setOwn(cookies, name, value);
|
|
3120
|
-
}
|
|
3121
|
-
this.#cookies = cookies;
|
|
3519
|
+
this.#cookies = parseCookieHeader(this.#req.headers?.cookie);
|
|
3122
3520
|
return this.#cookies;
|
|
3123
3521
|
}
|
|
3124
3522
|
};
|
|
3125
3523
|
|
|
3126
3524
|
// src/protect/notify.js
|
|
3127
|
-
var reported = /* @__PURE__ */ new
|
|
3525
|
+
var reported = /* @__PURE__ */ new WeakMap();
|
|
3526
|
+
var warnings = 0;
|
|
3527
|
+
var MAX_WARNINGS = 20;
|
|
3128
3528
|
function notify(fn, arg, label) {
|
|
3129
3529
|
if (typeof fn !== "function") return false;
|
|
3130
3530
|
try {
|
|
3131
3531
|
const result = fn(arg);
|
|
3132
3532
|
if (result !== null && typeof result === "object" && typeof result.then === "function") {
|
|
3133
3533
|
try {
|
|
3134
|
-
result.then(void 0, (err) => warnOnce2(label, err));
|
|
3534
|
+
result.then(void 0, (err) => warnOnce2(fn, label, err));
|
|
3135
3535
|
} catch {
|
|
3136
3536
|
}
|
|
3137
3537
|
}
|
|
3138
3538
|
return true;
|
|
3139
3539
|
} catch (err) {
|
|
3140
|
-
warnOnce2(label, err);
|
|
3540
|
+
warnOnce2(fn, label, err);
|
|
3141
3541
|
return false;
|
|
3142
3542
|
}
|
|
3143
3543
|
}
|
|
3144
|
-
function warnOnce2(label, err) {
|
|
3145
|
-
|
|
3146
|
-
|
|
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);
|
|
3550
|
+
}
|
|
3551
|
+
labels.add(label);
|
|
3552
|
+
if (warnings > MAX_WARNINGS) return;
|
|
3553
|
+
warnings++;
|
|
3147
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;
|
|
3558
|
+
}
|
|
3148
3559
|
console.warn(
|
|
3149
|
-
`Patchstack: the ${label} callback passed to createProtection failed and was ignored. Protection is unaffected; this
|
|
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)}`
|
|
3150
3561
|
);
|
|
3151
3562
|
} catch {
|
|
3152
3563
|
}
|
|
3153
3564
|
}
|
|
3154
3565
|
|
|
3155
|
-
// src/protect/engine/normalizer.js
|
|
3156
|
-
var HTML_ENTITIES = {
|
|
3157
|
-
"&": "&",
|
|
3158
|
-
"<": "<",
|
|
3159
|
-
">": ">",
|
|
3160
|
-
""": '"',
|
|
3161
|
-
"'": "'",
|
|
3162
|
-
"'": "'",
|
|
3163
|
-
" ": " ",
|
|
3164
|
-
"'": "'",
|
|
3165
|
-
""": '"',
|
|
3166
|
-
"<": "<",
|
|
3167
|
-
">": ">",
|
|
3168
|
-
"&": "&",
|
|
3169
|
-
""": '"',
|
|
3170
|
-
"<": "<",
|
|
3171
|
-
">": ">",
|
|
3172
|
-
"&": "&"
|
|
3173
|
-
};
|
|
3174
|
-
var MAX_DECODE_ITERATIONS = 5;
|
|
3175
|
-
function normalize(value, options = {}) {
|
|
3176
|
-
if (typeof value !== "string") {
|
|
3177
|
-
return value;
|
|
3178
|
-
}
|
|
3179
|
-
const opts = {
|
|
3180
|
-
urlDecode: true,
|
|
3181
|
-
htmlDecode: true,
|
|
3182
|
-
sqlComments: true,
|
|
3183
|
-
nullBytes: true,
|
|
3184
|
-
whitespace: true,
|
|
3185
|
-
...options
|
|
3186
|
-
};
|
|
3187
|
-
let result = value;
|
|
3188
|
-
if (opts.nullBytes) {
|
|
3189
|
-
result = removeNullBytes(result);
|
|
3190
|
-
}
|
|
3191
|
-
if (opts.urlDecode) {
|
|
3192
|
-
result = urlDecode(result);
|
|
3193
|
-
}
|
|
3194
|
-
if (opts.htmlDecode) {
|
|
3195
|
-
result = htmlEntityDecode(result);
|
|
3196
|
-
}
|
|
3197
|
-
if (opts.sqlComments) {
|
|
3198
|
-
result = removeSqlComments(result);
|
|
3199
|
-
}
|
|
3200
|
-
if (opts.whitespace) {
|
|
3201
|
-
result = normalizeWhitespace(result);
|
|
3202
|
-
}
|
|
3203
|
-
return result;
|
|
3204
|
-
}
|
|
3205
|
-
function urlDecode(value) {
|
|
3206
|
-
if (typeof value !== "string") {
|
|
3207
|
-
return value;
|
|
3208
|
-
}
|
|
3209
|
-
let result = value;
|
|
3210
|
-
let previous = "";
|
|
3211
|
-
let iterations = 0;
|
|
3212
|
-
while (result !== previous && iterations < MAX_DECODE_ITERATIONS) {
|
|
3213
|
-
previous = result;
|
|
3214
|
-
iterations++;
|
|
3215
|
-
try {
|
|
3216
|
-
result = decodeURIComponent(result);
|
|
3217
|
-
} catch {
|
|
3218
|
-
result = safeUrlDecode(result);
|
|
3219
|
-
break;
|
|
3220
|
-
}
|
|
3221
|
-
}
|
|
3222
|
-
return result;
|
|
3223
|
-
}
|
|
3224
|
-
function safeUrlDecode(value) {
|
|
3225
|
-
return value.replace(/%([0-9A-Fa-f]{2})/g, (match, hex) => {
|
|
3226
|
-
try {
|
|
3227
|
-
return String.fromCharCode(parseInt(hex, 16));
|
|
3228
|
-
} catch {
|
|
3229
|
-
return match;
|
|
3230
|
-
}
|
|
3231
|
-
});
|
|
3232
|
-
}
|
|
3233
|
-
function htmlEntityDecode(value) {
|
|
3234
|
-
if (typeof value !== "string") {
|
|
3235
|
-
return value;
|
|
3236
|
-
}
|
|
3237
|
-
let result = value;
|
|
3238
|
-
for (const [entity, char] of Object.entries(HTML_ENTITIES)) {
|
|
3239
|
-
result = result.split(entity).join(char);
|
|
3240
|
-
}
|
|
3241
|
-
result = result.replace(/&#(\d+);/g, (match, code) => {
|
|
3242
|
-
const num = parseInt(code, 10);
|
|
3243
|
-
return num > 0 && num < 65536 ? String.fromCharCode(num) : match;
|
|
3244
|
-
});
|
|
3245
|
-
result = result.replace(/&#x([0-9A-Fa-f]+);/g, (match, hex) => {
|
|
3246
|
-
const num = parseInt(hex, 16);
|
|
3247
|
-
return num > 0 && num < 65536 ? String.fromCharCode(num) : match;
|
|
3248
|
-
});
|
|
3249
|
-
return result;
|
|
3250
|
-
}
|
|
3251
|
-
function removeSqlComments(value) {
|
|
3252
|
-
if (typeof value !== "string") {
|
|
3253
|
-
return value;
|
|
3254
|
-
}
|
|
3255
|
-
let result = value;
|
|
3256
|
-
result = result.replace(/\/\*[\s\S]*?\*\//g, " ");
|
|
3257
|
-
result = result.replace(/\/\*![\s\S]*?\*\//g, " ");
|
|
3258
|
-
return result;
|
|
3259
|
-
}
|
|
3260
|
-
function removeNullBytes(value) {
|
|
3261
|
-
if (typeof value !== "string") {
|
|
3262
|
-
return value;
|
|
3263
|
-
}
|
|
3264
|
-
let result = value.replace(/\x00/g, "");
|
|
3265
|
-
result = result.replace(/[\x01-\x08\x0B\x0C\x0E-\x1F]/g, "");
|
|
3266
|
-
return result;
|
|
3267
|
-
}
|
|
3268
|
-
function normalizeWhitespace(value) {
|
|
3269
|
-
if (typeof value !== "string") {
|
|
3270
|
-
return value;
|
|
3271
|
-
}
|
|
3272
|
-
let result = value.replace(/[\t\r\n\f\v]+/g, " ");
|
|
3273
|
-
result = result.replace(/ {2,}/g, " ");
|
|
3274
|
-
return result;
|
|
3275
|
-
}
|
|
3276
|
-
var MAX_RAW_CHARS = 1024 * 1024;
|
|
3277
|
-
var MAX_RAW_NODES = 25e3;
|
|
3278
|
-
var MAX_RAW_DEPTH = 200;
|
|
3279
|
-
var MAX_RAW_ENTRIES = 2e3;
|
|
3280
|
-
function serializeForRawDetection(body) {
|
|
3281
|
-
if (body === null || body === void 0) return "";
|
|
3282
|
-
if (typeof body === "string") return body.slice(0, MAX_RAW_CHARS);
|
|
3283
|
-
const visited = /* @__PURE__ */ new Set();
|
|
3284
|
-
const output = [];
|
|
3285
|
-
const stack = [{ kind: "value", value: body, depth: 0, root: true }];
|
|
3286
|
-
let chars = 0;
|
|
3287
|
-
let nodes = 0;
|
|
3288
|
-
const append = (text) => {
|
|
3289
|
-
if (chars >= MAX_RAW_CHARS) return;
|
|
3290
|
-
const piece = String(text).slice(0, MAX_RAW_CHARS - chars);
|
|
3291
|
-
output.push(piece);
|
|
3292
|
-
chars += piece.length;
|
|
3293
|
-
};
|
|
3294
|
-
while (stack.length > 0 && chars < MAX_RAW_CHARS) {
|
|
3295
|
-
const task = stack.pop();
|
|
3296
|
-
if (task.kind === "text") {
|
|
3297
|
-
append(task.text);
|
|
3298
|
-
continue;
|
|
3299
|
-
}
|
|
3300
|
-
if (task.kind === "key") {
|
|
3301
|
-
append(boundedJsonString(task.value));
|
|
3302
|
-
append(":");
|
|
3303
|
-
continue;
|
|
3304
|
-
}
|
|
3305
|
-
nodes++;
|
|
3306
|
-
if (nodes > MAX_RAW_NODES) {
|
|
3307
|
-
append('"[NodeLimit]"');
|
|
3308
|
-
continue;
|
|
3309
|
-
}
|
|
3310
|
-
const value = task.value;
|
|
3311
|
-
if (value === null || value === void 0) {
|
|
3312
|
-
append(task.root ? "" : "null");
|
|
3313
|
-
continue;
|
|
3314
|
-
}
|
|
3315
|
-
if (typeof value === "string") {
|
|
3316
|
-
append(task.root ? value.slice(0, MAX_RAW_CHARS) : boundedJsonString(value));
|
|
3317
|
-
continue;
|
|
3318
|
-
}
|
|
3319
|
-
if (typeof value !== "object") {
|
|
3320
|
-
append(String(value).slice(0, MAX_RAW_CHARS));
|
|
3321
|
-
continue;
|
|
3322
|
-
}
|
|
3323
|
-
if (visited.has(value)) {
|
|
3324
|
-
append("[Circular]");
|
|
3325
|
-
continue;
|
|
3326
|
-
}
|
|
3327
|
-
if (task.depth >= MAX_RAW_DEPTH) {
|
|
3328
|
-
append('"[DepthLimit]"');
|
|
3329
|
-
continue;
|
|
3330
|
-
}
|
|
3331
|
-
visited.add(value);
|
|
3332
|
-
if (Array.isArray(value)) {
|
|
3333
|
-
const length2 = Math.min(value.length, MAX_RAW_ENTRIES);
|
|
3334
|
-
const items = [];
|
|
3335
|
-
for (let i = 0; i < length2; i++) {
|
|
3336
|
-
if (i > 0) items.push({ kind: "text", text: "," });
|
|
3337
|
-
items.push({ kind: "value", value: readOwnValue(value, String(i)), depth: task.depth + 1, root: false });
|
|
3338
|
-
}
|
|
3339
|
-
if (value.length > length2) {
|
|
3340
|
-
if (length2 > 0) items.push({ kind: "text", text: "," });
|
|
3341
|
-
items.push({ kind: "text", text: '"[EntryLimit]"' });
|
|
3342
|
-
}
|
|
3343
|
-
stack.push({ kind: "text", text: "]" });
|
|
3344
|
-
for (let i = items.length - 1; i >= 0; i--) stack.push(items[i]);
|
|
3345
|
-
stack.push({ kind: "text", text: "[" });
|
|
3346
|
-
continue;
|
|
3347
|
-
}
|
|
3348
|
-
let keys;
|
|
3349
|
-
try {
|
|
3350
|
-
keys = Object.getOwnPropertyNames(value);
|
|
3351
|
-
} catch {
|
|
3352
|
-
append('"[Unreadable]"');
|
|
3353
|
-
continue;
|
|
3354
|
-
}
|
|
3355
|
-
const length = Math.min(keys.length, MAX_RAW_ENTRIES);
|
|
3356
|
-
const entries = [];
|
|
3357
|
-
for (let i = 0; i < length; i++) {
|
|
3358
|
-
const key = keys[i];
|
|
3359
|
-
if (i > 0) entries.push({ kind: "text", text: "," });
|
|
3360
|
-
entries.push({ kind: "key", value: key });
|
|
3361
|
-
entries.push({ kind: "value", value: readOwnValue(value, key), depth: task.depth + 1, root: false });
|
|
3362
|
-
}
|
|
3363
|
-
if (keys.length > length) {
|
|
3364
|
-
if (length > 0) entries.push({ kind: "text", text: "," });
|
|
3365
|
-
entries.push({ kind: "text", text: '"[EntryLimit]":null' });
|
|
3366
|
-
}
|
|
3367
|
-
stack.push({ kind: "text", text: "}" });
|
|
3368
|
-
for (let i = entries.length - 1; i >= 0; i--) stack.push(entries[i]);
|
|
3369
|
-
stack.push({ kind: "text", text: "{" });
|
|
3370
|
-
}
|
|
3371
|
-
return output.join("");
|
|
3372
|
-
}
|
|
3373
|
-
function boundedJsonString(value) {
|
|
3374
|
-
return JSON.stringify(String(value).slice(0, MAX_RAW_CHARS));
|
|
3375
|
-
}
|
|
3376
|
-
function readOwnValue(value, key) {
|
|
3377
|
-
try {
|
|
3378
|
-
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
3379
|
-
return descriptor && Object.hasOwn(descriptor, "value") ? descriptor.value : "[Accessor]";
|
|
3380
|
-
} catch {
|
|
3381
|
-
return "[Unreadable]";
|
|
3382
|
-
}
|
|
3383
|
-
}
|
|
3384
|
-
var INHERITED_ACCESSORS = /* @__PURE__ */ new Set(["headers", "query"]);
|
|
3385
|
-
function requestField(req, key) {
|
|
3386
|
-
if (req === null || typeof req !== "object") return void 0;
|
|
3387
|
-
if (Object.hasOwn(req, key)) return req[key];
|
|
3388
|
-
if (!INHERITED_ACCESSORS.has(key)) return void 0;
|
|
3389
|
-
let holder = Object.getPrototypeOf(req);
|
|
3390
|
-
while (holder !== null && !Object.hasOwn(holder, key)) holder = Object.getPrototypeOf(holder);
|
|
3391
|
-
if (holder === null || holder === Object.prototype) return void 0;
|
|
3392
|
-
const descriptor = Object.getOwnPropertyDescriptor(holder, key);
|
|
3393
|
-
if (typeof descriptor?.get !== "function") return void 0;
|
|
3394
|
-
return req[key];
|
|
3395
|
-
}
|
|
3396
|
-
function normalizeRequest(req, options = {}) {
|
|
3397
|
-
const ownRaw = Object.hasOwn(req ?? {}, "_rawBody") && typeof req._rawBody === "string";
|
|
3398
|
-
const body = requestField(req, "body");
|
|
3399
|
-
const url = requestField(req, "url");
|
|
3400
|
-
const rawBody = ownRaw ? req._rawBody : serializeForRawDetection(body ?? null);
|
|
3401
|
-
return {
|
|
3402
|
-
query: normalizeObject(requestField(req, "query") || {}, options),
|
|
3403
|
-
body: normalizeObject(body || {}, options),
|
|
3404
|
-
headers: normalizeObject(requestField(req, "headers") || {}, options),
|
|
3405
|
-
url: normalize(url || "", options),
|
|
3406
|
-
originalUrl: normalize(requestField(req, "originalUrl") || url || "", options),
|
|
3407
|
-
_rawBody: rawBody
|
|
3408
|
-
};
|
|
3409
|
-
}
|
|
3410
|
-
var MAX_NORMALIZE_DEPTH = 200;
|
|
3411
|
-
function normalizeObject(value, options = {}, depth = 0) {
|
|
3412
|
-
if (typeof value === "string") {
|
|
3413
|
-
return normalize(value, options);
|
|
3414
|
-
}
|
|
3415
|
-
if (depth >= MAX_NORMALIZE_DEPTH) {
|
|
3416
|
-
return value;
|
|
3417
|
-
}
|
|
3418
|
-
if (Array.isArray(value)) {
|
|
3419
|
-
return value.map((item) => normalizeObject(item, options, depth + 1));
|
|
3420
|
-
}
|
|
3421
|
-
if (typeof value === "object" && value !== null) {
|
|
3422
|
-
const result = {};
|
|
3423
|
-
for (const [key, val] of Object.entries(value)) {
|
|
3424
|
-
setOwn(result, key, normalizeObject(val, options, depth + 1));
|
|
3425
|
-
}
|
|
3426
|
-
return result;
|
|
3427
|
-
}
|
|
3428
|
-
return value;
|
|
3429
|
-
}
|
|
3430
|
-
|
|
3431
3566
|
// src/protect/engine/engine.js
|
|
3432
3567
|
var GRP = "(?:[^()]|\\([^()]*\\))*";
|
|
3433
3568
|
var REDOS_PATTERNS = [
|
|
@@ -3527,6 +3662,184 @@ function hasAdjacentUnbounded(body, flags) {
|
|
|
3527
3662
|
}
|
|
3528
3663
|
return false;
|
|
3529
3664
|
}
|
|
3665
|
+
function parseRepetition(body) {
|
|
3666
|
+
let i = 0;
|
|
3667
|
+
const parseAlternation = () => {
|
|
3668
|
+
const branches = [parseSequence()];
|
|
3669
|
+
while (body[i] === "|") {
|
|
3670
|
+
i++;
|
|
3671
|
+
branches.push(parseSequence());
|
|
3672
|
+
}
|
|
3673
|
+
return branches.length === 1 ? branches[0] : { type: "alt", branches };
|
|
3674
|
+
};
|
|
3675
|
+
const parseSequence = () => {
|
|
3676
|
+
const items = [];
|
|
3677
|
+
while (i < body.length && body[i] !== "|" && body[i] !== ")") {
|
|
3678
|
+
const atom = parseAtom();
|
|
3679
|
+
if (atom === null) return null;
|
|
3680
|
+
const quantified = parseQuantifier(atom);
|
|
3681
|
+
if (quantified === null) return null;
|
|
3682
|
+
items.push(quantified);
|
|
3683
|
+
}
|
|
3684
|
+
return { type: "seq", items };
|
|
3685
|
+
};
|
|
3686
|
+
const parseAtom = () => {
|
|
3687
|
+
const ch = body[i];
|
|
3688
|
+
if (ch === "(") {
|
|
3689
|
+
const start2 = i;
|
|
3690
|
+
i++;
|
|
3691
|
+
let kind = "capture";
|
|
3692
|
+
if (body.startsWith("?:", i)) {
|
|
3693
|
+
kind = "group";
|
|
3694
|
+
i += 2;
|
|
3695
|
+
} else if (body.startsWith("?=", i) || body.startsWith("?!", i)) {
|
|
3696
|
+
kind = "look";
|
|
3697
|
+
i += 2;
|
|
3698
|
+
} else if (body.startsWith("?<=", i) || body.startsWith("?<!", i)) {
|
|
3699
|
+
kind = "look";
|
|
3700
|
+
i += 3;
|
|
3701
|
+
} else if (body.startsWith("?<", i)) {
|
|
3702
|
+
const close = body.indexOf(">", i + 2);
|
|
3703
|
+
if (close === -1) return null;
|
|
3704
|
+
i = close + 1;
|
|
3705
|
+
} else if (body[i] === "?") {
|
|
3706
|
+
return null;
|
|
3707
|
+
}
|
|
3708
|
+
const inner = parseAlternation();
|
|
3709
|
+
if (inner === null || body[i] !== ")") return null;
|
|
3710
|
+
i++;
|
|
3711
|
+
return { type: "group", kind, body: inner, source: body.slice(start2, i) };
|
|
3712
|
+
}
|
|
3713
|
+
const start = i;
|
|
3714
|
+
if (ch === "[") {
|
|
3715
|
+
i++;
|
|
3716
|
+
if (body[i] === "^") i++;
|
|
3717
|
+
if (body[i] === "]") i++;
|
|
3718
|
+
while (i < body.length && body[i] !== "]") i += body[i] === "\\" ? 2 : 1;
|
|
3719
|
+
if (body[i] !== "]") return null;
|
|
3720
|
+
i++;
|
|
3721
|
+
return { type: "atom", source: body.slice(start, i) };
|
|
3722
|
+
}
|
|
3723
|
+
if (ch === "\\") {
|
|
3724
|
+
const next = body[i + 1];
|
|
3725
|
+
if (next === void 0) return null;
|
|
3726
|
+
if ((next === "p" || next === "P" || next === "u") && body[i + 2] === "{") {
|
|
3727
|
+
const close = body.indexOf("}", i + 3);
|
|
3728
|
+
if (close === -1) return null;
|
|
3729
|
+
i = close + 1;
|
|
3730
|
+
} else if (next === "k" && body[i + 2] === "<") {
|
|
3731
|
+
const close = body.indexOf(">", i + 3);
|
|
3732
|
+
if (close === -1) return null;
|
|
3733
|
+
i = close + 1;
|
|
3734
|
+
} else {
|
|
3735
|
+
i += 2;
|
|
3736
|
+
}
|
|
3737
|
+
return { type: /[bB]/.test(next) ? "assert" : "atom", source: body.slice(start, i) };
|
|
3738
|
+
}
|
|
3739
|
+
if (ch === "^" || ch === "$") {
|
|
3740
|
+
i++;
|
|
3741
|
+
return { type: "assert" };
|
|
3742
|
+
}
|
|
3743
|
+
if (ch === "*" || ch === "+" || ch === "?") return null;
|
|
3744
|
+
i++;
|
|
3745
|
+
return { type: "atom", source: body.slice(start, i) };
|
|
3746
|
+
};
|
|
3747
|
+
const parseQuantifier = (atom) => {
|
|
3748
|
+
let min;
|
|
3749
|
+
let max;
|
|
3750
|
+
const ch = body[i];
|
|
3751
|
+
if (ch === "*") [min, max, i] = [0, Infinity, i + 1];
|
|
3752
|
+
else if (ch === "+") [min, max, i] = [1, Infinity, i + 1];
|
|
3753
|
+
else if (ch === "?") [min, max, i] = [0, 1, i + 1];
|
|
3754
|
+
else if (ch === "{") {
|
|
3755
|
+
const bounds = /^\{(\d+)(,(\d*))?\}/.exec(body.slice(i));
|
|
3756
|
+
if (!bounds) return atom;
|
|
3757
|
+
min = Number(bounds[1]);
|
|
3758
|
+
max = bounds[2] === void 0 ? min : bounds[3] === "" ? Infinity : Number(bounds[3]);
|
|
3759
|
+
i += bounds[0].length;
|
|
3760
|
+
} else {
|
|
3761
|
+
return atom;
|
|
3762
|
+
}
|
|
3763
|
+
if (body[i] === "?") i++;
|
|
3764
|
+
if (body[i] === "*" || body[i] === "+" || body[i] === "?" || body[i] === "{") return null;
|
|
3765
|
+
return { type: "quant", min, max, body: atom };
|
|
3766
|
+
};
|
|
3767
|
+
const root = parseAlternation();
|
|
3768
|
+
return root !== null && i === body.length ? root : null;
|
|
3769
|
+
}
|
|
3770
|
+
function* nodesOf(node) {
|
|
3771
|
+
if (!node) return;
|
|
3772
|
+
yield node;
|
|
3773
|
+
if (node.type === "alt") for (const branch of node.branches) yield* nodesOf(branch);
|
|
3774
|
+
else if (node.type === "seq") for (const item of node.items) yield* nodesOf(item);
|
|
3775
|
+
else if (node.type === "group" || node.type === "quant") yield* nodesOf(node.body);
|
|
3776
|
+
}
|
|
3777
|
+
var BOUNDED_NESTING_LIMIT = 100;
|
|
3778
|
+
var BOUNDED_ALTERNATION_LIMIT = 16;
|
|
3779
|
+
var OPTIONAL_REPEAT_LIMIT = 8;
|
|
3780
|
+
function hasFencedRepetitions(quant, flags) {
|
|
3781
|
+
let body = quant.body;
|
|
3782
|
+
if (body.type === "group" && body.kind !== "look") body = body.body;
|
|
3783
|
+
if (body.type !== "seq" || body.items.length < 2) return false;
|
|
3784
|
+
const variable = [...nodesOf(body)].filter((n) => n.type === "quant" && (n.max > 1 || n.min < n.max));
|
|
3785
|
+
if (variable.some((n) => n.body.type !== "atom" || n.body.source === void 0)) return false;
|
|
3786
|
+
const atomFlags = flags.replace(/[^i]/g, "");
|
|
3787
|
+
const unicode = /[uv]/.test(flags);
|
|
3788
|
+
let insides;
|
|
3789
|
+
try {
|
|
3790
|
+
insides = variable.map((n) => new RegExp(`^(?:${n.body.source})$`, `${atomFlags}${unicode ? "u" : ""}`));
|
|
3791
|
+
} catch {
|
|
3792
|
+
return false;
|
|
3793
|
+
}
|
|
3794
|
+
const literalOf = (source) => {
|
|
3795
|
+
if (source.length === 1 && !".[]()|^$\\".includes(source)) return source;
|
|
3796
|
+
if (source.length === 2 && source[0] === "\\" && /[^\w\s]/.test(source[1])) return source[1];
|
|
3797
|
+
return null;
|
|
3798
|
+
};
|
|
3799
|
+
const disjoint = (edge) => {
|
|
3800
|
+
if (edge?.type !== "atom" || edge.source === void 0) return false;
|
|
3801
|
+
const literal = literalOf(edge.source);
|
|
3802
|
+
if (literal !== null) return insides.every((re) => !re.test(literal));
|
|
3803
|
+
if (unicode) return false;
|
|
3804
|
+
let fence;
|
|
3805
|
+
try {
|
|
3806
|
+
fence = new RegExp(`^(?:${edge.source})$`, atomFlags);
|
|
3807
|
+
} catch {
|
|
3808
|
+
return false;
|
|
3809
|
+
}
|
|
3810
|
+
for (let unit = 0; unit <= 65535; unit++) {
|
|
3811
|
+
const ch = String.fromCharCode(unit);
|
|
3812
|
+
if (fence.test(ch) && insides.some((re) => re.test(ch))) return false;
|
|
3813
|
+
}
|
|
3814
|
+
return true;
|
|
3815
|
+
};
|
|
3816
|
+
return disjoint(body.items[0]) || disjoint(body.items[body.items.length - 1]);
|
|
3817
|
+
}
|
|
3818
|
+
function hasCatastrophicShape(body, flags) {
|
|
3819
|
+
const root = parseRepetition(body);
|
|
3820
|
+
if (root === null) return null;
|
|
3821
|
+
for (const node of nodesOf(root)) {
|
|
3822
|
+
if (node.type === "group" && node.kind === "look" && hasAdjacentUnbounded(lookaroundBody(node.source), flags)) return true;
|
|
3823
|
+
if (node.type !== "quant" || node.max <= 1) continue;
|
|
3824
|
+
const fenced = hasFencedRepetitions(node, flags);
|
|
3825
|
+
for (const inner of nodesOf(node.body)) {
|
|
3826
|
+
if (inner === node.body && inner.type === "quant") continue;
|
|
3827
|
+
if (inner.type === "quant" && inner.max > 1 && !fenced) {
|
|
3828
|
+
if (node.max === Infinity || inner.max === Infinity || node.max * inner.max > BOUNDED_NESTING_LIMIT) return true;
|
|
3829
|
+
}
|
|
3830
|
+
if (inner.type === "quant" && inner.min === 0 && inner.max <= 1 && !fenced && (node.max === Infinity || node.max > OPTIONAL_REPEAT_LIMIT)) {
|
|
3831
|
+
return true;
|
|
3832
|
+
}
|
|
3833
|
+
if (inner.type === "alt" && inner.branches.length > 1 && (node.max === Infinity || node.max > BOUNDED_ALTERNATION_LIMIT)) {
|
|
3834
|
+
return true;
|
|
3835
|
+
}
|
|
3836
|
+
}
|
|
3837
|
+
}
|
|
3838
|
+
return false;
|
|
3839
|
+
}
|
|
3840
|
+
function lookaroundBody(source) {
|
|
3841
|
+
return source.slice(source.startsWith("(?<") ? 4 : 3, -1);
|
|
3842
|
+
}
|
|
3530
3843
|
function groupEnd(body, start) {
|
|
3531
3844
|
let depth = 0;
|
|
3532
3845
|
let inClass = false;
|
|
@@ -3553,6 +3866,8 @@ function warnRejectedPatternOnce(pattern) {
|
|
|
3553
3866
|
);
|
|
3554
3867
|
}
|
|
3555
3868
|
var MAX_PATTERN_LENGTH = 1e3;
|
|
3869
|
+
var regexVerdicts = /* @__PURE__ */ new Map();
|
|
3870
|
+
var REGEX_VERDICT_LIMIT = 2048;
|
|
3556
3871
|
function safeRegExp(pattern) {
|
|
3557
3872
|
if (!pattern) {
|
|
3558
3873
|
return null;
|
|
@@ -3564,16 +3879,32 @@ function safeRegExp(pattern) {
|
|
|
3564
3879
|
if (!match) {
|
|
3565
3880
|
return null;
|
|
3566
3881
|
}
|
|
3567
|
-
|
|
3568
|
-
|
|
3882
|
+
let safe = regexVerdicts.get(pattern);
|
|
3883
|
+
if (safe === void 0) {
|
|
3884
|
+
safe = isSafeBody(match[1], match[2]);
|
|
3885
|
+
if (regexVerdicts.size >= REGEX_VERDICT_LIMIT) regexVerdicts.clear();
|
|
3886
|
+
regexVerdicts.set(pattern, safe);
|
|
3569
3887
|
}
|
|
3570
|
-
if (
|
|
3888
|
+
if (!safe) return null;
|
|
3571
3889
|
try {
|
|
3572
3890
|
return new RegExp(match[1], match[2]);
|
|
3573
3891
|
} catch {
|
|
3574
3892
|
return null;
|
|
3575
3893
|
}
|
|
3576
3894
|
}
|
|
3895
|
+
function isSafeBody(body, flags) {
|
|
3896
|
+
for (const dangerous of REDOS_PATTERNS) {
|
|
3897
|
+
if (dangerous.test(body)) return false;
|
|
3898
|
+
}
|
|
3899
|
+
if (hasAdjacentUnbounded(body, flags)) return false;
|
|
3900
|
+
if (hasCatastrophicShape(body, flags) === true) return false;
|
|
3901
|
+
try {
|
|
3902
|
+
new RegExp(body, flags);
|
|
3903
|
+
} catch {
|
|
3904
|
+
return false;
|
|
3905
|
+
}
|
|
3906
|
+
return true;
|
|
3907
|
+
}
|
|
3577
3908
|
function ctypeResult(strValue, isClass, matchVal) {
|
|
3578
3909
|
if (strValue === "") {
|
|
3579
3910
|
return false;
|
|
@@ -3617,17 +3948,21 @@ function arrayKeyValue(value, matchObj) {
|
|
|
3617
3948
|
}
|
|
3618
3949
|
var WHOLE_VALUE_MATCH_TYPES = /* @__PURE__ */ new Set(["isset", "array_in_array", "array_key_value"]);
|
|
3619
3950
|
function collectLeafValues(root, nodeCap = 2e4, maxDepth = 1e3) {
|
|
3620
|
-
const
|
|
3951
|
+
const leaves = [];
|
|
3621
3952
|
const stack = [[root, 0]];
|
|
3622
3953
|
let visited = 0;
|
|
3954
|
+
let truncated = false;
|
|
3623
3955
|
while (stack.length) {
|
|
3624
3956
|
const [node, depth] = stack.pop();
|
|
3625
3957
|
if (node === null || node === void 0) continue;
|
|
3626
3958
|
if (typeof node !== "object") {
|
|
3627
|
-
|
|
3959
|
+
leaves.push(node);
|
|
3960
|
+
continue;
|
|
3961
|
+
}
|
|
3962
|
+
if (depth >= maxDepth || visited >= nodeCap) {
|
|
3963
|
+
truncated = true;
|
|
3628
3964
|
continue;
|
|
3629
3965
|
}
|
|
3630
|
-
if (depth >= maxDepth || visited >= nodeCap) continue;
|
|
3631
3966
|
visited++;
|
|
3632
3967
|
if (Array.isArray(node)) {
|
|
3633
3968
|
for (let i = node.length - 1; i >= 0; i--) stack.push([node[i], depth + 1]);
|
|
@@ -3635,10 +3970,21 @@ function collectLeafValues(root, nodeCap = 2e4, maxDepth = 1e3) {
|
|
|
3635
3970
|
for (const k of Object.keys(node)) stack.push([node[k], depth + 1]);
|
|
3636
3971
|
}
|
|
3637
3972
|
}
|
|
3638
|
-
return
|
|
3973
|
+
return { leaves, truncated };
|
|
3639
3974
|
}
|
|
3640
|
-
|
|
3641
|
-
|
|
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
|
+
}
|
|
3985
|
+
}
|
|
3986
|
+
var warnedKeys = /* @__PURE__ */ new Set();
|
|
3987
|
+
function warnOnce3(key, message) {
|
|
3642
3988
|
if (warnedKeys.has(key)) return;
|
|
3643
3989
|
warnedKeys.add(key);
|
|
3644
3990
|
console.warn(message);
|
|
@@ -3653,12 +3999,22 @@ function warnUnsupportedMatchType(type) {
|
|
|
3653
3999
|
`[patchstack] Unsupported rule_v2 match type "${type}" \u2014 condition treated as no-match. Rules relying on it are not enforced by this engine.`
|
|
3654
4000
|
);
|
|
3655
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
|
+
}
|
|
3656
4010
|
function hostFromValue(value) {
|
|
3657
|
-
const raw = String(value ?? "").trim();
|
|
4011
|
+
const raw = trimControls(String(value ?? "").replace(/[\t\n\r]/g, "")).trim();
|
|
3658
4012
|
if (raw === "") return "";
|
|
3659
|
-
|
|
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)) {
|
|
3660
4016
|
try {
|
|
3661
|
-
return new URL(
|
|
4017
|
+
return new URL(relative ? `http:${raw}` : raw).hostname;
|
|
3662
4018
|
} catch {
|
|
3663
4019
|
return raw;
|
|
3664
4020
|
}
|
|
@@ -3686,10 +4042,18 @@ function isInternalHost(hostname) {
|
|
|
3686
4042
|
const allZeroHi = g[0] === 0 && g[1] === 0 && g[2] === 0 && g[3] === 0 && g[4] === 0;
|
|
3687
4043
|
if (allZeroHi && g[5] === 0 && g[6] === 0 && (g[7] === 0 || g[7] === 1)) return true;
|
|
3688
4044
|
if ((g[0] & 65472) === 65152) return true;
|
|
4045
|
+
if ((g[0] & 65472) === 65216) return true;
|
|
3689
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;
|
|
3690
4049
|
if (allZeroHi && (g[5] === 65535 || g[5] === 0)) {
|
|
3691
|
-
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);
|
|
3692
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);
|
|
3693
4057
|
return false;
|
|
3694
4058
|
}
|
|
3695
4059
|
const v4 = parseIPv4ToInt(host);
|
|
@@ -3699,6 +4063,10 @@ function isInternalHost(hostname) {
|
|
|
3699
4063
|
function isPrivateV4Int(n) {
|
|
3700
4064
|
const a = n >>> 24 & 255;
|
|
3701
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;
|
|
3702
4070
|
if (a === 127 || a === 10 || a === 0) return true;
|
|
3703
4071
|
if (a === 169 && b === 254) return true;
|
|
3704
4072
|
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
@@ -4047,8 +4415,12 @@ var RuleEngine = class {
|
|
|
4047
4415
|
let normalizedReq;
|
|
4048
4416
|
let resolver;
|
|
4049
4417
|
try {
|
|
4050
|
-
|
|
4418
|
+
let limited = false;
|
|
4419
|
+
normalizedReq = { ...req, ...normalizeRequest(req, { onLimit: () => {
|
|
4420
|
+
limited = true;
|
|
4421
|
+
} }) };
|
|
4051
4422
|
resolver = new RequestResolver(normalizedReq);
|
|
4423
|
+
if (limited) resolver.noteSkip("container-cap");
|
|
4052
4424
|
} catch (err) {
|
|
4053
4425
|
this.#reportError(err);
|
|
4054
4426
|
return { blocked: false, rule: null, message: null };
|
|
@@ -4074,14 +4446,15 @@ var RuleEngine = class {
|
|
|
4074
4446
|
// that re-read the request instead would be reading it a second time: a getter, a stream or
|
|
4075
4447
|
// anything else that answers once can give a different value, and evidence that disagrees
|
|
4076
4448
|
// with the match it belongs to is worse than none.
|
|
4077
|
-
resolver
|
|
4449
|
+
resolver,
|
|
4450
|
+
...skipsOf(resolver)
|
|
4078
4451
|
};
|
|
4079
4452
|
}
|
|
4080
4453
|
} catch (err) {
|
|
4081
4454
|
this.#reportError(err);
|
|
4082
4455
|
}
|
|
4083
4456
|
}
|
|
4084
|
-
return { blocked: false, rule: null, message: null };
|
|
4457
|
+
return { blocked: false, rule: null, message: null, ...skipsOf(resolver) };
|
|
4085
4458
|
}
|
|
4086
4459
|
#evaluateRule(conditions, resolver) {
|
|
4087
4460
|
const inclusiveConditions = conditions.filter((c) => c.inclusive);
|
|
@@ -4137,9 +4510,15 @@ var RuleEngine = class {
|
|
|
4137
4510
|
if (WHOLE_VALUE_MATCH_TYPES.has(match.type)) {
|
|
4138
4511
|
if (matchValue(match.type, value, match.value, match)) return true;
|
|
4139
4512
|
} else {
|
|
4140
|
-
|
|
4513
|
+
const { leaves, truncated } = collectLeafValues(value);
|
|
4514
|
+
for (const leaf of leaves) {
|
|
4141
4515
|
if (matchValue(match.type, leaf, match.value, match)) return true;
|
|
4142
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
|
+
}
|
|
4143
4522
|
}
|
|
4144
4523
|
continue;
|
|
4145
4524
|
}
|
|
@@ -4526,6 +4905,9 @@ function conditionShapeProblem(condition) {
|
|
|
4526
4905
|
if (nested && !named) {
|
|
4527
4906
|
return `condition carries nested rules but its parameter is ${JSON.stringify(condition.parameter)}; a group must be {"parameter":"${GROUP_PARAMETER}"}`;
|
|
4528
4907
|
}
|
|
4908
|
+
if (condition.inclusive !== void 0 && typeof condition.inclusive !== "boolean") {
|
|
4909
|
+
return "inclusive must be true or false";
|
|
4910
|
+
}
|
|
4529
4911
|
if (named && nested) {
|
|
4530
4912
|
if (condition.match !== void 0) return "a group carries no match of its own; the engine ignores it";
|
|
4531
4913
|
if (condition.mutations !== void 0) return "a group carries no mutations of its own; the engine ignores them";
|
|
@@ -5235,7 +5617,7 @@ async function fromFetchRequest(request, options = {}) {
|
|
|
5235
5617
|
// that guessed differently would attribute one request to two addresses.
|
|
5236
5618
|
ip: client.ip ?? "",
|
|
5237
5619
|
_clientIp: client,
|
|
5238
|
-
cookies:
|
|
5620
|
+
cookies: parseCookieHeader(headers.cookie),
|
|
5239
5621
|
// Verbatim body text: preserves literal keys (e.g. `__proto__`) that JSON.stringify
|
|
5240
5622
|
// drops, so prototype-pollution rules on `raw` are robust.
|
|
5241
5623
|
_rawBody: rawBody,
|
|
@@ -5297,7 +5679,7 @@ function concatChunks(chunks, total) {
|
|
|
5297
5679
|
}
|
|
5298
5680
|
function parseBody(rawBody, contentType) {
|
|
5299
5681
|
const ct = String(contentType || "").toLowerCase();
|
|
5300
|
-
const
|
|
5682
|
+
const isJson2 = ct.includes("application/json") || /\+json\b/.test(ct);
|
|
5301
5683
|
const isForm = ct.includes("application/x-www-form-urlencoded");
|
|
5302
5684
|
const isMultipart = ct.includes("multipart/form-data");
|
|
5303
5685
|
const isAmbiguous = ct === "" || ct.startsWith("text/plain") || ct.includes("csp-report") || ct.includes("/json");
|
|
@@ -5311,7 +5693,7 @@ function parseBody(rawBody, contentType) {
|
|
|
5311
5693
|
for (const [k, v] of new URLSearchParams(rawBody)) appendOwn(body, k, v);
|
|
5312
5694
|
return { body, files: void 0 };
|
|
5313
5695
|
}
|
|
5314
|
-
if (
|
|
5696
|
+
if (isJson2 || isAmbiguous) {
|
|
5315
5697
|
try {
|
|
5316
5698
|
const parsed = JSON.parse(rawBody);
|
|
5317
5699
|
if (parsed && typeof parsed === "object") return { body: parsed, files: void 0 };
|
|
@@ -5327,38 +5709,115 @@ function parseMultipart(rawBody, boundary) {
|
|
|
5327
5709
|
const sep = /\r?\n\r?\n/.exec(part);
|
|
5328
5710
|
if (!sep) continue;
|
|
5329
5711
|
const rawHeaders = part.slice(0, sep.index);
|
|
5330
|
-
const disposition = /content-disposition:[^\r\n]
|
|
5331
|
-
if (
|
|
5332
|
-
const
|
|
5333
|
-
if (
|
|
5712
|
+
const disposition = /content-disposition:([^\r\n]*)/i.exec(rawHeaders)?.[1];
|
|
5713
|
+
if (disposition === void 0) continue;
|
|
5714
|
+
const { names, filename } = dispositionFields(disposition);
|
|
5715
|
+
if (names.length === 0) continue;
|
|
5334
5716
|
const content = part.slice(sep.index + sep[0].length).replace(/\r?\n$/, "");
|
|
5335
|
-
const
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
appendOwn(files, name, file);
|
|
5340
|
-
} else {
|
|
5341
|
-
appendOwn(body, name, content);
|
|
5717
|
+
const file = filename === void 0 ? void 0 : { filename, type: /content-type:\s*([^\r\n;]+)/i.exec(rawHeaders)?.[1]?.trim() || "", content };
|
|
5718
|
+
for (const name of names) {
|
|
5719
|
+
if (file) appendOwn(files, name, file);
|
|
5720
|
+
else appendOwn(body, name, content);
|
|
5342
5721
|
}
|
|
5343
5722
|
}
|
|
5344
5723
|
return { body, files };
|
|
5345
5724
|
}
|
|
5346
|
-
function
|
|
5347
|
-
const
|
|
5348
|
-
|
|
5349
|
-
|
|
5350
|
-
|
|
5351
|
-
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
|
|
5725
|
+
function dispositionFields(value) {
|
|
5726
|
+
const text = String(value);
|
|
5727
|
+
const names = /* @__PURE__ */ new Set();
|
|
5728
|
+
let filename;
|
|
5729
|
+
let extendedFilename;
|
|
5730
|
+
let i = text.indexOf(";");
|
|
5731
|
+
while (i !== -1 && i < text.length) {
|
|
5732
|
+
i++;
|
|
5733
|
+
while (text[i] === " " || text[i] === " ") i++;
|
|
5734
|
+
let key = "";
|
|
5735
|
+
while (i < text.length && text[i] !== "=" && text[i] !== ";") key += text[i++];
|
|
5736
|
+
key = key.trim().toLowerCase();
|
|
5737
|
+
let val = null;
|
|
5738
|
+
if (text[i] === "=") {
|
|
5739
|
+
i++;
|
|
5740
|
+
while (text[i] === " " || text[i] === " ") i++;
|
|
5741
|
+
if (text[i] === '"') {
|
|
5742
|
+
i++;
|
|
5743
|
+
val = "";
|
|
5744
|
+
while (i < text.length && text[i] !== '"') {
|
|
5745
|
+
if (text[i] === "\\" && i + 1 < text.length) i++;
|
|
5746
|
+
val += text[i++];
|
|
5747
|
+
}
|
|
5748
|
+
i++;
|
|
5749
|
+
while (i < text.length && text[i] !== ";") i++;
|
|
5750
|
+
} else {
|
|
5751
|
+
let bare = "";
|
|
5752
|
+
while (i < text.length && text[i] !== ";") bare += text[i++];
|
|
5753
|
+
val = bare.trim();
|
|
5754
|
+
}
|
|
5755
|
+
}
|
|
5756
|
+
if (val !== null) {
|
|
5757
|
+
if (key === "name") names.add(val);
|
|
5758
|
+
else if (key === "filename") filename = val;
|
|
5759
|
+
else if (key === "filename*") extendedFilename = decodeExtendedValue(val);
|
|
5355
5760
|
}
|
|
5356
|
-
|
|
5761
|
+
i = i < text.length && text[i] === ";" ? i : -1;
|
|
5762
|
+
}
|
|
5763
|
+
return { names: [...names], filename: extendedFilename ?? filename };
|
|
5764
|
+
}
|
|
5765
|
+
function decodeExtendedValue(value) {
|
|
5766
|
+
const match = /^([^']*)'[^']*'(.*)$/.exec(value);
|
|
5767
|
+
if (!match) return value;
|
|
5768
|
+
try {
|
|
5769
|
+
return decodeURIComponent(match[2]);
|
|
5770
|
+
} catch {
|
|
5771
|
+
return match[2];
|
|
5357
5772
|
}
|
|
5358
|
-
return cookies;
|
|
5359
5773
|
}
|
|
5360
5774
|
|
|
5361
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
|
+
}
|
|
5362
5821
|
function fromNodeRequest(req, rawBody = "", options = {}) {
|
|
5363
5822
|
const method = (req.method || "GET").toUpperCase();
|
|
5364
5823
|
const headers = {};
|
|
@@ -5404,29 +5863,41 @@ function fromNodeRequest(req, rawBody = "", options = {}) {
|
|
|
5404
5863
|
headers,
|
|
5405
5864
|
ip: client.ip ?? "",
|
|
5406
5865
|
_clientIp: client,
|
|
5407
|
-
cookies:
|
|
5866
|
+
cookies: parseCookieHeader(headers.cookie),
|
|
5408
5867
|
// Verbatim body text: preserves literal keys (e.g. `__proto__`) that JSON.stringify drops.
|
|
5409
5868
|
_rawBody: rawBody
|
|
5410
5869
|
};
|
|
5411
5870
|
}
|
|
5412
|
-
|
|
5413
|
-
|
|
5414
|
-
|
|
5415
|
-
|
|
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;
|
|
5416
5891
|
}
|
|
5417
|
-
|
|
5418
|
-
|
|
5419
|
-
|
|
5420
|
-
|
|
5421
|
-
|
|
5422
|
-
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;
|
|
5423
5897
|
}
|
|
5424
|
-
return
|
|
5898
|
+
return refused;
|
|
5425
5899
|
}
|
|
5426
|
-
|
|
5427
|
-
// src/protect/egress.js
|
|
5428
5900
|
async function installEgressGuard({ shouldBlock, onBlock, onSkip, dnsScreen = true, lookup, allowHosts } = {}) {
|
|
5429
|
-
const restores = [];
|
|
5430
5901
|
if (typeof shouldBlock !== "function") return () => {
|
|
5431
5902
|
};
|
|
5432
5903
|
const exempt = new Set((allowHosts ?? []).map((h) => String(h).toLowerCase()));
|
|
@@ -5471,6 +5942,9 @@ async function installEgressGuard({ shouldBlock, onBlock, onSkip, dnsScreen = tr
|
|
|
5471
5942
|
resolve(false);
|
|
5472
5943
|
}
|
|
5473
5944
|
});
|
|
5945
|
+
const registry = egressRegistry();
|
|
5946
|
+
const own = { block, prescreen: resolvesToDisallowed, dns: screen, skip };
|
|
5947
|
+
registry.screens.add(own);
|
|
5474
5948
|
const originalFetch = globalThis.fetch;
|
|
5475
5949
|
if (typeof originalFetch === "function" && !originalFetch.__patchstackGuarded) {
|
|
5476
5950
|
const REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
|
|
@@ -5543,15 +6017,19 @@ async function installEgressGuard({ shouldBlock, onBlock, onSkip, dnsScreen = tr
|
|
|
5543
6017
|
} catch {
|
|
5544
6018
|
host = null;
|
|
5545
6019
|
}
|
|
5546
|
-
|
|
5547
|
-
|
|
5548
|
-
|
|
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);
|
|
5549
6024
|
};
|
|
5550
6025
|
const guarded = async (input, init) => {
|
|
5551
6026
|
let cur;
|
|
5552
6027
|
try {
|
|
5553
6028
|
cur = new Request(input, { ...init || {}, redirect: "manual" });
|
|
5554
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", {});
|
|
5555
6033
|
return originalFetch(input, init);
|
|
5556
6034
|
}
|
|
5557
6035
|
const callerRedirect = init && init.redirect || input && input.redirect || "follow";
|
|
@@ -5562,13 +6040,12 @@ async function installEgressGuard({ shouldBlock, onBlock, onSkip, dnsScreen = tr
|
|
|
5562
6040
|
const headers = new Headers(cur.headers);
|
|
5563
6041
|
const signal = cur.signal;
|
|
5564
6042
|
const replay = captureReplayBody(cur);
|
|
6043
|
+
const { body: _callerBody, ...callerOptions } = init || {};
|
|
5565
6044
|
let body;
|
|
5566
6045
|
for (let hop = 0; ; hop++) {
|
|
5567
6046
|
let resp;
|
|
5568
6047
|
try {
|
|
5569
|
-
resp = await originalFetch(
|
|
5570
|
-
hop === 0 ? cur : new Request(url, { method, headers, body, redirect: "manual", signal })
|
|
5571
|
-
);
|
|
6048
|
+
resp = hop === 0 ? await originalFetch(cur, { ...callerOptions, redirect: "manual" }) : await originalFetch(url, { ...callerOptions, method, headers, body, redirect: "manual", signal });
|
|
5572
6049
|
} catch (error) {
|
|
5573
6050
|
replay.cancel();
|
|
5574
6051
|
throw error;
|
|
@@ -5576,7 +6053,7 @@ async function installEgressGuard({ shouldBlock, onBlock, onSkip, dnsScreen = tr
|
|
|
5576
6053
|
const location = REDIRECT_STATUSES.has(resp.status) ? resp.headers.get("location") : null;
|
|
5577
6054
|
if (!location) {
|
|
5578
6055
|
replay.cancel();
|
|
5579
|
-
return resp;
|
|
6056
|
+
return hop === 0 ? resp : markRedirected(resp);
|
|
5580
6057
|
}
|
|
5581
6058
|
await discardResponseBody(resp);
|
|
5582
6059
|
if (hop >= MAX_REDIRECTS) {
|
|
@@ -5609,43 +6086,84 @@ async function installEgressGuard({ shouldBlock, onBlock, onSkip, dnsScreen = tr
|
|
|
5609
6086
|
};
|
|
5610
6087
|
guarded.__patchstackGuarded = true;
|
|
5611
6088
|
globalThis.fetch = guarded;
|
|
5612
|
-
|
|
5613
|
-
|
|
6089
|
+
registry.surfaces.set(guarded, {
|
|
6090
|
+
release() {
|
|
6091
|
+
if (globalThis.fetch !== guarded) return false;
|
|
6092
|
+
globalThis.fetch = originalFetch;
|
|
6093
|
+
return true;
|
|
6094
|
+
}
|
|
5614
6095
|
});
|
|
5615
6096
|
}
|
|
6097
|
+
let syncBuiltins = () => {
|
|
6098
|
+
};
|
|
6099
|
+
try {
|
|
6100
|
+
const { syncBuiltinESMExports } = await import("module");
|
|
6101
|
+
if (typeof syncBuiltinESMExports === "function") syncBuiltins = syncBuiltinESMExports;
|
|
6102
|
+
} catch {
|
|
6103
|
+
}
|
|
6104
|
+
let patchedAny = false;
|
|
5616
6105
|
for (const moduleName of ["node:http", "node:https"]) {
|
|
5617
6106
|
try {
|
|
5618
6107
|
const mod = await import(moduleName);
|
|
5619
|
-
|
|
5620
|
-
|
|
6108
|
+
if (registry.surfaces.has(moduleName)) continue;
|
|
6109
|
+
const release = patchHttpModule(mod.default ?? mod, registry);
|
|
6110
|
+
if (release) {
|
|
6111
|
+
patchedAny = true;
|
|
6112
|
+
registry.surfaces.set(moduleName, {
|
|
6113
|
+
release() {
|
|
6114
|
+
if (!release()) return false;
|
|
6115
|
+
syncBuiltins();
|
|
6116
|
+
return true;
|
|
6117
|
+
}
|
|
6118
|
+
});
|
|
6119
|
+
}
|
|
5621
6120
|
} catch {
|
|
5622
6121
|
}
|
|
5623
6122
|
}
|
|
6123
|
+
if (patchedAny) syncBuiltins();
|
|
5624
6124
|
return () => {
|
|
5625
|
-
|
|
6125
|
+
registry.screens.delete(own);
|
|
6126
|
+
if (registry.screens.size > 0) return;
|
|
6127
|
+
for (const [name, surface] of registry.surfaces) {
|
|
5626
6128
|
try {
|
|
5627
|
-
|
|
6129
|
+
if (surface.release()) registry.surfaces.delete(name);
|
|
5628
6130
|
} catch {
|
|
5629
6131
|
}
|
|
5630
6132
|
}
|
|
5631
6133
|
};
|
|
5632
6134
|
}
|
|
5633
|
-
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) {
|
|
5634
6143
|
if (!http || typeof http.request !== "function" || http.__patchstackGuarded) return null;
|
|
5635
6144
|
const originalRequest = http.request;
|
|
5636
6145
|
const originalGet = http.get;
|
|
5637
|
-
const
|
|
6146
|
+
const OriginalClientRequest = typeof http.ClientRequest === "function" ? http.ClientRequest : null;
|
|
6147
|
+
const guardArgs = (args) => {
|
|
5638
6148
|
const target = extractHttpTarget(args);
|
|
5639
|
-
if (target
|
|
5640
|
-
|
|
5641
|
-
|
|
5642
|
-
|
|
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
|
+
};
|
|
5643
6158
|
try {
|
|
5644
|
-
|
|
6159
|
+
return withScreeningLookup(args, target, block, resolving[0].dns.lookup, skip);
|
|
5645
6160
|
} catch {
|
|
5646
6161
|
}
|
|
5647
6162
|
}
|
|
5648
|
-
return
|
|
6163
|
+
return args;
|
|
6164
|
+
};
|
|
6165
|
+
const wrap = (original) => function(...args) {
|
|
6166
|
+
return original.apply(this, guardArgs(args));
|
|
5649
6167
|
};
|
|
5650
6168
|
const guardedRequest = wrap(originalRequest);
|
|
5651
6169
|
http.request = guardedRequest;
|
|
@@ -5654,11 +6172,26 @@ function patchHttpModule(http, block, screen, skip) {
|
|
|
5654
6172
|
guardedGet = wrap(originalGet);
|
|
5655
6173
|
http.get = guardedGet;
|
|
5656
6174
|
}
|
|
6175
|
+
let GuardedClientRequest;
|
|
6176
|
+
if (OriginalClientRequest) {
|
|
6177
|
+
GuardedClientRequest = function ClientRequest(...args) {
|
|
6178
|
+
const screened = guardArgs(args);
|
|
6179
|
+
return new.target ? Reflect.construct(OriginalClientRequest, screened, new.target) : Reflect.construct(OriginalClientRequest, screened);
|
|
6180
|
+
};
|
|
6181
|
+
GuardedClientRequest.prototype = OriginalClientRequest.prototype;
|
|
6182
|
+
Object.setPrototypeOf(GuardedClientRequest, OriginalClientRequest);
|
|
6183
|
+
http.ClientRequest = GuardedClientRequest;
|
|
6184
|
+
}
|
|
5657
6185
|
http.__patchstackGuarded = true;
|
|
5658
6186
|
return () => {
|
|
5659
|
-
if (http.request
|
|
5660
|
-
if (guardedGet && http.get
|
|
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;
|
|
5661
6193
|
delete http.__patchstackGuarded;
|
|
6194
|
+
return true;
|
|
5662
6195
|
};
|
|
5663
6196
|
}
|
|
5664
6197
|
function extractHttpTarget(args) {
|
|
@@ -5667,19 +6200,27 @@ function extractHttpTarget(args) {
|
|
|
5667
6200
|
if (typeof first === "string" || first instanceof URL) {
|
|
5668
6201
|
const url = new URL(String(first));
|
|
5669
6202
|
const opts = args.find((a) => a && typeof a === "object" && !(a instanceof URL));
|
|
5670
|
-
return { url
|
|
5671
|
-
}
|
|
5672
|
-
if (first && typeof first === "object") {
|
|
5673
|
-
const host = normalizeHost(first.hostname || first.host) || "localhost";
|
|
5674
|
-
const protocol = first.protocol || "http:";
|
|
5675
|
-
const port = first.port ? `:${first.port}` : "";
|
|
5676
|
-
const path9 = first.path || "/";
|
|
5677
|
-
return { url: `${protocol}//${host}${port}${path9}`, host, method: first.method || "GET" };
|
|
6203
|
+
return targetOf({ ...optionsFromUrl(url), ...opts ?? {} });
|
|
5678
6204
|
}
|
|
6205
|
+
if (first && typeof first === "object") return targetOf(first);
|
|
5679
6206
|
} catch {
|
|
5680
6207
|
}
|
|
5681
6208
|
return null;
|
|
5682
6209
|
}
|
|
6210
|
+
function optionsFromUrl(url) {
|
|
6211
|
+
const hostname = url.hostname.startsWith("[") ? url.hostname.slice(1, -1) : url.hostname;
|
|
6212
|
+
const options = { protocol: url.protocol, hostname, path: `${url.pathname || "/"}${url.search}` };
|
|
6213
|
+
if (url.port !== "") options.port = Number(url.port);
|
|
6214
|
+
return options;
|
|
6215
|
+
}
|
|
6216
|
+
function targetOf(options) {
|
|
6217
|
+
const host = normalizeHost(options.hostname || options.host) || "localhost";
|
|
6218
|
+
const protocol = options.protocol || "http:";
|
|
6219
|
+
const port = options.port ? `:${options.port}` : "";
|
|
6220
|
+
const path9 = options.path || "/";
|
|
6221
|
+
const authority = host.includes(":") ? `[${host}]` : host;
|
|
6222
|
+
return { url: `${protocol}//${authority}${port}${path9}`, host, method: options.method || "GET" };
|
|
6223
|
+
}
|
|
5683
6224
|
function normalizeHost(raw) {
|
|
5684
6225
|
const host = String(raw || "").trim();
|
|
5685
6226
|
const bracketed = /^\[([^\]]+)\]/.exec(host);
|
|
@@ -6061,19 +6602,49 @@ async function loadFs() {
|
|
|
6061
6602
|
}
|
|
6062
6603
|
return fsMod;
|
|
6063
6604
|
}
|
|
6605
|
+
async function sourceIdentity(options = {}) {
|
|
6606
|
+
if (typeof options.siteUuid === "string" && options.siteUuid.trim() !== "") {
|
|
6607
|
+
const endpoint = typeof options.pulseRulesUrl === "string" ? options.pulseRulesUrl : "";
|
|
6608
|
+
return `site:${options.siteUuid.trim().toLowerCase()}@${endpoint}`;
|
|
6609
|
+
}
|
|
6610
|
+
if (typeof options.token === "string" && options.token !== "") {
|
|
6611
|
+
const digest = await sha256Hex(options.token);
|
|
6612
|
+
if (digest === null) return null;
|
|
6613
|
+
const endpoint = typeof options.baseUrl === "string" ? options.baseUrl : "";
|
|
6614
|
+
return `token:${digest}@${endpoint}`;
|
|
6615
|
+
}
|
|
6616
|
+
return null;
|
|
6617
|
+
}
|
|
6618
|
+
async function sha256Hex(text) {
|
|
6619
|
+
try {
|
|
6620
|
+
const subtle = globalThis.crypto?.subtle;
|
|
6621
|
+
if (!subtle || typeof TextEncoder === "undefined") return null;
|
|
6622
|
+
const digest = await subtle.digest("SHA-256", new TextEncoder().encode(text));
|
|
6623
|
+
return Array.from(new Uint8Array(digest), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
6624
|
+
} catch {
|
|
6625
|
+
return null;
|
|
6626
|
+
}
|
|
6627
|
+
}
|
|
6064
6628
|
function makeStore(options = {}) {
|
|
6065
6629
|
let mem = null;
|
|
6066
6630
|
const durable = durableTier(options);
|
|
6631
|
+
let identity;
|
|
6632
|
+
const identityOf = () => identity ??= sourceIdentity(options);
|
|
6067
6633
|
return {
|
|
6068
6634
|
async read() {
|
|
6069
6635
|
if (mem) return mem;
|
|
6636
|
+
const source = await identityOf();
|
|
6637
|
+
if (source === null) return null;
|
|
6070
6638
|
const env = await durable.read();
|
|
6071
|
-
if (env)
|
|
6639
|
+
if (!env || env.source !== source) return null;
|
|
6640
|
+
mem = env;
|
|
6072
6641
|
return env;
|
|
6073
6642
|
},
|
|
6074
6643
|
async write(env) {
|
|
6075
|
-
if (env)
|
|
6076
|
-
await
|
|
6644
|
+
if (!env) return;
|
|
6645
|
+
const stamped = { ...env, source: await identityOf() };
|
|
6646
|
+
mem = stamped;
|
|
6647
|
+
await durable.write(stamped);
|
|
6077
6648
|
}
|
|
6078
6649
|
};
|
|
6079
6650
|
}
|
|
@@ -6195,11 +6766,12 @@ function toEnvelope(value) {
|
|
|
6195
6766
|
buildId,
|
|
6196
6767
|
// A confirmation belongs to the presentation stored beside it. A crossed or partially written
|
|
6197
6768
|
// envelope confirms nothing, even when one of its fields happens to name the current map.
|
|
6198
|
-
matchedBuildId: buildId !== null && matched === buildId ? matched : null
|
|
6769
|
+
matchedBuildId: buildId !== null && matched === buildId ? matched : null,
|
|
6770
|
+
source: typeof value.source === "string" ? value.source : null
|
|
6199
6771
|
};
|
|
6200
6772
|
}
|
|
6201
6773
|
if (Array.isArray(value.firewall) || Array.isArray(value.whitelists)) {
|
|
6202
|
-
return { bundle: value, etag: null, buildId: null, matchedBuildId: null };
|
|
6774
|
+
return { bundle: value, etag: null, buildId: null, matchedBuildId: null, source: null };
|
|
6203
6775
|
}
|
|
6204
6776
|
return null;
|
|
6205
6777
|
}
|
|
@@ -6267,13 +6839,17 @@ function withScopedDryRun(bundle, confirmed, reason, options = {}) {
|
|
|
6267
6839
|
...unusable > 0 ? [`${unusable} rule(s) carry an unusable ${BUILD_SCOPE_PROPERTY}`] : [],
|
|
6268
6840
|
...held > unusable ? [reason] : []
|
|
6269
6841
|
];
|
|
6270
|
-
|
|
6271
|
-
|
|
6272
|
-
|
|
6273
|
-
"onError"
|
|
6274
|
-
);
|
|
6842
|
+
const message = `${held} build-scoped rule(s) are detecting only, not blocking: ${explanations.join("; ")}`;
|
|
6843
|
+
if (typeof options.onError === "function") notify(options.onError, new Error(message), "onError");
|
|
6844
|
+
else warnHeldOnce(message);
|
|
6275
6845
|
return { ...bundle, firewall };
|
|
6276
6846
|
}
|
|
6847
|
+
var warnedHeld = /* @__PURE__ */ new Set();
|
|
6848
|
+
function warnHeldOnce(message) {
|
|
6849
|
+
if (warnedHeld.has(message)) return;
|
|
6850
|
+
warnedHeld.add(message);
|
|
6851
|
+
console.warn(`[patchstack] ${message}. Pass { onError } to handle this yourself.`);
|
|
6852
|
+
}
|
|
6277
6853
|
function confirmedBy(res, presented) {
|
|
6278
6854
|
if (presented === null) return null;
|
|
6279
6855
|
const verdict = res?.build;
|
|
@@ -6448,6 +7024,60 @@ function startRefresh(tick, { refreshMs, onError } = {}) {
|
|
|
6448
7024
|
}
|
|
6449
7025
|
};
|
|
6450
7026
|
}
|
|
7027
|
+
var RECOVERY_DELAYS_MS = [5e3, 15e3, 45e3, 12e4, 3e5, 6e5];
|
|
7028
|
+
function startRecovery(tick, { onError } = {}) {
|
|
7029
|
+
let stopped = false;
|
|
7030
|
+
let attempt = 0;
|
|
7031
|
+
let timer = null;
|
|
7032
|
+
const schedule = () => {
|
|
7033
|
+
if (stopped) return;
|
|
7034
|
+
const delay = RECOVERY_DELAYS_MS[Math.min(attempt, RECOVERY_DELAYS_MS.length - 1)];
|
|
7035
|
+
timer = setTimeout(run, delay * (1 - Math.random() * JITTER_FRACTION3));
|
|
7036
|
+
timer?.unref?.();
|
|
7037
|
+
};
|
|
7038
|
+
const run = async () => {
|
|
7039
|
+
if (stopped) return;
|
|
7040
|
+
attempt++;
|
|
7041
|
+
try {
|
|
7042
|
+
const status = await tick();
|
|
7043
|
+
if (!status || status.ok !== false) {
|
|
7044
|
+
stopped = true;
|
|
7045
|
+
return;
|
|
7046
|
+
}
|
|
7047
|
+
} catch (err) {
|
|
7048
|
+
notify(onError, err, "onError");
|
|
7049
|
+
}
|
|
7050
|
+
schedule();
|
|
7051
|
+
};
|
|
7052
|
+
schedule();
|
|
7053
|
+
return {
|
|
7054
|
+
stop() {
|
|
7055
|
+
stopped = true;
|
|
7056
|
+
if (timer) clearTimeout(timer);
|
|
7057
|
+
}
|
|
7058
|
+
};
|
|
7059
|
+
}
|
|
7060
|
+
function serialise(tick) {
|
|
7061
|
+
let running = null;
|
|
7062
|
+
let next = null;
|
|
7063
|
+
const run = () => {
|
|
7064
|
+
if (!running) {
|
|
7065
|
+
running = Promise.resolve().then(() => tick()).finally(() => {
|
|
7066
|
+
running = null;
|
|
7067
|
+
});
|
|
7068
|
+
return running;
|
|
7069
|
+
}
|
|
7070
|
+
if (!next) {
|
|
7071
|
+
next = running.catch(() => {
|
|
7072
|
+
}).then(() => {
|
|
7073
|
+
next = null;
|
|
7074
|
+
return run();
|
|
7075
|
+
});
|
|
7076
|
+
}
|
|
7077
|
+
return next;
|
|
7078
|
+
};
|
|
7079
|
+
return run;
|
|
7080
|
+
}
|
|
6451
7081
|
function makeRefreshHandler(tick, secret) {
|
|
6452
7082
|
let inflight2 = null;
|
|
6453
7083
|
const runOnce = () => {
|
|
@@ -6462,7 +7092,7 @@ function makeRefreshHandler(tick, secret) {
|
|
|
6462
7092
|
return async (request) => {
|
|
6463
7093
|
if (!secret) return new Response("not found", { status: 404 });
|
|
6464
7094
|
const provided = request?.headers?.get?.("x-patchstack-refresh") ?? null;
|
|
6465
|
-
if (provided
|
|
7095
|
+
if (!await sameSecret(provided, secret)) return new Response("forbidden", { status: 403 });
|
|
6466
7096
|
let refreshed = true;
|
|
6467
7097
|
try {
|
|
6468
7098
|
const status = await runOnce();
|
|
@@ -6473,6 +7103,33 @@ function makeRefreshHandler(tick, secret) {
|
|
|
6473
7103
|
return new Response(JSON.stringify({ refreshed }), { status: 200, headers: { "content-type": "application/json" } });
|
|
6474
7104
|
};
|
|
6475
7105
|
}
|
|
7106
|
+
async function sameSecret(provided, secret) {
|
|
7107
|
+
if (typeof provided !== "string" || typeof secret !== "string") return false;
|
|
7108
|
+
const subtle = globalThis.crypto?.subtle;
|
|
7109
|
+
if (subtle && typeof TextEncoder === "function") {
|
|
7110
|
+
try {
|
|
7111
|
+
const encoder2 = new TextEncoder();
|
|
7112
|
+
const [a, b] = await Promise.all([
|
|
7113
|
+
subtle.digest("SHA-256", encoder2.encode(provided)),
|
|
7114
|
+
subtle.digest("SHA-256", encoder2.encode(secret))
|
|
7115
|
+
]);
|
|
7116
|
+
return equalBytes(new Uint8Array(a), new Uint8Array(b));
|
|
7117
|
+
} catch {
|
|
7118
|
+
}
|
|
7119
|
+
}
|
|
7120
|
+
let diff = provided.length ^ secret.length;
|
|
7121
|
+
const length = Math.max(provided.length, secret.length);
|
|
7122
|
+
for (let i = 0; i < length; i++) {
|
|
7123
|
+
diff |= (provided.charCodeAt(i) || 0) ^ (secret.charCodeAt(i) || 0);
|
|
7124
|
+
}
|
|
7125
|
+
return diff === 0;
|
|
7126
|
+
}
|
|
7127
|
+
function equalBytes(a, b) {
|
|
7128
|
+
if (a.length !== b.length) return false;
|
|
7129
|
+
let diff = 0;
|
|
7130
|
+
for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i];
|
|
7131
|
+
return diff === 0;
|
|
7132
|
+
}
|
|
6476
7133
|
|
|
6477
7134
|
// src/protect/detections.js
|
|
6478
7135
|
init_pulse_token();
|
|
@@ -7208,9 +7865,180 @@ function hardensWithoutBody(rule) {
|
|
|
7208
7865
|
if (!rule || !HEADER_ACTIONS.includes(rule.action)) return false;
|
|
7209
7866
|
const { parameters, complete } = readRuleParameters(rule);
|
|
7210
7867
|
if (!complete) return false;
|
|
7211
|
-
return parameters.every(
|
|
7212
|
-
|
|
7868
|
+
return parameters.every(bodyIndependent);
|
|
7869
|
+
}
|
|
7870
|
+
function readsOnlyResponseHeaders(rule) {
|
|
7871
|
+
if (!rule) return false;
|
|
7872
|
+
const { parameters, complete } = readRuleParameters(rule);
|
|
7873
|
+
if (!complete || !parameters.some(isHeaderParameter)) return false;
|
|
7874
|
+
return parameters.every(bodyIndependent);
|
|
7875
|
+
}
|
|
7876
|
+
function isHeaderParameter(parameter) {
|
|
7877
|
+
return parameter === "response.headers" || parameter.startsWith(BODY_INDEPENDENT_PREFIX);
|
|
7878
|
+
}
|
|
7879
|
+
function bodyIndependent(parameter) {
|
|
7880
|
+
return BODY_INDEPENDENT.includes(parameter) || parameter.startsWith(BODY_INDEPENDENT_PREFIX);
|
|
7881
|
+
}
|
|
7882
|
+
|
|
7883
|
+
// src/protect/supabase-guard.js
|
|
7884
|
+
var GUARD_PATH = "/_patchstack/guard";
|
|
7885
|
+
var DEFAULT_MAX_BODY_BYTES = 1024 * 1024;
|
|
7886
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
7887
|
+
var REQUEST_HEADER_DENYLIST = /* @__PURE__ */ new Set([
|
|
7888
|
+
"connection",
|
|
7889
|
+
"cookie",
|
|
7890
|
+
"cookie2",
|
|
7891
|
+
"forwarded",
|
|
7892
|
+
"host",
|
|
7893
|
+
"keep-alive",
|
|
7894
|
+
"proxy-authenticate",
|
|
7895
|
+
"proxy-authorization",
|
|
7896
|
+
"te",
|
|
7897
|
+
"trailer",
|
|
7898
|
+
"transfer-encoding",
|
|
7899
|
+
"upgrade",
|
|
7900
|
+
"x-forwarded-for",
|
|
7901
|
+
"x-forwarded-host",
|
|
7902
|
+
"x-forwarded-port",
|
|
7903
|
+
"x-forwarded-proto",
|
|
7904
|
+
"x-ps-target",
|
|
7905
|
+
"x-real-ip"
|
|
7906
|
+
]);
|
|
7907
|
+
var RESPONSE_HEADER_DENYLIST = /* @__PURE__ */ new Set([
|
|
7908
|
+
"connection",
|
|
7909
|
+
"content-encoding",
|
|
7910
|
+
"content-length",
|
|
7911
|
+
"keep-alive",
|
|
7912
|
+
"proxy-authenticate",
|
|
7913
|
+
"proxy-authorization",
|
|
7914
|
+
"set-cookie",
|
|
7915
|
+
"set-cookie2",
|
|
7916
|
+
"te",
|
|
7917
|
+
"trailer",
|
|
7918
|
+
"transfer-encoding",
|
|
7919
|
+
"upgrade"
|
|
7920
|
+
]);
|
|
7921
|
+
var BodyTooLargeError = class extends Error {
|
|
7922
|
+
};
|
|
7923
|
+
async function boundedBody(request, maxBytes) {
|
|
7924
|
+
const stated2 = Number(request.headers.get("content-length"));
|
|
7925
|
+
if (Number.isFinite(stated2) && stated2 > maxBytes) throw new BodyTooLargeError();
|
|
7926
|
+
if (!request.body) return void 0;
|
|
7927
|
+
const reader = request.body.getReader();
|
|
7928
|
+
const chunks = [];
|
|
7929
|
+
let total = 0;
|
|
7930
|
+
try {
|
|
7931
|
+
for (; ; ) {
|
|
7932
|
+
const { done, value } = await reader.read();
|
|
7933
|
+
if (done) break;
|
|
7934
|
+
total += value.byteLength;
|
|
7935
|
+
if (total > maxBytes) {
|
|
7936
|
+
void reader.cancel().catch(() => {
|
|
7937
|
+
});
|
|
7938
|
+
throw new BodyTooLargeError();
|
|
7939
|
+
}
|
|
7940
|
+
chunks.push(value);
|
|
7941
|
+
}
|
|
7942
|
+
} finally {
|
|
7943
|
+
reader.releaseLock();
|
|
7944
|
+
}
|
|
7945
|
+
const body = new Uint8Array(total);
|
|
7946
|
+
let offset = 0;
|
|
7947
|
+
for (const chunk of chunks) {
|
|
7948
|
+
body.set(chunk, offset);
|
|
7949
|
+
offset += chunk.byteLength;
|
|
7950
|
+
}
|
|
7951
|
+
return body;
|
|
7952
|
+
}
|
|
7953
|
+
function requestHeaders(input) {
|
|
7954
|
+
const output = new Headers();
|
|
7955
|
+
const connectionTokens = new Set(
|
|
7956
|
+
(input.get("connection") ?? "").split(",").map((value) => value.trim().toLowerCase()).filter(Boolean)
|
|
7957
|
+
);
|
|
7958
|
+
input.forEach((value, key) => {
|
|
7959
|
+
const lower = key.toLowerCase();
|
|
7960
|
+
if (!REQUEST_HEADER_DENYLIST.has(lower) && !connectionTokens.has(lower)) output.append(key, value);
|
|
7961
|
+
});
|
|
7962
|
+
return output;
|
|
7963
|
+
}
|
|
7964
|
+
function responseHeaders(input) {
|
|
7965
|
+
const output = new Headers();
|
|
7966
|
+
const connectionTokens = new Set(
|
|
7967
|
+
(input.get("connection") ?? "").split(",").map((value) => value.trim().toLowerCase()).filter(Boolean)
|
|
7213
7968
|
);
|
|
7969
|
+
input.forEach((value, key) => {
|
|
7970
|
+
const lower = key.toLowerCase();
|
|
7971
|
+
if (!RESPONSE_HEADER_DENYLIST.has(lower) && !connectionTokens.has(lower)) output.append(key, value);
|
|
7972
|
+
});
|
|
7973
|
+
return output;
|
|
7974
|
+
}
|
|
7975
|
+
var SOURCE_REQUEST = /* @__PURE__ */ Symbol.for("@patchstack/connect.sourceRequest");
|
|
7976
|
+
function createSupabaseGuard({
|
|
7977
|
+
protection,
|
|
7978
|
+
supabaseUrl,
|
|
7979
|
+
fetchImpl = fetch,
|
|
7980
|
+
maxBodyBytes = DEFAULT_MAX_BODY_BYTES,
|
|
7981
|
+
timeoutMs = DEFAULT_TIMEOUT_MS
|
|
7982
|
+
}) {
|
|
7983
|
+
const guard = protection.fetchGuard();
|
|
7984
|
+
const allowedOrigin = supabaseUrl ? new URL(supabaseUrl).origin : null;
|
|
7985
|
+
const bodyLimit = Number.isFinite(maxBodyBytes) && maxBodyBytes > 0 ? maxBodyBytes : DEFAULT_MAX_BODY_BYTES;
|
|
7986
|
+
const upstreamTimeout = Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : DEFAULT_TIMEOUT_MS;
|
|
7987
|
+
return async function handleGuardRequest(request, ...hostArgs) {
|
|
7988
|
+
const target = request.headers.get("x-ps-target");
|
|
7989
|
+
if (!target) return new Response("patchstack: missing x-ps-target", { status: 400 });
|
|
7990
|
+
let targetUrl;
|
|
7991
|
+
try {
|
|
7992
|
+
targetUrl = new URL(target);
|
|
7993
|
+
} catch {
|
|
7994
|
+
return new Response("patchstack: invalid target", { status: 400 });
|
|
7995
|
+
}
|
|
7996
|
+
if (!allowedOrigin || targetUrl.protocol !== "https:" || targetUrl.origin !== allowedOrigin) {
|
|
7997
|
+
return new Response("patchstack: target not allowed", { status: 403 });
|
|
7998
|
+
}
|
|
7999
|
+
const hasBody = request.method !== "GET" && request.method !== "HEAD";
|
|
8000
|
+
let body;
|
|
8001
|
+
try {
|
|
8002
|
+
body = hasBody ? await boundedBody(request, bodyLimit) : void 0;
|
|
8003
|
+
} catch (error) {
|
|
8004
|
+
if (error instanceof BodyTooLargeError) {
|
|
8005
|
+
return new Response("patchstack: request body too large", { status: 413 });
|
|
8006
|
+
}
|
|
8007
|
+
throw error;
|
|
8008
|
+
}
|
|
8009
|
+
const forwardHeaders = requestHeaders(request.headers);
|
|
8010
|
+
const evalReq = new Request(targetUrl.toString(), {
|
|
8011
|
+
method: request.method,
|
|
8012
|
+
// Inspect what the browser sent, including app-origin cookies; only the forwarded copy is
|
|
8013
|
+
// sanitized at the cross-origin boundary below.
|
|
8014
|
+
headers: request.headers,
|
|
8015
|
+
body
|
|
8016
|
+
});
|
|
8017
|
+
Object.defineProperty(evalReq, SOURCE_REQUEST, { value: request });
|
|
8018
|
+
const blocked = await guard(evalReq, ...hostArgs);
|
|
8019
|
+
if (blocked) return blocked;
|
|
8020
|
+
let upstream;
|
|
8021
|
+
try {
|
|
8022
|
+
upstream = await fetchImpl(targetUrl.toString(), {
|
|
8023
|
+
method: request.method,
|
|
8024
|
+
headers: forwardHeaders,
|
|
8025
|
+
body,
|
|
8026
|
+
redirect: "manual",
|
|
8027
|
+
signal: AbortSignal.timeout(upstreamTimeout)
|
|
8028
|
+
});
|
|
8029
|
+
} catch (error) {
|
|
8030
|
+
if (error?.name === "TimeoutError" || error?.name === "AbortError") {
|
|
8031
|
+
return new Response("patchstack: upstream timed out", { status: 504 });
|
|
8032
|
+
}
|
|
8033
|
+
throw error;
|
|
8034
|
+
}
|
|
8035
|
+
const forwarded = new Response(upstream.body, {
|
|
8036
|
+
status: upstream.status,
|
|
8037
|
+
statusText: upstream.statusText,
|
|
8038
|
+
headers: responseHeaders(upstream.headers)
|
|
8039
|
+
});
|
|
8040
|
+
return protection.screenResponse ? protection.screenResponse(forwarded, evalReq) : forwarded;
|
|
8041
|
+
};
|
|
7214
8042
|
}
|
|
7215
8043
|
|
|
7216
8044
|
// src/protect/firewall-log.js
|
|
@@ -7261,8 +8089,13 @@ function resolveApiBase(pulseOrManifestUrl) {
|
|
|
7261
8089
|
function createFirewallLogReporter(opts) {
|
|
7262
8090
|
const creds = parseApiKey(opts.apiKey);
|
|
7263
8091
|
if (!creds) {
|
|
7264
|
-
return {
|
|
7265
|
-
|
|
8092
|
+
return {
|
|
8093
|
+
record() {
|
|
8094
|
+
},
|
|
8095
|
+
flush: () => Promise.resolve(),
|
|
8096
|
+
stop: () => Promise.resolve(),
|
|
8097
|
+
health: () => ({ recorded: 0, delivered: 0, failed: 0, dropped: 0, queued: 0 })
|
|
8098
|
+
};
|
|
7266
8099
|
}
|
|
7267
8100
|
const apiBase = safeBaseUrl(opts.apiBase, DEFAULT_API_BASE, "block-log API").replace(/\/$/, "");
|
|
7268
8101
|
const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
@@ -7275,6 +8108,19 @@ function createFirewallLogReporter(opts) {
|
|
|
7275
8108
|
let drainPromise = null;
|
|
7276
8109
|
let activeController = null;
|
|
7277
8110
|
let ended = false;
|
|
8111
|
+
let recorded = 0;
|
|
8112
|
+
let delivered = 0;
|
|
8113
|
+
let failed = 0;
|
|
8114
|
+
let dropped = 0;
|
|
8115
|
+
let activeBatch = null;
|
|
8116
|
+
const settle = (sent, outcome) => {
|
|
8117
|
+
if (sent.counted) return;
|
|
8118
|
+
sent.counted = true;
|
|
8119
|
+
if (outcome === "delivered") delivered += sent.size;
|
|
8120
|
+
else if (outcome === "failed") failed += sent.size;
|
|
8121
|
+
else dropped += sent.size;
|
|
8122
|
+
if (activeBatch === sent) activeBatch = null;
|
|
8123
|
+
};
|
|
7278
8124
|
let cachedToken = null;
|
|
7279
8125
|
let tokenInflight = null;
|
|
7280
8126
|
const fetchAccessToken = async (signal) => {
|
|
@@ -7328,10 +8174,16 @@ function createFirewallLogReporter(opts) {
|
|
|
7328
8174
|
const attemptTimer = setTimeout(() => controller?.abort(), ATTEMPT_TIMEOUT_MS2);
|
|
7329
8175
|
attemptTimer?.unref?.();
|
|
7330
8176
|
let entry;
|
|
8177
|
+
const sent = { size: batch.length, counted: false };
|
|
8178
|
+
activeBatch = sent;
|
|
7331
8179
|
entry = (async () => {
|
|
7332
8180
|
try {
|
|
7333
8181
|
const token = await fetchAccessToken(controller?.signal);
|
|
7334
|
-
if (
|
|
8182
|
+
if (ended) return;
|
|
8183
|
+
if (!token) {
|
|
8184
|
+
settle(sent, "failed");
|
|
8185
|
+
return;
|
|
8186
|
+
}
|
|
7335
8187
|
const body = new URLSearchParams();
|
|
7336
8188
|
body.set("type", "firewall");
|
|
7337
8189
|
body.set("logs", JSON.stringify(batch));
|
|
@@ -7348,10 +8200,11 @@ function createFirewallLogReporter(opts) {
|
|
|
7348
8200
|
// Both phases carry the attempt controller, so slow transports cannot accumulate work.
|
|
7349
8201
|
...controller ? { signal: controller.signal } : {}
|
|
7350
8202
|
});
|
|
7351
|
-
|
|
7352
|
-
|
|
8203
|
+
const res = p && typeof p.then === "function" ? await p.catch(() => null) : p;
|
|
8204
|
+
settle(sent, res && res.ok ? "delivered" : "failed");
|
|
7353
8205
|
} catch {
|
|
7354
8206
|
} finally {
|
|
8207
|
+
settle(sent, "failed");
|
|
7355
8208
|
clearTimeout(attemptTimer);
|
|
7356
8209
|
if (activeController === controller) activeController = null;
|
|
7357
8210
|
if (inFlight === entry) inFlight = null;
|
|
@@ -7378,234 +8231,85 @@ function createFirewallLogReporter(opts) {
|
|
|
7378
8231
|
if (stopped) return;
|
|
7379
8232
|
const fid = event?.rule?.id;
|
|
7380
8233
|
if (fid === void 0 || fid === null || fid === "") return;
|
|
7381
|
-
if (queue.length >= MAX_QUEUE2)
|
|
7382
|
-
|
|
7383
|
-
fid,
|
|
7384
|
-
method: event.method ?? null,
|
|
7385
|
-
request_uri: event.path ?? null,
|
|
7386
|
-
ip: event.ip ?? null,
|
|
7387
|
-
user_agent: event.userAgent ?? null,
|
|
7388
|
-
log_date: (/* @__PURE__ */ new Date()).toISOString()
|
|
7389
|
-
});
|
|
7390
|
-
if (queue.length >= MAX_BATCH2) {
|
|
7391
|
-
flush();
|
|
8234
|
+
if (queue.length >= MAX_QUEUE2) {
|
|
8235
|
+
dropped++;
|
|
7392
8236
|
return;
|
|
7393
8237
|
}
|
|
7394
|
-
|
|
7395
|
-
|
|
7396
|
-
|
|
7397
|
-
|
|
7398
|
-
|
|
7399
|
-
|
|
7400
|
-
|
|
7401
|
-
|
|
7402
|
-
* spinning would be worse than leaving the records where they are.
|
|
7403
|
-
*/
|
|
7404
|
-
stop() {
|
|
7405
|
-
if (drainPromise) return drainPromise;
|
|
7406
|
-
stopped = true;
|
|
7407
|
-
if (timer) {
|
|
7408
|
-
clearTimeout(timer);
|
|
7409
|
-
timer = null;
|
|
7410
|
-
}
|
|
7411
|
-
const terminate = () => {
|
|
7412
|
-
ended = true;
|
|
7413
|
-
activeController?.abort();
|
|
7414
|
-
activeController = null;
|
|
7415
|
-
queue = [];
|
|
7416
|
-
inFlight = null;
|
|
7417
|
-
};
|
|
7418
|
-
let budget = null;
|
|
7419
|
-
const spent = new Promise((resolve) => {
|
|
7420
|
-
budget = setTimeout(() => {
|
|
7421
|
-
terminate();
|
|
7422
|
-
resolve();
|
|
7423
|
-
}, STOP_BUDGET_MS2);
|
|
7424
|
-
if (typeof budget.unref === "function") budget.unref();
|
|
7425
|
-
});
|
|
7426
|
-
const work = (async () => {
|
|
7427
|
-
while (!ended) {
|
|
7428
|
-
if (inFlight) {
|
|
7429
|
-
await inFlight;
|
|
7430
|
-
continue;
|
|
7431
|
-
}
|
|
7432
|
-
if (queue.length === 0) break;
|
|
7433
|
-
const before = queue.length;
|
|
7434
|
-
await flush();
|
|
7435
|
-
if (queue.length >= before) break;
|
|
7436
|
-
}
|
|
7437
|
-
})();
|
|
7438
|
-
drainPromise = Promise.race([work, spent]).then(() => {
|
|
7439
|
-
if (budget) clearTimeout(budget);
|
|
7440
|
-
});
|
|
7441
|
-
return drainPromise;
|
|
7442
|
-
}
|
|
7443
|
-
};
|
|
7444
|
-
}
|
|
7445
|
-
function telemetryEnabled() {
|
|
7446
|
-
const v = typeof process !== "undefined" ? process.env?.PATCHSTACK_TELEMETRY : void 0;
|
|
7447
|
-
if (v === void 0 || v === "") return true;
|
|
7448
|
-
return !/^(0|false|off|no)$/i.test(String(v));
|
|
7449
|
-
}
|
|
7450
|
-
|
|
7451
|
-
// src/protect/supabase-guard.js
|
|
7452
|
-
var GUARD_PATH = "/_patchstack/guard";
|
|
7453
|
-
var DEFAULT_MAX_BODY_BYTES = 1024 * 1024;
|
|
7454
|
-
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
7455
|
-
var REQUEST_HEADER_DENYLIST = /* @__PURE__ */ new Set([
|
|
7456
|
-
"connection",
|
|
7457
|
-
"cookie",
|
|
7458
|
-
"cookie2",
|
|
7459
|
-
"forwarded",
|
|
7460
|
-
"host",
|
|
7461
|
-
"keep-alive",
|
|
7462
|
-
"proxy-authenticate",
|
|
7463
|
-
"proxy-authorization",
|
|
7464
|
-
"te",
|
|
7465
|
-
"trailer",
|
|
7466
|
-
"transfer-encoding",
|
|
7467
|
-
"upgrade",
|
|
7468
|
-
"x-forwarded-for",
|
|
7469
|
-
"x-forwarded-host",
|
|
7470
|
-
"x-forwarded-port",
|
|
7471
|
-
"x-forwarded-proto",
|
|
7472
|
-
"x-ps-target",
|
|
7473
|
-
"x-real-ip"
|
|
7474
|
-
]);
|
|
7475
|
-
var RESPONSE_HEADER_DENYLIST = /* @__PURE__ */ new Set([
|
|
7476
|
-
"connection",
|
|
7477
|
-
"content-encoding",
|
|
7478
|
-
"content-length",
|
|
7479
|
-
"keep-alive",
|
|
7480
|
-
"proxy-authenticate",
|
|
7481
|
-
"proxy-authorization",
|
|
7482
|
-
"set-cookie",
|
|
7483
|
-
"set-cookie2",
|
|
7484
|
-
"te",
|
|
7485
|
-
"trailer",
|
|
7486
|
-
"transfer-encoding",
|
|
7487
|
-
"upgrade"
|
|
7488
|
-
]);
|
|
7489
|
-
var BodyTooLargeError = class extends Error {
|
|
7490
|
-
};
|
|
7491
|
-
async function boundedBody(request, maxBytes) {
|
|
7492
|
-
const stated2 = Number(request.headers.get("content-length"));
|
|
7493
|
-
if (Number.isFinite(stated2) && stated2 > maxBytes) throw new BodyTooLargeError();
|
|
7494
|
-
if (!request.body) return void 0;
|
|
7495
|
-
const reader = request.body.getReader();
|
|
7496
|
-
const chunks = [];
|
|
7497
|
-
let total = 0;
|
|
7498
|
-
try {
|
|
7499
|
-
for (; ; ) {
|
|
7500
|
-
const { done, value } = await reader.read();
|
|
7501
|
-
if (done) break;
|
|
7502
|
-
total += value.byteLength;
|
|
7503
|
-
if (total > maxBytes) {
|
|
7504
|
-
void reader.cancel().catch(() => {
|
|
7505
|
-
});
|
|
7506
|
-
throw new BodyTooLargeError();
|
|
7507
|
-
}
|
|
7508
|
-
chunks.push(value);
|
|
7509
|
-
}
|
|
7510
|
-
} finally {
|
|
7511
|
-
reader.releaseLock();
|
|
7512
|
-
}
|
|
7513
|
-
const body = new Uint8Array(total);
|
|
7514
|
-
let offset = 0;
|
|
7515
|
-
for (const chunk of chunks) {
|
|
7516
|
-
body.set(chunk, offset);
|
|
7517
|
-
offset += chunk.byteLength;
|
|
7518
|
-
}
|
|
7519
|
-
return body;
|
|
7520
|
-
}
|
|
7521
|
-
function requestHeaders(input) {
|
|
7522
|
-
const output = new Headers();
|
|
7523
|
-
const connectionTokens = new Set(
|
|
7524
|
-
(input.get("connection") ?? "").split(",").map((value) => value.trim().toLowerCase()).filter(Boolean)
|
|
7525
|
-
);
|
|
7526
|
-
input.forEach((value, key) => {
|
|
7527
|
-
const lower = key.toLowerCase();
|
|
7528
|
-
if (!REQUEST_HEADER_DENYLIST.has(lower) && !connectionTokens.has(lower)) output.append(key, value);
|
|
7529
|
-
});
|
|
7530
|
-
return output;
|
|
7531
|
-
}
|
|
7532
|
-
function responseHeaders(input) {
|
|
7533
|
-
const output = new Headers();
|
|
7534
|
-
const connectionTokens = new Set(
|
|
7535
|
-
(input.get("connection") ?? "").split(",").map((value) => value.trim().toLowerCase()).filter(Boolean)
|
|
7536
|
-
);
|
|
7537
|
-
input.forEach((value, key) => {
|
|
7538
|
-
const lower = key.toLowerCase();
|
|
7539
|
-
if (!RESPONSE_HEADER_DENYLIST.has(lower) && !connectionTokens.has(lower)) output.append(key, value);
|
|
7540
|
-
});
|
|
7541
|
-
return output;
|
|
7542
|
-
}
|
|
7543
|
-
function createSupabaseGuard({
|
|
7544
|
-
protection,
|
|
7545
|
-
supabaseUrl,
|
|
7546
|
-
fetchImpl = fetch,
|
|
7547
|
-
maxBodyBytes = DEFAULT_MAX_BODY_BYTES,
|
|
7548
|
-
timeoutMs = DEFAULT_TIMEOUT_MS
|
|
7549
|
-
}) {
|
|
7550
|
-
const guard = protection.fetchGuard();
|
|
7551
|
-
const allowedOrigin = supabaseUrl ? new URL(supabaseUrl).origin : null;
|
|
7552
|
-
const bodyLimit = Number.isFinite(maxBodyBytes) && maxBodyBytes > 0 ? maxBodyBytes : DEFAULT_MAX_BODY_BYTES;
|
|
7553
|
-
const upstreamTimeout = Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : DEFAULT_TIMEOUT_MS;
|
|
7554
|
-
return async function handleGuardRequest(request) {
|
|
7555
|
-
const target = request.headers.get("x-ps-target");
|
|
7556
|
-
if (!target) return new Response("patchstack: missing x-ps-target", { status: 400 });
|
|
7557
|
-
let targetUrl;
|
|
7558
|
-
try {
|
|
7559
|
-
targetUrl = new URL(target);
|
|
7560
|
-
} catch {
|
|
7561
|
-
return new Response("patchstack: invalid target", { status: 400 });
|
|
7562
|
-
}
|
|
7563
|
-
if (!allowedOrigin || targetUrl.protocol !== "https:" || targetUrl.origin !== allowedOrigin) {
|
|
7564
|
-
return new Response("patchstack: target not allowed", { status: 403 });
|
|
7565
|
-
}
|
|
7566
|
-
const hasBody = request.method !== "GET" && request.method !== "HEAD";
|
|
7567
|
-
let body;
|
|
7568
|
-
try {
|
|
7569
|
-
body = hasBody ? await boundedBody(request, bodyLimit) : void 0;
|
|
7570
|
-
} catch (error) {
|
|
7571
|
-
if (error instanceof BodyTooLargeError) {
|
|
7572
|
-
return new Response("patchstack: request body too large", { status: 413 });
|
|
7573
|
-
}
|
|
7574
|
-
throw error;
|
|
7575
|
-
}
|
|
7576
|
-
const forwardHeaders = requestHeaders(request.headers);
|
|
7577
|
-
const evalReq = new Request(targetUrl.toString(), {
|
|
7578
|
-
method: request.method,
|
|
7579
|
-
// Inspect what the browser sent, including app-origin cookies; only the forwarded copy is
|
|
7580
|
-
// sanitized at the cross-origin boundary below.
|
|
7581
|
-
headers: request.headers,
|
|
7582
|
-
body
|
|
7583
|
-
});
|
|
7584
|
-
const blocked = await guard(evalReq);
|
|
7585
|
-
if (blocked) return blocked;
|
|
7586
|
-
let upstream;
|
|
7587
|
-
try {
|
|
7588
|
-
upstream = await fetchImpl(targetUrl.toString(), {
|
|
7589
|
-
method: request.method,
|
|
7590
|
-
headers: forwardHeaders,
|
|
7591
|
-
body,
|
|
7592
|
-
redirect: "manual",
|
|
7593
|
-
signal: AbortSignal.timeout(upstreamTimeout)
|
|
8238
|
+
recorded++;
|
|
8239
|
+
queue.push({
|
|
8240
|
+
fid,
|
|
8241
|
+
method: event.method ?? null,
|
|
8242
|
+
request_uri: event.path ?? null,
|
|
8243
|
+
ip: event.ip ?? null,
|
|
8244
|
+
user_agent: event.userAgent ?? null,
|
|
8245
|
+
log_date: (/* @__PURE__ */ new Date()).toISOString()
|
|
7594
8246
|
});
|
|
7595
|
-
|
|
7596
|
-
|
|
7597
|
-
return
|
|
8247
|
+
if (queue.length >= MAX_BATCH2) {
|
|
8248
|
+
flush();
|
|
8249
|
+
return;
|
|
7598
8250
|
}
|
|
7599
|
-
|
|
8251
|
+
if (!timer) timer = setTimeout(flush, flushMs);
|
|
8252
|
+
},
|
|
8253
|
+
flush,
|
|
8254
|
+
/** Where the records went so far; see the counters above. `queued` is what is waiting now. */
|
|
8255
|
+
health() {
|
|
8256
|
+
return { recorded, delivered, failed, dropped, queued: queue.length };
|
|
8257
|
+
},
|
|
8258
|
+
/**
|
|
8259
|
+
* Stop, and hand back a wait for what was outstanding.
|
|
8260
|
+
*
|
|
8261
|
+
* One flush sends at most a batch, so the queue is drained a batch at a time. The loop stops as soon
|
|
8262
|
+
* as a pass cannot shrink the queue — with no usable transport there is nothing to wait for, and
|
|
8263
|
+
* spinning would be worse than leaving the records where they are.
|
|
8264
|
+
*/
|
|
8265
|
+
stop() {
|
|
8266
|
+
if (drainPromise) return drainPromise;
|
|
8267
|
+
stopped = true;
|
|
8268
|
+
if (timer) {
|
|
8269
|
+
clearTimeout(timer);
|
|
8270
|
+
timer = null;
|
|
8271
|
+
}
|
|
8272
|
+
const terminate = () => {
|
|
8273
|
+
ended = true;
|
|
8274
|
+
activeController?.abort();
|
|
8275
|
+
activeController = null;
|
|
8276
|
+
if (activeBatch) settle(activeBatch, "dropped");
|
|
8277
|
+
dropped += queue.length;
|
|
8278
|
+
queue = [];
|
|
8279
|
+
inFlight = null;
|
|
8280
|
+
};
|
|
8281
|
+
let budget = null;
|
|
8282
|
+
const spent = new Promise((resolve) => {
|
|
8283
|
+
budget = setTimeout(() => {
|
|
8284
|
+
terminate();
|
|
8285
|
+
resolve();
|
|
8286
|
+
}, STOP_BUDGET_MS2);
|
|
8287
|
+
if (typeof budget.unref === "function") budget.unref();
|
|
8288
|
+
});
|
|
8289
|
+
const work = (async () => {
|
|
8290
|
+
while (!ended) {
|
|
8291
|
+
if (inFlight) {
|
|
8292
|
+
await inFlight;
|
|
8293
|
+
continue;
|
|
8294
|
+
}
|
|
8295
|
+
if (queue.length === 0) break;
|
|
8296
|
+
const before = queue.length;
|
|
8297
|
+
await flush();
|
|
8298
|
+
if (queue.length >= before) break;
|
|
8299
|
+
}
|
|
8300
|
+
})();
|
|
8301
|
+
drainPromise = Promise.race([work, spent]).then(() => {
|
|
8302
|
+
if (budget) clearTimeout(budget);
|
|
8303
|
+
});
|
|
8304
|
+
return drainPromise;
|
|
7600
8305
|
}
|
|
7601
|
-
const forwarded = new Response(upstream.body, {
|
|
7602
|
-
status: upstream.status,
|
|
7603
|
-
statusText: upstream.statusText,
|
|
7604
|
-
headers: responseHeaders(upstream.headers)
|
|
7605
|
-
});
|
|
7606
|
-
return protection.screenResponse ? protection.screenResponse(forwarded) : forwarded;
|
|
7607
8306
|
};
|
|
7608
8307
|
}
|
|
8308
|
+
function telemetryEnabled() {
|
|
8309
|
+
const v = typeof process !== "undefined" ? process.env?.PATCHSTACK_TELEMETRY : void 0;
|
|
8310
|
+
if (v === void 0 || v === "") return true;
|
|
8311
|
+
return !/^(0|false|off|no)$/i.test(String(v));
|
|
8312
|
+
}
|
|
7609
8313
|
|
|
7610
8314
|
// src/protect/verify-sentinel.js
|
|
7611
8315
|
var VERIFY_HEADER = "x-patchstack-verify";
|
|
@@ -7697,6 +8401,7 @@ async function createProtection(options = {}) {
|
|
|
7697
8401
|
"onDetect"
|
|
7698
8402
|
);
|
|
7699
8403
|
};
|
|
8404
|
+
const nodeHttp = await import("http").then((m) => m.default ?? m, () => null);
|
|
7700
8405
|
const store = makeStore(options);
|
|
7701
8406
|
const bootTimeoutMs = Number(options.bootTimeoutMs) > 0 ? Number(options.bootTimeoutMs) : 5e3;
|
|
7702
8407
|
const pulseAuth = await resolvePulseAuth(options);
|
|
@@ -7722,6 +8427,21 @@ async function createProtection(options = {}) {
|
|
|
7722
8427
|
pulseAuth,
|
|
7723
8428
|
detectionState: preFetchState
|
|
7724
8429
|
});
|
|
8430
|
+
let ruleSource = { ...bundle.source ?? { ok: true, origin: "empty" } };
|
|
8431
|
+
const warnedSources = /* @__PURE__ */ new Set();
|
|
8432
|
+
const hasLiveSource = Boolean(options.siteUuid || options.token);
|
|
8433
|
+
const noteRuleSource = (source) => {
|
|
8434
|
+
ruleSource = { ...source ?? { ok: true, origin: ruleSource.origin } };
|
|
8435
|
+
if (!hasLiveSource || typeof onError === "function" || ruleSource.ok !== false) return;
|
|
8436
|
+
const cause = `${ruleSource.origin}:${ruleSource.reason ?? ""}`;
|
|
8437
|
+
if (warnedSources.has(cause)) return;
|
|
8438
|
+
warnedSources.add(cause);
|
|
8439
|
+
const running = ruleSource.origin === "cache" ? "the last rules it received" : ruleSource.origin === "bundled" ? "its bundled fallback rules" : "no rules at all";
|
|
8440
|
+
console.warn(
|
|
8441
|
+
`[patchstack] the guard's rules are not current (${ruleSource.reason ?? "the rules source did not answer"}); it is running on ${running}. Pass { onError } to handle this yourself.`
|
|
8442
|
+
);
|
|
8443
|
+
};
|
|
8444
|
+
noteRuleSource(bundle.source);
|
|
7725
8445
|
let detectionReporting = "not-enrolled";
|
|
7726
8446
|
let currentOrigin;
|
|
7727
8447
|
const applyReportingState = async (origin) => {
|
|
@@ -7777,20 +8497,47 @@ async function createProtection(options = {}) {
|
|
|
7777
8497
|
whitelist_keys: delivered.whitelist_keys,
|
|
7778
8498
|
onError
|
|
7779
8499
|
});
|
|
7780
|
-
responseRuleSet = responseRules.map((rule) =>
|
|
7781
|
-
|
|
7782
|
-
|
|
7783
|
-
|
|
7784
|
-
|
|
7785
|
-
|
|
7786
|
-
|
|
7787
|
-
|
|
7788
|
-
|
|
7789
|
-
|
|
7790
|
-
|
|
7791
|
-
|
|
7792
|
-
|
|
7793
|
-
|
|
8500
|
+
responseRuleSet = responseRules.map((rule) => {
|
|
8501
|
+
const unscoped = [];
|
|
8502
|
+
const rewrites = rule.action === "redact" || rule.action === "encode";
|
|
8503
|
+
let derived;
|
|
8504
|
+
try {
|
|
8505
|
+
const redactors = rewrites ? extractRedactors(rule, (parameter) => unscoped.push(parameter)) : null;
|
|
8506
|
+
const mutatedSpan = rewrites && hasSpanMutations(rule);
|
|
8507
|
+
derived = {
|
|
8508
|
+
redactors,
|
|
8509
|
+
mutatedSpan,
|
|
8510
|
+
// A redaction that reads response headers only, and masks spans in them, and an explicit block
|
|
8511
|
+
// that reads response headers only, are decided and carried out without the body — so they
|
|
8512
|
+
// still apply to a response whose body was not screened.
|
|
8513
|
+
redactsHeaders: rule.action === "redact" && !mutatedSpan && (redactors ?? []).some((r) => !r.jsonPath) && readsOnlyResponseHeaders(rule),
|
|
8514
|
+
blocksOnHeaders: rule.action === "block" && readsOnlyResponseHeaders(rule)
|
|
8515
|
+
};
|
|
8516
|
+
} catch (err) {
|
|
8517
|
+
notify(onError, err, "onError");
|
|
8518
|
+
derived = { redactors: null, mutatedSpan: false, redactsHeaders: false, blocksOnHeaders: false };
|
|
8519
|
+
}
|
|
8520
|
+
if (unscoped.length) {
|
|
8521
|
+
notify(
|
|
8522
|
+
onError,
|
|
8523
|
+
new Error(
|
|
8524
|
+
`Patchstack: response rule ${JSON.stringify(rule.id ?? null)} has a condition whose parameter names no place in the response to mask (${unscoped.map((p) => JSON.stringify(p ?? null)).join(", ")}); its matches are not masked`
|
|
8525
|
+
),
|
|
8526
|
+
"onError"
|
|
8527
|
+
);
|
|
8528
|
+
}
|
|
8529
|
+
return {
|
|
8530
|
+
rule,
|
|
8531
|
+
engine: new RuleEngine({ firewall: [rule], onError }),
|
|
8532
|
+
...derived,
|
|
8533
|
+
// Optional cheap pre-filter: literal anchor(s) that MUST appear for the (expensive) regex to
|
|
8534
|
+
// have any chance of matching. Lets screenText skip the full scan on responses with no candidate
|
|
8535
|
+
// — the common case — cutting CPU/latency and shrinking the regex/ReDoS surface.
|
|
8536
|
+
// Case-insensitive, and checked against the header values as well as the body, since a rule may
|
|
8537
|
+
// read either.
|
|
8538
|
+
prefilter: Array.isArray(rule.prefilter) && rule.prefilter.length ? rule.prefilter.map((s) => String(s).toLowerCase()) : null
|
|
8539
|
+
};
|
|
8540
|
+
});
|
|
7794
8541
|
egressRuleSet = egressRules.map((rule) => ({
|
|
7795
8542
|
rule,
|
|
7796
8543
|
engine: new RuleEngine({ firewall: [rule], onError })
|
|
@@ -7804,6 +8551,9 @@ async function createProtection(options = {}) {
|
|
|
7804
8551
|
skipCounts[key] = (skipCounts[key] ?? 0) + 1;
|
|
7805
8552
|
notify(onSkip, { phase, reason, detail, count: skipCounts[key] }, "onSkip");
|
|
7806
8553
|
};
|
|
8554
|
+
const recordEvaluationSkips = (result) => {
|
|
8555
|
+
for (const reason of result?.skips ?? []) recordSkip("request", reason);
|
|
8556
|
+
};
|
|
7807
8557
|
const maskFn = typeof options.maskWith === "function" ? options.maskWith : () => typeof options.maskWith === "string" ? options.maskWith : "[REDACTED]";
|
|
7808
8558
|
const ruleMode = (rule) => rule?.enforcement === "dry-run" ? "dry-run" : mode;
|
|
7809
8559
|
const planCache = createPlanCache();
|
|
@@ -7846,15 +8596,19 @@ async function createProtection(options = {}) {
|
|
|
7846
8596
|
});
|
|
7847
8597
|
return effectiveMode === "block" ? block() : allow2();
|
|
7848
8598
|
};
|
|
8599
|
+
const decidedByHeaders = (_rule, entry) => Boolean(entry?.redactsHeaders || entry?.blocksOnHeaders);
|
|
8600
|
+
const decidedWithoutBody = (rule, entry) => hardensWithoutBody(rule) || decidedByHeaders(rule, entry);
|
|
7849
8601
|
const screenText = (text, meta, reqCtx, only) => {
|
|
7850
8602
|
let blockRule = null;
|
|
7851
8603
|
const redactions = [];
|
|
7852
8604
|
const headerMutations = [];
|
|
7853
8605
|
let lowerText = null;
|
|
7854
|
-
|
|
7855
|
-
|
|
8606
|
+
const responseSkips = /* @__PURE__ */ new Set();
|
|
8607
|
+
for (const entry of responseRuleSet) {
|
|
8608
|
+
const { rule, engine: re, redactors, prefilter, mutatedSpan } = entry;
|
|
8609
|
+
if (only && !only(rule, entry)) continue;
|
|
7856
8610
|
if (prefilter) {
|
|
7857
|
-
if (lowerText === null) lowerText = text.
|
|
8611
|
+
if (lowerText === null) lowerText = prefilterText(text, meta.headers);
|
|
7858
8612
|
if (!prefilter.some((p) => lowerText.includes(p))) continue;
|
|
7859
8613
|
}
|
|
7860
8614
|
let result;
|
|
@@ -7864,6 +8618,11 @@ async function createProtection(options = {}) {
|
|
|
7864
8618
|
notify(onError, err, "onError");
|
|
7865
8619
|
continue;
|
|
7866
8620
|
}
|
|
8621
|
+
for (const reason of result.skips ?? []) {
|
|
8622
|
+
if (responseSkips.has(reason)) continue;
|
|
8623
|
+
responseSkips.add(reason);
|
|
8624
|
+
recordSkip("response", reason);
|
|
8625
|
+
}
|
|
7867
8626
|
if (!result.blocked) continue;
|
|
7868
8627
|
const responseMode = ruleMode(rule);
|
|
7869
8628
|
onDetect({
|
|
@@ -7893,6 +8652,8 @@ async function createProtection(options = {}) {
|
|
|
7893
8652
|
if (blockRule) return { verdict: "block" };
|
|
7894
8653
|
let body = text;
|
|
7895
8654
|
const headers = { ...meta.headers || {} };
|
|
8655
|
+
let responseIsJson;
|
|
8656
|
+
let structure;
|
|
7896
8657
|
for (const { rule, redactors } of redactions) {
|
|
7897
8658
|
const mask = maskFn(rule.category);
|
|
7898
8659
|
const transform = rule.action === "encode" ? htmlEscape : null;
|
|
@@ -7900,17 +8661,29 @@ async function createProtection(options = {}) {
|
|
|
7900
8661
|
const spanRedactors = redactors.filter((r) => !r.jsonPath);
|
|
7901
8662
|
if (pathRedactors.length) body = applyPathRedactors(body, pathRedactors, mask, screenCap, transform);
|
|
7902
8663
|
if (!spanRedactors.length) continue;
|
|
7903
|
-
|
|
8664
|
+
const beforeSpan = body;
|
|
8665
|
+
const bodySpans = spanRedactors.filter((r) => r.scope.body === true);
|
|
8666
|
+
if (bodySpans.length) body = applyRedactors(body, bodySpans, mask, transform);
|
|
8667
|
+
if (body !== beforeSpan) {
|
|
8668
|
+
responseIsJson ??= isJson(text);
|
|
8669
|
+
if (responseIsJson) {
|
|
8670
|
+
if (structure?.text !== beforeSpan) structure = { text: beforeSpan, tokens: lexJson(beforeSpan) };
|
|
8671
|
+
const tokens = lexJson(body);
|
|
8672
|
+
if (!sameJsonStructure(structure.text, structure.tokens, body, tokens)) return { verdict: "block" };
|
|
8673
|
+
structure = { text: body, tokens };
|
|
8674
|
+
}
|
|
8675
|
+
}
|
|
7904
8676
|
if (transform) continue;
|
|
7905
8677
|
for (const name of Object.keys(headers)) {
|
|
7906
8678
|
const value = headers[name];
|
|
8679
|
+
const headerSpans = spanRedactors.filter((r) => masksHeader(r, name));
|
|
7907
8680
|
if (typeof value === "string") {
|
|
7908
|
-
setOwn(headers, name, applyRedactors(value,
|
|
8681
|
+
setOwn(headers, name, applyRedactors(value, headerSpans, mask));
|
|
7909
8682
|
} else if (Array.isArray(value)) {
|
|
7910
8683
|
setOwn(
|
|
7911
8684
|
headers,
|
|
7912
8685
|
name,
|
|
7913
|
-
value.map((item) => typeof item === "string" ? applyRedactors(item,
|
|
8686
|
+
value.map((item) => typeof item === "string" ? applyRedactors(item, headerSpans, mask) : item)
|
|
7914
8687
|
);
|
|
7915
8688
|
}
|
|
7916
8689
|
}
|
|
@@ -7957,18 +8730,53 @@ async function createProtection(options = {}) {
|
|
|
7957
8730
|
}
|
|
7958
8731
|
};
|
|
7959
8732
|
};
|
|
7960
|
-
const
|
|
8733
|
+
const peerOf = (request, hostArgs) => {
|
|
8734
|
+
if (typeof options.peerAddress !== "function") return void 0;
|
|
8735
|
+
try {
|
|
8736
|
+
return options.peerAddress(request?.[SOURCE_REQUEST] ?? request, ...hostArgs);
|
|
8737
|
+
} catch (err) {
|
|
8738
|
+
notify(onError, err, "onError");
|
|
8739
|
+
return void 0;
|
|
8740
|
+
}
|
|
8741
|
+
};
|
|
8742
|
+
let warnedNoPeer = false;
|
|
8743
|
+
const warnNoPeer = () => {
|
|
8744
|
+
if (warnedNoPeer) return;
|
|
8745
|
+
warnedNoPeer = true;
|
|
8746
|
+
const message = "[patchstack] trustedProxy is set, but this Fetch runtime supplied no peer address, so forwarded headers are not used and client addresses read as unavailable. Pass { peerAddress } to supply one.";
|
|
8747
|
+
if (typeof onError === "function") notify(onError, new Error(message), "onError");
|
|
8748
|
+
else console.warn(message);
|
|
8749
|
+
};
|
|
8750
|
+
const clientByRequest = /* @__PURE__ */ new WeakMap();
|
|
8751
|
+
const clientForResponse = (request, hostArgs) => {
|
|
8752
|
+
const known = clientByRequest.get(request);
|
|
8753
|
+
if (known) return known;
|
|
8754
|
+
const client = resolveClientIp({
|
|
8755
|
+
peer: peerOf(request, hostArgs),
|
|
8756
|
+
headers: headerObject(request.headers),
|
|
8757
|
+
trustedProxy: options.trustedProxy
|
|
8758
|
+
});
|
|
8759
|
+
if (options.trustedProxy !== void 0 && client.source === "unavailable") warnNoPeer();
|
|
8760
|
+
return client;
|
|
8761
|
+
};
|
|
8762
|
+
const screenFetchRequest = async (request, hostArgs = []) => {
|
|
7961
8763
|
let result;
|
|
7962
8764
|
let shaped;
|
|
7963
8765
|
try {
|
|
7964
8766
|
shaped = await fromFetchRequest(request, {
|
|
8767
|
+
peer: peerOf(request, hostArgs),
|
|
7965
8768
|
trustedProxy: options.trustedProxy,
|
|
7966
8769
|
maxBodyBytes: options.maxBodyBytes
|
|
7967
8770
|
});
|
|
8771
|
+
if (shaped?._clientIp) {
|
|
8772
|
+
clientByRequest.set(request, shaped._clientIp);
|
|
8773
|
+
if (options.trustedProxy !== void 0 && shaped._clientIp.source === "unavailable") warnNoPeer();
|
|
8774
|
+
}
|
|
7968
8775
|
if (shaped?._bodyInspectionSkip) {
|
|
7969
8776
|
recordSkip("request", shaped._bodyInspectionSkip, { limit: options.maxBodyBytes ?? 1024 * 1024 });
|
|
7970
8777
|
}
|
|
7971
8778
|
result = engine.evaluate(shaped);
|
|
8779
|
+
recordEvaluationSkips(result);
|
|
7972
8780
|
} catch (err) {
|
|
7973
8781
|
notify(onError, err, "onError");
|
|
7974
8782
|
return { blocked: null, client: void 0 };
|
|
@@ -8017,14 +8825,20 @@ async function createProtection(options = {}) {
|
|
|
8017
8825
|
const screenResp = async (response, reqCtx) => {
|
|
8018
8826
|
const read = await readTextResponse(response, screenCap);
|
|
8019
8827
|
if (read.skip) {
|
|
8020
|
-
if (read.skip !== "not-a-response")
|
|
8828
|
+
if (read.skip !== "not-a-response") {
|
|
8829
|
+
recordSkip("response", read.skip, {
|
|
8830
|
+
status: response?.status,
|
|
8831
|
+
...read.encoding ? { encoding: read.encoding } : {},
|
|
8832
|
+
...read.charset ? { charset: read.charset } : {}
|
|
8833
|
+
});
|
|
8834
|
+
}
|
|
8021
8835
|
if (read.skip === "not-a-response") return response;
|
|
8022
8836
|
return hardenHeadersOnly(response, reqCtx);
|
|
8023
8837
|
}
|
|
8024
|
-
const text = read.text;
|
|
8838
|
+
const text = readJsonEscapes(read.text);
|
|
8025
8839
|
const r = screenText(text, { status: response.status, headers: headerObject(response.headers) }, reqCtx);
|
|
8026
|
-
if (r.verdict === "block") return leakResponse();
|
|
8027
|
-
if (r.verdict === "redact") return rebuildResponse(response, r.body, r.headers);
|
|
8840
|
+
if (r.verdict === "block" || r.verdict === "redact" && outgrowsCharset(read.ascii, r.body)) return leakResponse();
|
|
8841
|
+
if (r.verdict === "redact") return rebuildResponse(response, withBom(read.bom, r.body), r.headers);
|
|
8028
8842
|
return response;
|
|
8029
8843
|
};
|
|
8030
8844
|
const headerValueChanged = (was, value) => {
|
|
@@ -8086,7 +8900,8 @@ async function createProtection(options = {}) {
|
|
|
8086
8900
|
const hardenHeadersOnly = (response, reqCtx) => {
|
|
8087
8901
|
try {
|
|
8088
8902
|
const meta = { status: response.status, headers: headerObject(response.headers) };
|
|
8089
|
-
const r = screenText("", meta, reqCtx,
|
|
8903
|
+
const r = screenText("", meta, reqCtx, decidedWithoutBody);
|
|
8904
|
+
if (r.verdict === "block") return leakResponse();
|
|
8090
8905
|
if (r.verdict !== "redact" || !r.headers) return response;
|
|
8091
8906
|
if (!headersChanged(meta.headers, r.headers)) return response;
|
|
8092
8907
|
return rebuildResponse(response, response.body, r.headers);
|
|
@@ -8098,9 +8913,16 @@ async function createProtection(options = {}) {
|
|
|
8098
8913
|
const wrapNodeResponse = (res, reqCtx) => {
|
|
8099
8914
|
const origWrite = res.write.bind(res);
|
|
8100
8915
|
const origEnd = res.end.bind(res);
|
|
8916
|
+
const setHeaderNow = typeof res.setHeader === "function" ? res.setHeader.bind(res) : null;
|
|
8917
|
+
const removeHeaderNow = typeof res.removeHeader === "function" ? res.removeHeader.bind(res) : null;
|
|
8918
|
+
const sentAccessor = accessorOf(res, "headersSent");
|
|
8919
|
+
let sentValue = sentAccessor ? void 0 : res.headersSent;
|
|
8920
|
+
const reallySent = () => Boolean(sentAccessor ? sentAccessor.call(res) : sentValue);
|
|
8101
8921
|
const chunks = [];
|
|
8102
8922
|
let size = 0;
|
|
8103
8923
|
let overflow = false;
|
|
8924
|
+
let withheld = false;
|
|
8925
|
+
let headScreened = false;
|
|
8104
8926
|
const MAX = screenCap;
|
|
8105
8927
|
let hardened = false;
|
|
8106
8928
|
let answeredWithoutBody = false;
|
|
@@ -8108,7 +8930,7 @@ async function createProtection(options = {}) {
|
|
|
8108
8930
|
if (hardened) return null;
|
|
8109
8931
|
hardened = true;
|
|
8110
8932
|
try {
|
|
8111
|
-
if (
|
|
8933
|
+
if (reallySent() || !setHeaderNow) return null;
|
|
8112
8934
|
const set2 = typeof res.getHeaders === "function" ? res.getHeaders() : {};
|
|
8113
8935
|
const headers = effective && effective.headers ? { ...set2, ...effective.headers } : set2;
|
|
8114
8936
|
const status = effective && effective.status !== void 0 ? effective.status : res.statusCode;
|
|
@@ -8120,8 +8942,8 @@ async function createProtection(options = {}) {
|
|
|
8120
8942
|
if (!headerValueChanged(headers[name], value)) continue;
|
|
8121
8943
|
changed.set(name.toLowerCase(), value);
|
|
8122
8944
|
try {
|
|
8123
|
-
if (value === null || value === void 0)
|
|
8124
|
-
else
|
|
8945
|
+
if (value === null || value === void 0) removeHeaderNow?.(name);
|
|
8946
|
+
else setHeaderNow(name, value);
|
|
8125
8947
|
} catch {
|
|
8126
8948
|
}
|
|
8127
8949
|
}
|
|
@@ -8132,26 +8954,187 @@ async function createProtection(options = {}) {
|
|
|
8132
8954
|
}
|
|
8133
8955
|
};
|
|
8134
8956
|
const stillToAnswer = (rule) => !hardensWithoutBody(rule);
|
|
8135
|
-
|
|
8136
|
-
|
|
8957
|
+
const notYetAnswered = (rule, entry) => (!answeredWithoutBody || stillToAnswer(rule)) && !(headScreened && decidedByHeaders(rule, entry));
|
|
8958
|
+
let pendingHead = null;
|
|
8959
|
+
let sending = false;
|
|
8960
|
+
const origWriteHead = typeof res.writeHead === "function" ? res.writeHead.bind(res) : null;
|
|
8961
|
+
if (origWriteHead) {
|
|
8137
8962
|
res.writeHead = function(...args) {
|
|
8138
|
-
|
|
8139
|
-
|
|
8140
|
-
const
|
|
8141
|
-
|
|
8142
|
-
|
|
8143
|
-
|
|
8144
|
-
|
|
8145
|
-
|
|
8146
|
-
|
|
8147
|
-
|
|
8963
|
+
if (sending || reallySent()) return origWriteHead(...args);
|
|
8964
|
+
if (pendingHead) throw headersSentError("write");
|
|
8965
|
+
const judged = judgeWriteHead(args);
|
|
8966
|
+
if (judged) {
|
|
8967
|
+
res.statusCode = judged.statusCode;
|
|
8968
|
+
res.statusMessage = judged.statusMessage;
|
|
8969
|
+
mirrorHeaders(judged.headers);
|
|
8970
|
+
pendingHead = args;
|
|
8971
|
+
} else {
|
|
8972
|
+
pendingHead = args;
|
|
8973
|
+
if (typeof args[0] === "number") res.statusCode = args[0];
|
|
8974
|
+
if (typeof args[1] === "string") res.statusMessage = args[1];
|
|
8975
|
+
}
|
|
8976
|
+
return res;
|
|
8977
|
+
};
|
|
8978
|
+
}
|
|
8979
|
+
function judgeWriteHead(args) {
|
|
8980
|
+
const ServerResponse = nodeHttp?.ServerResponse;
|
|
8981
|
+
if (typeof ServerResponse !== "function") return null;
|
|
8982
|
+
const detached = new ServerResponse({ method: res.req?.method ?? "GET", httpVersionMajor: 1, httpVersionMinor: 1, headers: {} });
|
|
8983
|
+
if (res.statusMessage !== void 0) detached.statusMessage = res.statusMessage;
|
|
8984
|
+
for (const [name, value] of headerEntries(res)) detached.setHeader(name, value);
|
|
8985
|
+
detached.writeHead(...args);
|
|
8986
|
+
return { statusCode: detached.statusCode, statusMessage: detached.statusMessage, headers: headerEntries(detached) };
|
|
8987
|
+
}
|
|
8988
|
+
function headerEntries(target) {
|
|
8989
|
+
const names = typeof target.getRawHeaderNames === "function" ? target.getRawHeaderNames() : typeof target.getHeaderNames === "function" ? target.getHeaderNames() : [];
|
|
8990
|
+
return names.map((name) => [name, target.getHeader(name)]);
|
|
8991
|
+
}
|
|
8992
|
+
function mirrorHeaders(entries) {
|
|
8993
|
+
if (!setHeaderNow) return;
|
|
8994
|
+
for (const [name, value] of entries) {
|
|
8995
|
+
const current = res.getHeader?.(name);
|
|
8996
|
+
if (current === void 0 || headerValueChanged(current, value)) setHeaderNow(name, value);
|
|
8997
|
+
}
|
|
8998
|
+
}
|
|
8999
|
+
Object.defineProperty(res, "headersSent", {
|
|
9000
|
+
configurable: true,
|
|
9001
|
+
enumerable: true,
|
|
9002
|
+
get: () => pendingHead !== null || reallySent(),
|
|
9003
|
+
// A response-like object that tracks the flag itself keeps doing so.
|
|
9004
|
+
set: (value) => {
|
|
9005
|
+
if (!sentAccessor) sentValue = value;
|
|
9006
|
+
}
|
|
9007
|
+
});
|
|
9008
|
+
for (const [method, verb] of [["setHeader", "set"], ["removeHeader", "remove"], ["appendHeader", "append"]]) {
|
|
9009
|
+
const original = typeof res[method] === "function" ? res[method].bind(res) : null;
|
|
9010
|
+
if (!original) continue;
|
|
9011
|
+
res[method] = function(...args) {
|
|
9012
|
+
if (pendingHead) throw headersSentError(verb);
|
|
9013
|
+
return original(...args);
|
|
9014
|
+
};
|
|
9015
|
+
}
|
|
9016
|
+
if (typeof res.flushHeaders === "function") {
|
|
9017
|
+
const origFlushHeaders = res.flushHeaders.bind(res);
|
|
9018
|
+
res.flushHeaders = function() {
|
|
9019
|
+
const unread = screenUnreadHead();
|
|
9020
|
+
if (unread?.withhold) {
|
|
9021
|
+
sendWithheld();
|
|
9022
|
+
return void 0;
|
|
9023
|
+
}
|
|
9024
|
+
sendHead(unread);
|
|
9025
|
+
return origFlushHeaders();
|
|
8148
9026
|
};
|
|
8149
9027
|
}
|
|
9028
|
+
let hardenDone = false;
|
|
9029
|
+
let hardenedValues = null;
|
|
9030
|
+
const hardenOnce = () => {
|
|
9031
|
+
if (hardenDone) return hardenedValues;
|
|
9032
|
+
hardenDone = true;
|
|
9033
|
+
if (!pendingHead) {
|
|
9034
|
+
hardenedValues = hardenBeforeFlush();
|
|
9035
|
+
return hardenedValues;
|
|
9036
|
+
}
|
|
9037
|
+
const at = writeHeadHeaderIndex(pendingHead);
|
|
9038
|
+
hardenedValues = hardenBeforeFlush({
|
|
9039
|
+
status: typeof pendingHead[0] === "number" ? pendingHead[0] : void 0,
|
|
9040
|
+
headers: at === -1 ? null : writeHeadHeaderObject(pendingHead[at])
|
|
9041
|
+
});
|
|
9042
|
+
return hardenedValues;
|
|
9043
|
+
};
|
|
9044
|
+
const effectiveHead = () => {
|
|
9045
|
+
const set2 = typeof res.getHeaders === "function" ? { ...res.getHeaders() } : {};
|
|
9046
|
+
if (typeof res.getHeaders !== "function" && typeof res.getHeader === "function") {
|
|
9047
|
+
for (const name of ["content-type", "content-length"]) {
|
|
9048
|
+
const value = res.getHeader(name);
|
|
9049
|
+
if (value !== void 0) set2[name] = value;
|
|
9050
|
+
}
|
|
9051
|
+
}
|
|
9052
|
+
if (!pendingHead) return { status: res.statusCode, headers: set2 };
|
|
9053
|
+
const at = writeHeadHeaderIndex(pendingHead);
|
|
9054
|
+
return {
|
|
9055
|
+
status: typeof pendingHead[0] === "number" ? pendingHead[0] : res.statusCode,
|
|
9056
|
+
headers: at === -1 ? set2 : { ...set2, ...writeHeadHeaderObject(pendingHead[at]) }
|
|
9057
|
+
};
|
|
9058
|
+
};
|
|
9059
|
+
const screenUnreadHead = () => {
|
|
9060
|
+
if (headScreened) return void 0;
|
|
9061
|
+
headScreened = true;
|
|
9062
|
+
try {
|
|
9063
|
+
const head = effectiveHead();
|
|
9064
|
+
const r = screenText("", head, reqCtx, decidedByHeaders);
|
|
9065
|
+
if (r.verdict === "block") {
|
|
9066
|
+
if (!reallySent()) return { withhold: true };
|
|
9067
|
+
recordSkip("response", "headers-sent", { action: "block" });
|
|
9068
|
+
return void 0;
|
|
9069
|
+
}
|
|
9070
|
+
if (r.verdict !== "redact" || !r.headers) return void 0;
|
|
9071
|
+
const changed = /* @__PURE__ */ new Map();
|
|
9072
|
+
for (const [name, value] of Object.entries(r.headers)) {
|
|
9073
|
+
if (headerValueChanged(head.headers[name], value)) changed.set(name.toLowerCase(), value);
|
|
9074
|
+
}
|
|
9075
|
+
if (changed.size && reallySent()) {
|
|
9076
|
+
recordSkip("response", "headers-sent", { headers: [...changed.keys()] });
|
|
9077
|
+
return void 0;
|
|
9078
|
+
}
|
|
9079
|
+
return changed.size ? { headers: changed } : void 0;
|
|
9080
|
+
} catch (err) {
|
|
9081
|
+
notify(onError, err, "onError");
|
|
9082
|
+
return void 0;
|
|
9083
|
+
}
|
|
9084
|
+
};
|
|
9085
|
+
const sendHead = (changes) => {
|
|
9086
|
+
if (sending) return;
|
|
9087
|
+
const hardened2 = hardenOnce();
|
|
9088
|
+
sending = true;
|
|
9089
|
+
if (!pendingHead) {
|
|
9090
|
+
if (changes) applyToResponse(changes);
|
|
9091
|
+
return;
|
|
9092
|
+
}
|
|
9093
|
+
const args = [...pendingHead];
|
|
9094
|
+
pendingHead = null;
|
|
9095
|
+
const merged = new Map(hardened2 ?? []);
|
|
9096
|
+
for (const [name, value] of changes?.headers ?? []) merged.set(name, value);
|
|
9097
|
+
if (changes?.status !== void 0) {
|
|
9098
|
+
args[0] = changes.status;
|
|
9099
|
+
if (typeof args[1] === "string") args.splice(1, 1);
|
|
9100
|
+
}
|
|
9101
|
+
const headerAt = writeHeadHeaderIndex(args);
|
|
9102
|
+
if (headerAt !== -1 && merged.size) args[headerAt] = rewriteWriteHeadHeaders(args[headerAt], merged);
|
|
9103
|
+
applyToResponse({ headers: merged });
|
|
9104
|
+
origWriteHead(...args);
|
|
9105
|
+
};
|
|
9106
|
+
const sendWithheld = (cb) => {
|
|
9107
|
+
const body = JSON.stringify({ error: "Response withheld by Patchstack (sensitive data detected)" });
|
|
9108
|
+
const headers = /* @__PURE__ */ new Map();
|
|
9109
|
+
for (const name of Object.keys(effectiveHead().headers)) headers.set(name.toLowerCase(), null);
|
|
9110
|
+
headers.set("content-type", "application/json");
|
|
9111
|
+
headers.set("content-length", String(Buffer.byteLength(body)));
|
|
9112
|
+
headers.set("transfer-encoding", null);
|
|
9113
|
+
withheld = true;
|
|
9114
|
+
sendHead({ status: 500, headers });
|
|
9115
|
+
return origEnd(body, cb);
|
|
9116
|
+
};
|
|
9117
|
+
const applyToResponse = (changes) => {
|
|
9118
|
+
if (changes.status !== void 0) res.statusCode = changes.status;
|
|
9119
|
+
for (const [name, value] of changes.headers ?? []) {
|
|
9120
|
+
try {
|
|
9121
|
+
if (value === null || value === void 0) removeHeaderNow?.(name);
|
|
9122
|
+
else setHeaderNow?.(name, value);
|
|
9123
|
+
} catch {
|
|
9124
|
+
}
|
|
9125
|
+
}
|
|
9126
|
+
};
|
|
8150
9127
|
const collect = (chunk, enc) => {
|
|
8151
9128
|
if (chunk == null) return;
|
|
8152
9129
|
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, typeof enc === "string" ? enc : "utf8");
|
|
8153
9130
|
size += buf.length;
|
|
8154
9131
|
if (size > MAX) {
|
|
9132
|
+
const unread = screenUnreadHead();
|
|
9133
|
+
if (unread?.withhold) {
|
|
9134
|
+
sendWithheld();
|
|
9135
|
+
return;
|
|
9136
|
+
}
|
|
9137
|
+
sendHead(unread);
|
|
8155
9138
|
for (const c of chunks) origWrite(c);
|
|
8156
9139
|
chunks.length = 0;
|
|
8157
9140
|
origWrite(buf);
|
|
@@ -8162,7 +9145,11 @@ async function createProtection(options = {}) {
|
|
|
8162
9145
|
chunks.push(buf);
|
|
8163
9146
|
};
|
|
8164
9147
|
res.write = function(chunk, enc, cb) {
|
|
8165
|
-
|
|
9148
|
+
if (withheld) {
|
|
9149
|
+
if (typeof enc === "function") enc();
|
|
9150
|
+
else if (typeof cb === "function") cb();
|
|
9151
|
+
return true;
|
|
9152
|
+
}
|
|
8166
9153
|
if (overflow) return origWrite(chunk, enc, cb);
|
|
8167
9154
|
collect(chunk, enc);
|
|
8168
9155
|
if (typeof enc === "function") enc();
|
|
@@ -8170,7 +9157,6 @@ async function createProtection(options = {}) {
|
|
|
8170
9157
|
return true;
|
|
8171
9158
|
};
|
|
8172
9159
|
res.end = function(chunk, enc, cb) {
|
|
8173
|
-
hardenBeforeFlush();
|
|
8174
9160
|
if (typeof chunk === "function") {
|
|
8175
9161
|
cb = chunk;
|
|
8176
9162
|
chunk = void 0;
|
|
@@ -8179,74 +9165,90 @@ async function createProtection(options = {}) {
|
|
|
8179
9165
|
cb = enc;
|
|
8180
9166
|
enc = void 0;
|
|
8181
9167
|
}
|
|
9168
|
+
if (withheld) {
|
|
9169
|
+
if (typeof cb === "function") cb();
|
|
9170
|
+
return res;
|
|
9171
|
+
}
|
|
8182
9172
|
if (overflow) {
|
|
8183
9173
|
if (chunk != null) origWrite(chunk, enc);
|
|
8184
9174
|
return origEnd(cb);
|
|
8185
9175
|
}
|
|
8186
9176
|
collect(chunk, enc);
|
|
9177
|
+
if (withheld) {
|
|
9178
|
+
if (typeof cb === "function") cb();
|
|
9179
|
+
return res;
|
|
9180
|
+
}
|
|
8187
9181
|
if (overflow) return origEnd(cb);
|
|
9182
|
+
hardenOnce();
|
|
9183
|
+
const passThrough = (changes) => {
|
|
9184
|
+
if (changes?.withhold) return sendWithheld(cb);
|
|
9185
|
+
sendHead(changes);
|
|
9186
|
+
for (const c of chunks) origWrite(c);
|
|
9187
|
+
return origEnd(cb);
|
|
9188
|
+
};
|
|
8188
9189
|
const buffer = Buffer.concat(chunks);
|
|
8189
|
-
|
|
9190
|
+
const head = effectiveHead();
|
|
9191
|
+
let ct = head.headers["content-type"];
|
|
8190
9192
|
if (Array.isArray(ct)) ct = ct[0];
|
|
8191
9193
|
const kind = screenableContentType(ct);
|
|
8192
9194
|
if (kind === "skip" || kind === "sniff" && looksBinary(buffer)) {
|
|
8193
9195
|
recordSkip("response", kind === "skip" ? baseContentType(ct) === "text/event-stream" ? "live-stream" : "non-text-content-type" : "binary-body");
|
|
8194
|
-
|
|
8195
|
-
|
|
9196
|
+
return passThrough(screenUnreadHead());
|
|
9197
|
+
}
|
|
9198
|
+
const codings = declaredCodings(head.headers["content-encoding"]);
|
|
9199
|
+
if (codings.length > 0 && stillEncoded(buffer)) {
|
|
9200
|
+
recordSkip("response", "encoded-body", { encoding: codings.join(", ") });
|
|
9201
|
+
return passThrough(screenUnreadHead());
|
|
9202
|
+
}
|
|
9203
|
+
const reading = charsetReading(ct, buffer);
|
|
9204
|
+
if (reading.skip) {
|
|
9205
|
+
recordSkip("response", "unsupported-charset", { charset: reading.skip });
|
|
9206
|
+
return passThrough(screenUnreadHead());
|
|
8196
9207
|
}
|
|
8197
|
-
const text = buffer.toString("utf8");
|
|
9208
|
+
const text = readJsonEscapes(buffer.toString("utf8", reading.bom ? 3 : 0));
|
|
8198
9209
|
let r;
|
|
8199
9210
|
try {
|
|
8200
|
-
r = screenText(
|
|
8201
|
-
text,
|
|
8202
|
-
{ status: res.statusCode, headers: res.getHeaders ? res.getHeaders() : {} },
|
|
8203
|
-
reqCtx,
|
|
8204
|
-
answeredWithoutBody ? stillToAnswer : void 0
|
|
8205
|
-
);
|
|
9211
|
+
r = screenText(text, head, reqCtx, notYetAnswered);
|
|
8206
9212
|
} catch (err) {
|
|
8207
9213
|
notify(onError, err, "onError");
|
|
8208
|
-
|
|
8209
|
-
|
|
9214
|
+
return passThrough();
|
|
9215
|
+
}
|
|
9216
|
+
if (r.verdict === "redact" && outgrowsCharset(reading.ascii, r.body)) r = { verdict: "block" };
|
|
9217
|
+
if (r.verdict === "redact") r = { ...r, body: withBom(reading.bom, r.body) };
|
|
9218
|
+
if (r.verdict !== "block" && r.verdict !== "redact") return passThrough();
|
|
9219
|
+
if (reallySent() && res.chunkedEncoding !== true) {
|
|
9220
|
+
notify(onError, new Error("Patchstack: response withheld \u2014 its headers were sent before the body could be screened"), "onError");
|
|
9221
|
+
res.destroy?.();
|
|
9222
|
+
return res;
|
|
8210
9223
|
}
|
|
8211
9224
|
if (r.verdict === "block") {
|
|
8212
|
-
|
|
8213
|
-
|
|
8214
|
-
res
|
|
8215
|
-
} catch {
|
|
9225
|
+
if (reallySent()) {
|
|
9226
|
+
res.destroy?.();
|
|
9227
|
+
return res;
|
|
8216
9228
|
}
|
|
8217
|
-
return
|
|
9229
|
+
return sendWithheld(cb);
|
|
8218
9230
|
}
|
|
8219
|
-
|
|
8220
|
-
|
|
8221
|
-
|
|
8222
|
-
|
|
9231
|
+
const changed = /* @__PURE__ */ new Map();
|
|
9232
|
+
if (r.headers) {
|
|
9233
|
+
for (const [name, value] of Object.entries(r.headers)) {
|
|
9234
|
+
if (name.toLowerCase() === "content-length") continue;
|
|
9235
|
+
if (!headerValueChanged(head.headers[name], value)) continue;
|
|
9236
|
+
changed.set(name.toLowerCase(), value);
|
|
8223
9237
|
}
|
|
8224
|
-
|
|
8225
|
-
|
|
8226
|
-
|
|
8227
|
-
|
|
8228
|
-
|
|
8229
|
-
|
|
8230
|
-
|
|
8231
|
-
|
|
8232
|
-
}
|
|
8233
|
-
} else if (Array.isArray(value)) {
|
|
8234
|
-
try {
|
|
8235
|
-
res.setHeader(name, value);
|
|
8236
|
-
} catch {
|
|
8237
|
-
}
|
|
8238
|
-
} else if (typeof value === "string" && current[name] !== value) {
|
|
8239
|
-
try {
|
|
8240
|
-
res.setHeader(name, value);
|
|
8241
|
-
} catch {
|
|
8242
|
-
}
|
|
8243
|
-
}
|
|
8244
|
-
}
|
|
9238
|
+
}
|
|
9239
|
+
if (!reallySent()) {
|
|
9240
|
+
const declared = head.headers["content-length"];
|
|
9241
|
+
if (declared === void 0) {
|
|
9242
|
+
changed.set("content-length", null);
|
|
9243
|
+
} else {
|
|
9244
|
+
changed.set("content-length", String(Buffer.byteLength(r.body)));
|
|
9245
|
+
changed.set("transfer-encoding", null);
|
|
8245
9246
|
}
|
|
8246
|
-
|
|
9247
|
+
sendHead({ headers: changed });
|
|
9248
|
+
} else if (changed.size) {
|
|
9249
|
+
recordSkip("response", "headers-sent", { headers: [...changed.keys()] });
|
|
8247
9250
|
}
|
|
8248
|
-
|
|
8249
|
-
return origEnd(cb);
|
|
9251
|
+
return origEnd(r.body, cb);
|
|
8250
9252
|
};
|
|
8251
9253
|
};
|
|
8252
9254
|
const allow = new Set(allowHosts.map((h) => String(h).toLowerCase()));
|
|
@@ -8320,21 +9322,15 @@ async function createProtection(options = {}) {
|
|
|
8320
9322
|
// .fetch(), and by the Supabase guard on its forwarded upstream response.
|
|
8321
9323
|
// A standalone response screen with no request phase of its own — the Supabase guard's forwarded
|
|
8322
9324
|
// upstream response. It resolves once here, which is the only resolution for this call.
|
|
8323
|
-
screenResponse: (response, request) => screenResp(
|
|
8324
|
-
response,
|
|
8325
|
-
request ? reqContextFromFetch(
|
|
8326
|
-
request,
|
|
8327
|
-
resolveClientIp({ headers: headerObject(request.headers), trustedProxy: options.trustedProxy })
|
|
8328
|
-
) : void 0
|
|
8329
|
-
),
|
|
9325
|
+
screenResponse: (response, request, ...hostArgs) => screenResp(response, request ? reqContextFromFetch(request, clientForResponse(request, hostArgs)) : void 0),
|
|
8330
9326
|
// (request) => Response | null (null = allow, caller proceeds). Request phase only.
|
|
8331
9327
|
fetchGuard() {
|
|
8332
|
-
return async (request) => (await screenFetchRequest(request)).blocked;
|
|
9328
|
+
return async (request, ...hostArgs) => (await screenFetchRequest(request, hostArgs)).blocked;
|
|
8333
9329
|
},
|
|
8334
9330
|
// Wrap a fetch handler: screens the request, then the response (redact/block).
|
|
8335
9331
|
fetch(handler) {
|
|
8336
9332
|
return async (request, ...rest) => {
|
|
8337
|
-
const { blocked, client } = await screenFetchRequest(request);
|
|
9333
|
+
const { blocked, client } = await screenFetchRequest(request, rest);
|
|
8338
9334
|
if (blocked) return blocked;
|
|
8339
9335
|
const response = await handler(request, ...rest);
|
|
8340
9336
|
return screenResp(response, reqContextFromFetch(request, client));
|
|
@@ -8348,6 +9344,7 @@ async function createProtection(options = {}) {
|
|
|
8348
9344
|
const { shaped, client } = shapeExpressRequest(req);
|
|
8349
9345
|
try {
|
|
8350
9346
|
result = engine.evaluate(shaped);
|
|
9347
|
+
recordEvaluationSkips(result);
|
|
8351
9348
|
} catch (err) {
|
|
8352
9349
|
notify(onError, err, "onError");
|
|
8353
9350
|
if (exprOptions.screenResponses) wrapNodeResponse(res, reqContextFromNode(req, client));
|
|
@@ -8382,33 +9379,25 @@ async function createProtection(options = {}) {
|
|
|
8382
9379
|
screenNodeRequest(req, res, next, "", req.body);
|
|
8383
9380
|
return;
|
|
8384
9381
|
}
|
|
8385
|
-
|
|
8386
|
-
|
|
8387
|
-
|
|
8388
|
-
|
|
8389
|
-
size += chunk.length;
|
|
8390
|
-
if (size > maxBytes) {
|
|
8391
|
-
overflow = true;
|
|
9382
|
+
readBodyPrefix(req, maxBytes, (err, read) => {
|
|
9383
|
+
if (err) {
|
|
9384
|
+
notify(onError, err, "onError");
|
|
9385
|
+
next();
|
|
8392
9386
|
return;
|
|
8393
9387
|
}
|
|
8394
|
-
|
|
8395
|
-
|
|
8396
|
-
|
|
8397
|
-
notify(onError, err, "onError");
|
|
8398
|
-
next();
|
|
8399
|
-
});
|
|
8400
|
-
req.on("end", () => {
|
|
8401
|
-
if (overflow) recordSkip("request", "body-cap", { bytes: size, limit: maxBytes });
|
|
8402
|
-
screenNodeRequest(req, res, next, overflow ? "" : Buffer.concat(chunks).toString("utf8"));
|
|
9388
|
+
if (read.overflow) recordSkip("request", "body-cap", { bytes: read.size, limit: maxBytes });
|
|
9389
|
+
if (read.failed) recordSkip("request", "read-failed", { bytes: read.size });
|
|
9390
|
+
screenNodeRequest(req, res, next, read.text, void 0, read.overflow || read.failed);
|
|
8403
9391
|
});
|
|
8404
9392
|
};
|
|
8405
|
-
function screenNodeRequest(req, res, next, rawBody, parsedBody) {
|
|
9393
|
+
function screenNodeRequest(req, res, next, rawBody, parsedBody, truncated = false) {
|
|
8406
9394
|
let shaped;
|
|
8407
9395
|
let result;
|
|
8408
9396
|
try {
|
|
8409
9397
|
shaped = fromNodeRequest(req, rawBody, { trustedProxy: options.trustedProxy });
|
|
8410
9398
|
if (parsedBody !== void 0 && parsedBody !== null) shaped.body = parsedBody;
|
|
8411
9399
|
result = engine.evaluate(shaped);
|
|
9400
|
+
recordEvaluationSkips(result);
|
|
8412
9401
|
} catch (err) {
|
|
8413
9402
|
notify(onError, err, "onError");
|
|
8414
9403
|
return next();
|
|
@@ -8427,7 +9416,7 @@ async function createProtection(options = {}) {
|
|
|
8427
9416
|
}
|
|
8428
9417
|
},
|
|
8429
9418
|
() => {
|
|
8430
|
-
if (req.body === void 0) req.body = shaped.body;
|
|
9419
|
+
if (req.body === void 0 && !truncated) req.body = shaped.body;
|
|
8431
9420
|
if (nodeOptions.screenResponses) wrapNodeResponse(res, reqContextFromNode(req, shaped?._clientIp));
|
|
8432
9421
|
next();
|
|
8433
9422
|
},
|
|
@@ -8459,6 +9448,7 @@ async function createProtection(options = {}) {
|
|
|
8459
9448
|
notify(onError, err, "onError");
|
|
8460
9449
|
}
|
|
8461
9450
|
}
|
|
9451
|
+
let recovery = null;
|
|
8462
9452
|
const runRefreshTick = async () => {
|
|
8463
9453
|
if (reporter) {
|
|
8464
9454
|
try {
|
|
@@ -8475,23 +9465,38 @@ async function createProtection(options = {}) {
|
|
|
8475
9465
|
});
|
|
8476
9466
|
mode = resolveMode(options, next);
|
|
8477
9467
|
applyBundle(next);
|
|
9468
|
+
noteRuleSource(next.source);
|
|
9469
|
+
if (next.source?.ok !== false) recovery?.stop();
|
|
8478
9470
|
await applyAndAcknowledge(next.source?.origin, declaredState);
|
|
8479
9471
|
if (detections) detections.setRulesEtag((await store.read())?.etag ?? null);
|
|
8480
9472
|
return next.source ?? { ok: true };
|
|
8481
9473
|
};
|
|
9474
|
+
const refreshTick = serialise(runRefreshTick);
|
|
8482
9475
|
if (live) {
|
|
8483
|
-
protection.refresh = () =>
|
|
8484
|
-
protection.refreshHandler = () => makeRefreshHandler(
|
|
9476
|
+
protection.refresh = () => refreshTick();
|
|
9477
|
+
protection.refreshHandler = () => makeRefreshHandler(refreshTick, refreshSecret);
|
|
8485
9478
|
}
|
|
8486
|
-
const loop = options.refreshMs > 0 && live ? startRefresh(
|
|
9479
|
+
const loop = options.refreshMs > 0 && live ? startRefresh(refreshTick, { refreshMs: options.refreshMs, onError }) : null;
|
|
9480
|
+
const canAsk = Boolean(options.token || pulseAuth);
|
|
9481
|
+
recovery = live && canAsk && !loop && ruleSource.ok === false ? startRecovery(refreshTick, { onError }) : null;
|
|
8487
9482
|
protection.stop = () => {
|
|
8488
9483
|
loop?.stop();
|
|
9484
|
+
recovery?.stop();
|
|
9485
|
+
protection.uninstallEgress?.();
|
|
8489
9486
|
const outstanding = [firewallLog?.stop(), detections?.stop()].filter(
|
|
8490
9487
|
(wait) => wait && typeof wait.then === "function"
|
|
8491
9488
|
);
|
|
8492
9489
|
return Promise.all(outstanding).then(() => void 0);
|
|
8493
9490
|
};
|
|
8494
|
-
protection.stopRefresh =
|
|
9491
|
+
protection.stopRefresh = () => {
|
|
9492
|
+
loop?.stop();
|
|
9493
|
+
recovery?.stop();
|
|
9494
|
+
return Promise.resolve();
|
|
9495
|
+
};
|
|
9496
|
+
Object.defineProperty(protection, "ruleSource", {
|
|
9497
|
+
get: () => ({ ...ruleSource }),
|
|
9498
|
+
enumerable: true
|
|
9499
|
+
});
|
|
8495
9500
|
Object.defineProperty(protection, "detectionReporting", {
|
|
8496
9501
|
get: () => detectionReporting,
|
|
8497
9502
|
enumerable: true
|
|
@@ -8500,8 +9505,22 @@ async function createProtection(options = {}) {
|
|
|
8500
9505
|
get: () => detections ? () => detections.health() : void 0,
|
|
8501
9506
|
enumerable: true
|
|
8502
9507
|
});
|
|
9508
|
+
Object.defineProperty(protection, "blockLogHealth", {
|
|
9509
|
+
get: () => firewallLog ? () => firewallLog.health() : void 0,
|
|
9510
|
+
enumerable: true
|
|
9511
|
+
});
|
|
8503
9512
|
return protection;
|
|
8504
9513
|
}
|
|
9514
|
+
function accessorOf(obj, name) {
|
|
9515
|
+
for (let o = obj; o; o = Object.getPrototypeOf(o)) {
|
|
9516
|
+
const d = Object.getOwnPropertyDescriptor(o, name);
|
|
9517
|
+
if (d) return typeof d.get === "function" ? d.get : null;
|
|
9518
|
+
}
|
|
9519
|
+
return null;
|
|
9520
|
+
}
|
|
9521
|
+
function headersSentError(verb) {
|
|
9522
|
+
return Object.assign(new Error(`Cannot ${verb} headers after they are sent to the client`), { code: "ERR_HTTP_HEADERS_SENT" });
|
|
9523
|
+
}
|
|
8505
9524
|
function resolveMode(options, bundle) {
|
|
8506
9525
|
const env = typeof process !== "undefined" ? process.env?.PATCHSTACK_MODE : void 0;
|
|
8507
9526
|
if (env === "block" || env === "dry-run") return env;
|
|
@@ -8564,6 +9583,14 @@ function screenableContentType(ct) {
|
|
|
8564
9583
|
if (base === "application/octet-stream") return "sniff";
|
|
8565
9584
|
return "skip";
|
|
8566
9585
|
}
|
|
9586
|
+
function prefilterText(body, headers) {
|
|
9587
|
+
const values = [];
|
|
9588
|
+
for (const value of Object.values(headers ?? {})) {
|
|
9589
|
+
if (Array.isArray(value)) values.push(...value.map(String));
|
|
9590
|
+
else if (value !== void 0 && value !== null) values.push(String(value));
|
|
9591
|
+
}
|
|
9592
|
+
return [body, ...values].join("\n").toLowerCase();
|
|
9593
|
+
}
|
|
8567
9594
|
function looksBinary(bytes) {
|
|
8568
9595
|
const n = Math.min(bytes.length, 512);
|
|
8569
9596
|
let ctrl = 0;
|
|
@@ -8574,11 +9601,72 @@ function looksBinary(bytes) {
|
|
|
8574
9601
|
}
|
|
8575
9602
|
return n > 0 && ctrl / n > 0.1;
|
|
8576
9603
|
}
|
|
9604
|
+
function declaredCodings(value) {
|
|
9605
|
+
const raw = Array.isArray(value) ? value.join(",") : String(value ?? "");
|
|
9606
|
+
return raw.toLowerCase().split(",").map((c) => c.trim()).filter((c) => c !== "" && c !== "identity");
|
|
9607
|
+
}
|
|
9608
|
+
function stillEncoded(bytes) {
|
|
9609
|
+
if (looksBinary(bytes)) return true;
|
|
9610
|
+
try {
|
|
9611
|
+
new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
9612
|
+
return false;
|
|
9613
|
+
} catch {
|
|
9614
|
+
return true;
|
|
9615
|
+
}
|
|
9616
|
+
}
|
|
9617
|
+
var UTF8_CHARSETS = /* @__PURE__ */ new Set(["utf-8", "utf8", "unicode-1-1-utf-8", "us-ascii", "ascii"]);
|
|
9618
|
+
var WIDE_CHARSET = /^(?:utf-?(?:16|32)|ucs-?[24]|unicode|iso-10646)/;
|
|
9619
|
+
function declaredCharset(ct) {
|
|
9620
|
+
const m = /;\s*charset\s*=\s*(?:"([^"]*)"|([^;\s]*))/i.exec(String(ct ?? ""));
|
|
9621
|
+
const label = m ? (m[1] ?? m[2] ?? "").trim().toLowerCase() : "";
|
|
9622
|
+
return label || null;
|
|
9623
|
+
}
|
|
9624
|
+
function charsetReading(ct, bytes) {
|
|
9625
|
+
if (bytes.length >= 3 && bytes[0] === 239 && bytes[1] === 187 && bytes[2] === 191) return { ascii: false, bom: true };
|
|
9626
|
+
if (bytes.length >= 2 && bytes[0] === 255 && bytes[1] === 254) return { skip: "utf-16le" };
|
|
9627
|
+
if (bytes.length >= 2 && bytes[0] === 254 && bytes[1] === 255) return { skip: "utf-16be" };
|
|
9628
|
+
const charset = declaredCharset(ct);
|
|
9629
|
+
if (!charset || UTF8_CHARSETS.has(charset)) return { ascii: false };
|
|
9630
|
+
if (WIDE_CHARSET.test(charset)) return { skip: charset };
|
|
9631
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
9632
|
+
const b = bytes[i];
|
|
9633
|
+
if (b >= 128 || b === 0 || b === 27) return { skip: charset };
|
|
9634
|
+
}
|
|
9635
|
+
return { ascii: true };
|
|
9636
|
+
}
|
|
9637
|
+
function decodeForScreening(ct, bytes) {
|
|
9638
|
+
const reading = charsetReading(ct, bytes);
|
|
9639
|
+
if (reading.skip) return { skip: "unsupported-charset", charset: reading.skip };
|
|
9640
|
+
return { text: new TextDecoder().decode(bytes), ascii: reading.ascii, bom: reading.bom === true };
|
|
9641
|
+
}
|
|
9642
|
+
function withBom(bom, body) {
|
|
9643
|
+
return bom ? "\uFEFF" + body : body;
|
|
9644
|
+
}
|
|
9645
|
+
function outgrowsCharset(ascii, body) {
|
|
9646
|
+
return ascii === true && /[^\x00-\x7f]/.test(body);
|
|
9647
|
+
}
|
|
9648
|
+
var KEPT_ESCAPES = /* @__PURE__ */ new Set([34, 92, 60, 62, 38, 39]);
|
|
9649
|
+
function readJsonEscapes(text) {
|
|
9650
|
+
if (!text.includes("\\u") && !text.includes("\\/")) return text;
|
|
9651
|
+
try {
|
|
9652
|
+
JSON.parse(text);
|
|
9653
|
+
} catch {
|
|
9654
|
+
return text;
|
|
9655
|
+
}
|
|
9656
|
+
return text.replace(/\\(?:u([0-9a-fA-F]{4})|([\s\S]))/g, (whole, hex, ch, offset) => {
|
|
9657
|
+
if (hex === void 0) return ch === "/" && text[offset - 1] !== "<" ? "/" : whole;
|
|
9658
|
+
const code = parseInt(hex, 16);
|
|
9659
|
+
if (code < 32 || code > 126 || KEPT_ESCAPES.has(code)) return whole;
|
|
9660
|
+
return String.fromCharCode(code);
|
|
9661
|
+
});
|
|
9662
|
+
}
|
|
8577
9663
|
async function readTextResponse(response, cap = DEFAULT_SCREEN_CAP) {
|
|
8578
9664
|
if (!response || typeof response.clone !== "function") return { skip: "not-a-response" };
|
|
8579
9665
|
const ct = response.headers?.get?.("content-type") || "";
|
|
8580
9666
|
const kind = screenableContentType(ct);
|
|
8581
9667
|
if (kind === "skip") return { skip: baseContentType(ct) === "text/event-stream" ? "live-stream" : "non-text-content-type" };
|
|
9668
|
+
const codings = declaredCodings(response.headers?.get?.("content-encoding"));
|
|
9669
|
+
const encodedSkip = () => ({ skip: "encoded-body", encoding: codings.join(", ") });
|
|
8582
9670
|
const sniff = kind === "sniff";
|
|
8583
9671
|
const len = Number(response.headers?.get?.("content-length") || 0);
|
|
8584
9672
|
if (len && len > cap) return { skip: "body-cap" };
|
|
@@ -8622,13 +9710,22 @@ async function readTextResponse(response, cap = DEFAULT_SCREEN_CAP) {
|
|
|
8622
9710
|
} finally {
|
|
8623
9711
|
reader.releaseLock();
|
|
8624
9712
|
}
|
|
9713
|
+
const bytes = concatBytes(chunks, size);
|
|
9714
|
+
if (codings.length > 0 && stillEncoded(bytes)) return encodedSkip();
|
|
8625
9715
|
try {
|
|
8626
|
-
return
|
|
9716
|
+
return decodeForScreening(ct, bytes);
|
|
8627
9717
|
} catch {
|
|
8628
9718
|
return { skip: "decode-failed" };
|
|
8629
9719
|
}
|
|
8630
9720
|
}
|
|
8631
9721
|
try {
|
|
9722
|
+
if (codings.length > 0 || typeof clone.arrayBuffer === "function") {
|
|
9723
|
+
const bytes = new Uint8Array(await clone.arrayBuffer());
|
|
9724
|
+
if (bytes.byteLength > cap) return { skip: "body-cap" };
|
|
9725
|
+
if (codings.length > 0 && stillEncoded(bytes)) return encodedSkip();
|
|
9726
|
+
if (codings.length === 0 && sniff && looksBinary(bytes)) return { skip: "binary-body" };
|
|
9727
|
+
return decodeForScreening(ct, bytes);
|
|
9728
|
+
}
|
|
8632
9729
|
const text = await clone.text();
|
|
8633
9730
|
if (text.length > cap) return { skip: "body-cap" };
|
|
8634
9731
|
if (sniff && looksBinary(new TextEncoder().encode(text.slice(0, 512)))) return { skip: "binary-body" };
|
|
@@ -8658,8 +9755,9 @@ function headerObject(headers) {
|
|
|
8658
9755
|
function hasSpanMutations(rule) {
|
|
8659
9756
|
let found = false;
|
|
8660
9757
|
const walk2 = (conds) => {
|
|
8661
|
-
for (const c of conds
|
|
9758
|
+
for (const c of Array.isArray(conds) ? conds : []) {
|
|
8662
9759
|
if (found) return;
|
|
9760
|
+
if (!c || typeof c !== "object") continue;
|
|
8663
9761
|
if (Array.isArray(c.rules)) walk2(c.rules);
|
|
8664
9762
|
const isSpan = c.match && (c.match.type === "regex" || c.match.type === "contains" || c.match.type === "stripos");
|
|
8665
9763
|
if (isSpan && Array.isArray(c.mutations) && c.mutations.length) found = true;
|
|
@@ -8668,10 +9766,34 @@ function hasSpanMutations(rule) {
|
|
|
8668
9766
|
walk2(rule.rule_v2);
|
|
8669
9767
|
return found;
|
|
8670
9768
|
}
|
|
8671
|
-
function
|
|
9769
|
+
function redactorScope(parameter) {
|
|
9770
|
+
if (parameter === void 0 || parameter === null || parameterProblem(parameter) !== null) return null;
|
|
9771
|
+
const scope = { body: false, allHeaders: false, headers: /* @__PURE__ */ new Set() };
|
|
9772
|
+
for (const name of Array.isArray(parameter) ? parameter : [parameter]) {
|
|
9773
|
+
if (typeof name !== "string" || name === "") return null;
|
|
9774
|
+
if (name.startsWith("response.header.")) scope.headers.add(name.slice("response.header.".length).toLowerCase());
|
|
9775
|
+
else if (name === "response.headers") scope.allHeaders = true;
|
|
9776
|
+
else if (name === "response.body" || name === "response.status" || BROAD_SCOPE_SOURCES.has(name.split(".")[0])) {
|
|
9777
|
+
scope.body = true;
|
|
9778
|
+
scope.allHeaders = true;
|
|
9779
|
+
} else return null;
|
|
9780
|
+
}
|
|
9781
|
+
return scope;
|
|
9782
|
+
}
|
|
9783
|
+
var BROAD_SCOPE_SOURCES = /* @__PURE__ */ new Set(["get", "post", "request", "cookie", "files", "server", "raw", "all"]);
|
|
9784
|
+
function masksHeader(redactor, name) {
|
|
9785
|
+
return redactor.scope.allHeaders || redactor.scope.headers.has(name);
|
|
9786
|
+
}
|
|
9787
|
+
function extractRedactors(rule, onUnscoped) {
|
|
8672
9788
|
const out = [];
|
|
9789
|
+
const scoped = (c) => {
|
|
9790
|
+
const scope = redactorScope(c.parameter);
|
|
9791
|
+
if (scope === null) onUnscoped?.(c.parameter);
|
|
9792
|
+
return scope;
|
|
9793
|
+
};
|
|
8673
9794
|
const walk2 = (conds) => {
|
|
8674
|
-
for (const c of conds
|
|
9795
|
+
for (const c of Array.isArray(conds) ? conds : []) {
|
|
9796
|
+
if (!c || typeof c !== "object") continue;
|
|
8675
9797
|
if (Array.isArray(c.rules)) walk2(c.rules);
|
|
8676
9798
|
const m = c.match;
|
|
8677
9799
|
if (!m) continue;
|
|
@@ -8679,16 +9801,21 @@ function extractRedactors(rule) {
|
|
|
8679
9801
|
const safe = safeRegExp(m.value);
|
|
8680
9802
|
if (safe) {
|
|
8681
9803
|
const flags = safe.flags.includes("g") ? safe.flags : safe.flags + "g";
|
|
9804
|
+
const scope = scoped(c);
|
|
8682
9805
|
try {
|
|
8683
|
-
out.push({ re: new RegExp(safe.source, flags) });
|
|
9806
|
+
if (scope) out.push({ re: new RegExp(safe.source, flags), scope });
|
|
8684
9807
|
} catch {
|
|
8685
9808
|
}
|
|
8686
9809
|
}
|
|
8687
9810
|
} else if ((m.type === "contains" || m.type === "stripos") && m.value != null) {
|
|
8688
|
-
|
|
9811
|
+
const scope = scoped(c);
|
|
9812
|
+
if (scope) out.push({ literal: String(m.value), scope });
|
|
8689
9813
|
} else if (m.type === "jwt_claim_equals" && typeof m.claim === "string") {
|
|
8690
|
-
|
|
8691
|
-
|
|
9814
|
+
const scope = scoped(c);
|
|
9815
|
+
if (scope) out.push({ jwtClaim: { claim: m.claim, value: String(m.value ?? "") }, scope });
|
|
9816
|
+
} else if (m.type === "array_key_value" && m.match && Array.isArray(c.parameter) && !readsBody(c.parameter) && redactorScope(c.parameter) === null) {
|
|
9817
|
+
onUnscoped?.(c.parameter);
|
|
9818
|
+
} else if (m.type === "array_key_value" && m.match && readsBody(c.parameter)) {
|
|
8692
9819
|
const keys = Array.isArray(m.key) ? m.key : [m.key];
|
|
8693
9820
|
for (const key of keys) {
|
|
8694
9821
|
out.push({ jsonPath: String(key).split("."), condition: m.match });
|
|
@@ -8702,6 +9829,110 @@ function extractRedactors(rule) {
|
|
|
8702
9829
|
function htmlEscape(str) {
|
|
8703
9830
|
return String(str).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
8704
9831
|
}
|
|
9832
|
+
function isJson(text) {
|
|
9833
|
+
try {
|
|
9834
|
+
JSON.parse(text);
|
|
9835
|
+
return true;
|
|
9836
|
+
} catch {
|
|
9837
|
+
return false;
|
|
9838
|
+
}
|
|
9839
|
+
}
|
|
9840
|
+
var JSON_STRING = 1;
|
|
9841
|
+
var JSON_NUMBER = 2;
|
|
9842
|
+
var JSON_LITERAL = 3;
|
|
9843
|
+
var JSON_NUMBER_TOKEN = /-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/y;
|
|
9844
|
+
function isHexCode(c) {
|
|
9845
|
+
return c >= 48 && c <= 57 || c >= 65 && c <= 70 || c >= 97 && c <= 102;
|
|
9846
|
+
}
|
|
9847
|
+
var PLAIN_STRING_SEGMENT = /^[^"\\\u0000-\u001f]*$/;
|
|
9848
|
+
function lexJson(text) {
|
|
9849
|
+
let kinds = new Int32Array(1024);
|
|
9850
|
+
let spans = new Int32Array(2048);
|
|
9851
|
+
let count = 0;
|
|
9852
|
+
const n = text.length;
|
|
9853
|
+
let i = 0;
|
|
9854
|
+
while (i < n) {
|
|
9855
|
+
const c = text.charCodeAt(i);
|
|
9856
|
+
if (c === 32 || c === 9 || c === 10 || c === 13) {
|
|
9857
|
+
i++;
|
|
9858
|
+
continue;
|
|
9859
|
+
}
|
|
9860
|
+
const start = i;
|
|
9861
|
+
let kind;
|
|
9862
|
+
if (c === 34) {
|
|
9863
|
+
i++;
|
|
9864
|
+
const close = text.indexOf('"', i);
|
|
9865
|
+
if (close === -1) return null;
|
|
9866
|
+
if (PLAIN_STRING_SEGMENT.test(text.slice(i, close))) {
|
|
9867
|
+
i = close;
|
|
9868
|
+
} else {
|
|
9869
|
+
for (; ; ) {
|
|
9870
|
+
if (i >= n) return null;
|
|
9871
|
+
const d = text.charCodeAt(i);
|
|
9872
|
+
if (d === 34) break;
|
|
9873
|
+
if (d < 32) return null;
|
|
9874
|
+
if (d !== 92) {
|
|
9875
|
+
i++;
|
|
9876
|
+
continue;
|
|
9877
|
+
}
|
|
9878
|
+
const e = text.charCodeAt(i + 1);
|
|
9879
|
+
if (e === 117) {
|
|
9880
|
+
for (let k = 2; k < 6; k++) if (!isHexCode(text.charCodeAt(i + k))) return null;
|
|
9881
|
+
i += 6;
|
|
9882
|
+
} else if (e === 34 || e === 92 || e === 47 || e === 98 || e === 102 || e === 110 || e === 114 || e === 116) {
|
|
9883
|
+
i += 2;
|
|
9884
|
+
} else {
|
|
9885
|
+
return null;
|
|
9886
|
+
}
|
|
9887
|
+
}
|
|
9888
|
+
}
|
|
9889
|
+
i++;
|
|
9890
|
+
kind = JSON_STRING;
|
|
9891
|
+
} else if (c === 123 || c === 125 || c === 91 || c === 93 || c === 58 || c === 44) {
|
|
9892
|
+
i++;
|
|
9893
|
+
kind = c;
|
|
9894
|
+
} else if (text.startsWith("true", i) || text.startsWith("null", i)) {
|
|
9895
|
+
i += 4;
|
|
9896
|
+
kind = JSON_LITERAL;
|
|
9897
|
+
} else if (text.startsWith("false", i)) {
|
|
9898
|
+
i += 5;
|
|
9899
|
+
kind = JSON_LITERAL;
|
|
9900
|
+
} else {
|
|
9901
|
+
JSON_NUMBER_TOKEN.lastIndex = i;
|
|
9902
|
+
if (!JSON_NUMBER_TOKEN.test(text)) return null;
|
|
9903
|
+
i = JSON_NUMBER_TOKEN.lastIndex;
|
|
9904
|
+
kind = JSON_NUMBER;
|
|
9905
|
+
}
|
|
9906
|
+
if (count === kinds.length) {
|
|
9907
|
+
const grownKinds = new Int32Array(count * 2);
|
|
9908
|
+
grownKinds.set(kinds);
|
|
9909
|
+
kinds = grownKinds;
|
|
9910
|
+
const grownSpans = new Int32Array(count * 4);
|
|
9911
|
+
grownSpans.set(spans);
|
|
9912
|
+
spans = grownSpans;
|
|
9913
|
+
}
|
|
9914
|
+
kinds[count] = kind;
|
|
9915
|
+
spans[2 * count] = start;
|
|
9916
|
+
spans[2 * count + 1] = i;
|
|
9917
|
+
count++;
|
|
9918
|
+
}
|
|
9919
|
+
return { kinds, spans, count };
|
|
9920
|
+
}
|
|
9921
|
+
function sameJsonStructure(beforeText, before, afterText, after) {
|
|
9922
|
+
if (!before || !after || before.count !== after.count) return false;
|
|
9923
|
+
const { kinds, spans, count } = before;
|
|
9924
|
+
for (let k = 0; k < count; k++) {
|
|
9925
|
+
const kind = kinds[k];
|
|
9926
|
+
if (after.kinds[k] !== kind) return false;
|
|
9927
|
+
if (kind > JSON_LITERAL) continue;
|
|
9928
|
+
if (kind === JSON_STRING && kinds[k + 1] !== 58) continue;
|
|
9929
|
+
const was = beforeText.slice(spans[2 * k], spans[2 * k + 1]);
|
|
9930
|
+
const now = afterText.slice(after.spans[2 * k], after.spans[2 * k + 1]);
|
|
9931
|
+
if (was === now) continue;
|
|
9932
|
+
if (kind !== JSON_STRING || JSON.parse(was) !== JSON.parse(now)) return false;
|
|
9933
|
+
}
|
|
9934
|
+
return true;
|
|
9935
|
+
}
|
|
8705
9936
|
function applyRedactors(body, redactors, mask, transform) {
|
|
8706
9937
|
let out = body;
|
|
8707
9938
|
for (const r of redactors) {
|
|
@@ -8717,6 +9948,11 @@ function applyRedactors(body, redactors, mask, transform) {
|
|
|
8717
9948
|
}
|
|
8718
9949
|
return out;
|
|
8719
9950
|
}
|
|
9951
|
+
function readsBody(parameter) {
|
|
9952
|
+
if (!Array.isArray(parameter)) return isBodyParam(parameter);
|
|
9953
|
+
const names = parameter.every((member) => typeof member === "string" && member !== "");
|
|
9954
|
+
return names && parameterProblem(parameter) === null && parameter.some((member) => isBodyParam(member));
|
|
9955
|
+
}
|
|
8720
9956
|
function isBodyParam(parameter) {
|
|
8721
9957
|
return parameter == null || parameter === "response.body" || parameter === "raw" || parameter === "response.raw";
|
|
8722
9958
|
}
|
|
@@ -8744,10 +9980,12 @@ function applyPathRedactors(text, pathRedactors, mask, cap, transform) {
|
|
|
8744
9980
|
if (!pathRedactors.length || typeof text !== "string" || text.length > cap) return text;
|
|
8745
9981
|
const head = text.trimStart()[0];
|
|
8746
9982
|
if (head !== "{" && head !== "[") return text;
|
|
8747
|
-
|
|
9983
|
+
let preserved;
|
|
8748
9984
|
let obj;
|
|
8749
9985
|
try {
|
|
8750
|
-
|
|
9986
|
+
JSON.parse(text);
|
|
9987
|
+
preserved = preserveNumbers(text, mask);
|
|
9988
|
+
obj = JSON.parse(preserved.text);
|
|
8751
9989
|
} catch {
|
|
8752
9990
|
return text;
|
|
8753
9991
|
}
|
|
@@ -8756,65 +9994,44 @@ function applyPathRedactors(text, pathRedactors, mask, cap, transform) {
|
|
|
8756
9994
|
const pred = conditionPredicate(r.condition);
|
|
8757
9995
|
walkLeaves(obj, r.jsonPath, (loc) => {
|
|
8758
9996
|
try {
|
|
8759
|
-
|
|
8760
|
-
|
|
9997
|
+
const number = preserved.numbers.get(loc.value);
|
|
9998
|
+
if (pred(number === void 0 ? loc.value : Number(number))) {
|
|
9999
|
+
setOwn(loc.parent, loc.key, transform ? transform(number ?? String(loc.value)) : mask);
|
|
8761
10000
|
changed = true;
|
|
8762
10001
|
}
|
|
8763
10002
|
} catch {
|
|
8764
10003
|
}
|
|
8765
10004
|
});
|
|
8766
10005
|
}
|
|
8767
|
-
return changed ?
|
|
8768
|
-
}
|
|
8769
|
-
var BIGINT_OPEN = "__PSBIGINT_9c2f__";
|
|
8770
|
-
var BIGINT_CLOSE = "__DNEGIB__";
|
|
8771
|
-
function preserveBigInts(text) {
|
|
8772
|
-
let out = "";
|
|
8773
|
-
let inStr = false;
|
|
8774
|
-
for (let i = 0; i < text.length; ) {
|
|
8775
|
-
const ch = text[i];
|
|
8776
|
-
if (inStr) {
|
|
8777
|
-
out += ch;
|
|
8778
|
-
if (ch === "\\") {
|
|
8779
|
-
out += text[i + 1] ?? "";
|
|
8780
|
-
i += 2;
|
|
8781
|
-
continue;
|
|
8782
|
-
}
|
|
8783
|
-
if (ch === '"') inStr = false;
|
|
8784
|
-
i++;
|
|
8785
|
-
continue;
|
|
8786
|
-
}
|
|
8787
|
-
if (ch === '"') {
|
|
8788
|
-
inStr = true;
|
|
8789
|
-
out += ch;
|
|
8790
|
-
i++;
|
|
8791
|
-
continue;
|
|
8792
|
-
}
|
|
8793
|
-
if (ch === "-" || ch >= "0" && ch <= "9") {
|
|
8794
|
-
let j = ch === "-" ? i + 1 : i;
|
|
8795
|
-
let digits = 0;
|
|
8796
|
-
while (j < text.length && text[j] >= "0" && text[j] <= "9") {
|
|
8797
|
-
digits++;
|
|
8798
|
-
j++;
|
|
8799
|
-
}
|
|
8800
|
-
const next = text[j];
|
|
8801
|
-
const isIntToken = digits > 0 && next !== "." && next !== "e" && next !== "E";
|
|
8802
|
-
if (isIntToken && digits >= 16) {
|
|
8803
|
-
out += `"${BIGINT_OPEN}${text.slice(i, j)}${BIGINT_CLOSE}"`;
|
|
8804
|
-
} else {
|
|
8805
|
-
out += text.slice(i, j || i + 1);
|
|
8806
|
-
}
|
|
8807
|
-
i = j > i ? j : i + 1;
|
|
8808
|
-
continue;
|
|
8809
|
-
}
|
|
8810
|
-
out += ch;
|
|
8811
|
-
i++;
|
|
8812
|
-
}
|
|
8813
|
-
return out;
|
|
10006
|
+
return changed ? restoreNumbers(JSON.stringify(obj), preserved.numbers) : text;
|
|
8814
10007
|
}
|
|
8815
|
-
function
|
|
8816
|
-
|
|
8817
|
-
|
|
10008
|
+
function preserveNumbers(text, mask) {
|
|
10009
|
+
const occupied = /* @__PURE__ */ new Set([mask]);
|
|
10010
|
+
const found = [];
|
|
10011
|
+
const tokens = /"(?:[^"\\]|\\[\s\S])*"|-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/g;
|
|
10012
|
+
for (const match of text.matchAll(tokens)) {
|
|
10013
|
+
const token = match[0];
|
|
10014
|
+
if (token[0] === '"') occupied.add(JSON.parse(token));
|
|
10015
|
+
else found.push({ start: match.index, token });
|
|
10016
|
+
}
|
|
10017
|
+
const numbers = /* @__PURE__ */ new Map();
|
|
10018
|
+
const chunks = [];
|
|
10019
|
+
let offset = 0;
|
|
10020
|
+
let index = 0;
|
|
10021
|
+
for (const { start, token } of found) {
|
|
10022
|
+
let marker;
|
|
10023
|
+
do {
|
|
10024
|
+
marker = "__PSNUMBER_" + index++ + "__";
|
|
10025
|
+
} while (occupied.has(marker));
|
|
10026
|
+
numbers.set(marker, token);
|
|
10027
|
+
chunks.push(text.slice(offset, start), JSON.stringify(marker));
|
|
10028
|
+
offset = start + token.length;
|
|
10029
|
+
}
|
|
10030
|
+
chunks.push(text.slice(offset));
|
|
10031
|
+
return { text: chunks.join(""), numbers };
|
|
10032
|
+
}
|
|
10033
|
+
function restoreNumbers(text, numbers) {
|
|
10034
|
+
return text.replace(/"(__PSNUMBER_\d+__)"/g, (token, marker) => numbers.get(marker) ?? token);
|
|
8818
10035
|
}
|
|
8819
10036
|
function isHeaderMutation(action) {
|
|
8820
10037
|
return action === "set-header" || action === "remove-header" || action === "harden-cookie";
|