@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.js
CHANGED
|
@@ -376,18 +376,124 @@ function appendOwn(target, key, value) {
|
|
|
376
376
|
return setOwn(target, key, Array.isArray(previous) ? [...previous, value] : [previous, value]);
|
|
377
377
|
}
|
|
378
378
|
|
|
379
|
-
// src/protect/engine/
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
const
|
|
385
|
-
|
|
386
|
-
|
|
379
|
+
// src/protect/engine/cookies.js
|
|
380
|
+
function parseCookieHeader(header) {
|
|
381
|
+
const cookies = {};
|
|
382
|
+
if (typeof header !== "string" || header === "") return cookies;
|
|
383
|
+
const repeated = /* @__PURE__ */ new Map();
|
|
384
|
+
for (const pair of header.split(";")) {
|
|
385
|
+
const eq = pair.indexOf("=");
|
|
386
|
+
if (eq === -1) continue;
|
|
387
|
+
const name = pair.slice(0, eq).trim();
|
|
388
|
+
if (name === "") continue;
|
|
389
|
+
let value = pair.slice(eq + 1).trim();
|
|
390
|
+
if (value.length >= 2 && value[0] === '"' && value[value.length - 1] === '"') value = value.slice(1, -1);
|
|
391
|
+
if (!Object.hasOwn(cookies, name)) {
|
|
392
|
+
setOwn(cookies, name, value);
|
|
393
|
+
} else {
|
|
394
|
+
let values = repeated.get(name);
|
|
395
|
+
if (values === void 0) {
|
|
396
|
+
values = [cookies[name]];
|
|
397
|
+
repeated.set(name, values);
|
|
398
|
+
setOwn(cookies, name, values);
|
|
399
|
+
}
|
|
400
|
+
values.push(value);
|
|
401
|
+
}
|
|
387
402
|
}
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
403
|
+
return cookies;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// src/protect/engine/normalizer.js
|
|
407
|
+
var HTML_ENTITIES = {
|
|
408
|
+
"&": "&",
|
|
409
|
+
"<": "<",
|
|
410
|
+
">": ">",
|
|
411
|
+
""": '"',
|
|
412
|
+
"'": "'",
|
|
413
|
+
"'": "'",
|
|
414
|
+
" ": " ",
|
|
415
|
+
"'": "'",
|
|
416
|
+
""": '"',
|
|
417
|
+
"<": "<",
|
|
418
|
+
">": ">",
|
|
419
|
+
"&": "&",
|
|
420
|
+
""": '"',
|
|
421
|
+
"<": "<",
|
|
422
|
+
">": ">",
|
|
423
|
+
"&": "&"
|
|
424
|
+
};
|
|
425
|
+
var MAX_DECODE_ITERATIONS = 5;
|
|
426
|
+
function normalize(value, options = {}) {
|
|
427
|
+
if (typeof value !== "string") {
|
|
428
|
+
return value;
|
|
429
|
+
}
|
|
430
|
+
const opts = {
|
|
431
|
+
urlDecode: true,
|
|
432
|
+
htmlDecode: true,
|
|
433
|
+
sqlComments: true,
|
|
434
|
+
nullBytes: true,
|
|
435
|
+
whitespace: true,
|
|
436
|
+
...options
|
|
437
|
+
};
|
|
438
|
+
let result = value;
|
|
439
|
+
if (opts.nullBytes) {
|
|
440
|
+
result = removeNullBytes(result);
|
|
441
|
+
}
|
|
442
|
+
if (opts.urlDecode) {
|
|
443
|
+
result = urlDecode(result);
|
|
444
|
+
}
|
|
445
|
+
if (opts.htmlDecode) {
|
|
446
|
+
result = htmlEntityDecode(result);
|
|
447
|
+
}
|
|
448
|
+
if (opts.sqlComments) {
|
|
449
|
+
result = removeSqlComments(result);
|
|
450
|
+
}
|
|
451
|
+
if (opts.whitespace) {
|
|
452
|
+
result = normalizeWhitespace(result);
|
|
453
|
+
}
|
|
454
|
+
return result;
|
|
455
|
+
}
|
|
456
|
+
function urlDecode(value) {
|
|
457
|
+
if (typeof value !== "string") {
|
|
458
|
+
return value;
|
|
459
|
+
}
|
|
460
|
+
let result = value;
|
|
461
|
+
let previous = "";
|
|
462
|
+
let iterations = 0;
|
|
463
|
+
while (result !== previous && iterations < MAX_DECODE_ITERATIONS) {
|
|
464
|
+
previous = result;
|
|
465
|
+
iterations++;
|
|
466
|
+
result = safeUrlDecode(result);
|
|
467
|
+
}
|
|
468
|
+
return result;
|
|
469
|
+
}
|
|
470
|
+
var utf8 = new TextDecoder();
|
|
471
|
+
function safeUrlDecode(value) {
|
|
472
|
+
return value.replace(/(?:%[0-9A-Fa-f]{2})+/g, (run) => {
|
|
473
|
+
try {
|
|
474
|
+
return decodeURIComponent(run);
|
|
475
|
+
} catch {
|
|
476
|
+
const bytes = new Uint8Array(run.length / 3);
|
|
477
|
+
for (let i = 0; i < bytes.length; i++) bytes[i] = parseInt(run.slice(i * 3 + 1, i * 3 + 3), 16);
|
|
478
|
+
return utf8.decode(bytes);
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
function decodeQueryPlus(target) {
|
|
483
|
+
if (typeof target !== "string") return target;
|
|
484
|
+
const query = target.indexOf("?");
|
|
485
|
+
if (query === -1) return target;
|
|
486
|
+
return target.slice(0, query + 1) + target.slice(query + 1).replace(/\+/g, " ");
|
|
487
|
+
}
|
|
488
|
+
function htmlEntityDecode(value) {
|
|
489
|
+
if (typeof value !== "string") {
|
|
490
|
+
return value;
|
|
491
|
+
}
|
|
492
|
+
let result = value;
|
|
493
|
+
for (const [entity, char] of Object.entries(HTML_ENTITIES)) {
|
|
494
|
+
result = result.split(entity).join(char);
|
|
495
|
+
}
|
|
496
|
+
return decodeHtmlEntities(result);
|
|
391
497
|
}
|
|
392
498
|
var NAMED_ENTITIES = {
|
|
393
499
|
lt: "<",
|
|
@@ -395,7 +501,6 @@ var NAMED_ENTITIES = {
|
|
|
395
501
|
amp: "&",
|
|
396
502
|
quot: '"',
|
|
397
503
|
apos: "'",
|
|
398
|
-
"#39": "'",
|
|
399
504
|
nbsp: "\xA0",
|
|
400
505
|
sol: "/",
|
|
401
506
|
bsol: "\\",
|
|
@@ -414,7 +519,7 @@ var NAMED_ENTITIES = {
|
|
|
414
519
|
ast: "*"
|
|
415
520
|
};
|
|
416
521
|
function decodeHtmlEntities(input) {
|
|
417
|
-
return input.replace(/&(#[xX]
|
|
522
|
+
return input.replace(/&(#[xX][0-9a-fA-F]+|#\d+|[A-Za-z][A-Za-z0-9]*);?/g, (whole, body) => {
|
|
418
523
|
if (body[0] === "#") {
|
|
419
524
|
const hex = body[1] === "x" || body[1] === "X";
|
|
420
525
|
const code = Number.parseInt(hex ? body.slice(2) : body.slice(1), hex ? 16 : 10);
|
|
@@ -428,74 +533,359 @@ function decodeHtmlEntities(input) {
|
|
|
428
533
|
return Object.prototype.hasOwnProperty.call(NAMED_ENTITIES, body) ? NAMED_ENTITIES[body] : whole;
|
|
429
534
|
});
|
|
430
535
|
}
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
constructor(req) {
|
|
435
|
-
this.#req = req;
|
|
436
|
-
this.#cookies = null;
|
|
536
|
+
function removeSqlComments(value) {
|
|
537
|
+
if (typeof value !== "string") {
|
|
538
|
+
return value;
|
|
437
539
|
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
540
|
+
let result = value;
|
|
541
|
+
result = result.replace(/\/\*[\s\S]*?\*\//g, " ");
|
|
542
|
+
result = result.replace(/\/\*![\s\S]*?\*\//g, " ");
|
|
543
|
+
return result;
|
|
544
|
+
}
|
|
545
|
+
function removeNullBytes(value) {
|
|
546
|
+
if (typeof value !== "string") {
|
|
547
|
+
return value;
|
|
548
|
+
}
|
|
549
|
+
let result = value.replace(/\x00/g, "");
|
|
550
|
+
result = result.replace(/[\x01-\x08\x0B\x0C\x0E-\x1F]/g, "");
|
|
551
|
+
return result;
|
|
552
|
+
}
|
|
553
|
+
function normalizeWhitespace(value) {
|
|
554
|
+
if (typeof value !== "string") {
|
|
555
|
+
return value;
|
|
556
|
+
}
|
|
557
|
+
let result = value.replace(/[\t\r\n\f\v]+/g, " ");
|
|
558
|
+
result = result.replace(/ {2,}/g, " ");
|
|
559
|
+
return result;
|
|
560
|
+
}
|
|
561
|
+
var MAX_RAW_CHARS = 1024 * 1024;
|
|
562
|
+
var MAX_RAW_NODES = 25e3;
|
|
563
|
+
var MAX_RAW_DEPTH = 200;
|
|
564
|
+
var MAX_RAW_ENTRIES = 2e3;
|
|
565
|
+
function serializeForRawDetection(body) {
|
|
566
|
+
if (body === null || body === void 0) return "";
|
|
567
|
+
if (typeof body === "string") return body.slice(0, MAX_RAW_CHARS);
|
|
568
|
+
const visited = /* @__PURE__ */ new Set();
|
|
569
|
+
const output = [];
|
|
570
|
+
const stack = [{ kind: "value", value: body, depth: 0, root: true }];
|
|
571
|
+
let chars = 0;
|
|
572
|
+
let nodes = 0;
|
|
573
|
+
const append = (text) => {
|
|
574
|
+
if (chars >= MAX_RAW_CHARS) return;
|
|
575
|
+
const piece = String(text).slice(0, MAX_RAW_CHARS - chars);
|
|
576
|
+
output.push(piece);
|
|
577
|
+
chars += piece.length;
|
|
578
|
+
};
|
|
579
|
+
while (stack.length > 0 && chars < MAX_RAW_CHARS) {
|
|
580
|
+
const task = stack.pop();
|
|
581
|
+
if (task.kind === "text") {
|
|
582
|
+
append(task.text);
|
|
583
|
+
continue;
|
|
441
584
|
}
|
|
442
|
-
if (
|
|
443
|
-
|
|
585
|
+
if (task.kind === "key") {
|
|
586
|
+
append(boundedJsonString(task.value));
|
|
587
|
+
append(":");
|
|
588
|
+
continue;
|
|
444
589
|
}
|
|
445
|
-
|
|
446
|
-
|
|
590
|
+
nodes++;
|
|
591
|
+
if (nodes > MAX_RAW_NODES) {
|
|
592
|
+
append('"[NodeLimit]"');
|
|
593
|
+
continue;
|
|
447
594
|
}
|
|
448
|
-
|
|
449
|
-
|
|
595
|
+
const value = task.value;
|
|
596
|
+
if (value === null || value === void 0) {
|
|
597
|
+
append(task.root ? "" : "null");
|
|
598
|
+
continue;
|
|
450
599
|
}
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
600
|
+
if (typeof value === "string") {
|
|
601
|
+
append(task.root ? value.slice(0, MAX_RAW_CHARS) : boundedJsonString(value));
|
|
602
|
+
continue;
|
|
454
603
|
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
case "get":
|
|
459
|
-
return this.#resolveGet(key);
|
|
460
|
-
case "post":
|
|
461
|
-
return this.#resolvePost(key);
|
|
462
|
-
case "request":
|
|
463
|
-
return this.#resolveRequest(key);
|
|
464
|
-
case "cookie":
|
|
465
|
-
return this.#resolveCookie(key);
|
|
466
|
-
case "server":
|
|
467
|
-
return this.#resolveServer(key);
|
|
468
|
-
case "files":
|
|
469
|
-
return this.#resolveFiles(key);
|
|
470
|
-
case "response":
|
|
471
|
-
return this.#resolveResponse(key);
|
|
472
|
-
case "egress":
|
|
473
|
-
return this.#resolveEgress(key);
|
|
474
|
-
default:
|
|
475
|
-
return [];
|
|
604
|
+
if (typeof value !== "object") {
|
|
605
|
+
append(String(value).slice(0, MAX_RAW_CHARS));
|
|
606
|
+
continue;
|
|
476
607
|
}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
#resolveResponse(key) {
|
|
481
|
-
const resp = this.#req._response;
|
|
482
|
-
if (!resp) {
|
|
483
|
-
return [];
|
|
608
|
+
if (visited.has(value)) {
|
|
609
|
+
append("[Circular]");
|
|
610
|
+
continue;
|
|
484
611
|
}
|
|
485
|
-
if (
|
|
486
|
-
|
|
612
|
+
if (task.depth >= MAX_RAW_DEPTH) {
|
|
613
|
+
append('"[DepthLimit]"');
|
|
614
|
+
continue;
|
|
487
615
|
}
|
|
488
|
-
|
|
489
|
-
|
|
616
|
+
visited.add(value);
|
|
617
|
+
if (Array.isArray(value)) {
|
|
618
|
+
const length2 = Math.min(value.length, MAX_RAW_ENTRIES);
|
|
619
|
+
const items = [];
|
|
620
|
+
for (let i = 0; i < length2; i++) {
|
|
621
|
+
if (i > 0) items.push({ kind: "text", text: "," });
|
|
622
|
+
items.push({ kind: "value", value: readOwnValue(value, String(i)), depth: task.depth + 1, root: false });
|
|
623
|
+
}
|
|
624
|
+
if (value.length > length2) {
|
|
625
|
+
if (length2 > 0) items.push({ kind: "text", text: "," });
|
|
626
|
+
items.push({ kind: "text", text: '"[EntryLimit]"' });
|
|
627
|
+
}
|
|
628
|
+
stack.push({ kind: "text", text: "]" });
|
|
629
|
+
for (let i = items.length - 1; i >= 0; i--) stack.push(items[i]);
|
|
630
|
+
stack.push({ kind: "text", text: "[" });
|
|
631
|
+
continue;
|
|
490
632
|
}
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
633
|
+
let keys;
|
|
634
|
+
try {
|
|
635
|
+
keys = Object.getOwnPropertyNames(value);
|
|
636
|
+
} catch {
|
|
637
|
+
append('"[Unreadable]"');
|
|
638
|
+
continue;
|
|
494
639
|
}
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
640
|
+
const length = Math.min(keys.length, MAX_RAW_ENTRIES);
|
|
641
|
+
const entries = [];
|
|
642
|
+
for (let i = 0; i < length; i++) {
|
|
643
|
+
const key = keys[i];
|
|
644
|
+
if (i > 0) entries.push({ kind: "text", text: "," });
|
|
645
|
+
entries.push({ kind: "key", value: key });
|
|
646
|
+
entries.push({ kind: "value", value: readOwnValue(value, key), depth: task.depth + 1, root: false });
|
|
647
|
+
}
|
|
648
|
+
if (keys.length > length) {
|
|
649
|
+
if (length > 0) entries.push({ kind: "text", text: "," });
|
|
650
|
+
entries.push({ kind: "text", text: '"[EntryLimit]":null' });
|
|
651
|
+
}
|
|
652
|
+
stack.push({ kind: "text", text: "}" });
|
|
653
|
+
for (let i = entries.length - 1; i >= 0; i--) stack.push(entries[i]);
|
|
654
|
+
stack.push({ kind: "text", text: "{" });
|
|
655
|
+
}
|
|
656
|
+
return output.join("");
|
|
657
|
+
}
|
|
658
|
+
function boundedJsonString(value) {
|
|
659
|
+
return JSON.stringify(String(value).slice(0, MAX_RAW_CHARS));
|
|
660
|
+
}
|
|
661
|
+
function readOwnValue(value, key) {
|
|
662
|
+
try {
|
|
663
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
664
|
+
return descriptor && Object.hasOwn(descriptor, "value") ? descriptor.value : "[Accessor]";
|
|
665
|
+
} catch {
|
|
666
|
+
return "[Unreadable]";
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
var INHERITED_ACCESSORS = /* @__PURE__ */ new Set(["headers", "query"]);
|
|
670
|
+
function requestField(req, key) {
|
|
671
|
+
if (req === null || typeof req !== "object") return void 0;
|
|
672
|
+
if (Object.hasOwn(req, key)) return req[key];
|
|
673
|
+
if (!INHERITED_ACCESSORS.has(key)) return void 0;
|
|
674
|
+
let holder = Object.getPrototypeOf(req);
|
|
675
|
+
while (holder !== null && !Object.hasOwn(holder, key)) holder = Object.getPrototypeOf(holder);
|
|
676
|
+
if (holder === null || holder === Object.prototype) return void 0;
|
|
677
|
+
const descriptor = Object.getOwnPropertyDescriptor(holder, key);
|
|
678
|
+
if (typeof descriptor?.get !== "function") return void 0;
|
|
679
|
+
return req[key];
|
|
680
|
+
}
|
|
681
|
+
function normalizeRequest(req, options = {}) {
|
|
682
|
+
const ownRaw = Object.hasOwn(req ?? {}, "_rawBody") && typeof req._rawBody === "string";
|
|
683
|
+
const body = requestField(req, "body");
|
|
684
|
+
const url = requestField(req, "url");
|
|
685
|
+
const rawBody = ownRaw ? req._rawBody : serializeForRawDetection(body ?? null);
|
|
686
|
+
const headers = requestField(req, "headers") || {};
|
|
687
|
+
return {
|
|
688
|
+
query: normalizeObject(requestField(req, "query") || {}, options),
|
|
689
|
+
body: normalizeObject(body || {}, options),
|
|
690
|
+
headers: normalizeObject(headers, options),
|
|
691
|
+
// Cookies are split into name/value pairs BEFORE their values are normalised, and normalised the
|
|
692
|
+
// same way whether a framework parsed them or they come from the header — so a rule on a cookie
|
|
693
|
+
// sees one value, independent of which cookie parser (if any) ran first.
|
|
694
|
+
cookies: normalizeObject(cookieValues(requestField(req, "cookies"), headers), options),
|
|
695
|
+
url: normalize(decodeQueryPlus(url || ""), options),
|
|
696
|
+
originalUrl: normalize(decodeQueryPlus(requestField(req, "originalUrl") || url || ""), options),
|
|
697
|
+
_rawBody: rawBody
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
function cookieValues(parsed, headers) {
|
|
701
|
+
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
|
|
702
|
+
return parseCookieHeader(headers?.cookie);
|
|
703
|
+
}
|
|
704
|
+
var MAX_NORMALIZE_DEPTH = 1e3;
|
|
705
|
+
function normalizeObject(value, options = {}, depth = 0) {
|
|
706
|
+
if (typeof value === "string") {
|
|
707
|
+
return normalize(value, options);
|
|
708
|
+
}
|
|
709
|
+
if (value === null || typeof value !== "object") {
|
|
710
|
+
return value;
|
|
711
|
+
}
|
|
712
|
+
if (depth >= MAX_NORMALIZE_DEPTH) {
|
|
713
|
+
options.onLimit?.();
|
|
714
|
+
return value;
|
|
715
|
+
}
|
|
716
|
+
const copies = /* @__PURE__ */ new Map();
|
|
717
|
+
const copyOf = (node) => {
|
|
718
|
+
const copy = Array.isArray(node) ? new Array(node.length) : {};
|
|
719
|
+
copies.set(node, copy);
|
|
720
|
+
return copy;
|
|
721
|
+
};
|
|
722
|
+
const top = copyOf(value);
|
|
723
|
+
const stack = [[value, top, depth]];
|
|
724
|
+
while (stack.length > 0) {
|
|
725
|
+
const [node, copy, level] = stack.pop();
|
|
726
|
+
for (const key of Object.keys(node)) {
|
|
727
|
+
const child = node[key];
|
|
728
|
+
if (typeof child === "string") {
|
|
729
|
+
setOwn(copy, key, normalize(child, options));
|
|
730
|
+
} else if (child === null || typeof child !== "object") {
|
|
731
|
+
setOwn(copy, key, child);
|
|
732
|
+
} else if (copies.has(child)) {
|
|
733
|
+
setOwn(copy, key, copies.get(child));
|
|
734
|
+
} else if (level + 1 >= MAX_NORMALIZE_DEPTH) {
|
|
735
|
+
setOwn(copy, key, child);
|
|
736
|
+
options.onLimit?.();
|
|
737
|
+
} else {
|
|
738
|
+
const childCopy = copyOf(child);
|
|
739
|
+
setOwn(copy, key, childCopy);
|
|
740
|
+
stack.push([child, childCopy, level + 1]);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
return top;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// src/protect/engine/request.js
|
|
748
|
+
var FILE_ATTRS = /* @__PURE__ */ new Set(["content", "filename", "type"]);
|
|
749
|
+
var ownValue = (obj, key) => obj !== null && typeof obj === "object" && Object.prototype.hasOwnProperty.call(obj, key) ? obj[key] : void 0;
|
|
750
|
+
function fieldSpellings(key) {
|
|
751
|
+
const spellings = [{ key, nested: true }];
|
|
752
|
+
if (key.includes("[")) {
|
|
753
|
+
const dotted = key.replace(/\[\]$/, "").replace(/\[([^\][]*)\]/g, ".$1");
|
|
754
|
+
if (dotted !== key && dotted !== "" && !dotted.includes("[") && !dotted.includes("]")) {
|
|
755
|
+
spellings.push({ key: dotted, nested: true });
|
|
756
|
+
}
|
|
757
|
+
return spellings;
|
|
758
|
+
}
|
|
759
|
+
const [head, ...rest] = key.split(".");
|
|
760
|
+
const bracketed = head + rest.map((part) => `[${part}]`).join("");
|
|
761
|
+
if (bracketed !== key) spellings.push({ key: bracketed, nested: false });
|
|
762
|
+
spellings.push({ key: `${bracketed}[]`, nested: false });
|
|
763
|
+
return spellings;
|
|
764
|
+
}
|
|
765
|
+
var fileFilename = (f) => f && typeof f === "object" ? f.filename : f;
|
|
766
|
+
var fileAttribute = (f, attr) => f && typeof f === "object" ? f[attr] : attr === "filename" ? f : void 0;
|
|
767
|
+
function base64DecodeUtf8(value) {
|
|
768
|
+
const str = String(value);
|
|
769
|
+
if (typeof Buffer !== "undefined") {
|
|
770
|
+
return Buffer.from(str, "base64").toString("utf-8");
|
|
771
|
+
}
|
|
772
|
+
const binary = atob(str);
|
|
773
|
+
const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
|
|
774
|
+
return new TextDecoder().decode(bytes);
|
|
775
|
+
}
|
|
776
|
+
var TEXT_MUTATIONS = /* @__PURE__ */ new Set(["base64_decode", "urldecode", "htmlentitydecode"]);
|
|
777
|
+
var MAX_MAPPED_NODES = 2e4;
|
|
778
|
+
var MAX_MAPPED_DEPTH = 1e3;
|
|
779
|
+
function mapStringLeaves(root, fn, onLimit) {
|
|
780
|
+
const copies = /* @__PURE__ */ new Map();
|
|
781
|
+
const copyOf = (node) => {
|
|
782
|
+
const copy = Array.isArray(node) ? [] : {};
|
|
783
|
+
copies.set(node, copy);
|
|
784
|
+
return copy;
|
|
785
|
+
};
|
|
786
|
+
const top = copyOf(root);
|
|
787
|
+
const stack = [[root, top, 0]];
|
|
788
|
+
let visited = 0;
|
|
789
|
+
while (stack.length) {
|
|
790
|
+
const [node, copy, depth] = stack.pop();
|
|
791
|
+
visited++;
|
|
792
|
+
for (const key of Object.keys(node)) {
|
|
793
|
+
const child = node[key];
|
|
794
|
+
if (typeof child === "string") {
|
|
795
|
+
setOwn(copy, key, fn(child));
|
|
796
|
+
} else if (child === null || typeof child !== "object") {
|
|
797
|
+
setOwn(copy, key, child);
|
|
798
|
+
} else if (copies.has(child)) {
|
|
799
|
+
setOwn(copy, key, copies.get(child));
|
|
800
|
+
} else if (depth + 1 >= MAX_MAPPED_DEPTH || visited + stack.length >= MAX_MAPPED_NODES) {
|
|
801
|
+
setOwn(copy, key, child);
|
|
802
|
+
onLimit();
|
|
803
|
+
} else {
|
|
804
|
+
const childCopy = copyOf(child);
|
|
805
|
+
setOwn(copy, key, childCopy);
|
|
806
|
+
stack.push([child, childCopy, depth + 1]);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
return top;
|
|
811
|
+
}
|
|
812
|
+
var RequestResolver = class {
|
|
813
|
+
#req;
|
|
814
|
+
#cookies;
|
|
815
|
+
#skips = /* @__PURE__ */ new Set();
|
|
816
|
+
constructor(req) {
|
|
817
|
+
this.#req = req;
|
|
818
|
+
this.#cookies = null;
|
|
819
|
+
}
|
|
820
|
+
/** Record an inspection limit this evaluation reached (reported once per reason). */
|
|
821
|
+
noteSkip(reason) {
|
|
822
|
+
this.#skips.add(reason);
|
|
823
|
+
}
|
|
824
|
+
/** The inspection limits recorded so far, in the order first reached. */
|
|
825
|
+
get skips() {
|
|
826
|
+
return [...this.#skips];
|
|
827
|
+
}
|
|
828
|
+
resolve(parameter) {
|
|
829
|
+
if (!parameter || parameter === "false") {
|
|
830
|
+
return [null];
|
|
831
|
+
}
|
|
832
|
+
if (parameter === "rules") {
|
|
833
|
+
return [null];
|
|
834
|
+
}
|
|
835
|
+
if (parameter === "raw") {
|
|
836
|
+
return this.#resolveRaw();
|
|
837
|
+
}
|
|
838
|
+
if (parameter === "all") {
|
|
839
|
+
return this.#resolveAll();
|
|
840
|
+
}
|
|
841
|
+
const dotIndex = parameter.indexOf(".");
|
|
842
|
+
if (dotIndex === -1) {
|
|
843
|
+
return [];
|
|
844
|
+
}
|
|
845
|
+
const source = parameter.substring(0, dotIndex);
|
|
846
|
+
const key = parameter.substring(dotIndex + 1);
|
|
847
|
+
switch (source) {
|
|
848
|
+
case "get":
|
|
849
|
+
return this.#resolveGet(key);
|
|
850
|
+
case "post":
|
|
851
|
+
return this.#resolvePost(key);
|
|
852
|
+
case "request":
|
|
853
|
+
return this.#resolveRequest(key);
|
|
854
|
+
case "cookie":
|
|
855
|
+
return this.#resolveCookie(key);
|
|
856
|
+
case "server":
|
|
857
|
+
return this.#resolveServer(key);
|
|
858
|
+
case "files":
|
|
859
|
+
return this.#resolveFiles(key);
|
|
860
|
+
case "response":
|
|
861
|
+
return this.#resolveResponse(key);
|
|
862
|
+
case "egress":
|
|
863
|
+
return this.#resolveEgress(key);
|
|
864
|
+
default:
|
|
865
|
+
return [];
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
// Response-phase sources (req._response = { status, headers, body }). Lets rules inspect
|
|
869
|
+
// what the app is about to SEND — e.g. a leaked secret in the body — regardless of route.
|
|
870
|
+
#resolveResponse(key) {
|
|
871
|
+
const resp = this.#req._response;
|
|
872
|
+
if (!resp) {
|
|
873
|
+
return [];
|
|
874
|
+
}
|
|
875
|
+
if (key === "status") {
|
|
876
|
+
return resp.status !== void 0 ? [String(resp.status)] : [];
|
|
877
|
+
}
|
|
878
|
+
if (key === "body") {
|
|
879
|
+
return resp.body != null && resp.body !== "" ? [resp.body] : [];
|
|
880
|
+
}
|
|
881
|
+
if (key === "headers") {
|
|
882
|
+
const headers = resp.headers ?? {};
|
|
883
|
+
return [Object.entries(headers).map(([k, v]) => `${k}: ${v}`).join("\n")];
|
|
884
|
+
}
|
|
885
|
+
if (key.startsWith("header.")) {
|
|
886
|
+
const name = key.slice("header.".length).toLowerCase();
|
|
887
|
+
const headers = resp.headers ?? {};
|
|
888
|
+
return Object.hasOwn(headers, name) ? [headers[name]] : [];
|
|
499
889
|
}
|
|
500
890
|
return [];
|
|
501
891
|
}
|
|
@@ -525,6 +915,9 @@ var RequestResolver = class {
|
|
|
525
915
|
if (value === null || value === void 0) {
|
|
526
916
|
return value;
|
|
527
917
|
}
|
|
918
|
+
if (typeof value === "object" && TEXT_MUTATIONS.has(mutation)) {
|
|
919
|
+
return mapStringLeaves(value, (leaf) => this.#applyMutation(mutation, leaf), () => this.noteSkip("container-cap"));
|
|
920
|
+
}
|
|
528
921
|
switch (mutation) {
|
|
529
922
|
case "base64_decode":
|
|
530
923
|
try {
|
|
@@ -545,11 +938,7 @@ var RequestResolver = class {
|
|
|
545
938
|
return value;
|
|
546
939
|
}
|
|
547
940
|
case "urldecode":
|
|
548
|
-
|
|
549
|
-
return decodeURIComponent(String(value));
|
|
550
|
-
} catch {
|
|
551
|
-
return value;
|
|
552
|
-
}
|
|
941
|
+
return safeUrlDecode(String(value).replace(/\+/g, " "));
|
|
553
942
|
case "htmlentitydecode":
|
|
554
943
|
return decodeHtmlEntities(String(value));
|
|
555
944
|
case "intval":
|
|
@@ -568,16 +957,30 @@ var RequestResolver = class {
|
|
|
568
957
|
if (key.endsWith("*")) {
|
|
569
958
|
return this.#resolveWildcard(query, key);
|
|
570
959
|
}
|
|
571
|
-
|
|
572
|
-
return value !== void 0 ? [value] : [];
|
|
960
|
+
return this.#lookup(query, key);
|
|
573
961
|
}
|
|
574
962
|
#resolvePost(key) {
|
|
575
963
|
const body = this.#req.body ?? {};
|
|
576
964
|
if (key.endsWith("*")) {
|
|
577
965
|
return this.#resolveWildcard(body, key);
|
|
578
966
|
}
|
|
579
|
-
|
|
580
|
-
|
|
967
|
+
return this.#lookup(body, key);
|
|
968
|
+
}
|
|
969
|
+
/**
|
|
970
|
+
* Every value a field path names in `obj`, in either spelling a form or query field can take.
|
|
971
|
+
*
|
|
972
|
+
* A parser that expands brackets (`qs`, as Express uses) turns `user[name]=x` into `{ user: { name } }`
|
|
973
|
+
* and `id[]=x` into `{ id: [x] }`; a flat parser (`URLSearchParams`, as the Fetch and Node adapters use)
|
|
974
|
+
* keeps `user[name]` and `id[]` as literal keys. The rule names the field once, so both shapes answer
|
|
975
|
+
* to both spellings: `user.name` and `user[name]`, `id` and `id[]`.
|
|
976
|
+
*/
|
|
977
|
+
#lookup(obj, key) {
|
|
978
|
+
const values = [];
|
|
979
|
+
for (const candidate of fieldSpellings(key)) {
|
|
980
|
+
const value = candidate.nested ? this.#getNestedValue(obj, candidate.key) : ownValue(obj, candidate.key);
|
|
981
|
+
if (value !== void 0) values.push(value);
|
|
982
|
+
}
|
|
983
|
+
return values;
|
|
581
984
|
}
|
|
582
985
|
#resolveRequest(key) {
|
|
583
986
|
const query = this.#req.query ?? {};
|
|
@@ -590,8 +993,11 @@ var RequestResolver = class {
|
|
|
590
993
|
...this.#resolveWildcard(cookies, key)
|
|
591
994
|
];
|
|
592
995
|
}
|
|
593
|
-
const
|
|
594
|
-
|
|
996
|
+
const fromQuery = this.#lookup(query, key);
|
|
997
|
+
if (fromQuery.length > 0) return fromQuery;
|
|
998
|
+
const fromBody = this.#lookup(body, key);
|
|
999
|
+
if (fromBody.length > 0) return fromBody;
|
|
1000
|
+
return Object.hasOwn(cookies, key) ? [cookies[key]] : [];
|
|
595
1001
|
}
|
|
596
1002
|
#resolveCookie(key) {
|
|
597
1003
|
const cookies = this.#parseCookies();
|
|
@@ -666,424 +1072,146 @@ var RequestResolver = class {
|
|
|
666
1072
|
}
|
|
667
1073
|
const body = this.#req.body;
|
|
668
1074
|
if (body === void 0 || body === null) {
|
|
669
|
-
return [];
|
|
670
|
-
}
|
|
671
|
-
if (typeof body === "string") {
|
|
672
|
-
return [body];
|
|
673
|
-
}
|
|
674
|
-
try {
|
|
675
|
-
return [JSON.stringify(body)];
|
|
676
|
-
} catch {
|
|
677
|
-
return [String(body)];
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
#resolveAll() {
|
|
681
|
-
const parts = [];
|
|
682
|
-
const uri = this.#req.originalUrl ?? this.#req.url ?? "/";
|
|
683
|
-
parts.push(uri);
|
|
684
|
-
const queryString = uri.includes("?") ? uri.split("?")[1] : "";
|
|
685
|
-
if (queryString) {
|
|
686
|
-
parts.push(queryString);
|
|
687
|
-
}
|
|
688
|
-
const body = this.#req.body;
|
|
689
|
-
if (body) {
|
|
690
|
-
parts.push(typeof body === "string" ? body : JSON.stringify(body));
|
|
691
|
-
}
|
|
692
|
-
if (typeof this.#req._rawBody === "string" && this.#req._rawBody) {
|
|
693
|
-
parts.push(this.#req._rawBody);
|
|
694
|
-
}
|
|
695
|
-
const headers = this.#req.headers ?? {};
|
|
696
|
-
const excludedHeaders = /* @__PURE__ */ new Set([
|
|
697
|
-
"host",
|
|
698
|
-
"connection",
|
|
699
|
-
"cache-control",
|
|
700
|
-
"accept",
|
|
701
|
-
"accept-encoding",
|
|
702
|
-
"accept-language",
|
|
703
|
-
"priority",
|
|
704
|
-
"sec-ch-ua",
|
|
705
|
-
"sec-ch-ua-mobile",
|
|
706
|
-
"sec-ch-ua-platform",
|
|
707
|
-
"sec-fetch-dest",
|
|
708
|
-
"sec-fetch-mode",
|
|
709
|
-
"sec-fetch-site",
|
|
710
|
-
"sec-fetch-user",
|
|
711
|
-
"upgrade-insecure-requests"
|
|
712
|
-
]);
|
|
713
|
-
for (const [name, value] of Object.entries(headers)) {
|
|
714
|
-
if (!excludedHeaders.has(name)) {
|
|
715
|
-
parts.push(`${name}: ${value}`);
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
const cookies = this.#parseCookies();
|
|
719
|
-
const cookieStr = Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join("; ");
|
|
720
|
-
if (cookieStr) {
|
|
721
|
-
parts.push(cookieStr);
|
|
722
|
-
}
|
|
723
|
-
return [parts.join(" ")];
|
|
724
|
-
}
|
|
725
|
-
#resolveWildcard(obj, pattern) {
|
|
726
|
-
if (typeof obj !== "object" || obj === null) {
|
|
727
|
-
return [];
|
|
728
|
-
}
|
|
729
|
-
const prefix = pattern.slice(0, -1);
|
|
730
|
-
const values = [];
|
|
731
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
732
|
-
if (key.startsWith(prefix)) {
|
|
733
|
-
values.push(value);
|
|
734
|
-
}
|
|
735
|
-
}
|
|
736
|
-
return values;
|
|
737
|
-
}
|
|
738
|
-
#getNestedValue(obj, key) {
|
|
739
|
-
if (typeof obj !== "object" || obj === null) {
|
|
740
|
-
return void 0;
|
|
741
|
-
}
|
|
742
|
-
const own = (o, k) => o !== null && typeof o === "object" && Object.prototype.hasOwnProperty.call(o, k);
|
|
743
|
-
if (own(obj, key)) {
|
|
744
|
-
return obj[key];
|
|
745
|
-
}
|
|
746
|
-
const parts = key.split(".");
|
|
747
|
-
let current = obj;
|
|
748
|
-
for (const part of parts) {
|
|
749
|
-
if (!own(current, part)) {
|
|
750
|
-
return void 0;
|
|
751
|
-
}
|
|
752
|
-
current = current[part];
|
|
753
|
-
}
|
|
754
|
-
return current;
|
|
755
|
-
}
|
|
756
|
-
#parseCookies() {
|
|
757
|
-
if (this.#cookies !== null) {
|
|
758
|
-
return this.#cookies;
|
|
759
|
-
}
|
|
760
|
-
if (this.#req.cookies) {
|
|
761
|
-
this.#cookies = this.#req.cookies;
|
|
762
|
-
return this.#cookies;
|
|
763
|
-
}
|
|
764
|
-
const header = this.#req.headers?.cookie;
|
|
765
|
-
if (!header) {
|
|
766
|
-
this.#cookies = {};
|
|
767
|
-
return this.#cookies;
|
|
768
|
-
}
|
|
769
|
-
const cookies = {};
|
|
770
|
-
for (const pair of header.split(";")) {
|
|
771
|
-
const eqIndex = pair.indexOf("=");
|
|
772
|
-
if (eqIndex === -1) {
|
|
773
|
-
continue;
|
|
774
|
-
}
|
|
775
|
-
const name = pair.substring(0, eqIndex).trim();
|
|
776
|
-
const value = pair.substring(eqIndex + 1).trim();
|
|
777
|
-
setOwn(cookies, name, value);
|
|
778
|
-
}
|
|
779
|
-
this.#cookies = cookies;
|
|
780
|
-
return this.#cookies;
|
|
781
|
-
}
|
|
782
|
-
};
|
|
783
|
-
|
|
784
|
-
// src/protect/notify.js
|
|
785
|
-
var reported = /* @__PURE__ */ new Set();
|
|
786
|
-
function notify(fn, arg, label) {
|
|
787
|
-
if (typeof fn !== "function") return false;
|
|
788
|
-
try {
|
|
789
|
-
const result = fn(arg);
|
|
790
|
-
if (result !== null && typeof result === "object" && typeof result.then === "function") {
|
|
791
|
-
try {
|
|
792
|
-
result.then(void 0, (err) => warnOnce(label, err));
|
|
793
|
-
} catch {
|
|
794
|
-
}
|
|
795
|
-
}
|
|
796
|
-
return true;
|
|
797
|
-
} catch (err) {
|
|
798
|
-
warnOnce(label, err);
|
|
799
|
-
return false;
|
|
800
|
-
}
|
|
801
|
-
}
|
|
802
|
-
function warnOnce(label, err) {
|
|
803
|
-
if (reported.has(label)) return;
|
|
804
|
-
reported.add(label);
|
|
805
|
-
try {
|
|
806
|
-
console.warn(
|
|
807
|
-
`Patchstack: the ${label} callback passed to createProtection failed and was ignored. Protection is unaffected; this is reported once per process. Cause: ${err && err.message ? err.message : String(err)}`
|
|
808
|
-
);
|
|
809
|
-
} catch {
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
// src/protect/engine/normalizer.js
|
|
814
|
-
var HTML_ENTITIES = {
|
|
815
|
-
"&": "&",
|
|
816
|
-
"<": "<",
|
|
817
|
-
">": ">",
|
|
818
|
-
""": '"',
|
|
819
|
-
"'": "'",
|
|
820
|
-
"'": "'",
|
|
821
|
-
" ": " ",
|
|
822
|
-
"'": "'",
|
|
823
|
-
""": '"',
|
|
824
|
-
"<": "<",
|
|
825
|
-
">": ">",
|
|
826
|
-
"&": "&",
|
|
827
|
-
""": '"',
|
|
828
|
-
"<": "<",
|
|
829
|
-
">": ">",
|
|
830
|
-
"&": "&"
|
|
831
|
-
};
|
|
832
|
-
var MAX_DECODE_ITERATIONS = 5;
|
|
833
|
-
function normalize(value, options = {}) {
|
|
834
|
-
if (typeof value !== "string") {
|
|
835
|
-
return value;
|
|
836
|
-
}
|
|
837
|
-
const opts = {
|
|
838
|
-
urlDecode: true,
|
|
839
|
-
htmlDecode: true,
|
|
840
|
-
sqlComments: true,
|
|
841
|
-
nullBytes: true,
|
|
842
|
-
whitespace: true,
|
|
843
|
-
...options
|
|
844
|
-
};
|
|
845
|
-
let result = value;
|
|
846
|
-
if (opts.nullBytes) {
|
|
847
|
-
result = removeNullBytes(result);
|
|
848
|
-
}
|
|
849
|
-
if (opts.urlDecode) {
|
|
850
|
-
result = urlDecode(result);
|
|
851
|
-
}
|
|
852
|
-
if (opts.htmlDecode) {
|
|
853
|
-
result = htmlEntityDecode(result);
|
|
854
|
-
}
|
|
855
|
-
if (opts.sqlComments) {
|
|
856
|
-
result = removeSqlComments(result);
|
|
857
|
-
}
|
|
858
|
-
if (opts.whitespace) {
|
|
859
|
-
result = normalizeWhitespace(result);
|
|
860
|
-
}
|
|
861
|
-
return result;
|
|
862
|
-
}
|
|
863
|
-
function urlDecode(value) {
|
|
864
|
-
if (typeof value !== "string") {
|
|
865
|
-
return value;
|
|
866
|
-
}
|
|
867
|
-
let result = value;
|
|
868
|
-
let previous = "";
|
|
869
|
-
let iterations = 0;
|
|
870
|
-
while (result !== previous && iterations < MAX_DECODE_ITERATIONS) {
|
|
871
|
-
previous = result;
|
|
872
|
-
iterations++;
|
|
873
|
-
try {
|
|
874
|
-
result = decodeURIComponent(result);
|
|
875
|
-
} catch {
|
|
876
|
-
result = safeUrlDecode(result);
|
|
877
|
-
break;
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
return result;
|
|
881
|
-
}
|
|
882
|
-
function safeUrlDecode(value) {
|
|
883
|
-
return value.replace(/%([0-9A-Fa-f]{2})/g, (match, hex) => {
|
|
884
|
-
try {
|
|
885
|
-
return String.fromCharCode(parseInt(hex, 16));
|
|
886
|
-
} catch {
|
|
887
|
-
return match;
|
|
888
|
-
}
|
|
889
|
-
});
|
|
890
|
-
}
|
|
891
|
-
function htmlEntityDecode(value) {
|
|
892
|
-
if (typeof value !== "string") {
|
|
893
|
-
return value;
|
|
894
|
-
}
|
|
895
|
-
let result = value;
|
|
896
|
-
for (const [entity, char] of Object.entries(HTML_ENTITIES)) {
|
|
897
|
-
result = result.split(entity).join(char);
|
|
898
|
-
}
|
|
899
|
-
result = result.replace(/&#(\d+);/g, (match, code) => {
|
|
900
|
-
const num = parseInt(code, 10);
|
|
901
|
-
return num > 0 && num < 65536 ? String.fromCharCode(num) : match;
|
|
902
|
-
});
|
|
903
|
-
result = result.replace(/&#x([0-9A-Fa-f]+);/g, (match, hex) => {
|
|
904
|
-
const num = parseInt(hex, 16);
|
|
905
|
-
return num > 0 && num < 65536 ? String.fromCharCode(num) : match;
|
|
906
|
-
});
|
|
907
|
-
return result;
|
|
908
|
-
}
|
|
909
|
-
function removeSqlComments(value) {
|
|
910
|
-
if (typeof value !== "string") {
|
|
911
|
-
return value;
|
|
912
|
-
}
|
|
913
|
-
let result = value;
|
|
914
|
-
result = result.replace(/\/\*[\s\S]*?\*\//g, " ");
|
|
915
|
-
result = result.replace(/\/\*![\s\S]*?\*\//g, " ");
|
|
916
|
-
return result;
|
|
917
|
-
}
|
|
918
|
-
function removeNullBytes(value) {
|
|
919
|
-
if (typeof value !== "string") {
|
|
920
|
-
return value;
|
|
921
|
-
}
|
|
922
|
-
let result = value.replace(/\x00/g, "");
|
|
923
|
-
result = result.replace(/[\x01-\x08\x0B\x0C\x0E-\x1F]/g, "");
|
|
924
|
-
return result;
|
|
925
|
-
}
|
|
926
|
-
function normalizeWhitespace(value) {
|
|
927
|
-
if (typeof value !== "string") {
|
|
928
|
-
return value;
|
|
929
|
-
}
|
|
930
|
-
let result = value.replace(/[\t\r\n\f\v]+/g, " ");
|
|
931
|
-
result = result.replace(/ {2,}/g, " ");
|
|
932
|
-
return result;
|
|
933
|
-
}
|
|
934
|
-
var MAX_RAW_CHARS = 1024 * 1024;
|
|
935
|
-
var MAX_RAW_NODES = 25e3;
|
|
936
|
-
var MAX_RAW_DEPTH = 200;
|
|
937
|
-
var MAX_RAW_ENTRIES = 2e3;
|
|
938
|
-
function serializeForRawDetection(body) {
|
|
939
|
-
if (body === null || body === void 0) return "";
|
|
940
|
-
if (typeof body === "string") return body.slice(0, MAX_RAW_CHARS);
|
|
941
|
-
const visited = /* @__PURE__ */ new Set();
|
|
942
|
-
const output = [];
|
|
943
|
-
const stack = [{ kind: "value", value: body, depth: 0, root: true }];
|
|
944
|
-
let chars = 0;
|
|
945
|
-
let nodes = 0;
|
|
946
|
-
const append = (text) => {
|
|
947
|
-
if (chars >= MAX_RAW_CHARS) return;
|
|
948
|
-
const piece = String(text).slice(0, MAX_RAW_CHARS - chars);
|
|
949
|
-
output.push(piece);
|
|
950
|
-
chars += piece.length;
|
|
951
|
-
};
|
|
952
|
-
while (stack.length > 0 && chars < MAX_RAW_CHARS) {
|
|
953
|
-
const task = stack.pop();
|
|
954
|
-
if (task.kind === "text") {
|
|
955
|
-
append(task.text);
|
|
956
|
-
continue;
|
|
957
|
-
}
|
|
958
|
-
if (task.kind === "key") {
|
|
959
|
-
append(boundedJsonString(task.value));
|
|
960
|
-
append(":");
|
|
961
|
-
continue;
|
|
962
|
-
}
|
|
963
|
-
nodes++;
|
|
964
|
-
if (nodes > MAX_RAW_NODES) {
|
|
965
|
-
append('"[NodeLimit]"');
|
|
966
|
-
continue;
|
|
1075
|
+
return [];
|
|
967
1076
|
}
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
append(task.root ? "" : "null");
|
|
971
|
-
continue;
|
|
1077
|
+
if (typeof body === "string") {
|
|
1078
|
+
return [body];
|
|
972
1079
|
}
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
1080
|
+
try {
|
|
1081
|
+
return [JSON.stringify(body)];
|
|
1082
|
+
} catch {
|
|
1083
|
+
return [String(body)];
|
|
976
1084
|
}
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
1085
|
+
}
|
|
1086
|
+
#resolveAll() {
|
|
1087
|
+
const parts = [];
|
|
1088
|
+
const uri = this.#req.originalUrl ?? this.#req.url ?? "/";
|
|
1089
|
+
parts.push(uri);
|
|
1090
|
+
const queryString = uri.includes("?") ? uri.split("?")[1] : "";
|
|
1091
|
+
if (queryString) {
|
|
1092
|
+
parts.push(queryString);
|
|
980
1093
|
}
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
1094
|
+
const body = this.#req.body;
|
|
1095
|
+
if (body) {
|
|
1096
|
+
parts.push(typeof body === "string" ? body : JSON.stringify(body));
|
|
984
1097
|
}
|
|
985
|
-
if (
|
|
986
|
-
|
|
987
|
-
continue;
|
|
1098
|
+
if (typeof this.#req._rawBody === "string" && this.#req._rawBody) {
|
|
1099
|
+
parts.push(this.#req._rawBody);
|
|
988
1100
|
}
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
1101
|
+
const headers = this.#req.headers ?? {};
|
|
1102
|
+
const excludedHeaders = /* @__PURE__ */ new Set([
|
|
1103
|
+
"host",
|
|
1104
|
+
"connection",
|
|
1105
|
+
"cache-control",
|
|
1106
|
+
"accept",
|
|
1107
|
+
"accept-encoding",
|
|
1108
|
+
"accept-language",
|
|
1109
|
+
"priority",
|
|
1110
|
+
"sec-ch-ua",
|
|
1111
|
+
"sec-ch-ua-mobile",
|
|
1112
|
+
"sec-ch-ua-platform",
|
|
1113
|
+
"sec-fetch-dest",
|
|
1114
|
+
"sec-fetch-mode",
|
|
1115
|
+
"sec-fetch-site",
|
|
1116
|
+
"sec-fetch-user",
|
|
1117
|
+
"upgrade-insecure-requests"
|
|
1118
|
+
]);
|
|
1119
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
1120
|
+
if (!excludedHeaders.has(name)) {
|
|
1121
|
+
parts.push(`${name}: ${value}`);
|
|
996
1122
|
}
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1123
|
+
}
|
|
1124
|
+
const cookies = this.#parseCookies();
|
|
1125
|
+
const cookieStr = Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join("; ");
|
|
1126
|
+
if (cookieStr) {
|
|
1127
|
+
parts.push(cookieStr);
|
|
1128
|
+
}
|
|
1129
|
+
return [parts.join(" ")];
|
|
1130
|
+
}
|
|
1131
|
+
#resolveWildcard(obj, pattern) {
|
|
1132
|
+
if (typeof obj !== "object" || obj === null) {
|
|
1133
|
+
return [];
|
|
1134
|
+
}
|
|
1135
|
+
const prefix = pattern.slice(0, -1);
|
|
1136
|
+
const values = [];
|
|
1137
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
1138
|
+
if (key.startsWith(prefix)) {
|
|
1139
|
+
values.push(value);
|
|
1000
1140
|
}
|
|
1001
|
-
stack.push({ kind: "text", text: "]" });
|
|
1002
|
-
for (let i = items.length - 1; i >= 0; i--) stack.push(items[i]);
|
|
1003
|
-
stack.push({ kind: "text", text: "[" });
|
|
1004
|
-
continue;
|
|
1005
1141
|
}
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
continue;
|
|
1142
|
+
return values;
|
|
1143
|
+
}
|
|
1144
|
+
#getNestedValue(obj, key) {
|
|
1145
|
+
if (typeof obj !== "object" || obj === null) {
|
|
1146
|
+
return void 0;
|
|
1012
1147
|
}
|
|
1013
|
-
const
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
const key = keys[i];
|
|
1017
|
-
if (i > 0) entries.push({ kind: "text", text: "," });
|
|
1018
|
-
entries.push({ kind: "key", value: key });
|
|
1019
|
-
entries.push({ kind: "value", value: readOwnValue(value, key), depth: task.depth + 1, root: false });
|
|
1148
|
+
const own = (o, k) => o !== null && typeof o === "object" && Object.prototype.hasOwnProperty.call(o, k);
|
|
1149
|
+
if (own(obj, key)) {
|
|
1150
|
+
return obj[key];
|
|
1020
1151
|
}
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1152
|
+
const parts = key.split(".");
|
|
1153
|
+
let current = obj;
|
|
1154
|
+
for (const part of parts) {
|
|
1155
|
+
if (!own(current, part)) {
|
|
1156
|
+
return void 0;
|
|
1157
|
+
}
|
|
1158
|
+
current = current[part];
|
|
1024
1159
|
}
|
|
1025
|
-
|
|
1026
|
-
for (let i = entries.length - 1; i >= 0; i--) stack.push(entries[i]);
|
|
1027
|
-
stack.push({ kind: "text", text: "{" });
|
|
1160
|
+
return current;
|
|
1028
1161
|
}
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1162
|
+
#parseCookies() {
|
|
1163
|
+
if (this.#cookies !== null) {
|
|
1164
|
+
return this.#cookies;
|
|
1165
|
+
}
|
|
1166
|
+
if (this.#req.cookies) {
|
|
1167
|
+
this.#cookies = this.#req.cookies;
|
|
1168
|
+
return this.#cookies;
|
|
1169
|
+
}
|
|
1170
|
+
this.#cookies = parseCookieHeader(this.#req.headers?.cookie);
|
|
1171
|
+
return this.#cookies;
|
|
1172
|
+
}
|
|
1173
|
+
};
|
|
1174
|
+
|
|
1175
|
+
// src/protect/notify.js
|
|
1176
|
+
var reported = /* @__PURE__ */ new WeakMap();
|
|
1177
|
+
var warnings = 0;
|
|
1178
|
+
var MAX_WARNINGS = 20;
|
|
1179
|
+
function notify(fn, arg, label) {
|
|
1180
|
+
if (typeof fn !== "function") return false;
|
|
1035
1181
|
try {
|
|
1036
|
-
const
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1182
|
+
const result = fn(arg);
|
|
1183
|
+
if (result !== null && typeof result === "object" && typeof result.then === "function") {
|
|
1184
|
+
try {
|
|
1185
|
+
result.then(void 0, (err) => warnOnce(fn, label, err));
|
|
1186
|
+
} catch {
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
return true;
|
|
1190
|
+
} catch (err) {
|
|
1191
|
+
warnOnce(fn, label, err);
|
|
1192
|
+
return false;
|
|
1040
1193
|
}
|
|
1041
1194
|
}
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
if (
|
|
1045
|
-
if (
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
while (holder !== null && !Object.hasOwn(holder, key)) holder = Object.getPrototypeOf(holder);
|
|
1049
|
-
if (holder === null || holder === Object.prototype) return void 0;
|
|
1050
|
-
const descriptor = Object.getOwnPropertyDescriptor(holder, key);
|
|
1051
|
-
if (typeof descriptor?.get !== "function") return void 0;
|
|
1052
|
-
return req[key];
|
|
1053
|
-
}
|
|
1054
|
-
function normalizeRequest(req, options = {}) {
|
|
1055
|
-
const ownRaw = Object.hasOwn(req ?? {}, "_rawBody") && typeof req._rawBody === "string";
|
|
1056
|
-
const body = requestField(req, "body");
|
|
1057
|
-
const url = requestField(req, "url");
|
|
1058
|
-
const rawBody = ownRaw ? req._rawBody : serializeForRawDetection(body ?? null);
|
|
1059
|
-
return {
|
|
1060
|
-
query: normalizeObject(requestField(req, "query") || {}, options),
|
|
1061
|
-
body: normalizeObject(body || {}, options),
|
|
1062
|
-
headers: normalizeObject(requestField(req, "headers") || {}, options),
|
|
1063
|
-
url: normalize(url || "", options),
|
|
1064
|
-
originalUrl: normalize(requestField(req, "originalUrl") || url || "", options),
|
|
1065
|
-
_rawBody: rawBody
|
|
1066
|
-
};
|
|
1067
|
-
}
|
|
1068
|
-
var MAX_NORMALIZE_DEPTH = 200;
|
|
1069
|
-
function normalizeObject(value, options = {}, depth = 0) {
|
|
1070
|
-
if (typeof value === "string") {
|
|
1071
|
-
return normalize(value, options);
|
|
1072
|
-
}
|
|
1073
|
-
if (depth >= MAX_NORMALIZE_DEPTH) {
|
|
1074
|
-
return value;
|
|
1075
|
-
}
|
|
1076
|
-
if (Array.isArray(value)) {
|
|
1077
|
-
return value.map((item) => normalizeObject(item, options, depth + 1));
|
|
1195
|
+
function warnOnce(fn, label, err) {
|
|
1196
|
+
let labels = reported.get(fn);
|
|
1197
|
+
if (labels?.has(label)) return;
|
|
1198
|
+
if (!labels) {
|
|
1199
|
+
labels = /* @__PURE__ */ new Set();
|
|
1200
|
+
reported.set(fn, labels);
|
|
1078
1201
|
}
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1202
|
+
labels.add(label);
|
|
1203
|
+
if (warnings > MAX_WARNINGS) return;
|
|
1204
|
+
warnings++;
|
|
1205
|
+
try {
|
|
1206
|
+
if (warnings > MAX_WARNINGS) {
|
|
1207
|
+
console.warn("Patchstack: further failing callbacks passed to createProtection are not reported in this process.");
|
|
1208
|
+
return;
|
|
1083
1209
|
}
|
|
1084
|
-
|
|
1210
|
+
console.warn(
|
|
1211
|
+
`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)}`
|
|
1212
|
+
);
|
|
1213
|
+
} catch {
|
|
1085
1214
|
}
|
|
1086
|
-
return value;
|
|
1087
1215
|
}
|
|
1088
1216
|
|
|
1089
1217
|
// src/protect/engine/engine.js
|
|
@@ -1185,6 +1313,184 @@ function hasAdjacentUnbounded(body, flags) {
|
|
|
1185
1313
|
}
|
|
1186
1314
|
return false;
|
|
1187
1315
|
}
|
|
1316
|
+
function parseRepetition(body) {
|
|
1317
|
+
let i = 0;
|
|
1318
|
+
const parseAlternation = () => {
|
|
1319
|
+
const branches = [parseSequence()];
|
|
1320
|
+
while (body[i] === "|") {
|
|
1321
|
+
i++;
|
|
1322
|
+
branches.push(parseSequence());
|
|
1323
|
+
}
|
|
1324
|
+
return branches.length === 1 ? branches[0] : { type: "alt", branches };
|
|
1325
|
+
};
|
|
1326
|
+
const parseSequence = () => {
|
|
1327
|
+
const items = [];
|
|
1328
|
+
while (i < body.length && body[i] !== "|" && body[i] !== ")") {
|
|
1329
|
+
const atom = parseAtom();
|
|
1330
|
+
if (atom === null) return null;
|
|
1331
|
+
const quantified = parseQuantifier(atom);
|
|
1332
|
+
if (quantified === null) return null;
|
|
1333
|
+
items.push(quantified);
|
|
1334
|
+
}
|
|
1335
|
+
return { type: "seq", items };
|
|
1336
|
+
};
|
|
1337
|
+
const parseAtom = () => {
|
|
1338
|
+
const ch = body[i];
|
|
1339
|
+
if (ch === "(") {
|
|
1340
|
+
const start2 = i;
|
|
1341
|
+
i++;
|
|
1342
|
+
let kind = "capture";
|
|
1343
|
+
if (body.startsWith("?:", i)) {
|
|
1344
|
+
kind = "group";
|
|
1345
|
+
i += 2;
|
|
1346
|
+
} else if (body.startsWith("?=", i) || body.startsWith("?!", i)) {
|
|
1347
|
+
kind = "look";
|
|
1348
|
+
i += 2;
|
|
1349
|
+
} else if (body.startsWith("?<=", i) || body.startsWith("?<!", i)) {
|
|
1350
|
+
kind = "look";
|
|
1351
|
+
i += 3;
|
|
1352
|
+
} else if (body.startsWith("?<", i)) {
|
|
1353
|
+
const close = body.indexOf(">", i + 2);
|
|
1354
|
+
if (close === -1) return null;
|
|
1355
|
+
i = close + 1;
|
|
1356
|
+
} else if (body[i] === "?") {
|
|
1357
|
+
return null;
|
|
1358
|
+
}
|
|
1359
|
+
const inner = parseAlternation();
|
|
1360
|
+
if (inner === null || body[i] !== ")") return null;
|
|
1361
|
+
i++;
|
|
1362
|
+
return { type: "group", kind, body: inner, source: body.slice(start2, i) };
|
|
1363
|
+
}
|
|
1364
|
+
const start = i;
|
|
1365
|
+
if (ch === "[") {
|
|
1366
|
+
i++;
|
|
1367
|
+
if (body[i] === "^") i++;
|
|
1368
|
+
if (body[i] === "]") i++;
|
|
1369
|
+
while (i < body.length && body[i] !== "]") i += body[i] === "\\" ? 2 : 1;
|
|
1370
|
+
if (body[i] !== "]") return null;
|
|
1371
|
+
i++;
|
|
1372
|
+
return { type: "atom", source: body.slice(start, i) };
|
|
1373
|
+
}
|
|
1374
|
+
if (ch === "\\") {
|
|
1375
|
+
const next = body[i + 1];
|
|
1376
|
+
if (next === void 0) return null;
|
|
1377
|
+
if ((next === "p" || next === "P" || next === "u") && body[i + 2] === "{") {
|
|
1378
|
+
const close = body.indexOf("}", i + 3);
|
|
1379
|
+
if (close === -1) return null;
|
|
1380
|
+
i = close + 1;
|
|
1381
|
+
} else if (next === "k" && body[i + 2] === "<") {
|
|
1382
|
+
const close = body.indexOf(">", i + 3);
|
|
1383
|
+
if (close === -1) return null;
|
|
1384
|
+
i = close + 1;
|
|
1385
|
+
} else {
|
|
1386
|
+
i += 2;
|
|
1387
|
+
}
|
|
1388
|
+
return { type: /[bB]/.test(next) ? "assert" : "atom", source: body.slice(start, i) };
|
|
1389
|
+
}
|
|
1390
|
+
if (ch === "^" || ch === "$") {
|
|
1391
|
+
i++;
|
|
1392
|
+
return { type: "assert" };
|
|
1393
|
+
}
|
|
1394
|
+
if (ch === "*" || ch === "+" || ch === "?") return null;
|
|
1395
|
+
i++;
|
|
1396
|
+
return { type: "atom", source: body.slice(start, i) };
|
|
1397
|
+
};
|
|
1398
|
+
const parseQuantifier = (atom) => {
|
|
1399
|
+
let min;
|
|
1400
|
+
let max;
|
|
1401
|
+
const ch = body[i];
|
|
1402
|
+
if (ch === "*") [min, max, i] = [0, Infinity, i + 1];
|
|
1403
|
+
else if (ch === "+") [min, max, i] = [1, Infinity, i + 1];
|
|
1404
|
+
else if (ch === "?") [min, max, i] = [0, 1, i + 1];
|
|
1405
|
+
else if (ch === "{") {
|
|
1406
|
+
const bounds = /^\{(\d+)(,(\d*))?\}/.exec(body.slice(i));
|
|
1407
|
+
if (!bounds) return atom;
|
|
1408
|
+
min = Number(bounds[1]);
|
|
1409
|
+
max = bounds[2] === void 0 ? min : bounds[3] === "" ? Infinity : Number(bounds[3]);
|
|
1410
|
+
i += bounds[0].length;
|
|
1411
|
+
} else {
|
|
1412
|
+
return atom;
|
|
1413
|
+
}
|
|
1414
|
+
if (body[i] === "?") i++;
|
|
1415
|
+
if (body[i] === "*" || body[i] === "+" || body[i] === "?" || body[i] === "{") return null;
|
|
1416
|
+
return { type: "quant", min, max, body: atom };
|
|
1417
|
+
};
|
|
1418
|
+
const root = parseAlternation();
|
|
1419
|
+
return root !== null && i === body.length ? root : null;
|
|
1420
|
+
}
|
|
1421
|
+
function* nodesOf(node) {
|
|
1422
|
+
if (!node) return;
|
|
1423
|
+
yield node;
|
|
1424
|
+
if (node.type === "alt") for (const branch of node.branches) yield* nodesOf(branch);
|
|
1425
|
+
else if (node.type === "seq") for (const item of node.items) yield* nodesOf(item);
|
|
1426
|
+
else if (node.type === "group" || node.type === "quant") yield* nodesOf(node.body);
|
|
1427
|
+
}
|
|
1428
|
+
var BOUNDED_NESTING_LIMIT = 100;
|
|
1429
|
+
var BOUNDED_ALTERNATION_LIMIT = 16;
|
|
1430
|
+
var OPTIONAL_REPEAT_LIMIT = 8;
|
|
1431
|
+
function hasFencedRepetitions(quant, flags) {
|
|
1432
|
+
let body = quant.body;
|
|
1433
|
+
if (body.type === "group" && body.kind !== "look") body = body.body;
|
|
1434
|
+
if (body.type !== "seq" || body.items.length < 2) return false;
|
|
1435
|
+
const variable = [...nodesOf(body)].filter((n) => n.type === "quant" && (n.max > 1 || n.min < n.max));
|
|
1436
|
+
if (variable.some((n) => n.body.type !== "atom" || n.body.source === void 0)) return false;
|
|
1437
|
+
const atomFlags = flags.replace(/[^i]/g, "");
|
|
1438
|
+
const unicode = /[uv]/.test(flags);
|
|
1439
|
+
let insides;
|
|
1440
|
+
try {
|
|
1441
|
+
insides = variable.map((n) => new RegExp(`^(?:${n.body.source})$`, `${atomFlags}${unicode ? "u" : ""}`));
|
|
1442
|
+
} catch {
|
|
1443
|
+
return false;
|
|
1444
|
+
}
|
|
1445
|
+
const literalOf = (source) => {
|
|
1446
|
+
if (source.length === 1 && !".[]()|^$\\".includes(source)) return source;
|
|
1447
|
+
if (source.length === 2 && source[0] === "\\" && /[^\w\s]/.test(source[1])) return source[1];
|
|
1448
|
+
return null;
|
|
1449
|
+
};
|
|
1450
|
+
const disjoint = (edge) => {
|
|
1451
|
+
if (edge?.type !== "atom" || edge.source === void 0) return false;
|
|
1452
|
+
const literal = literalOf(edge.source);
|
|
1453
|
+
if (literal !== null) return insides.every((re) => !re.test(literal));
|
|
1454
|
+
if (unicode) return false;
|
|
1455
|
+
let fence;
|
|
1456
|
+
try {
|
|
1457
|
+
fence = new RegExp(`^(?:${edge.source})$`, atomFlags);
|
|
1458
|
+
} catch {
|
|
1459
|
+
return false;
|
|
1460
|
+
}
|
|
1461
|
+
for (let unit = 0; unit <= 65535; unit++) {
|
|
1462
|
+
const ch = String.fromCharCode(unit);
|
|
1463
|
+
if (fence.test(ch) && insides.some((re) => re.test(ch))) return false;
|
|
1464
|
+
}
|
|
1465
|
+
return true;
|
|
1466
|
+
};
|
|
1467
|
+
return disjoint(body.items[0]) || disjoint(body.items[body.items.length - 1]);
|
|
1468
|
+
}
|
|
1469
|
+
function hasCatastrophicShape(body, flags) {
|
|
1470
|
+
const root = parseRepetition(body);
|
|
1471
|
+
if (root === null) return null;
|
|
1472
|
+
for (const node of nodesOf(root)) {
|
|
1473
|
+
if (node.type === "group" && node.kind === "look" && hasAdjacentUnbounded(lookaroundBody(node.source), flags)) return true;
|
|
1474
|
+
if (node.type !== "quant" || node.max <= 1) continue;
|
|
1475
|
+
const fenced = hasFencedRepetitions(node, flags);
|
|
1476
|
+
for (const inner of nodesOf(node.body)) {
|
|
1477
|
+
if (inner === node.body && inner.type === "quant") continue;
|
|
1478
|
+
if (inner.type === "quant" && inner.max > 1 && !fenced) {
|
|
1479
|
+
if (node.max === Infinity || inner.max === Infinity || node.max * inner.max > BOUNDED_NESTING_LIMIT) return true;
|
|
1480
|
+
}
|
|
1481
|
+
if (inner.type === "quant" && inner.min === 0 && inner.max <= 1 && !fenced && (node.max === Infinity || node.max > OPTIONAL_REPEAT_LIMIT)) {
|
|
1482
|
+
return true;
|
|
1483
|
+
}
|
|
1484
|
+
if (inner.type === "alt" && inner.branches.length > 1 && (node.max === Infinity || node.max > BOUNDED_ALTERNATION_LIMIT)) {
|
|
1485
|
+
return true;
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
return false;
|
|
1490
|
+
}
|
|
1491
|
+
function lookaroundBody(source) {
|
|
1492
|
+
return source.slice(source.startsWith("(?<") ? 4 : 3, -1);
|
|
1493
|
+
}
|
|
1188
1494
|
function groupEnd(body, start) {
|
|
1189
1495
|
let depth = 0;
|
|
1190
1496
|
let inClass = false;
|
|
@@ -1211,6 +1517,8 @@ function warnRejectedPatternOnce(pattern) {
|
|
|
1211
1517
|
);
|
|
1212
1518
|
}
|
|
1213
1519
|
var MAX_PATTERN_LENGTH = 1e3;
|
|
1520
|
+
var regexVerdicts = /* @__PURE__ */ new Map();
|
|
1521
|
+
var REGEX_VERDICT_LIMIT = 2048;
|
|
1214
1522
|
function safeRegExp(pattern) {
|
|
1215
1523
|
if (!pattern) {
|
|
1216
1524
|
return null;
|
|
@@ -1222,16 +1530,32 @@ function safeRegExp(pattern) {
|
|
|
1222
1530
|
if (!match) {
|
|
1223
1531
|
return null;
|
|
1224
1532
|
}
|
|
1225
|
-
|
|
1226
|
-
|
|
1533
|
+
let safe = regexVerdicts.get(pattern);
|
|
1534
|
+
if (safe === void 0) {
|
|
1535
|
+
safe = isSafeBody(match[1], match[2]);
|
|
1536
|
+
if (regexVerdicts.size >= REGEX_VERDICT_LIMIT) regexVerdicts.clear();
|
|
1537
|
+
regexVerdicts.set(pattern, safe);
|
|
1227
1538
|
}
|
|
1228
|
-
if (
|
|
1539
|
+
if (!safe) return null;
|
|
1229
1540
|
try {
|
|
1230
1541
|
return new RegExp(match[1], match[2]);
|
|
1231
1542
|
} catch {
|
|
1232
1543
|
return null;
|
|
1233
1544
|
}
|
|
1234
1545
|
}
|
|
1546
|
+
function isSafeBody(body, flags) {
|
|
1547
|
+
for (const dangerous of REDOS_PATTERNS) {
|
|
1548
|
+
if (dangerous.test(body)) return false;
|
|
1549
|
+
}
|
|
1550
|
+
if (hasAdjacentUnbounded(body, flags)) return false;
|
|
1551
|
+
if (hasCatastrophicShape(body, flags) === true) return false;
|
|
1552
|
+
try {
|
|
1553
|
+
new RegExp(body, flags);
|
|
1554
|
+
} catch {
|
|
1555
|
+
return false;
|
|
1556
|
+
}
|
|
1557
|
+
return true;
|
|
1558
|
+
}
|
|
1235
1559
|
function ctypeResult(strValue, isClass, matchVal) {
|
|
1236
1560
|
if (strValue === "") {
|
|
1237
1561
|
return false;
|
|
@@ -1275,17 +1599,21 @@ function arrayKeyValue(value, matchObj) {
|
|
|
1275
1599
|
}
|
|
1276
1600
|
var WHOLE_VALUE_MATCH_TYPES = /* @__PURE__ */ new Set(["isset", "array_in_array", "array_key_value"]);
|
|
1277
1601
|
function collectLeafValues(root, nodeCap = 2e4, maxDepth = 1e3) {
|
|
1278
|
-
const
|
|
1602
|
+
const leaves = [];
|
|
1279
1603
|
const stack = [[root, 0]];
|
|
1280
1604
|
let visited = 0;
|
|
1605
|
+
let truncated = false;
|
|
1281
1606
|
while (stack.length) {
|
|
1282
1607
|
const [node, depth] = stack.pop();
|
|
1283
1608
|
if (node === null || node === void 0) continue;
|
|
1284
1609
|
if (typeof node !== "object") {
|
|
1285
|
-
|
|
1610
|
+
leaves.push(node);
|
|
1611
|
+
continue;
|
|
1612
|
+
}
|
|
1613
|
+
if (depth >= maxDepth || visited >= nodeCap) {
|
|
1614
|
+
truncated = true;
|
|
1286
1615
|
continue;
|
|
1287
1616
|
}
|
|
1288
|
-
if (depth >= maxDepth || visited >= nodeCap) continue;
|
|
1289
1617
|
visited++;
|
|
1290
1618
|
if (Array.isArray(node)) {
|
|
1291
1619
|
for (let i = node.length - 1; i >= 0; i--) stack.push([node[i], depth + 1]);
|
|
@@ -1293,7 +1621,18 @@ function collectLeafValues(root, nodeCap = 2e4, maxDepth = 1e3) {
|
|
|
1293
1621
|
for (const k of Object.keys(node)) stack.push([node[k], depth + 1]);
|
|
1294
1622
|
}
|
|
1295
1623
|
}
|
|
1296
|
-
return
|
|
1624
|
+
return { leaves, truncated };
|
|
1625
|
+
}
|
|
1626
|
+
function skipsOf(resolver) {
|
|
1627
|
+
const skips = resolver?.skips;
|
|
1628
|
+
return skips && skips.length > 0 ? { skips } : {};
|
|
1629
|
+
}
|
|
1630
|
+
function serialisedValue(value) {
|
|
1631
|
+
try {
|
|
1632
|
+
return JSON.stringify(value);
|
|
1633
|
+
} catch {
|
|
1634
|
+
return void 0;
|
|
1635
|
+
}
|
|
1297
1636
|
}
|
|
1298
1637
|
var warnedKeys = /* @__PURE__ */ new Set();
|
|
1299
1638
|
function warnOnce2(key, message) {
|
|
@@ -1311,12 +1650,22 @@ function warnUnsupportedMatchType(type) {
|
|
|
1311
1650
|
`[patchstack] Unsupported rule_v2 match type "${type}" \u2014 condition treated as no-match. Rules relying on it are not enforced by this engine.`
|
|
1312
1651
|
);
|
|
1313
1652
|
}
|
|
1653
|
+
var SPECIAL_SCHEMES = /* @__PURE__ */ new Set(["http", "https", "ws", "wss", "ftp", "file"]);
|
|
1654
|
+
function trimControls(text) {
|
|
1655
|
+
let start = 0;
|
|
1656
|
+
let end = text.length;
|
|
1657
|
+
while (start < end && text.charCodeAt(start) <= 32) start++;
|
|
1658
|
+
while (end > start && text.charCodeAt(end - 1) <= 32) end--;
|
|
1659
|
+
return text.slice(start, end);
|
|
1660
|
+
}
|
|
1314
1661
|
function hostFromValue(value) {
|
|
1315
|
-
const raw = String(value ?? "").trim();
|
|
1662
|
+
const raw = trimControls(String(value ?? "").replace(/[\t\n\r]/g, "")).trim();
|
|
1316
1663
|
if (raw === "") return "";
|
|
1317
|
-
|
|
1664
|
+
const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(raw)?.[1]?.toLowerCase();
|
|
1665
|
+
const relative = /^[\\/]{2}/.test(raw);
|
|
1666
|
+
if (relative || /^[a-z][a-z0-9+.-]*:\/\//i.test(raw) || scheme !== void 0 && SPECIAL_SCHEMES.has(scheme)) {
|
|
1318
1667
|
try {
|
|
1319
|
-
return new URL(
|
|
1668
|
+
return new URL(relative ? `http:${raw}` : raw).hostname;
|
|
1320
1669
|
} catch {
|
|
1321
1670
|
return raw;
|
|
1322
1671
|
}
|
|
@@ -1344,10 +1693,18 @@ function isInternalHost(hostname) {
|
|
|
1344
1693
|
const allZeroHi = g[0] === 0 && g[1] === 0 && g[2] === 0 && g[3] === 0 && g[4] === 0;
|
|
1345
1694
|
if (allZeroHi && g[5] === 0 && g[6] === 0 && (g[7] === 0 || g[7] === 1)) return true;
|
|
1346
1695
|
if ((g[0] & 65472) === 65152) return true;
|
|
1696
|
+
if ((g[0] & 65472) === 65216) return true;
|
|
1347
1697
|
if ((g[0] & 65024) === 64512) return true;
|
|
1698
|
+
if ((g[0] & 65280) === 65280) return true;
|
|
1699
|
+
const low32 = (g[6] << 16 >>> 0 | g[7]) >>> 0;
|
|
1348
1700
|
if (allZeroHi && (g[5] === 65535 || g[5] === 0)) {
|
|
1349
|
-
return isPrivateV4Int(
|
|
1701
|
+
return isPrivateV4Int(low32);
|
|
1350
1702
|
}
|
|
1703
|
+
if (g[0] === 100 && g[1] === 65435 && g[2] === 0 && g[3] === 0 && g[4] === 0 && g[5] === 0) {
|
|
1704
|
+
return isPrivateV4Int(low32);
|
|
1705
|
+
}
|
|
1706
|
+
if (g[0] === 100 && g[1] === 65435 && g[2] === 1) return true;
|
|
1707
|
+
if (g[0] === 8194) return isPrivateV4Int((g[1] << 16 >>> 0 | g[2]) >>> 0);
|
|
1351
1708
|
return false;
|
|
1352
1709
|
}
|
|
1353
1710
|
const v4 = parseIPv4ToInt(host);
|
|
@@ -1357,6 +1714,10 @@ function isInternalHost(hostname) {
|
|
|
1357
1714
|
function isPrivateV4Int(n) {
|
|
1358
1715
|
const a = n >>> 24 & 255;
|
|
1359
1716
|
const b = n >>> 16 & 255;
|
|
1717
|
+
const c = n >>> 8 & 255;
|
|
1718
|
+
if (a >= 224) return true;
|
|
1719
|
+
if (a === 192 && b === 0 && c === 0) return true;
|
|
1720
|
+
if (a === 198 && (b === 18 || b === 19)) return true;
|
|
1360
1721
|
if (a === 127 || a === 10 || a === 0) return true;
|
|
1361
1722
|
if (a === 169 && b === 254) return true;
|
|
1362
1723
|
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
@@ -1705,8 +2066,12 @@ var RuleEngine = class {
|
|
|
1705
2066
|
let normalizedReq;
|
|
1706
2067
|
let resolver;
|
|
1707
2068
|
try {
|
|
1708
|
-
|
|
2069
|
+
let limited = false;
|
|
2070
|
+
normalizedReq = { ...req, ...normalizeRequest(req, { onLimit: () => {
|
|
2071
|
+
limited = true;
|
|
2072
|
+
} }) };
|
|
1709
2073
|
resolver = new RequestResolver(normalizedReq);
|
|
2074
|
+
if (limited) resolver.noteSkip("container-cap");
|
|
1710
2075
|
} catch (err) {
|
|
1711
2076
|
this.#reportError(err);
|
|
1712
2077
|
return { blocked: false, rule: null, message: null };
|
|
@@ -1732,14 +2097,15 @@ var RuleEngine = class {
|
|
|
1732
2097
|
// that re-read the request instead would be reading it a second time: a getter, a stream or
|
|
1733
2098
|
// anything else that answers once can give a different value, and evidence that disagrees
|
|
1734
2099
|
// with the match it belongs to is worse than none.
|
|
1735
|
-
resolver
|
|
2100
|
+
resolver,
|
|
2101
|
+
...skipsOf(resolver)
|
|
1736
2102
|
};
|
|
1737
2103
|
}
|
|
1738
2104
|
} catch (err) {
|
|
1739
2105
|
this.#reportError(err);
|
|
1740
2106
|
}
|
|
1741
2107
|
}
|
|
1742
|
-
return { blocked: false, rule: null, message: null };
|
|
2108
|
+
return { blocked: false, rule: null, message: null, ...skipsOf(resolver) };
|
|
1743
2109
|
}
|
|
1744
2110
|
#evaluateRule(conditions, resolver) {
|
|
1745
2111
|
const inclusiveConditions = conditions.filter((c) => c.inclusive);
|
|
@@ -1795,9 +2161,15 @@ var RuleEngine = class {
|
|
|
1795
2161
|
if (WHOLE_VALUE_MATCH_TYPES.has(match.type)) {
|
|
1796
2162
|
if (matchValue(match.type, value, match.value, match)) return true;
|
|
1797
2163
|
} else {
|
|
1798
|
-
|
|
2164
|
+
const { leaves, truncated } = collectLeafValues(value);
|
|
2165
|
+
for (const leaf of leaves) {
|
|
1799
2166
|
if (matchValue(match.type, leaf, match.value, match)) return true;
|
|
1800
2167
|
}
|
|
2168
|
+
if (truncated) {
|
|
2169
|
+
resolver.noteSkip("container-cap");
|
|
2170
|
+
const text = serialisedValue(value);
|
|
2171
|
+
if (text !== void 0 && matchValue(match.type, text, match.value, match)) return true;
|
|
2172
|
+
}
|
|
1801
2173
|
}
|
|
1802
2174
|
continue;
|
|
1803
2175
|
}
|
|
@@ -2184,6 +2556,9 @@ function conditionShapeProblem(condition) {
|
|
|
2184
2556
|
if (nested && !named) {
|
|
2185
2557
|
return `condition carries nested rules but its parameter is ${JSON.stringify(condition.parameter)}; a group must be {"parameter":"${GROUP_PARAMETER}"}`;
|
|
2186
2558
|
}
|
|
2559
|
+
if (condition.inclusive !== void 0 && typeof condition.inclusive !== "boolean") {
|
|
2560
|
+
return "inclusive must be true or false";
|
|
2561
|
+
}
|
|
2187
2562
|
if (named && nested) {
|
|
2188
2563
|
if (condition.match !== void 0) return "a group carries no match of its own; the engine ignores it";
|
|
2189
2564
|
if (condition.mutations !== void 0) return "a group carries no mutations of its own; the engine ignores them";
|
|
@@ -2890,7 +3265,7 @@ async function fromFetchRequest(request, options = {}) {
|
|
|
2890
3265
|
// that guessed differently would attribute one request to two addresses.
|
|
2891
3266
|
ip: client.ip ?? "",
|
|
2892
3267
|
_clientIp: client,
|
|
2893
|
-
cookies:
|
|
3268
|
+
cookies: parseCookieHeader(headers.cookie),
|
|
2894
3269
|
// Verbatim body text: preserves literal keys (e.g. `__proto__`) that JSON.stringify
|
|
2895
3270
|
// drops, so prototype-pollution rules on `raw` are robust.
|
|
2896
3271
|
_rawBody: rawBody,
|
|
@@ -2952,7 +3327,7 @@ function concatChunks(chunks, total) {
|
|
|
2952
3327
|
}
|
|
2953
3328
|
function parseBody(rawBody, contentType) {
|
|
2954
3329
|
const ct = String(contentType || "").toLowerCase();
|
|
2955
|
-
const
|
|
3330
|
+
const isJson2 = ct.includes("application/json") || /\+json\b/.test(ct);
|
|
2956
3331
|
const isForm = ct.includes("application/x-www-form-urlencoded");
|
|
2957
3332
|
const isMultipart = ct.includes("multipart/form-data");
|
|
2958
3333
|
const isAmbiguous = ct === "" || ct.startsWith("text/plain") || ct.includes("csp-report") || ct.includes("/json");
|
|
@@ -2966,7 +3341,7 @@ function parseBody(rawBody, contentType) {
|
|
|
2966
3341
|
for (const [k, v] of new URLSearchParams(rawBody)) appendOwn(body, k, v);
|
|
2967
3342
|
return { body, files: void 0 };
|
|
2968
3343
|
}
|
|
2969
|
-
if (
|
|
3344
|
+
if (isJson2 || isAmbiguous) {
|
|
2970
3345
|
try {
|
|
2971
3346
|
const parsed = JSON.parse(rawBody);
|
|
2972
3347
|
if (parsed && typeof parsed === "object") return { body: parsed, files: void 0 };
|
|
@@ -2982,38 +3357,115 @@ function parseMultipart(rawBody, boundary) {
|
|
|
2982
3357
|
const sep = /\r?\n\r?\n/.exec(part);
|
|
2983
3358
|
if (!sep) continue;
|
|
2984
3359
|
const rawHeaders = part.slice(0, sep.index);
|
|
2985
|
-
const disposition = /content-disposition:[^\r\n]
|
|
2986
|
-
if (
|
|
2987
|
-
const
|
|
2988
|
-
if (
|
|
3360
|
+
const disposition = /content-disposition:([^\r\n]*)/i.exec(rawHeaders)?.[1];
|
|
3361
|
+
if (disposition === void 0) continue;
|
|
3362
|
+
const { names, filename } = dispositionFields(disposition);
|
|
3363
|
+
if (names.length === 0) continue;
|
|
2989
3364
|
const content = part.slice(sep.index + sep[0].length).replace(/\r?\n$/, "");
|
|
2990
|
-
const
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
appendOwn(files, name, file);
|
|
2995
|
-
} else {
|
|
2996
|
-
appendOwn(body, name, content);
|
|
3365
|
+
const file = filename === void 0 ? void 0 : { filename, type: /content-type:\s*([^\r\n;]+)/i.exec(rawHeaders)?.[1]?.trim() || "", content };
|
|
3366
|
+
for (const name of names) {
|
|
3367
|
+
if (file) appendOwn(files, name, file);
|
|
3368
|
+
else appendOwn(body, name, content);
|
|
2997
3369
|
}
|
|
2998
3370
|
}
|
|
2999
3371
|
return { body, files };
|
|
3000
3372
|
}
|
|
3001
|
-
function
|
|
3002
|
-
const
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3373
|
+
function dispositionFields(value) {
|
|
3374
|
+
const text = String(value);
|
|
3375
|
+
const names = /* @__PURE__ */ new Set();
|
|
3376
|
+
let filename;
|
|
3377
|
+
let extendedFilename;
|
|
3378
|
+
let i = text.indexOf(";");
|
|
3379
|
+
while (i !== -1 && i < text.length) {
|
|
3380
|
+
i++;
|
|
3381
|
+
while (text[i] === " " || text[i] === " ") i++;
|
|
3382
|
+
let key = "";
|
|
3383
|
+
while (i < text.length && text[i] !== "=" && text[i] !== ";") key += text[i++];
|
|
3384
|
+
key = key.trim().toLowerCase();
|
|
3385
|
+
let val = null;
|
|
3386
|
+
if (text[i] === "=") {
|
|
3387
|
+
i++;
|
|
3388
|
+
while (text[i] === " " || text[i] === " ") i++;
|
|
3389
|
+
if (text[i] === '"') {
|
|
3390
|
+
i++;
|
|
3391
|
+
val = "";
|
|
3392
|
+
while (i < text.length && text[i] !== '"') {
|
|
3393
|
+
if (text[i] === "\\" && i + 1 < text.length) i++;
|
|
3394
|
+
val += text[i++];
|
|
3395
|
+
}
|
|
3396
|
+
i++;
|
|
3397
|
+
while (i < text.length && text[i] !== ";") i++;
|
|
3398
|
+
} else {
|
|
3399
|
+
let bare = "";
|
|
3400
|
+
while (i < text.length && text[i] !== ";") bare += text[i++];
|
|
3401
|
+
val = bare.trim();
|
|
3402
|
+
}
|
|
3403
|
+
}
|
|
3404
|
+
if (val !== null) {
|
|
3405
|
+
if (key === "name") names.add(val);
|
|
3406
|
+
else if (key === "filename") filename = val;
|
|
3407
|
+
else if (key === "filename*") extendedFilename = decodeExtendedValue(val);
|
|
3010
3408
|
}
|
|
3011
|
-
|
|
3409
|
+
i = i < text.length && text[i] === ";" ? i : -1;
|
|
3410
|
+
}
|
|
3411
|
+
return { names: [...names], filename: extendedFilename ?? filename };
|
|
3412
|
+
}
|
|
3413
|
+
function decodeExtendedValue(value) {
|
|
3414
|
+
const match = /^([^']*)'[^']*'(.*)$/.exec(value);
|
|
3415
|
+
if (!match) return value;
|
|
3416
|
+
try {
|
|
3417
|
+
return decodeURIComponent(match[2]);
|
|
3418
|
+
} catch {
|
|
3419
|
+
return match[2];
|
|
3012
3420
|
}
|
|
3013
|
-
return cookies;
|
|
3014
3421
|
}
|
|
3015
3422
|
|
|
3016
3423
|
// src/protect/engine/node.js
|
|
3424
|
+
function readBodyPrefix(req, maxBytes, done) {
|
|
3425
|
+
const chunks = [];
|
|
3426
|
+
let retained = 0;
|
|
3427
|
+
let size = 0;
|
|
3428
|
+
let failed = false;
|
|
3429
|
+
let finished = false;
|
|
3430
|
+
const finish = (error, read) => {
|
|
3431
|
+
if (finished) return;
|
|
3432
|
+
finished = true;
|
|
3433
|
+
done(error, read);
|
|
3434
|
+
};
|
|
3435
|
+
req.on("data", (chunk) => {
|
|
3436
|
+
if (failed) return;
|
|
3437
|
+
try {
|
|
3438
|
+
const bytes = typeof chunk === "string" ? Buffer.from(chunk, req.readableEncoding || "utf8") : chunk;
|
|
3439
|
+
size += bytes.length;
|
|
3440
|
+
const take = Math.min(bytes.length, maxBytes - retained);
|
|
3441
|
+
if (take <= 0) return;
|
|
3442
|
+
chunks.push(take === bytes.length ? bytes : bytes.subarray(0, take));
|
|
3443
|
+
retained += take;
|
|
3444
|
+
} catch {
|
|
3445
|
+
failed = true;
|
|
3446
|
+
chunks.length = 0;
|
|
3447
|
+
}
|
|
3448
|
+
});
|
|
3449
|
+
req.on("error", (error) => finish(error));
|
|
3450
|
+
req.on("end", () => {
|
|
3451
|
+
if (failed) {
|
|
3452
|
+
finish(null, { text: "", overflow: false, size, failed: true });
|
|
3453
|
+
return;
|
|
3454
|
+
}
|
|
3455
|
+
const prefix = Buffer.concat(chunks);
|
|
3456
|
+
const overflow = size > maxBytes;
|
|
3457
|
+
const text = (overflow ? prefix.subarray(0, completeUtf8Length(prefix)) : prefix).toString("utf8");
|
|
3458
|
+
finish(null, { text, overflow, size, failed: false });
|
|
3459
|
+
});
|
|
3460
|
+
}
|
|
3461
|
+
function completeUtf8Length(bytes) {
|
|
3462
|
+
let start = bytes.length - 1;
|
|
3463
|
+
while (start >= 0 && bytes.length - start <= 3 && (bytes[start] & 192) === 128) start--;
|
|
3464
|
+
if (start < 0) return bytes.length;
|
|
3465
|
+
const lead = bytes[start];
|
|
3466
|
+
const needed = lead >= 240 ? 4 : lead >= 224 ? 3 : lead >= 192 ? 2 : 1;
|
|
3467
|
+
return bytes.length - start < needed ? start : bytes.length;
|
|
3468
|
+
}
|
|
3017
3469
|
function fromNodeRequest(req, rawBody = "", options = {}) {
|
|
3018
3470
|
const method = (req.method || "GET").toUpperCase();
|
|
3019
3471
|
const headers = {};
|
|
@@ -3059,29 +3511,41 @@ function fromNodeRequest(req, rawBody = "", options = {}) {
|
|
|
3059
3511
|
headers,
|
|
3060
3512
|
ip: client.ip ?? "",
|
|
3061
3513
|
_clientIp: client,
|
|
3062
|
-
cookies:
|
|
3514
|
+
cookies: parseCookieHeader(headers.cookie),
|
|
3063
3515
|
// Verbatim body text: preserves literal keys (e.g. `__proto__`) that JSON.stringify drops.
|
|
3064
3516
|
_rawBody: rawBody
|
|
3065
3517
|
};
|
|
3066
3518
|
}
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3519
|
+
|
|
3520
|
+
// src/protect/egress.js
|
|
3521
|
+
var REGISTRY = /* @__PURE__ */ Symbol.for("patchstack.connect.egress-guard");
|
|
3522
|
+
function egressRegistry() {
|
|
3523
|
+
const existing = globalThis[REGISTRY];
|
|
3524
|
+
if (existing && existing.screens instanceof Set && existing.surfaces instanceof Map) return existing;
|
|
3525
|
+
const created = { screens: /* @__PURE__ */ new Set(), surfaces: /* @__PURE__ */ new Map() };
|
|
3526
|
+
Object.defineProperty(globalThis, REGISTRY, { value: created, configurable: true, writable: true });
|
|
3527
|
+
return created;
|
|
3528
|
+
}
|
|
3529
|
+
var refusal = (host) => new Error(`Patchstack blocked an outbound request to a disallowed address: ${host}`);
|
|
3530
|
+
function readableDestination(input, init) {
|
|
3531
|
+
try {
|
|
3532
|
+
const raw = typeof input === "string" || input instanceof URL ? String(input) : input?.url ?? input?.href;
|
|
3533
|
+
if (typeof raw !== "string" && !(raw instanceof URL)) return null;
|
|
3534
|
+
const url = new URL(String(raw)).href;
|
|
3535
|
+
const method = String(init?.method ?? input?.method ?? "GET").toUpperCase();
|
|
3536
|
+
return { url, method };
|
|
3537
|
+
} catch {
|
|
3538
|
+
return null;
|
|
3071
3539
|
}
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
setOwn(cookies, pair.slice(0, idx).trim(), pair.slice(idx + 1).trim());
|
|
3540
|
+
}
|
|
3541
|
+
function anyRefuses(screens, url, host, method) {
|
|
3542
|
+
let refused = false;
|
|
3543
|
+
for (const screen of screens) {
|
|
3544
|
+
if (screen.block(url, host, method)) refused = true;
|
|
3078
3545
|
}
|
|
3079
|
-
return
|
|
3546
|
+
return refused;
|
|
3080
3547
|
}
|
|
3081
|
-
|
|
3082
|
-
// src/protect/egress.js
|
|
3083
3548
|
async function installEgressGuard({ shouldBlock, onBlock, onSkip, dnsScreen = true, lookup, allowHosts } = {}) {
|
|
3084
|
-
const restores = [];
|
|
3085
3549
|
if (typeof shouldBlock !== "function") return () => {
|
|
3086
3550
|
};
|
|
3087
3551
|
const exempt = new Set((allowHosts ?? []).map((h) => String(h).toLowerCase()));
|
|
@@ -3126,6 +3590,9 @@ async function installEgressGuard({ shouldBlock, onBlock, onSkip, dnsScreen = tr
|
|
|
3126
3590
|
resolve(false);
|
|
3127
3591
|
}
|
|
3128
3592
|
});
|
|
3593
|
+
const registry = egressRegistry();
|
|
3594
|
+
const own = { block, prescreen: resolvesToDisallowed, dns: screen, skip };
|
|
3595
|
+
registry.screens.add(own);
|
|
3129
3596
|
const originalFetch = globalThis.fetch;
|
|
3130
3597
|
if (typeof originalFetch === "function" && !originalFetch.__patchstackGuarded) {
|
|
3131
3598
|
const REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
|
|
@@ -3198,15 +3665,19 @@ async function installEgressGuard({ shouldBlock, onBlock, onSkip, dnsScreen = tr
|
|
|
3198
3665
|
} catch {
|
|
3199
3666
|
host = null;
|
|
3200
3667
|
}
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3668
|
+
const screens = [...registry.screens];
|
|
3669
|
+
if (anyRefuses(screens, u, host, method)) throw refusal(host ?? u);
|
|
3670
|
+
const resolved = await Promise.all(screens.map((each) => each.prescreen(u, host, method)));
|
|
3671
|
+
if (resolved.includes(true)) throw refusal(host ?? u);
|
|
3204
3672
|
};
|
|
3205
3673
|
const guarded = async (input, init) => {
|
|
3206
3674
|
let cur;
|
|
3207
3675
|
try {
|
|
3208
3676
|
cur = new Request(input, { ...init || {}, redirect: "manual" });
|
|
3209
3677
|
} catch {
|
|
3678
|
+
const destination = readableDestination(input, init);
|
|
3679
|
+
if (destination) await screenUrl(destination.url, destination.method);
|
|
3680
|
+
else for (const each of registry.screens) each.skip("unrecognised-request", {});
|
|
3210
3681
|
return originalFetch(input, init);
|
|
3211
3682
|
}
|
|
3212
3683
|
const callerRedirect = init && init.redirect || input && input.redirect || "follow";
|
|
@@ -3217,13 +3688,12 @@ async function installEgressGuard({ shouldBlock, onBlock, onSkip, dnsScreen = tr
|
|
|
3217
3688
|
const headers = new Headers(cur.headers);
|
|
3218
3689
|
const signal = cur.signal;
|
|
3219
3690
|
const replay = captureReplayBody(cur);
|
|
3691
|
+
const { body: _callerBody, ...callerOptions } = init || {};
|
|
3220
3692
|
let body;
|
|
3221
3693
|
for (let hop = 0; ; hop++) {
|
|
3222
3694
|
let resp;
|
|
3223
3695
|
try {
|
|
3224
|
-
resp = await originalFetch(
|
|
3225
|
-
hop === 0 ? cur : new Request(url, { method, headers, body, redirect: "manual", signal })
|
|
3226
|
-
);
|
|
3696
|
+
resp = hop === 0 ? await originalFetch(cur, { ...callerOptions, redirect: "manual" }) : await originalFetch(url, { ...callerOptions, method, headers, body, redirect: "manual", signal });
|
|
3227
3697
|
} catch (error) {
|
|
3228
3698
|
replay.cancel();
|
|
3229
3699
|
throw error;
|
|
@@ -3231,7 +3701,7 @@ async function installEgressGuard({ shouldBlock, onBlock, onSkip, dnsScreen = tr
|
|
|
3231
3701
|
const location = REDIRECT_STATUSES.has(resp.status) ? resp.headers.get("location") : null;
|
|
3232
3702
|
if (!location) {
|
|
3233
3703
|
replay.cancel();
|
|
3234
|
-
return resp;
|
|
3704
|
+
return hop === 0 ? resp : markRedirected(resp);
|
|
3235
3705
|
}
|
|
3236
3706
|
await discardResponseBody(resp);
|
|
3237
3707
|
if (hop >= MAX_REDIRECTS) {
|
|
@@ -3264,43 +3734,84 @@ async function installEgressGuard({ shouldBlock, onBlock, onSkip, dnsScreen = tr
|
|
|
3264
3734
|
};
|
|
3265
3735
|
guarded.__patchstackGuarded = true;
|
|
3266
3736
|
globalThis.fetch = guarded;
|
|
3267
|
-
|
|
3268
|
-
|
|
3737
|
+
registry.surfaces.set(guarded, {
|
|
3738
|
+
release() {
|
|
3739
|
+
if (globalThis.fetch !== guarded) return false;
|
|
3740
|
+
globalThis.fetch = originalFetch;
|
|
3741
|
+
return true;
|
|
3742
|
+
}
|
|
3269
3743
|
});
|
|
3270
3744
|
}
|
|
3745
|
+
let syncBuiltins = () => {
|
|
3746
|
+
};
|
|
3747
|
+
try {
|
|
3748
|
+
const { syncBuiltinESMExports } = await import("module");
|
|
3749
|
+
if (typeof syncBuiltinESMExports === "function") syncBuiltins = syncBuiltinESMExports;
|
|
3750
|
+
} catch {
|
|
3751
|
+
}
|
|
3752
|
+
let patchedAny = false;
|
|
3271
3753
|
for (const moduleName of ["node:http", "node:https"]) {
|
|
3272
3754
|
try {
|
|
3273
3755
|
const mod = await import(moduleName);
|
|
3274
|
-
|
|
3275
|
-
|
|
3756
|
+
if (registry.surfaces.has(moduleName)) continue;
|
|
3757
|
+
const release = patchHttpModule(mod.default ?? mod, registry);
|
|
3758
|
+
if (release) {
|
|
3759
|
+
patchedAny = true;
|
|
3760
|
+
registry.surfaces.set(moduleName, {
|
|
3761
|
+
release() {
|
|
3762
|
+
if (!release()) return false;
|
|
3763
|
+
syncBuiltins();
|
|
3764
|
+
return true;
|
|
3765
|
+
}
|
|
3766
|
+
});
|
|
3767
|
+
}
|
|
3276
3768
|
} catch {
|
|
3277
3769
|
}
|
|
3278
3770
|
}
|
|
3771
|
+
if (patchedAny) syncBuiltins();
|
|
3279
3772
|
return () => {
|
|
3280
|
-
|
|
3773
|
+
registry.screens.delete(own);
|
|
3774
|
+
if (registry.screens.size > 0) return;
|
|
3775
|
+
for (const [name, surface] of registry.surfaces) {
|
|
3281
3776
|
try {
|
|
3282
|
-
|
|
3777
|
+
if (surface.release()) registry.surfaces.delete(name);
|
|
3283
3778
|
} catch {
|
|
3284
3779
|
}
|
|
3285
3780
|
}
|
|
3286
3781
|
};
|
|
3287
3782
|
}
|
|
3288
|
-
function
|
|
3783
|
+
function markRedirected(response) {
|
|
3784
|
+
try {
|
|
3785
|
+
Object.defineProperty(response, "redirected", { value: true, configurable: true });
|
|
3786
|
+
} catch {
|
|
3787
|
+
}
|
|
3788
|
+
return response;
|
|
3789
|
+
}
|
|
3790
|
+
function patchHttpModule(http, registry) {
|
|
3289
3791
|
if (!http || typeof http.request !== "function" || http.__patchstackGuarded) return null;
|
|
3290
3792
|
const originalRequest = http.request;
|
|
3291
3793
|
const originalGet = http.get;
|
|
3292
|
-
const
|
|
3794
|
+
const OriginalClientRequest = typeof http.ClientRequest === "function" ? http.ClientRequest : null;
|
|
3795
|
+
const guardArgs = (args) => {
|
|
3293
3796
|
const target = extractHttpTarget(args);
|
|
3294
|
-
if (target
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3797
|
+
if (!target) return args;
|
|
3798
|
+
const screens = [...registry.screens];
|
|
3799
|
+
if (anyRefuses(screens, target.url, target.host, target.method)) throw refusal(target.host ?? target.url);
|
|
3800
|
+
const resolving = target.host ? screens.filter((each) => each.dns && each.dns.isIP(target.host) === 0 && !each.dns.isExempt(target.host)) : [];
|
|
3801
|
+
if (resolving.length > 0) {
|
|
3802
|
+
const block = (url, host, method) => anyRefuses(resolving, url, host, method);
|
|
3803
|
+
const skip = (reason, detail) => {
|
|
3804
|
+
for (const each of resolving) each.skip(reason, detail);
|
|
3805
|
+
};
|
|
3298
3806
|
try {
|
|
3299
|
-
|
|
3807
|
+
return withScreeningLookup(args, target, block, resolving[0].dns.lookup, skip);
|
|
3300
3808
|
} catch {
|
|
3301
3809
|
}
|
|
3302
3810
|
}
|
|
3303
|
-
return
|
|
3811
|
+
return args;
|
|
3812
|
+
};
|
|
3813
|
+
const wrap = (original) => function(...args) {
|
|
3814
|
+
return original.apply(this, guardArgs(args));
|
|
3304
3815
|
};
|
|
3305
3816
|
const guardedRequest = wrap(originalRequest);
|
|
3306
3817
|
http.request = guardedRequest;
|
|
@@ -3309,11 +3820,26 @@ function patchHttpModule(http, block, screen, skip) {
|
|
|
3309
3820
|
guardedGet = wrap(originalGet);
|
|
3310
3821
|
http.get = guardedGet;
|
|
3311
3822
|
}
|
|
3823
|
+
let GuardedClientRequest;
|
|
3824
|
+
if (OriginalClientRequest) {
|
|
3825
|
+
GuardedClientRequest = function ClientRequest(...args) {
|
|
3826
|
+
const screened = guardArgs(args);
|
|
3827
|
+
return new.target ? Reflect.construct(OriginalClientRequest, screened, new.target) : Reflect.construct(OriginalClientRequest, screened);
|
|
3828
|
+
};
|
|
3829
|
+
GuardedClientRequest.prototype = OriginalClientRequest.prototype;
|
|
3830
|
+
Object.setPrototypeOf(GuardedClientRequest, OriginalClientRequest);
|
|
3831
|
+
http.ClientRequest = GuardedClientRequest;
|
|
3832
|
+
}
|
|
3312
3833
|
http.__patchstackGuarded = true;
|
|
3313
3834
|
return () => {
|
|
3314
|
-
if (http.request
|
|
3315
|
-
if (guardedGet && http.get
|
|
3835
|
+
if (http.request !== guardedRequest) return false;
|
|
3836
|
+
if (guardedGet && http.get !== guardedGet) return false;
|
|
3837
|
+
if (GuardedClientRequest && http.ClientRequest !== GuardedClientRequest) return false;
|
|
3838
|
+
http.request = originalRequest;
|
|
3839
|
+
if (guardedGet) http.get = originalGet;
|
|
3840
|
+
if (GuardedClientRequest) http.ClientRequest = OriginalClientRequest;
|
|
3316
3841
|
delete http.__patchstackGuarded;
|
|
3842
|
+
return true;
|
|
3317
3843
|
};
|
|
3318
3844
|
}
|
|
3319
3845
|
function extractHttpTarget(args) {
|
|
@@ -3322,19 +3848,27 @@ function extractHttpTarget(args) {
|
|
|
3322
3848
|
if (typeof first === "string" || first instanceof URL) {
|
|
3323
3849
|
const url = new URL(String(first));
|
|
3324
3850
|
const opts = args.find((a) => a && typeof a === "object" && !(a instanceof URL));
|
|
3325
|
-
return { url
|
|
3326
|
-
}
|
|
3327
|
-
if (first && typeof first === "object") {
|
|
3328
|
-
const host = normalizeHost(first.hostname || first.host) || "localhost";
|
|
3329
|
-
const protocol = first.protocol || "http:";
|
|
3330
|
-
const port = first.port ? `:${first.port}` : "";
|
|
3331
|
-
const path = first.path || "/";
|
|
3332
|
-
return { url: `${protocol}//${host}${port}${path}`, host, method: first.method || "GET" };
|
|
3851
|
+
return targetOf({ ...optionsFromUrl(url), ...opts ?? {} });
|
|
3333
3852
|
}
|
|
3853
|
+
if (first && typeof first === "object") return targetOf(first);
|
|
3334
3854
|
} catch {
|
|
3335
3855
|
}
|
|
3336
3856
|
return null;
|
|
3337
3857
|
}
|
|
3858
|
+
function optionsFromUrl(url) {
|
|
3859
|
+
const hostname = url.hostname.startsWith("[") ? url.hostname.slice(1, -1) : url.hostname;
|
|
3860
|
+
const options = { protocol: url.protocol, hostname, path: `${url.pathname || "/"}${url.search}` };
|
|
3861
|
+
if (url.port !== "") options.port = Number(url.port);
|
|
3862
|
+
return options;
|
|
3863
|
+
}
|
|
3864
|
+
function targetOf(options) {
|
|
3865
|
+
const host = normalizeHost(options.hostname || options.host) || "localhost";
|
|
3866
|
+
const protocol = options.protocol || "http:";
|
|
3867
|
+
const port = options.port ? `:${options.port}` : "";
|
|
3868
|
+
const path = options.path || "/";
|
|
3869
|
+
const authority = host.includes(":") ? `[${host}]` : host;
|
|
3870
|
+
return { url: `${protocol}//${authority}${port}${path}`, host, method: options.method || "GET" };
|
|
3871
|
+
}
|
|
3338
3872
|
function normalizeHost(raw) {
|
|
3339
3873
|
const host = String(raw || "").trim();
|
|
3340
3874
|
const bracketed = /^\[([^\]]+)\]/.exec(host);
|
|
@@ -3715,19 +4249,49 @@ async function loadFs() {
|
|
|
3715
4249
|
}
|
|
3716
4250
|
return fsMod;
|
|
3717
4251
|
}
|
|
4252
|
+
async function sourceIdentity(options = {}) {
|
|
4253
|
+
if (typeof options.siteUuid === "string" && options.siteUuid.trim() !== "") {
|
|
4254
|
+
const endpoint = typeof options.pulseRulesUrl === "string" ? options.pulseRulesUrl : "";
|
|
4255
|
+
return `site:${options.siteUuid.trim().toLowerCase()}@${endpoint}`;
|
|
4256
|
+
}
|
|
4257
|
+
if (typeof options.token === "string" && options.token !== "") {
|
|
4258
|
+
const digest = await sha256Hex(options.token);
|
|
4259
|
+
if (digest === null) return null;
|
|
4260
|
+
const endpoint = typeof options.baseUrl === "string" ? options.baseUrl : "";
|
|
4261
|
+
return `token:${digest}@${endpoint}`;
|
|
4262
|
+
}
|
|
4263
|
+
return null;
|
|
4264
|
+
}
|
|
4265
|
+
async function sha256Hex(text) {
|
|
4266
|
+
try {
|
|
4267
|
+
const subtle = globalThis.crypto?.subtle;
|
|
4268
|
+
if (!subtle || typeof TextEncoder === "undefined") return null;
|
|
4269
|
+
const digest = await subtle.digest("SHA-256", new TextEncoder().encode(text));
|
|
4270
|
+
return Array.from(new Uint8Array(digest), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
4271
|
+
} catch {
|
|
4272
|
+
return null;
|
|
4273
|
+
}
|
|
4274
|
+
}
|
|
3718
4275
|
function makeStore(options = {}) {
|
|
3719
4276
|
let mem = null;
|
|
3720
4277
|
const durable = durableTier(options);
|
|
4278
|
+
let identity;
|
|
4279
|
+
const identityOf = () => identity ??= sourceIdentity(options);
|
|
3721
4280
|
return {
|
|
3722
4281
|
async read() {
|
|
3723
4282
|
if (mem) return mem;
|
|
4283
|
+
const source = await identityOf();
|
|
4284
|
+
if (source === null) return null;
|
|
3724
4285
|
const env = await durable.read();
|
|
3725
|
-
if (env)
|
|
4286
|
+
if (!env || env.source !== source) return null;
|
|
4287
|
+
mem = env;
|
|
3726
4288
|
return env;
|
|
3727
4289
|
},
|
|
3728
4290
|
async write(env) {
|
|
3729
|
-
if (env)
|
|
3730
|
-
await
|
|
4291
|
+
if (!env) return;
|
|
4292
|
+
const stamped = { ...env, source: await identityOf() };
|
|
4293
|
+
mem = stamped;
|
|
4294
|
+
await durable.write(stamped);
|
|
3731
4295
|
}
|
|
3732
4296
|
};
|
|
3733
4297
|
}
|
|
@@ -3849,11 +4413,12 @@ function toEnvelope(value) {
|
|
|
3849
4413
|
buildId,
|
|
3850
4414
|
// A confirmation belongs to the presentation stored beside it. A crossed or partially written
|
|
3851
4415
|
// envelope confirms nothing, even when one of its fields happens to name the current map.
|
|
3852
|
-
matchedBuildId: buildId !== null && matched === buildId ? matched : null
|
|
4416
|
+
matchedBuildId: buildId !== null && matched === buildId ? matched : null,
|
|
4417
|
+
source: typeof value.source === "string" ? value.source : null
|
|
3853
4418
|
};
|
|
3854
4419
|
}
|
|
3855
4420
|
if (Array.isArray(value.firewall) || Array.isArray(value.whitelists)) {
|
|
3856
|
-
return { bundle: value, etag: null, buildId: null, matchedBuildId: null };
|
|
4421
|
+
return { bundle: value, etag: null, buildId: null, matchedBuildId: null, source: null };
|
|
3857
4422
|
}
|
|
3858
4423
|
return null;
|
|
3859
4424
|
}
|
|
@@ -3920,13 +4485,17 @@ function withScopedDryRun(bundle, confirmed, reason, options = {}) {
|
|
|
3920
4485
|
...unusable > 0 ? [`${unusable} rule(s) carry an unusable ${BUILD_SCOPE_PROPERTY}`] : [],
|
|
3921
4486
|
...held > unusable ? [reason] : []
|
|
3922
4487
|
];
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
"onError"
|
|
3927
|
-
);
|
|
4488
|
+
const message = `${held} build-scoped rule(s) are detecting only, not blocking: ${explanations.join("; ")}`;
|
|
4489
|
+
if (typeof options.onError === "function") notify(options.onError, new Error(message), "onError");
|
|
4490
|
+
else warnHeldOnce(message);
|
|
3928
4491
|
return { ...bundle, firewall };
|
|
3929
4492
|
}
|
|
4493
|
+
var warnedHeld = /* @__PURE__ */ new Set();
|
|
4494
|
+
function warnHeldOnce(message) {
|
|
4495
|
+
if (warnedHeld.has(message)) return;
|
|
4496
|
+
warnedHeld.add(message);
|
|
4497
|
+
console.warn(`[patchstack] ${message}. Pass { onError } to handle this yourself.`);
|
|
4498
|
+
}
|
|
3930
4499
|
function confirmedBy(res, presented) {
|
|
3931
4500
|
if (presented === null) return null;
|
|
3932
4501
|
const verdict = res?.build;
|
|
@@ -4101,6 +4670,60 @@ function startRefresh(tick, { refreshMs, onError } = {}) {
|
|
|
4101
4670
|
}
|
|
4102
4671
|
};
|
|
4103
4672
|
}
|
|
4673
|
+
var RECOVERY_DELAYS_MS = [5e3, 15e3, 45e3, 12e4, 3e5, 6e5];
|
|
4674
|
+
function startRecovery(tick, { onError } = {}) {
|
|
4675
|
+
let stopped = false;
|
|
4676
|
+
let attempt = 0;
|
|
4677
|
+
let timer = null;
|
|
4678
|
+
const schedule = () => {
|
|
4679
|
+
if (stopped) return;
|
|
4680
|
+
const delay = RECOVERY_DELAYS_MS[Math.min(attempt, RECOVERY_DELAYS_MS.length - 1)];
|
|
4681
|
+
timer = setTimeout(run, delay * (1 - Math.random() * JITTER_FRACTION3));
|
|
4682
|
+
timer?.unref?.();
|
|
4683
|
+
};
|
|
4684
|
+
const run = async () => {
|
|
4685
|
+
if (stopped) return;
|
|
4686
|
+
attempt++;
|
|
4687
|
+
try {
|
|
4688
|
+
const status = await tick();
|
|
4689
|
+
if (!status || status.ok !== false) {
|
|
4690
|
+
stopped = true;
|
|
4691
|
+
return;
|
|
4692
|
+
}
|
|
4693
|
+
} catch (err) {
|
|
4694
|
+
notify(onError, err, "onError");
|
|
4695
|
+
}
|
|
4696
|
+
schedule();
|
|
4697
|
+
};
|
|
4698
|
+
schedule();
|
|
4699
|
+
return {
|
|
4700
|
+
stop() {
|
|
4701
|
+
stopped = true;
|
|
4702
|
+
if (timer) clearTimeout(timer);
|
|
4703
|
+
}
|
|
4704
|
+
};
|
|
4705
|
+
}
|
|
4706
|
+
function serialise(tick) {
|
|
4707
|
+
let running = null;
|
|
4708
|
+
let next = null;
|
|
4709
|
+
const run = () => {
|
|
4710
|
+
if (!running) {
|
|
4711
|
+
running = Promise.resolve().then(() => tick()).finally(() => {
|
|
4712
|
+
running = null;
|
|
4713
|
+
});
|
|
4714
|
+
return running;
|
|
4715
|
+
}
|
|
4716
|
+
if (!next) {
|
|
4717
|
+
next = running.catch(() => {
|
|
4718
|
+
}).then(() => {
|
|
4719
|
+
next = null;
|
|
4720
|
+
return run();
|
|
4721
|
+
});
|
|
4722
|
+
}
|
|
4723
|
+
return next;
|
|
4724
|
+
};
|
|
4725
|
+
return run;
|
|
4726
|
+
}
|
|
4104
4727
|
function makeRefreshHandler(tick, secret) {
|
|
4105
4728
|
let inflight = null;
|
|
4106
4729
|
const runOnce = () => {
|
|
@@ -4115,7 +4738,7 @@ function makeRefreshHandler(tick, secret) {
|
|
|
4115
4738
|
return async (request) => {
|
|
4116
4739
|
if (!secret) return new Response("not found", { status: 404 });
|
|
4117
4740
|
const provided = request?.headers?.get?.("x-patchstack-refresh") ?? null;
|
|
4118
|
-
if (provided
|
|
4741
|
+
if (!await sameSecret(provided, secret)) return new Response("forbidden", { status: 403 });
|
|
4119
4742
|
let refreshed = true;
|
|
4120
4743
|
try {
|
|
4121
4744
|
const status = await runOnce();
|
|
@@ -4126,6 +4749,33 @@ function makeRefreshHandler(tick, secret) {
|
|
|
4126
4749
|
return new Response(JSON.stringify({ refreshed }), { status: 200, headers: { "content-type": "application/json" } });
|
|
4127
4750
|
};
|
|
4128
4751
|
}
|
|
4752
|
+
async function sameSecret(provided, secret) {
|
|
4753
|
+
if (typeof provided !== "string" || typeof secret !== "string") return false;
|
|
4754
|
+
const subtle = globalThis.crypto?.subtle;
|
|
4755
|
+
if (subtle && typeof TextEncoder === "function") {
|
|
4756
|
+
try {
|
|
4757
|
+
const encoder2 = new TextEncoder();
|
|
4758
|
+
const [a, b] = await Promise.all([
|
|
4759
|
+
subtle.digest("SHA-256", encoder2.encode(provided)),
|
|
4760
|
+
subtle.digest("SHA-256", encoder2.encode(secret))
|
|
4761
|
+
]);
|
|
4762
|
+
return equalBytes(new Uint8Array(a), new Uint8Array(b));
|
|
4763
|
+
} catch {
|
|
4764
|
+
}
|
|
4765
|
+
}
|
|
4766
|
+
let diff = provided.length ^ secret.length;
|
|
4767
|
+
const length = Math.max(provided.length, secret.length);
|
|
4768
|
+
for (let i = 0; i < length; i++) {
|
|
4769
|
+
diff |= (provided.charCodeAt(i) || 0) ^ (secret.charCodeAt(i) || 0);
|
|
4770
|
+
}
|
|
4771
|
+
return diff === 0;
|
|
4772
|
+
}
|
|
4773
|
+
function equalBytes(a, b) {
|
|
4774
|
+
if (a.length !== b.length) return false;
|
|
4775
|
+
let diff = 0;
|
|
4776
|
+
for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i];
|
|
4777
|
+
return diff === 0;
|
|
4778
|
+
}
|
|
4129
4779
|
|
|
4130
4780
|
// src/protect/detections.js
|
|
4131
4781
|
var DEFAULT_BASE_URL3 = "https://api.patchstack.com/monitor/pulse";
|
|
@@ -4859,9 +5509,180 @@ function hardensWithoutBody(rule) {
|
|
|
4859
5509
|
if (!rule || !HEADER_ACTIONS.includes(rule.action)) return false;
|
|
4860
5510
|
const { parameters, complete } = readRuleParameters(rule);
|
|
4861
5511
|
if (!complete) return false;
|
|
4862
|
-
return parameters.every(
|
|
4863
|
-
|
|
5512
|
+
return parameters.every(bodyIndependent);
|
|
5513
|
+
}
|
|
5514
|
+
function readsOnlyResponseHeaders(rule) {
|
|
5515
|
+
if (!rule) return false;
|
|
5516
|
+
const { parameters, complete } = readRuleParameters(rule);
|
|
5517
|
+
if (!complete || !parameters.some(isHeaderParameter)) return false;
|
|
5518
|
+
return parameters.every(bodyIndependent);
|
|
5519
|
+
}
|
|
5520
|
+
function isHeaderParameter(parameter) {
|
|
5521
|
+
return parameter === "response.headers" || parameter.startsWith(BODY_INDEPENDENT_PREFIX);
|
|
5522
|
+
}
|
|
5523
|
+
function bodyIndependent(parameter) {
|
|
5524
|
+
return BODY_INDEPENDENT.includes(parameter) || parameter.startsWith(BODY_INDEPENDENT_PREFIX);
|
|
5525
|
+
}
|
|
5526
|
+
|
|
5527
|
+
// src/protect/supabase-guard.js
|
|
5528
|
+
var GUARD_PATH = "/_patchstack/guard";
|
|
5529
|
+
var DEFAULT_MAX_BODY_BYTES = 1024 * 1024;
|
|
5530
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
5531
|
+
var REQUEST_HEADER_DENYLIST = /* @__PURE__ */ new Set([
|
|
5532
|
+
"connection",
|
|
5533
|
+
"cookie",
|
|
5534
|
+
"cookie2",
|
|
5535
|
+
"forwarded",
|
|
5536
|
+
"host",
|
|
5537
|
+
"keep-alive",
|
|
5538
|
+
"proxy-authenticate",
|
|
5539
|
+
"proxy-authorization",
|
|
5540
|
+
"te",
|
|
5541
|
+
"trailer",
|
|
5542
|
+
"transfer-encoding",
|
|
5543
|
+
"upgrade",
|
|
5544
|
+
"x-forwarded-for",
|
|
5545
|
+
"x-forwarded-host",
|
|
5546
|
+
"x-forwarded-port",
|
|
5547
|
+
"x-forwarded-proto",
|
|
5548
|
+
"x-ps-target",
|
|
5549
|
+
"x-real-ip"
|
|
5550
|
+
]);
|
|
5551
|
+
var RESPONSE_HEADER_DENYLIST = /* @__PURE__ */ new Set([
|
|
5552
|
+
"connection",
|
|
5553
|
+
"content-encoding",
|
|
5554
|
+
"content-length",
|
|
5555
|
+
"keep-alive",
|
|
5556
|
+
"proxy-authenticate",
|
|
5557
|
+
"proxy-authorization",
|
|
5558
|
+
"set-cookie",
|
|
5559
|
+
"set-cookie2",
|
|
5560
|
+
"te",
|
|
5561
|
+
"trailer",
|
|
5562
|
+
"transfer-encoding",
|
|
5563
|
+
"upgrade"
|
|
5564
|
+
]);
|
|
5565
|
+
var BodyTooLargeError = class extends Error {
|
|
5566
|
+
};
|
|
5567
|
+
async function boundedBody(request, maxBytes) {
|
|
5568
|
+
const stated = Number(request.headers.get("content-length"));
|
|
5569
|
+
if (Number.isFinite(stated) && stated > maxBytes) throw new BodyTooLargeError();
|
|
5570
|
+
if (!request.body) return void 0;
|
|
5571
|
+
const reader = request.body.getReader();
|
|
5572
|
+
const chunks = [];
|
|
5573
|
+
let total = 0;
|
|
5574
|
+
try {
|
|
5575
|
+
for (; ; ) {
|
|
5576
|
+
const { done, value } = await reader.read();
|
|
5577
|
+
if (done) break;
|
|
5578
|
+
total += value.byteLength;
|
|
5579
|
+
if (total > maxBytes) {
|
|
5580
|
+
void reader.cancel().catch(() => {
|
|
5581
|
+
});
|
|
5582
|
+
throw new BodyTooLargeError();
|
|
5583
|
+
}
|
|
5584
|
+
chunks.push(value);
|
|
5585
|
+
}
|
|
5586
|
+
} finally {
|
|
5587
|
+
reader.releaseLock();
|
|
5588
|
+
}
|
|
5589
|
+
const body = new Uint8Array(total);
|
|
5590
|
+
let offset = 0;
|
|
5591
|
+
for (const chunk of chunks) {
|
|
5592
|
+
body.set(chunk, offset);
|
|
5593
|
+
offset += chunk.byteLength;
|
|
5594
|
+
}
|
|
5595
|
+
return body;
|
|
5596
|
+
}
|
|
5597
|
+
function requestHeaders(input) {
|
|
5598
|
+
const output = new Headers();
|
|
5599
|
+
const connectionTokens = new Set(
|
|
5600
|
+
(input.get("connection") ?? "").split(",").map((value) => value.trim().toLowerCase()).filter(Boolean)
|
|
5601
|
+
);
|
|
5602
|
+
input.forEach((value, key) => {
|
|
5603
|
+
const lower = key.toLowerCase();
|
|
5604
|
+
if (!REQUEST_HEADER_DENYLIST.has(lower) && !connectionTokens.has(lower)) output.append(key, value);
|
|
5605
|
+
});
|
|
5606
|
+
return output;
|
|
5607
|
+
}
|
|
5608
|
+
function responseHeaders(input) {
|
|
5609
|
+
const output = new Headers();
|
|
5610
|
+
const connectionTokens = new Set(
|
|
5611
|
+
(input.get("connection") ?? "").split(",").map((value) => value.trim().toLowerCase()).filter(Boolean)
|
|
4864
5612
|
);
|
|
5613
|
+
input.forEach((value, key) => {
|
|
5614
|
+
const lower = key.toLowerCase();
|
|
5615
|
+
if (!RESPONSE_HEADER_DENYLIST.has(lower) && !connectionTokens.has(lower)) output.append(key, value);
|
|
5616
|
+
});
|
|
5617
|
+
return output;
|
|
5618
|
+
}
|
|
5619
|
+
var SOURCE_REQUEST = /* @__PURE__ */ Symbol.for("@patchstack/connect.sourceRequest");
|
|
5620
|
+
function createSupabaseGuard({
|
|
5621
|
+
protection,
|
|
5622
|
+
supabaseUrl,
|
|
5623
|
+
fetchImpl = fetch,
|
|
5624
|
+
maxBodyBytes = DEFAULT_MAX_BODY_BYTES,
|
|
5625
|
+
timeoutMs = DEFAULT_TIMEOUT_MS
|
|
5626
|
+
}) {
|
|
5627
|
+
const guard = protection.fetchGuard();
|
|
5628
|
+
const allowedOrigin = supabaseUrl ? new URL(supabaseUrl).origin : null;
|
|
5629
|
+
const bodyLimit = Number.isFinite(maxBodyBytes) && maxBodyBytes > 0 ? maxBodyBytes : DEFAULT_MAX_BODY_BYTES;
|
|
5630
|
+
const upstreamTimeout = Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : DEFAULT_TIMEOUT_MS;
|
|
5631
|
+
return async function handleGuardRequest(request, ...hostArgs) {
|
|
5632
|
+
const target = request.headers.get("x-ps-target");
|
|
5633
|
+
if (!target) return new Response("patchstack: missing x-ps-target", { status: 400 });
|
|
5634
|
+
let targetUrl;
|
|
5635
|
+
try {
|
|
5636
|
+
targetUrl = new URL(target);
|
|
5637
|
+
} catch {
|
|
5638
|
+
return new Response("patchstack: invalid target", { status: 400 });
|
|
5639
|
+
}
|
|
5640
|
+
if (!allowedOrigin || targetUrl.protocol !== "https:" || targetUrl.origin !== allowedOrigin) {
|
|
5641
|
+
return new Response("patchstack: target not allowed", { status: 403 });
|
|
5642
|
+
}
|
|
5643
|
+
const hasBody = request.method !== "GET" && request.method !== "HEAD";
|
|
5644
|
+
let body;
|
|
5645
|
+
try {
|
|
5646
|
+
body = hasBody ? await boundedBody(request, bodyLimit) : void 0;
|
|
5647
|
+
} catch (error) {
|
|
5648
|
+
if (error instanceof BodyTooLargeError) {
|
|
5649
|
+
return new Response("patchstack: request body too large", { status: 413 });
|
|
5650
|
+
}
|
|
5651
|
+
throw error;
|
|
5652
|
+
}
|
|
5653
|
+
const forwardHeaders = requestHeaders(request.headers);
|
|
5654
|
+
const evalReq = new Request(targetUrl.toString(), {
|
|
5655
|
+
method: request.method,
|
|
5656
|
+
// Inspect what the browser sent, including app-origin cookies; only the forwarded copy is
|
|
5657
|
+
// sanitized at the cross-origin boundary below.
|
|
5658
|
+
headers: request.headers,
|
|
5659
|
+
body
|
|
5660
|
+
});
|
|
5661
|
+
Object.defineProperty(evalReq, SOURCE_REQUEST, { value: request });
|
|
5662
|
+
const blocked = await guard(evalReq, ...hostArgs);
|
|
5663
|
+
if (blocked) return blocked;
|
|
5664
|
+
let upstream;
|
|
5665
|
+
try {
|
|
5666
|
+
upstream = await fetchImpl(targetUrl.toString(), {
|
|
5667
|
+
method: request.method,
|
|
5668
|
+
headers: forwardHeaders,
|
|
5669
|
+
body,
|
|
5670
|
+
redirect: "manual",
|
|
5671
|
+
signal: AbortSignal.timeout(upstreamTimeout)
|
|
5672
|
+
});
|
|
5673
|
+
} catch (error) {
|
|
5674
|
+
if (error?.name === "TimeoutError" || error?.name === "AbortError") {
|
|
5675
|
+
return new Response("patchstack: upstream timed out", { status: 504 });
|
|
5676
|
+
}
|
|
5677
|
+
throw error;
|
|
5678
|
+
}
|
|
5679
|
+
const forwarded = new Response(upstream.body, {
|
|
5680
|
+
status: upstream.status,
|
|
5681
|
+
statusText: upstream.statusText,
|
|
5682
|
+
headers: responseHeaders(upstream.headers)
|
|
5683
|
+
});
|
|
5684
|
+
return protection.screenResponse ? protection.screenResponse(forwarded, evalReq) : forwarded;
|
|
5685
|
+
};
|
|
4865
5686
|
}
|
|
4866
5687
|
|
|
4867
5688
|
// src/protect/firewall-log.js
|
|
@@ -4910,8 +5731,13 @@ function resolveApiBase(pulseOrManifestUrl) {
|
|
|
4910
5731
|
function createFirewallLogReporter(opts) {
|
|
4911
5732
|
const creds = parseApiKey(opts.apiKey);
|
|
4912
5733
|
if (!creds) {
|
|
4913
|
-
return {
|
|
4914
|
-
|
|
5734
|
+
return {
|
|
5735
|
+
record() {
|
|
5736
|
+
},
|
|
5737
|
+
flush: () => Promise.resolve(),
|
|
5738
|
+
stop: () => Promise.resolve(),
|
|
5739
|
+
health: () => ({ recorded: 0, delivered: 0, failed: 0, dropped: 0, queued: 0 })
|
|
5740
|
+
};
|
|
4915
5741
|
}
|
|
4916
5742
|
const apiBase = safeBaseUrl(opts.apiBase, DEFAULT_API_BASE, "block-log API").replace(/\/$/, "");
|
|
4917
5743
|
const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
@@ -4924,6 +5750,19 @@ function createFirewallLogReporter(opts) {
|
|
|
4924
5750
|
let drainPromise = null;
|
|
4925
5751
|
let activeController = null;
|
|
4926
5752
|
let ended = false;
|
|
5753
|
+
let recorded = 0;
|
|
5754
|
+
let delivered = 0;
|
|
5755
|
+
let failed = 0;
|
|
5756
|
+
let dropped = 0;
|
|
5757
|
+
let activeBatch = null;
|
|
5758
|
+
const settle = (sent, outcome) => {
|
|
5759
|
+
if (sent.counted) return;
|
|
5760
|
+
sent.counted = true;
|
|
5761
|
+
if (outcome === "delivered") delivered += sent.size;
|
|
5762
|
+
else if (outcome === "failed") failed += sent.size;
|
|
5763
|
+
else dropped += sent.size;
|
|
5764
|
+
if (activeBatch === sent) activeBatch = null;
|
|
5765
|
+
};
|
|
4927
5766
|
let cachedToken = null;
|
|
4928
5767
|
let tokenInflight = null;
|
|
4929
5768
|
const fetchAccessToken = async (signal) => {
|
|
@@ -4977,10 +5816,16 @@ function createFirewallLogReporter(opts) {
|
|
|
4977
5816
|
const attemptTimer = setTimeout(() => controller?.abort(), ATTEMPT_TIMEOUT_MS2);
|
|
4978
5817
|
attemptTimer?.unref?.();
|
|
4979
5818
|
let entry;
|
|
5819
|
+
const sent = { size: batch.length, counted: false };
|
|
5820
|
+
activeBatch = sent;
|
|
4980
5821
|
entry = (async () => {
|
|
4981
5822
|
try {
|
|
4982
5823
|
const token = await fetchAccessToken(controller?.signal);
|
|
4983
|
-
if (
|
|
5824
|
+
if (ended) return;
|
|
5825
|
+
if (!token) {
|
|
5826
|
+
settle(sent, "failed");
|
|
5827
|
+
return;
|
|
5828
|
+
}
|
|
4984
5829
|
const body = new URLSearchParams();
|
|
4985
5830
|
body.set("type", "firewall");
|
|
4986
5831
|
body.set("logs", JSON.stringify(batch));
|
|
@@ -4997,10 +5842,11 @@ function createFirewallLogReporter(opts) {
|
|
|
4997
5842
|
// Both phases carry the attempt controller, so slow transports cannot accumulate work.
|
|
4998
5843
|
...controller ? { signal: controller.signal } : {}
|
|
4999
5844
|
});
|
|
5000
|
-
|
|
5001
|
-
|
|
5845
|
+
const res = p && typeof p.then === "function" ? await p.catch(() => null) : p;
|
|
5846
|
+
settle(sent, res && res.ok ? "delivered" : "failed");
|
|
5002
5847
|
} catch {
|
|
5003
5848
|
} finally {
|
|
5849
|
+
settle(sent, "failed");
|
|
5004
5850
|
clearTimeout(attemptTimer);
|
|
5005
5851
|
if (activeController === controller) activeController = null;
|
|
5006
5852
|
if (inFlight === entry) inFlight = null;
|
|
@@ -5027,234 +5873,85 @@ function createFirewallLogReporter(opts) {
|
|
|
5027
5873
|
if (stopped) return;
|
|
5028
5874
|
const fid = event?.rule?.id;
|
|
5029
5875
|
if (fid === void 0 || fid === null || fid === "") return;
|
|
5030
|
-
if (queue.length >= MAX_QUEUE2)
|
|
5031
|
-
|
|
5032
|
-
fid,
|
|
5033
|
-
method: event.method ?? null,
|
|
5034
|
-
request_uri: event.path ?? null,
|
|
5035
|
-
ip: event.ip ?? null,
|
|
5036
|
-
user_agent: event.userAgent ?? null,
|
|
5037
|
-
log_date: (/* @__PURE__ */ new Date()).toISOString()
|
|
5038
|
-
});
|
|
5039
|
-
if (queue.length >= MAX_BATCH2) {
|
|
5040
|
-
flush();
|
|
5876
|
+
if (queue.length >= MAX_QUEUE2) {
|
|
5877
|
+
dropped++;
|
|
5041
5878
|
return;
|
|
5042
|
-
}
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
* spinning would be worse than leaving the records where they are.
|
|
5052
|
-
*/
|
|
5053
|
-
stop() {
|
|
5054
|
-
if (drainPromise) return drainPromise;
|
|
5055
|
-
stopped = true;
|
|
5056
|
-
if (timer) {
|
|
5057
|
-
clearTimeout(timer);
|
|
5058
|
-
timer = null;
|
|
5059
|
-
}
|
|
5060
|
-
const terminate = () => {
|
|
5061
|
-
ended = true;
|
|
5062
|
-
activeController?.abort();
|
|
5063
|
-
activeController = null;
|
|
5064
|
-
queue = [];
|
|
5065
|
-
inFlight = null;
|
|
5066
|
-
};
|
|
5067
|
-
let budget = null;
|
|
5068
|
-
const spent = new Promise((resolve) => {
|
|
5069
|
-
budget = setTimeout(() => {
|
|
5070
|
-
terminate();
|
|
5071
|
-
resolve();
|
|
5072
|
-
}, STOP_BUDGET_MS2);
|
|
5073
|
-
if (typeof budget.unref === "function") budget.unref();
|
|
5074
|
-
});
|
|
5075
|
-
const work = (async () => {
|
|
5076
|
-
while (!ended) {
|
|
5077
|
-
if (inFlight) {
|
|
5078
|
-
await inFlight;
|
|
5079
|
-
continue;
|
|
5080
|
-
}
|
|
5081
|
-
if (queue.length === 0) break;
|
|
5082
|
-
const before = queue.length;
|
|
5083
|
-
await flush();
|
|
5084
|
-
if (queue.length >= before) break;
|
|
5085
|
-
}
|
|
5086
|
-
})();
|
|
5087
|
-
drainPromise = Promise.race([work, spent]).then(() => {
|
|
5088
|
-
if (budget) clearTimeout(budget);
|
|
5089
|
-
});
|
|
5090
|
-
return drainPromise;
|
|
5091
|
-
}
|
|
5092
|
-
};
|
|
5093
|
-
}
|
|
5094
|
-
function telemetryEnabled() {
|
|
5095
|
-
const v = typeof process !== "undefined" ? process.env?.PATCHSTACK_TELEMETRY : void 0;
|
|
5096
|
-
if (v === void 0 || v === "") return true;
|
|
5097
|
-
return !/^(0|false|off|no)$/i.test(String(v));
|
|
5098
|
-
}
|
|
5099
|
-
|
|
5100
|
-
// src/protect/supabase-guard.js
|
|
5101
|
-
var GUARD_PATH = "/_patchstack/guard";
|
|
5102
|
-
var DEFAULT_MAX_BODY_BYTES = 1024 * 1024;
|
|
5103
|
-
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
5104
|
-
var REQUEST_HEADER_DENYLIST = /* @__PURE__ */ new Set([
|
|
5105
|
-
"connection",
|
|
5106
|
-
"cookie",
|
|
5107
|
-
"cookie2",
|
|
5108
|
-
"forwarded",
|
|
5109
|
-
"host",
|
|
5110
|
-
"keep-alive",
|
|
5111
|
-
"proxy-authenticate",
|
|
5112
|
-
"proxy-authorization",
|
|
5113
|
-
"te",
|
|
5114
|
-
"trailer",
|
|
5115
|
-
"transfer-encoding",
|
|
5116
|
-
"upgrade",
|
|
5117
|
-
"x-forwarded-for",
|
|
5118
|
-
"x-forwarded-host",
|
|
5119
|
-
"x-forwarded-port",
|
|
5120
|
-
"x-forwarded-proto",
|
|
5121
|
-
"x-ps-target",
|
|
5122
|
-
"x-real-ip"
|
|
5123
|
-
]);
|
|
5124
|
-
var RESPONSE_HEADER_DENYLIST = /* @__PURE__ */ new Set([
|
|
5125
|
-
"connection",
|
|
5126
|
-
"content-encoding",
|
|
5127
|
-
"content-length",
|
|
5128
|
-
"keep-alive",
|
|
5129
|
-
"proxy-authenticate",
|
|
5130
|
-
"proxy-authorization",
|
|
5131
|
-
"set-cookie",
|
|
5132
|
-
"set-cookie2",
|
|
5133
|
-
"te",
|
|
5134
|
-
"trailer",
|
|
5135
|
-
"transfer-encoding",
|
|
5136
|
-
"upgrade"
|
|
5137
|
-
]);
|
|
5138
|
-
var BodyTooLargeError = class extends Error {
|
|
5139
|
-
};
|
|
5140
|
-
async function boundedBody(request, maxBytes) {
|
|
5141
|
-
const stated = Number(request.headers.get("content-length"));
|
|
5142
|
-
if (Number.isFinite(stated) && stated > maxBytes) throw new BodyTooLargeError();
|
|
5143
|
-
if (!request.body) return void 0;
|
|
5144
|
-
const reader = request.body.getReader();
|
|
5145
|
-
const chunks = [];
|
|
5146
|
-
let total = 0;
|
|
5147
|
-
try {
|
|
5148
|
-
for (; ; ) {
|
|
5149
|
-
const { done, value } = await reader.read();
|
|
5150
|
-
if (done) break;
|
|
5151
|
-
total += value.byteLength;
|
|
5152
|
-
if (total > maxBytes) {
|
|
5153
|
-
void reader.cancel().catch(() => {
|
|
5154
|
-
});
|
|
5155
|
-
throw new BodyTooLargeError();
|
|
5156
|
-
}
|
|
5157
|
-
chunks.push(value);
|
|
5158
|
-
}
|
|
5159
|
-
} finally {
|
|
5160
|
-
reader.releaseLock();
|
|
5161
|
-
}
|
|
5162
|
-
const body = new Uint8Array(total);
|
|
5163
|
-
let offset = 0;
|
|
5164
|
-
for (const chunk of chunks) {
|
|
5165
|
-
body.set(chunk, offset);
|
|
5166
|
-
offset += chunk.byteLength;
|
|
5167
|
-
}
|
|
5168
|
-
return body;
|
|
5169
|
-
}
|
|
5170
|
-
function requestHeaders(input) {
|
|
5171
|
-
const output = new Headers();
|
|
5172
|
-
const connectionTokens = new Set(
|
|
5173
|
-
(input.get("connection") ?? "").split(",").map((value) => value.trim().toLowerCase()).filter(Boolean)
|
|
5174
|
-
);
|
|
5175
|
-
input.forEach((value, key) => {
|
|
5176
|
-
const lower = key.toLowerCase();
|
|
5177
|
-
if (!REQUEST_HEADER_DENYLIST.has(lower) && !connectionTokens.has(lower)) output.append(key, value);
|
|
5178
|
-
});
|
|
5179
|
-
return output;
|
|
5180
|
-
}
|
|
5181
|
-
function responseHeaders(input) {
|
|
5182
|
-
const output = new Headers();
|
|
5183
|
-
const connectionTokens = new Set(
|
|
5184
|
-
(input.get("connection") ?? "").split(",").map((value) => value.trim().toLowerCase()).filter(Boolean)
|
|
5185
|
-
);
|
|
5186
|
-
input.forEach((value, key) => {
|
|
5187
|
-
const lower = key.toLowerCase();
|
|
5188
|
-
if (!RESPONSE_HEADER_DENYLIST.has(lower) && !connectionTokens.has(lower)) output.append(key, value);
|
|
5189
|
-
});
|
|
5190
|
-
return output;
|
|
5191
|
-
}
|
|
5192
|
-
function createSupabaseGuard({
|
|
5193
|
-
protection,
|
|
5194
|
-
supabaseUrl,
|
|
5195
|
-
fetchImpl = fetch,
|
|
5196
|
-
maxBodyBytes = DEFAULT_MAX_BODY_BYTES,
|
|
5197
|
-
timeoutMs = DEFAULT_TIMEOUT_MS
|
|
5198
|
-
}) {
|
|
5199
|
-
const guard = protection.fetchGuard();
|
|
5200
|
-
const allowedOrigin = supabaseUrl ? new URL(supabaseUrl).origin : null;
|
|
5201
|
-
const bodyLimit = Number.isFinite(maxBodyBytes) && maxBodyBytes > 0 ? maxBodyBytes : DEFAULT_MAX_BODY_BYTES;
|
|
5202
|
-
const upstreamTimeout = Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : DEFAULT_TIMEOUT_MS;
|
|
5203
|
-
return async function handleGuardRequest(request) {
|
|
5204
|
-
const target = request.headers.get("x-ps-target");
|
|
5205
|
-
if (!target) return new Response("patchstack: missing x-ps-target", { status: 400 });
|
|
5206
|
-
let targetUrl;
|
|
5207
|
-
try {
|
|
5208
|
-
targetUrl = new URL(target);
|
|
5209
|
-
} catch {
|
|
5210
|
-
return new Response("patchstack: invalid target", { status: 400 });
|
|
5211
|
-
}
|
|
5212
|
-
if (!allowedOrigin || targetUrl.protocol !== "https:" || targetUrl.origin !== allowedOrigin) {
|
|
5213
|
-
return new Response("patchstack: target not allowed", { status: 403 });
|
|
5214
|
-
}
|
|
5215
|
-
const hasBody = request.method !== "GET" && request.method !== "HEAD";
|
|
5216
|
-
let body;
|
|
5217
|
-
try {
|
|
5218
|
-
body = hasBody ? await boundedBody(request, bodyLimit) : void 0;
|
|
5219
|
-
} catch (error) {
|
|
5220
|
-
if (error instanceof BodyTooLargeError) {
|
|
5221
|
-
return new Response("patchstack: request body too large", { status: 413 });
|
|
5222
|
-
}
|
|
5223
|
-
throw error;
|
|
5224
|
-
}
|
|
5225
|
-
const forwardHeaders = requestHeaders(request.headers);
|
|
5226
|
-
const evalReq = new Request(targetUrl.toString(), {
|
|
5227
|
-
method: request.method,
|
|
5228
|
-
// Inspect what the browser sent, including app-origin cookies; only the forwarded copy is
|
|
5229
|
-
// sanitized at the cross-origin boundary below.
|
|
5230
|
-
headers: request.headers,
|
|
5231
|
-
body
|
|
5232
|
-
});
|
|
5233
|
-
const blocked = await guard(evalReq);
|
|
5234
|
-
if (blocked) return blocked;
|
|
5235
|
-
let upstream;
|
|
5236
|
-
try {
|
|
5237
|
-
upstream = await fetchImpl(targetUrl.toString(), {
|
|
5238
|
-
method: request.method,
|
|
5239
|
-
headers: forwardHeaders,
|
|
5240
|
-
body,
|
|
5241
|
-
redirect: "manual",
|
|
5242
|
-
signal: AbortSignal.timeout(upstreamTimeout)
|
|
5879
|
+
}
|
|
5880
|
+
recorded++;
|
|
5881
|
+
queue.push({
|
|
5882
|
+
fid,
|
|
5883
|
+
method: event.method ?? null,
|
|
5884
|
+
request_uri: event.path ?? null,
|
|
5885
|
+
ip: event.ip ?? null,
|
|
5886
|
+
user_agent: event.userAgent ?? null,
|
|
5887
|
+
log_date: (/* @__PURE__ */ new Date()).toISOString()
|
|
5243
5888
|
});
|
|
5244
|
-
|
|
5245
|
-
|
|
5246
|
-
return
|
|
5889
|
+
if (queue.length >= MAX_BATCH2) {
|
|
5890
|
+
flush();
|
|
5891
|
+
return;
|
|
5247
5892
|
}
|
|
5248
|
-
|
|
5893
|
+
if (!timer) timer = setTimeout(flush, flushMs);
|
|
5894
|
+
},
|
|
5895
|
+
flush,
|
|
5896
|
+
/** Where the records went so far; see the counters above. `queued` is what is waiting now. */
|
|
5897
|
+
health() {
|
|
5898
|
+
return { recorded, delivered, failed, dropped, queued: queue.length };
|
|
5899
|
+
},
|
|
5900
|
+
/**
|
|
5901
|
+
* Stop, and hand back a wait for what was outstanding.
|
|
5902
|
+
*
|
|
5903
|
+
* One flush sends at most a batch, so the queue is drained a batch at a time. The loop stops as soon
|
|
5904
|
+
* as a pass cannot shrink the queue — with no usable transport there is nothing to wait for, and
|
|
5905
|
+
* spinning would be worse than leaving the records where they are.
|
|
5906
|
+
*/
|
|
5907
|
+
stop() {
|
|
5908
|
+
if (drainPromise) return drainPromise;
|
|
5909
|
+
stopped = true;
|
|
5910
|
+
if (timer) {
|
|
5911
|
+
clearTimeout(timer);
|
|
5912
|
+
timer = null;
|
|
5913
|
+
}
|
|
5914
|
+
const terminate = () => {
|
|
5915
|
+
ended = true;
|
|
5916
|
+
activeController?.abort();
|
|
5917
|
+
activeController = null;
|
|
5918
|
+
if (activeBatch) settle(activeBatch, "dropped");
|
|
5919
|
+
dropped += queue.length;
|
|
5920
|
+
queue = [];
|
|
5921
|
+
inFlight = null;
|
|
5922
|
+
};
|
|
5923
|
+
let budget = null;
|
|
5924
|
+
const spent = new Promise((resolve) => {
|
|
5925
|
+
budget = setTimeout(() => {
|
|
5926
|
+
terminate();
|
|
5927
|
+
resolve();
|
|
5928
|
+
}, STOP_BUDGET_MS2);
|
|
5929
|
+
if (typeof budget.unref === "function") budget.unref();
|
|
5930
|
+
});
|
|
5931
|
+
const work = (async () => {
|
|
5932
|
+
while (!ended) {
|
|
5933
|
+
if (inFlight) {
|
|
5934
|
+
await inFlight;
|
|
5935
|
+
continue;
|
|
5936
|
+
}
|
|
5937
|
+
if (queue.length === 0) break;
|
|
5938
|
+
const before = queue.length;
|
|
5939
|
+
await flush();
|
|
5940
|
+
if (queue.length >= before) break;
|
|
5941
|
+
}
|
|
5942
|
+
})();
|
|
5943
|
+
drainPromise = Promise.race([work, spent]).then(() => {
|
|
5944
|
+
if (budget) clearTimeout(budget);
|
|
5945
|
+
});
|
|
5946
|
+
return drainPromise;
|
|
5249
5947
|
}
|
|
5250
|
-
const forwarded = new Response(upstream.body, {
|
|
5251
|
-
status: upstream.status,
|
|
5252
|
-
statusText: upstream.statusText,
|
|
5253
|
-
headers: responseHeaders(upstream.headers)
|
|
5254
|
-
});
|
|
5255
|
-
return protection.screenResponse ? protection.screenResponse(forwarded) : forwarded;
|
|
5256
5948
|
};
|
|
5257
5949
|
}
|
|
5950
|
+
function telemetryEnabled() {
|
|
5951
|
+
const v = typeof process !== "undefined" ? process.env?.PATCHSTACK_TELEMETRY : void 0;
|
|
5952
|
+
if (v === void 0 || v === "") return true;
|
|
5953
|
+
return !/^(0|false|off|no)$/i.test(String(v));
|
|
5954
|
+
}
|
|
5258
5955
|
|
|
5259
5956
|
// src/protect/verify-sentinel.js
|
|
5260
5957
|
var VERIFY_HEADER = "x-patchstack-verify";
|
|
@@ -5346,6 +6043,7 @@ async function createProtection(options = {}) {
|
|
|
5346
6043
|
"onDetect"
|
|
5347
6044
|
);
|
|
5348
6045
|
};
|
|
6046
|
+
const nodeHttp = await import("http").then((m) => m.default ?? m, () => null);
|
|
5349
6047
|
const store = makeStore(options);
|
|
5350
6048
|
const bootTimeoutMs = Number(options.bootTimeoutMs) > 0 ? Number(options.bootTimeoutMs) : 5e3;
|
|
5351
6049
|
const pulseAuth = await resolvePulseAuth(options);
|
|
@@ -5371,6 +6069,21 @@ async function createProtection(options = {}) {
|
|
|
5371
6069
|
pulseAuth,
|
|
5372
6070
|
detectionState: preFetchState
|
|
5373
6071
|
});
|
|
6072
|
+
let ruleSource = { ...bundle.source ?? { ok: true, origin: "empty" } };
|
|
6073
|
+
const warnedSources = /* @__PURE__ */ new Set();
|
|
6074
|
+
const hasLiveSource = Boolean(options.siteUuid || options.token);
|
|
6075
|
+
const noteRuleSource = (source) => {
|
|
6076
|
+
ruleSource = { ...source ?? { ok: true, origin: ruleSource.origin } };
|
|
6077
|
+
if (!hasLiveSource || typeof onError === "function" || ruleSource.ok !== false) return;
|
|
6078
|
+
const cause = `${ruleSource.origin}:${ruleSource.reason ?? ""}`;
|
|
6079
|
+
if (warnedSources.has(cause)) return;
|
|
6080
|
+
warnedSources.add(cause);
|
|
6081
|
+
const running = ruleSource.origin === "cache" ? "the last rules it received" : ruleSource.origin === "bundled" ? "its bundled fallback rules" : "no rules at all";
|
|
6082
|
+
console.warn(
|
|
6083
|
+
`[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.`
|
|
6084
|
+
);
|
|
6085
|
+
};
|
|
6086
|
+
noteRuleSource(bundle.source);
|
|
5374
6087
|
let detectionReporting = "not-enrolled";
|
|
5375
6088
|
let currentOrigin;
|
|
5376
6089
|
const applyReportingState = async (origin) => {
|
|
@@ -5426,20 +6139,47 @@ async function createProtection(options = {}) {
|
|
|
5426
6139
|
whitelist_keys: delivered.whitelist_keys,
|
|
5427
6140
|
onError
|
|
5428
6141
|
});
|
|
5429
|
-
responseRuleSet = responseRules.map((rule) =>
|
|
5430
|
-
|
|
5431
|
-
|
|
5432
|
-
|
|
5433
|
-
|
|
5434
|
-
|
|
5435
|
-
|
|
5436
|
-
|
|
5437
|
-
|
|
5438
|
-
|
|
5439
|
-
|
|
5440
|
-
|
|
5441
|
-
|
|
5442
|
-
|
|
6142
|
+
responseRuleSet = responseRules.map((rule) => {
|
|
6143
|
+
const unscoped = [];
|
|
6144
|
+
const rewrites = rule.action === "redact" || rule.action === "encode";
|
|
6145
|
+
let derived;
|
|
6146
|
+
try {
|
|
6147
|
+
const redactors = rewrites ? extractRedactors(rule, (parameter) => unscoped.push(parameter)) : null;
|
|
6148
|
+
const mutatedSpan = rewrites && hasSpanMutations(rule);
|
|
6149
|
+
derived = {
|
|
6150
|
+
redactors,
|
|
6151
|
+
mutatedSpan,
|
|
6152
|
+
// A redaction that reads response headers only, and masks spans in them, and an explicit block
|
|
6153
|
+
// that reads response headers only, are decided and carried out without the body — so they
|
|
6154
|
+
// still apply to a response whose body was not screened.
|
|
6155
|
+
redactsHeaders: rule.action === "redact" && !mutatedSpan && (redactors ?? []).some((r) => !r.jsonPath) && readsOnlyResponseHeaders(rule),
|
|
6156
|
+
blocksOnHeaders: rule.action === "block" && readsOnlyResponseHeaders(rule)
|
|
6157
|
+
};
|
|
6158
|
+
} catch (err) {
|
|
6159
|
+
notify(onError, err, "onError");
|
|
6160
|
+
derived = { redactors: null, mutatedSpan: false, redactsHeaders: false, blocksOnHeaders: false };
|
|
6161
|
+
}
|
|
6162
|
+
if (unscoped.length) {
|
|
6163
|
+
notify(
|
|
6164
|
+
onError,
|
|
6165
|
+
new Error(
|
|
6166
|
+
`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`
|
|
6167
|
+
),
|
|
6168
|
+
"onError"
|
|
6169
|
+
);
|
|
6170
|
+
}
|
|
6171
|
+
return {
|
|
6172
|
+
rule,
|
|
6173
|
+
engine: new RuleEngine({ firewall: [rule], onError }),
|
|
6174
|
+
...derived,
|
|
6175
|
+
// Optional cheap pre-filter: literal anchor(s) that MUST appear for the (expensive) regex to
|
|
6176
|
+
// have any chance of matching. Lets screenText skip the full scan on responses with no candidate
|
|
6177
|
+
// — the common case — cutting CPU/latency and shrinking the regex/ReDoS surface.
|
|
6178
|
+
// Case-insensitive, and checked against the header values as well as the body, since a rule may
|
|
6179
|
+
// read either.
|
|
6180
|
+
prefilter: Array.isArray(rule.prefilter) && rule.prefilter.length ? rule.prefilter.map((s) => String(s).toLowerCase()) : null
|
|
6181
|
+
};
|
|
6182
|
+
});
|
|
5443
6183
|
egressRuleSet = egressRules.map((rule) => ({
|
|
5444
6184
|
rule,
|
|
5445
6185
|
engine: new RuleEngine({ firewall: [rule], onError })
|
|
@@ -5453,6 +6193,9 @@ async function createProtection(options = {}) {
|
|
|
5453
6193
|
skipCounts[key] = (skipCounts[key] ?? 0) + 1;
|
|
5454
6194
|
notify(onSkip, { phase, reason, detail, count: skipCounts[key] }, "onSkip");
|
|
5455
6195
|
};
|
|
6196
|
+
const recordEvaluationSkips = (result) => {
|
|
6197
|
+
for (const reason of result?.skips ?? []) recordSkip("request", reason);
|
|
6198
|
+
};
|
|
5456
6199
|
const maskFn = typeof options.maskWith === "function" ? options.maskWith : () => typeof options.maskWith === "string" ? options.maskWith : "[REDACTED]";
|
|
5457
6200
|
const ruleMode = (rule) => rule?.enforcement === "dry-run" ? "dry-run" : mode;
|
|
5458
6201
|
const planCache = createPlanCache();
|
|
@@ -5495,15 +6238,19 @@ async function createProtection(options = {}) {
|
|
|
5495
6238
|
});
|
|
5496
6239
|
return effectiveMode === "block" ? block() : allow2();
|
|
5497
6240
|
};
|
|
6241
|
+
const decidedByHeaders = (_rule, entry) => Boolean(entry?.redactsHeaders || entry?.blocksOnHeaders);
|
|
6242
|
+
const decidedWithoutBody = (rule, entry) => hardensWithoutBody(rule) || decidedByHeaders(rule, entry);
|
|
5498
6243
|
const screenText = (text, meta, reqCtx, only) => {
|
|
5499
6244
|
let blockRule = null;
|
|
5500
6245
|
const redactions = [];
|
|
5501
6246
|
const headerMutations = [];
|
|
5502
6247
|
let lowerText = null;
|
|
5503
|
-
|
|
5504
|
-
|
|
6248
|
+
const responseSkips = /* @__PURE__ */ new Set();
|
|
6249
|
+
for (const entry of responseRuleSet) {
|
|
6250
|
+
const { rule, engine: re, redactors, prefilter, mutatedSpan } = entry;
|
|
6251
|
+
if (only && !only(rule, entry)) continue;
|
|
5505
6252
|
if (prefilter) {
|
|
5506
|
-
if (lowerText === null) lowerText = text.
|
|
6253
|
+
if (lowerText === null) lowerText = prefilterText(text, meta.headers);
|
|
5507
6254
|
if (!prefilter.some((p) => lowerText.includes(p))) continue;
|
|
5508
6255
|
}
|
|
5509
6256
|
let result;
|
|
@@ -5513,6 +6260,11 @@ async function createProtection(options = {}) {
|
|
|
5513
6260
|
notify(onError, err, "onError");
|
|
5514
6261
|
continue;
|
|
5515
6262
|
}
|
|
6263
|
+
for (const reason of result.skips ?? []) {
|
|
6264
|
+
if (responseSkips.has(reason)) continue;
|
|
6265
|
+
responseSkips.add(reason);
|
|
6266
|
+
recordSkip("response", reason);
|
|
6267
|
+
}
|
|
5516
6268
|
if (!result.blocked) continue;
|
|
5517
6269
|
const responseMode = ruleMode(rule);
|
|
5518
6270
|
onDetect({
|
|
@@ -5542,6 +6294,8 @@ async function createProtection(options = {}) {
|
|
|
5542
6294
|
if (blockRule) return { verdict: "block" };
|
|
5543
6295
|
let body = text;
|
|
5544
6296
|
const headers = { ...meta.headers || {} };
|
|
6297
|
+
let responseIsJson;
|
|
6298
|
+
let structure;
|
|
5545
6299
|
for (const { rule, redactors } of redactions) {
|
|
5546
6300
|
const mask = maskFn(rule.category);
|
|
5547
6301
|
const transform = rule.action === "encode" ? htmlEscape : null;
|
|
@@ -5549,17 +6303,29 @@ async function createProtection(options = {}) {
|
|
|
5549
6303
|
const spanRedactors = redactors.filter((r) => !r.jsonPath);
|
|
5550
6304
|
if (pathRedactors.length) body = applyPathRedactors(body, pathRedactors, mask, screenCap, transform);
|
|
5551
6305
|
if (!spanRedactors.length) continue;
|
|
5552
|
-
|
|
6306
|
+
const beforeSpan = body;
|
|
6307
|
+
const bodySpans = spanRedactors.filter((r) => r.scope.body === true);
|
|
6308
|
+
if (bodySpans.length) body = applyRedactors(body, bodySpans, mask, transform);
|
|
6309
|
+
if (body !== beforeSpan) {
|
|
6310
|
+
responseIsJson ??= isJson(text);
|
|
6311
|
+
if (responseIsJson) {
|
|
6312
|
+
if (structure?.text !== beforeSpan) structure = { text: beforeSpan, tokens: lexJson(beforeSpan) };
|
|
6313
|
+
const tokens = lexJson(body);
|
|
6314
|
+
if (!sameJsonStructure(structure.text, structure.tokens, body, tokens)) return { verdict: "block" };
|
|
6315
|
+
structure = { text: body, tokens };
|
|
6316
|
+
}
|
|
6317
|
+
}
|
|
5553
6318
|
if (transform) continue;
|
|
5554
6319
|
for (const name of Object.keys(headers)) {
|
|
5555
6320
|
const value = headers[name];
|
|
6321
|
+
const headerSpans = spanRedactors.filter((r) => masksHeader(r, name));
|
|
5556
6322
|
if (typeof value === "string") {
|
|
5557
|
-
setOwn(headers, name, applyRedactors(value,
|
|
6323
|
+
setOwn(headers, name, applyRedactors(value, headerSpans, mask));
|
|
5558
6324
|
} else if (Array.isArray(value)) {
|
|
5559
6325
|
setOwn(
|
|
5560
6326
|
headers,
|
|
5561
6327
|
name,
|
|
5562
|
-
value.map((item) => typeof item === "string" ? applyRedactors(item,
|
|
6328
|
+
value.map((item) => typeof item === "string" ? applyRedactors(item, headerSpans, mask) : item)
|
|
5563
6329
|
);
|
|
5564
6330
|
}
|
|
5565
6331
|
}
|
|
@@ -5606,18 +6372,53 @@ async function createProtection(options = {}) {
|
|
|
5606
6372
|
}
|
|
5607
6373
|
};
|
|
5608
6374
|
};
|
|
5609
|
-
const
|
|
6375
|
+
const peerOf = (request, hostArgs) => {
|
|
6376
|
+
if (typeof options.peerAddress !== "function") return void 0;
|
|
6377
|
+
try {
|
|
6378
|
+
return options.peerAddress(request?.[SOURCE_REQUEST] ?? request, ...hostArgs);
|
|
6379
|
+
} catch (err) {
|
|
6380
|
+
notify(onError, err, "onError");
|
|
6381
|
+
return void 0;
|
|
6382
|
+
}
|
|
6383
|
+
};
|
|
6384
|
+
let warnedNoPeer = false;
|
|
6385
|
+
const warnNoPeer = () => {
|
|
6386
|
+
if (warnedNoPeer) return;
|
|
6387
|
+
warnedNoPeer = true;
|
|
6388
|
+
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.";
|
|
6389
|
+
if (typeof onError === "function") notify(onError, new Error(message), "onError");
|
|
6390
|
+
else console.warn(message);
|
|
6391
|
+
};
|
|
6392
|
+
const clientByRequest = /* @__PURE__ */ new WeakMap();
|
|
6393
|
+
const clientForResponse = (request, hostArgs) => {
|
|
6394
|
+
const known = clientByRequest.get(request);
|
|
6395
|
+
if (known) return known;
|
|
6396
|
+
const client = resolveClientIp({
|
|
6397
|
+
peer: peerOf(request, hostArgs),
|
|
6398
|
+
headers: headerObject(request.headers),
|
|
6399
|
+
trustedProxy: options.trustedProxy
|
|
6400
|
+
});
|
|
6401
|
+
if (options.trustedProxy !== void 0 && client.source === "unavailable") warnNoPeer();
|
|
6402
|
+
return client;
|
|
6403
|
+
};
|
|
6404
|
+
const screenFetchRequest = async (request, hostArgs = []) => {
|
|
5610
6405
|
let result;
|
|
5611
6406
|
let shaped;
|
|
5612
6407
|
try {
|
|
5613
6408
|
shaped = await fromFetchRequest(request, {
|
|
6409
|
+
peer: peerOf(request, hostArgs),
|
|
5614
6410
|
trustedProxy: options.trustedProxy,
|
|
5615
6411
|
maxBodyBytes: options.maxBodyBytes
|
|
5616
6412
|
});
|
|
6413
|
+
if (shaped?._clientIp) {
|
|
6414
|
+
clientByRequest.set(request, shaped._clientIp);
|
|
6415
|
+
if (options.trustedProxy !== void 0 && shaped._clientIp.source === "unavailable") warnNoPeer();
|
|
6416
|
+
}
|
|
5617
6417
|
if (shaped?._bodyInspectionSkip) {
|
|
5618
6418
|
recordSkip("request", shaped._bodyInspectionSkip, { limit: options.maxBodyBytes ?? 1024 * 1024 });
|
|
5619
6419
|
}
|
|
5620
6420
|
result = engine.evaluate(shaped);
|
|
6421
|
+
recordEvaluationSkips(result);
|
|
5621
6422
|
} catch (err) {
|
|
5622
6423
|
notify(onError, err, "onError");
|
|
5623
6424
|
return { blocked: null, client: void 0 };
|
|
@@ -5666,14 +6467,20 @@ async function createProtection(options = {}) {
|
|
|
5666
6467
|
const screenResp = async (response, reqCtx) => {
|
|
5667
6468
|
const read = await readTextResponse(response, screenCap);
|
|
5668
6469
|
if (read.skip) {
|
|
5669
|
-
if (read.skip !== "not-a-response")
|
|
6470
|
+
if (read.skip !== "not-a-response") {
|
|
6471
|
+
recordSkip("response", read.skip, {
|
|
6472
|
+
status: response?.status,
|
|
6473
|
+
...read.encoding ? { encoding: read.encoding } : {},
|
|
6474
|
+
...read.charset ? { charset: read.charset } : {}
|
|
6475
|
+
});
|
|
6476
|
+
}
|
|
5670
6477
|
if (read.skip === "not-a-response") return response;
|
|
5671
6478
|
return hardenHeadersOnly(response, reqCtx);
|
|
5672
6479
|
}
|
|
5673
|
-
const text = read.text;
|
|
6480
|
+
const text = readJsonEscapes(read.text);
|
|
5674
6481
|
const r = screenText(text, { status: response.status, headers: headerObject(response.headers) }, reqCtx);
|
|
5675
|
-
if (r.verdict === "block") return leakResponse();
|
|
5676
|
-
if (r.verdict === "redact") return rebuildResponse(response, r.body, r.headers);
|
|
6482
|
+
if (r.verdict === "block" || r.verdict === "redact" && outgrowsCharset(read.ascii, r.body)) return leakResponse();
|
|
6483
|
+
if (r.verdict === "redact") return rebuildResponse(response, withBom(read.bom, r.body), r.headers);
|
|
5677
6484
|
return response;
|
|
5678
6485
|
};
|
|
5679
6486
|
const headerValueChanged = (was, value) => {
|
|
@@ -5735,7 +6542,8 @@ async function createProtection(options = {}) {
|
|
|
5735
6542
|
const hardenHeadersOnly = (response, reqCtx) => {
|
|
5736
6543
|
try {
|
|
5737
6544
|
const meta = { status: response.status, headers: headerObject(response.headers) };
|
|
5738
|
-
const r = screenText("", meta, reqCtx,
|
|
6545
|
+
const r = screenText("", meta, reqCtx, decidedWithoutBody);
|
|
6546
|
+
if (r.verdict === "block") return leakResponse();
|
|
5739
6547
|
if (r.verdict !== "redact" || !r.headers) return response;
|
|
5740
6548
|
if (!headersChanged(meta.headers, r.headers)) return response;
|
|
5741
6549
|
return rebuildResponse(response, response.body, r.headers);
|
|
@@ -5747,9 +6555,16 @@ async function createProtection(options = {}) {
|
|
|
5747
6555
|
const wrapNodeResponse = (res, reqCtx) => {
|
|
5748
6556
|
const origWrite = res.write.bind(res);
|
|
5749
6557
|
const origEnd = res.end.bind(res);
|
|
6558
|
+
const setHeaderNow = typeof res.setHeader === "function" ? res.setHeader.bind(res) : null;
|
|
6559
|
+
const removeHeaderNow = typeof res.removeHeader === "function" ? res.removeHeader.bind(res) : null;
|
|
6560
|
+
const sentAccessor = accessorOf(res, "headersSent");
|
|
6561
|
+
let sentValue = sentAccessor ? void 0 : res.headersSent;
|
|
6562
|
+
const reallySent = () => Boolean(sentAccessor ? sentAccessor.call(res) : sentValue);
|
|
5750
6563
|
const chunks = [];
|
|
5751
6564
|
let size = 0;
|
|
5752
6565
|
let overflow = false;
|
|
6566
|
+
let withheld = false;
|
|
6567
|
+
let headScreened = false;
|
|
5753
6568
|
const MAX = screenCap;
|
|
5754
6569
|
let hardened = false;
|
|
5755
6570
|
let answeredWithoutBody = false;
|
|
@@ -5757,7 +6572,7 @@ async function createProtection(options = {}) {
|
|
|
5757
6572
|
if (hardened) return null;
|
|
5758
6573
|
hardened = true;
|
|
5759
6574
|
try {
|
|
5760
|
-
if (
|
|
6575
|
+
if (reallySent() || !setHeaderNow) return null;
|
|
5761
6576
|
const set = typeof res.getHeaders === "function" ? res.getHeaders() : {};
|
|
5762
6577
|
const headers = effective && effective.headers ? { ...set, ...effective.headers } : set;
|
|
5763
6578
|
const status = effective && effective.status !== void 0 ? effective.status : res.statusCode;
|
|
@@ -5769,8 +6584,8 @@ async function createProtection(options = {}) {
|
|
|
5769
6584
|
if (!headerValueChanged(headers[name], value)) continue;
|
|
5770
6585
|
changed.set(name.toLowerCase(), value);
|
|
5771
6586
|
try {
|
|
5772
|
-
if (value === null || value === void 0)
|
|
5773
|
-
else
|
|
6587
|
+
if (value === null || value === void 0) removeHeaderNow?.(name);
|
|
6588
|
+
else setHeaderNow(name, value);
|
|
5774
6589
|
} catch {
|
|
5775
6590
|
}
|
|
5776
6591
|
}
|
|
@@ -5781,26 +6596,187 @@ async function createProtection(options = {}) {
|
|
|
5781
6596
|
}
|
|
5782
6597
|
};
|
|
5783
6598
|
const stillToAnswer = (rule) => !hardensWithoutBody(rule);
|
|
5784
|
-
|
|
5785
|
-
|
|
6599
|
+
const notYetAnswered = (rule, entry) => (!answeredWithoutBody || stillToAnswer(rule)) && !(headScreened && decidedByHeaders(rule, entry));
|
|
6600
|
+
let pendingHead = null;
|
|
6601
|
+
let sending = false;
|
|
6602
|
+
const origWriteHead = typeof res.writeHead === "function" ? res.writeHead.bind(res) : null;
|
|
6603
|
+
if (origWriteHead) {
|
|
5786
6604
|
res.writeHead = function(...args) {
|
|
5787
|
-
|
|
5788
|
-
|
|
5789
|
-
const
|
|
5790
|
-
|
|
5791
|
-
|
|
5792
|
-
|
|
5793
|
-
|
|
5794
|
-
|
|
5795
|
-
|
|
5796
|
-
|
|
6605
|
+
if (sending || reallySent()) return origWriteHead(...args);
|
|
6606
|
+
if (pendingHead) throw headersSentError("write");
|
|
6607
|
+
const judged = judgeWriteHead(args);
|
|
6608
|
+
if (judged) {
|
|
6609
|
+
res.statusCode = judged.statusCode;
|
|
6610
|
+
res.statusMessage = judged.statusMessage;
|
|
6611
|
+
mirrorHeaders(judged.headers);
|
|
6612
|
+
pendingHead = args;
|
|
6613
|
+
} else {
|
|
6614
|
+
pendingHead = args;
|
|
6615
|
+
if (typeof args[0] === "number") res.statusCode = args[0];
|
|
6616
|
+
if (typeof args[1] === "string") res.statusMessage = args[1];
|
|
6617
|
+
}
|
|
6618
|
+
return res;
|
|
6619
|
+
};
|
|
6620
|
+
}
|
|
6621
|
+
function judgeWriteHead(args) {
|
|
6622
|
+
const ServerResponse = nodeHttp?.ServerResponse;
|
|
6623
|
+
if (typeof ServerResponse !== "function") return null;
|
|
6624
|
+
const detached = new ServerResponse({ method: res.req?.method ?? "GET", httpVersionMajor: 1, httpVersionMinor: 1, headers: {} });
|
|
6625
|
+
if (res.statusMessage !== void 0) detached.statusMessage = res.statusMessage;
|
|
6626
|
+
for (const [name, value] of headerEntries(res)) detached.setHeader(name, value);
|
|
6627
|
+
detached.writeHead(...args);
|
|
6628
|
+
return { statusCode: detached.statusCode, statusMessage: detached.statusMessage, headers: headerEntries(detached) };
|
|
6629
|
+
}
|
|
6630
|
+
function headerEntries(target) {
|
|
6631
|
+
const names = typeof target.getRawHeaderNames === "function" ? target.getRawHeaderNames() : typeof target.getHeaderNames === "function" ? target.getHeaderNames() : [];
|
|
6632
|
+
return names.map((name) => [name, target.getHeader(name)]);
|
|
6633
|
+
}
|
|
6634
|
+
function mirrorHeaders(entries) {
|
|
6635
|
+
if (!setHeaderNow) return;
|
|
6636
|
+
for (const [name, value] of entries) {
|
|
6637
|
+
const current = res.getHeader?.(name);
|
|
6638
|
+
if (current === void 0 || headerValueChanged(current, value)) setHeaderNow(name, value);
|
|
6639
|
+
}
|
|
6640
|
+
}
|
|
6641
|
+
Object.defineProperty(res, "headersSent", {
|
|
6642
|
+
configurable: true,
|
|
6643
|
+
enumerable: true,
|
|
6644
|
+
get: () => pendingHead !== null || reallySent(),
|
|
6645
|
+
// A response-like object that tracks the flag itself keeps doing so.
|
|
6646
|
+
set: (value) => {
|
|
6647
|
+
if (!sentAccessor) sentValue = value;
|
|
6648
|
+
}
|
|
6649
|
+
});
|
|
6650
|
+
for (const [method, verb] of [["setHeader", "set"], ["removeHeader", "remove"], ["appendHeader", "append"]]) {
|
|
6651
|
+
const original = typeof res[method] === "function" ? res[method].bind(res) : null;
|
|
6652
|
+
if (!original) continue;
|
|
6653
|
+
res[method] = function(...args) {
|
|
6654
|
+
if (pendingHead) throw headersSentError(verb);
|
|
6655
|
+
return original(...args);
|
|
6656
|
+
};
|
|
6657
|
+
}
|
|
6658
|
+
if (typeof res.flushHeaders === "function") {
|
|
6659
|
+
const origFlushHeaders = res.flushHeaders.bind(res);
|
|
6660
|
+
res.flushHeaders = function() {
|
|
6661
|
+
const unread = screenUnreadHead();
|
|
6662
|
+
if (unread?.withhold) {
|
|
6663
|
+
sendWithheld();
|
|
6664
|
+
return void 0;
|
|
6665
|
+
}
|
|
6666
|
+
sendHead(unread);
|
|
6667
|
+
return origFlushHeaders();
|
|
5797
6668
|
};
|
|
5798
6669
|
}
|
|
6670
|
+
let hardenDone = false;
|
|
6671
|
+
let hardenedValues = null;
|
|
6672
|
+
const hardenOnce = () => {
|
|
6673
|
+
if (hardenDone) return hardenedValues;
|
|
6674
|
+
hardenDone = true;
|
|
6675
|
+
if (!pendingHead) {
|
|
6676
|
+
hardenedValues = hardenBeforeFlush();
|
|
6677
|
+
return hardenedValues;
|
|
6678
|
+
}
|
|
6679
|
+
const at = writeHeadHeaderIndex(pendingHead);
|
|
6680
|
+
hardenedValues = hardenBeforeFlush({
|
|
6681
|
+
status: typeof pendingHead[0] === "number" ? pendingHead[0] : void 0,
|
|
6682
|
+
headers: at === -1 ? null : writeHeadHeaderObject(pendingHead[at])
|
|
6683
|
+
});
|
|
6684
|
+
return hardenedValues;
|
|
6685
|
+
};
|
|
6686
|
+
const effectiveHead = () => {
|
|
6687
|
+
const set = typeof res.getHeaders === "function" ? { ...res.getHeaders() } : {};
|
|
6688
|
+
if (typeof res.getHeaders !== "function" && typeof res.getHeader === "function") {
|
|
6689
|
+
for (const name of ["content-type", "content-length"]) {
|
|
6690
|
+
const value = res.getHeader(name);
|
|
6691
|
+
if (value !== void 0) set[name] = value;
|
|
6692
|
+
}
|
|
6693
|
+
}
|
|
6694
|
+
if (!pendingHead) return { status: res.statusCode, headers: set };
|
|
6695
|
+
const at = writeHeadHeaderIndex(pendingHead);
|
|
6696
|
+
return {
|
|
6697
|
+
status: typeof pendingHead[0] === "number" ? pendingHead[0] : res.statusCode,
|
|
6698
|
+
headers: at === -1 ? set : { ...set, ...writeHeadHeaderObject(pendingHead[at]) }
|
|
6699
|
+
};
|
|
6700
|
+
};
|
|
6701
|
+
const screenUnreadHead = () => {
|
|
6702
|
+
if (headScreened) return void 0;
|
|
6703
|
+
headScreened = true;
|
|
6704
|
+
try {
|
|
6705
|
+
const head = effectiveHead();
|
|
6706
|
+
const r = screenText("", head, reqCtx, decidedByHeaders);
|
|
6707
|
+
if (r.verdict === "block") {
|
|
6708
|
+
if (!reallySent()) return { withhold: true };
|
|
6709
|
+
recordSkip("response", "headers-sent", { action: "block" });
|
|
6710
|
+
return void 0;
|
|
6711
|
+
}
|
|
6712
|
+
if (r.verdict !== "redact" || !r.headers) return void 0;
|
|
6713
|
+
const changed = /* @__PURE__ */ new Map();
|
|
6714
|
+
for (const [name, value] of Object.entries(r.headers)) {
|
|
6715
|
+
if (headerValueChanged(head.headers[name], value)) changed.set(name.toLowerCase(), value);
|
|
6716
|
+
}
|
|
6717
|
+
if (changed.size && reallySent()) {
|
|
6718
|
+
recordSkip("response", "headers-sent", { headers: [...changed.keys()] });
|
|
6719
|
+
return void 0;
|
|
6720
|
+
}
|
|
6721
|
+
return changed.size ? { headers: changed } : void 0;
|
|
6722
|
+
} catch (err) {
|
|
6723
|
+
notify(onError, err, "onError");
|
|
6724
|
+
return void 0;
|
|
6725
|
+
}
|
|
6726
|
+
};
|
|
6727
|
+
const sendHead = (changes) => {
|
|
6728
|
+
if (sending) return;
|
|
6729
|
+
const hardened2 = hardenOnce();
|
|
6730
|
+
sending = true;
|
|
6731
|
+
if (!pendingHead) {
|
|
6732
|
+
if (changes) applyToResponse(changes);
|
|
6733
|
+
return;
|
|
6734
|
+
}
|
|
6735
|
+
const args = [...pendingHead];
|
|
6736
|
+
pendingHead = null;
|
|
6737
|
+
const merged = new Map(hardened2 ?? []);
|
|
6738
|
+
for (const [name, value] of changes?.headers ?? []) merged.set(name, value);
|
|
6739
|
+
if (changes?.status !== void 0) {
|
|
6740
|
+
args[0] = changes.status;
|
|
6741
|
+
if (typeof args[1] === "string") args.splice(1, 1);
|
|
6742
|
+
}
|
|
6743
|
+
const headerAt = writeHeadHeaderIndex(args);
|
|
6744
|
+
if (headerAt !== -1 && merged.size) args[headerAt] = rewriteWriteHeadHeaders(args[headerAt], merged);
|
|
6745
|
+
applyToResponse({ headers: merged });
|
|
6746
|
+
origWriteHead(...args);
|
|
6747
|
+
};
|
|
6748
|
+
const sendWithheld = (cb) => {
|
|
6749
|
+
const body = JSON.stringify({ error: "Response withheld by Patchstack (sensitive data detected)" });
|
|
6750
|
+
const headers = /* @__PURE__ */ new Map();
|
|
6751
|
+
for (const name of Object.keys(effectiveHead().headers)) headers.set(name.toLowerCase(), null);
|
|
6752
|
+
headers.set("content-type", "application/json");
|
|
6753
|
+
headers.set("content-length", String(Buffer.byteLength(body)));
|
|
6754
|
+
headers.set("transfer-encoding", null);
|
|
6755
|
+
withheld = true;
|
|
6756
|
+
sendHead({ status: 500, headers });
|
|
6757
|
+
return origEnd(body, cb);
|
|
6758
|
+
};
|
|
6759
|
+
const applyToResponse = (changes) => {
|
|
6760
|
+
if (changes.status !== void 0) res.statusCode = changes.status;
|
|
6761
|
+
for (const [name, value] of changes.headers ?? []) {
|
|
6762
|
+
try {
|
|
6763
|
+
if (value === null || value === void 0) removeHeaderNow?.(name);
|
|
6764
|
+
else setHeaderNow?.(name, value);
|
|
6765
|
+
} catch {
|
|
6766
|
+
}
|
|
6767
|
+
}
|
|
6768
|
+
};
|
|
5799
6769
|
const collect = (chunk, enc) => {
|
|
5800
6770
|
if (chunk == null) return;
|
|
5801
6771
|
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, typeof enc === "string" ? enc : "utf8");
|
|
5802
6772
|
size += buf.length;
|
|
5803
6773
|
if (size > MAX) {
|
|
6774
|
+
const unread = screenUnreadHead();
|
|
6775
|
+
if (unread?.withhold) {
|
|
6776
|
+
sendWithheld();
|
|
6777
|
+
return;
|
|
6778
|
+
}
|
|
6779
|
+
sendHead(unread);
|
|
5804
6780
|
for (const c of chunks) origWrite(c);
|
|
5805
6781
|
chunks.length = 0;
|
|
5806
6782
|
origWrite(buf);
|
|
@@ -5811,7 +6787,11 @@ async function createProtection(options = {}) {
|
|
|
5811
6787
|
chunks.push(buf);
|
|
5812
6788
|
};
|
|
5813
6789
|
res.write = function(chunk, enc, cb) {
|
|
5814
|
-
|
|
6790
|
+
if (withheld) {
|
|
6791
|
+
if (typeof enc === "function") enc();
|
|
6792
|
+
else if (typeof cb === "function") cb();
|
|
6793
|
+
return true;
|
|
6794
|
+
}
|
|
5815
6795
|
if (overflow) return origWrite(chunk, enc, cb);
|
|
5816
6796
|
collect(chunk, enc);
|
|
5817
6797
|
if (typeof enc === "function") enc();
|
|
@@ -5819,7 +6799,6 @@ async function createProtection(options = {}) {
|
|
|
5819
6799
|
return true;
|
|
5820
6800
|
};
|
|
5821
6801
|
res.end = function(chunk, enc, cb) {
|
|
5822
|
-
hardenBeforeFlush();
|
|
5823
6802
|
if (typeof chunk === "function") {
|
|
5824
6803
|
cb = chunk;
|
|
5825
6804
|
chunk = void 0;
|
|
@@ -5828,74 +6807,90 @@ async function createProtection(options = {}) {
|
|
|
5828
6807
|
cb = enc;
|
|
5829
6808
|
enc = void 0;
|
|
5830
6809
|
}
|
|
6810
|
+
if (withheld) {
|
|
6811
|
+
if (typeof cb === "function") cb();
|
|
6812
|
+
return res;
|
|
6813
|
+
}
|
|
5831
6814
|
if (overflow) {
|
|
5832
6815
|
if (chunk != null) origWrite(chunk, enc);
|
|
5833
6816
|
return origEnd(cb);
|
|
5834
6817
|
}
|
|
5835
6818
|
collect(chunk, enc);
|
|
6819
|
+
if (withheld) {
|
|
6820
|
+
if (typeof cb === "function") cb();
|
|
6821
|
+
return res;
|
|
6822
|
+
}
|
|
5836
6823
|
if (overflow) return origEnd(cb);
|
|
6824
|
+
hardenOnce();
|
|
6825
|
+
const passThrough = (changes) => {
|
|
6826
|
+
if (changes?.withhold) return sendWithheld(cb);
|
|
6827
|
+
sendHead(changes);
|
|
6828
|
+
for (const c of chunks) origWrite(c);
|
|
6829
|
+
return origEnd(cb);
|
|
6830
|
+
};
|
|
5837
6831
|
const buffer = Buffer.concat(chunks);
|
|
5838
|
-
|
|
6832
|
+
const head = effectiveHead();
|
|
6833
|
+
let ct = head.headers["content-type"];
|
|
5839
6834
|
if (Array.isArray(ct)) ct = ct[0];
|
|
5840
6835
|
const kind = screenableContentType(ct);
|
|
5841
6836
|
if (kind === "skip" || kind === "sniff" && looksBinary(buffer)) {
|
|
5842
6837
|
recordSkip("response", kind === "skip" ? baseContentType(ct) === "text/event-stream" ? "live-stream" : "non-text-content-type" : "binary-body");
|
|
5843
|
-
|
|
5844
|
-
return origEnd(cb);
|
|
6838
|
+
return passThrough(screenUnreadHead());
|
|
5845
6839
|
}
|
|
5846
|
-
const
|
|
6840
|
+
const codings = declaredCodings(head.headers["content-encoding"]);
|
|
6841
|
+
if (codings.length > 0 && stillEncoded(buffer)) {
|
|
6842
|
+
recordSkip("response", "encoded-body", { encoding: codings.join(", ") });
|
|
6843
|
+
return passThrough(screenUnreadHead());
|
|
6844
|
+
}
|
|
6845
|
+
const reading = charsetReading(ct, buffer);
|
|
6846
|
+
if (reading.skip) {
|
|
6847
|
+
recordSkip("response", "unsupported-charset", { charset: reading.skip });
|
|
6848
|
+
return passThrough(screenUnreadHead());
|
|
6849
|
+
}
|
|
6850
|
+
const text = readJsonEscapes(buffer.toString("utf8", reading.bom ? 3 : 0));
|
|
5847
6851
|
let r;
|
|
5848
6852
|
try {
|
|
5849
|
-
r = screenText(
|
|
5850
|
-
text,
|
|
5851
|
-
{ status: res.statusCode, headers: res.getHeaders ? res.getHeaders() : {} },
|
|
5852
|
-
reqCtx,
|
|
5853
|
-
answeredWithoutBody ? stillToAnswer : void 0
|
|
5854
|
-
);
|
|
6853
|
+
r = screenText(text, head, reqCtx, notYetAnswered);
|
|
5855
6854
|
} catch (err) {
|
|
5856
6855
|
notify(onError, err, "onError");
|
|
5857
|
-
|
|
5858
|
-
|
|
6856
|
+
return passThrough();
|
|
6857
|
+
}
|
|
6858
|
+
if (r.verdict === "redact" && outgrowsCharset(reading.ascii, r.body)) r = { verdict: "block" };
|
|
6859
|
+
if (r.verdict === "redact") r = { ...r, body: withBom(reading.bom, r.body) };
|
|
6860
|
+
if (r.verdict !== "block" && r.verdict !== "redact") return passThrough();
|
|
6861
|
+
if (reallySent() && res.chunkedEncoding !== true) {
|
|
6862
|
+
notify(onError, new Error("Patchstack: response withheld \u2014 its headers were sent before the body could be screened"), "onError");
|
|
6863
|
+
res.destroy?.();
|
|
6864
|
+
return res;
|
|
5859
6865
|
}
|
|
5860
6866
|
if (r.verdict === "block") {
|
|
5861
|
-
|
|
5862
|
-
|
|
5863
|
-
res
|
|
5864
|
-
} catch {
|
|
6867
|
+
if (reallySent()) {
|
|
6868
|
+
res.destroy?.();
|
|
6869
|
+
return res;
|
|
5865
6870
|
}
|
|
5866
|
-
return
|
|
6871
|
+
return sendWithheld(cb);
|
|
5867
6872
|
}
|
|
5868
|
-
|
|
5869
|
-
|
|
5870
|
-
|
|
5871
|
-
|
|
6873
|
+
const changed = /* @__PURE__ */ new Map();
|
|
6874
|
+
if (r.headers) {
|
|
6875
|
+
for (const [name, value] of Object.entries(r.headers)) {
|
|
6876
|
+
if (name.toLowerCase() === "content-length") continue;
|
|
6877
|
+
if (!headerValueChanged(head.headers[name], value)) continue;
|
|
6878
|
+
changed.set(name.toLowerCase(), value);
|
|
5872
6879
|
}
|
|
5873
|
-
|
|
5874
|
-
|
|
5875
|
-
|
|
5876
|
-
|
|
5877
|
-
|
|
5878
|
-
|
|
5879
|
-
|
|
5880
|
-
|
|
5881
|
-
}
|
|
5882
|
-
} else if (Array.isArray(value)) {
|
|
5883
|
-
try {
|
|
5884
|
-
res.setHeader(name, value);
|
|
5885
|
-
} catch {
|
|
5886
|
-
}
|
|
5887
|
-
} else if (typeof value === "string" && current[name] !== value) {
|
|
5888
|
-
try {
|
|
5889
|
-
res.setHeader(name, value);
|
|
5890
|
-
} catch {
|
|
5891
|
-
}
|
|
5892
|
-
}
|
|
5893
|
-
}
|
|
6880
|
+
}
|
|
6881
|
+
if (!reallySent()) {
|
|
6882
|
+
const declared = head.headers["content-length"];
|
|
6883
|
+
if (declared === void 0) {
|
|
6884
|
+
changed.set("content-length", null);
|
|
6885
|
+
} else {
|
|
6886
|
+
changed.set("content-length", String(Buffer.byteLength(r.body)));
|
|
6887
|
+
changed.set("transfer-encoding", null);
|
|
5894
6888
|
}
|
|
5895
|
-
|
|
6889
|
+
sendHead({ headers: changed });
|
|
6890
|
+
} else if (changed.size) {
|
|
6891
|
+
recordSkip("response", "headers-sent", { headers: [...changed.keys()] });
|
|
5896
6892
|
}
|
|
5897
|
-
|
|
5898
|
-
return origEnd(cb);
|
|
6893
|
+
return origEnd(r.body, cb);
|
|
5899
6894
|
};
|
|
5900
6895
|
};
|
|
5901
6896
|
const allow = new Set(allowHosts.map((h) => String(h).toLowerCase()));
|
|
@@ -5969,21 +6964,15 @@ async function createProtection(options = {}) {
|
|
|
5969
6964
|
// .fetch(), and by the Supabase guard on its forwarded upstream response.
|
|
5970
6965
|
// A standalone response screen with no request phase of its own — the Supabase guard's forwarded
|
|
5971
6966
|
// upstream response. It resolves once here, which is the only resolution for this call.
|
|
5972
|
-
screenResponse: (response, request) => screenResp(
|
|
5973
|
-
response,
|
|
5974
|
-
request ? reqContextFromFetch(
|
|
5975
|
-
request,
|
|
5976
|
-
resolveClientIp({ headers: headerObject(request.headers), trustedProxy: options.trustedProxy })
|
|
5977
|
-
) : void 0
|
|
5978
|
-
),
|
|
6967
|
+
screenResponse: (response, request, ...hostArgs) => screenResp(response, request ? reqContextFromFetch(request, clientForResponse(request, hostArgs)) : void 0),
|
|
5979
6968
|
// (request) => Response | null (null = allow, caller proceeds). Request phase only.
|
|
5980
6969
|
fetchGuard() {
|
|
5981
|
-
return async (request) => (await screenFetchRequest(request)).blocked;
|
|
6970
|
+
return async (request, ...hostArgs) => (await screenFetchRequest(request, hostArgs)).blocked;
|
|
5982
6971
|
},
|
|
5983
6972
|
// Wrap a fetch handler: screens the request, then the response (redact/block).
|
|
5984
6973
|
fetch(handler) {
|
|
5985
6974
|
return async (request, ...rest) => {
|
|
5986
|
-
const { blocked, client } = await screenFetchRequest(request);
|
|
6975
|
+
const { blocked, client } = await screenFetchRequest(request, rest);
|
|
5987
6976
|
if (blocked) return blocked;
|
|
5988
6977
|
const response = await handler(request, ...rest);
|
|
5989
6978
|
return screenResp(response, reqContextFromFetch(request, client));
|
|
@@ -5997,6 +6986,7 @@ async function createProtection(options = {}) {
|
|
|
5997
6986
|
const { shaped, client } = shapeExpressRequest(req);
|
|
5998
6987
|
try {
|
|
5999
6988
|
result = engine.evaluate(shaped);
|
|
6989
|
+
recordEvaluationSkips(result);
|
|
6000
6990
|
} catch (err) {
|
|
6001
6991
|
notify(onError, err, "onError");
|
|
6002
6992
|
if (exprOptions.screenResponses) wrapNodeResponse(res, reqContextFromNode(req, client));
|
|
@@ -6031,33 +7021,25 @@ async function createProtection(options = {}) {
|
|
|
6031
7021
|
screenNodeRequest(req, res, next, "", req.body);
|
|
6032
7022
|
return;
|
|
6033
7023
|
}
|
|
6034
|
-
|
|
6035
|
-
|
|
6036
|
-
|
|
6037
|
-
|
|
6038
|
-
size += chunk.length;
|
|
6039
|
-
if (size > maxBytes) {
|
|
6040
|
-
overflow = true;
|
|
7024
|
+
readBodyPrefix(req, maxBytes, (err, read) => {
|
|
7025
|
+
if (err) {
|
|
7026
|
+
notify(onError, err, "onError");
|
|
7027
|
+
next();
|
|
6041
7028
|
return;
|
|
6042
7029
|
}
|
|
6043
|
-
|
|
6044
|
-
|
|
6045
|
-
|
|
6046
|
-
notify(onError, err, "onError");
|
|
6047
|
-
next();
|
|
6048
|
-
});
|
|
6049
|
-
req.on("end", () => {
|
|
6050
|
-
if (overflow) recordSkip("request", "body-cap", { bytes: size, limit: maxBytes });
|
|
6051
|
-
screenNodeRequest(req, res, next, overflow ? "" : Buffer.concat(chunks).toString("utf8"));
|
|
7030
|
+
if (read.overflow) recordSkip("request", "body-cap", { bytes: read.size, limit: maxBytes });
|
|
7031
|
+
if (read.failed) recordSkip("request", "read-failed", { bytes: read.size });
|
|
7032
|
+
screenNodeRequest(req, res, next, read.text, void 0, read.overflow || read.failed);
|
|
6052
7033
|
});
|
|
6053
7034
|
};
|
|
6054
|
-
function screenNodeRequest(req, res, next, rawBody, parsedBody) {
|
|
7035
|
+
function screenNodeRequest(req, res, next, rawBody, parsedBody, truncated = false) {
|
|
6055
7036
|
let shaped;
|
|
6056
7037
|
let result;
|
|
6057
7038
|
try {
|
|
6058
7039
|
shaped = fromNodeRequest(req, rawBody, { trustedProxy: options.trustedProxy });
|
|
6059
7040
|
if (parsedBody !== void 0 && parsedBody !== null) shaped.body = parsedBody;
|
|
6060
7041
|
result = engine.evaluate(shaped);
|
|
7042
|
+
recordEvaluationSkips(result);
|
|
6061
7043
|
} catch (err) {
|
|
6062
7044
|
notify(onError, err, "onError");
|
|
6063
7045
|
return next();
|
|
@@ -6076,7 +7058,7 @@ async function createProtection(options = {}) {
|
|
|
6076
7058
|
}
|
|
6077
7059
|
},
|
|
6078
7060
|
() => {
|
|
6079
|
-
if (req.body === void 0) req.body = shaped.body;
|
|
7061
|
+
if (req.body === void 0 && !truncated) req.body = shaped.body;
|
|
6080
7062
|
if (nodeOptions.screenResponses) wrapNodeResponse(res, reqContextFromNode(req, shaped?._clientIp));
|
|
6081
7063
|
next();
|
|
6082
7064
|
},
|
|
@@ -6103,11 +7085,12 @@ async function createProtection(options = {}) {
|
|
|
6103
7085
|
let reporter = null;
|
|
6104
7086
|
if (refreshable && options.siteUuid && options.reportManifest !== false && cwd) {
|
|
6105
7087
|
try {
|
|
6106
|
-
({ reportManifest: reporter } = await import("./refresh-manifest-
|
|
7088
|
+
({ reportManifest: reporter } = await import("./refresh-manifest-DZQOEO3Q.js"));
|
|
6107
7089
|
} catch (err) {
|
|
6108
7090
|
notify(onError, err, "onError");
|
|
6109
7091
|
}
|
|
6110
7092
|
}
|
|
7093
|
+
let recovery = null;
|
|
6111
7094
|
const runRefreshTick = async () => {
|
|
6112
7095
|
if (reporter) {
|
|
6113
7096
|
try {
|
|
@@ -6124,23 +7107,38 @@ async function createProtection(options = {}) {
|
|
|
6124
7107
|
});
|
|
6125
7108
|
mode = resolveMode(options, next);
|
|
6126
7109
|
applyBundle(next);
|
|
7110
|
+
noteRuleSource(next.source);
|
|
7111
|
+
if (next.source?.ok !== false) recovery?.stop();
|
|
6127
7112
|
await applyAndAcknowledge(next.source?.origin, declaredState);
|
|
6128
7113
|
if (detections) detections.setRulesEtag((await store.read())?.etag ?? null);
|
|
6129
7114
|
return next.source ?? { ok: true };
|
|
6130
7115
|
};
|
|
7116
|
+
const refreshTick = serialise(runRefreshTick);
|
|
6131
7117
|
if (live) {
|
|
6132
|
-
protection.refresh = () =>
|
|
6133
|
-
protection.refreshHandler = () => makeRefreshHandler(
|
|
7118
|
+
protection.refresh = () => refreshTick();
|
|
7119
|
+
protection.refreshHandler = () => makeRefreshHandler(refreshTick, refreshSecret);
|
|
6134
7120
|
}
|
|
6135
|
-
const loop = options.refreshMs > 0 && live ? startRefresh(
|
|
7121
|
+
const loop = options.refreshMs > 0 && live ? startRefresh(refreshTick, { refreshMs: options.refreshMs, onError }) : null;
|
|
7122
|
+
const canAsk = Boolean(options.token || pulseAuth);
|
|
7123
|
+
recovery = live && canAsk && !loop && ruleSource.ok === false ? startRecovery(refreshTick, { onError }) : null;
|
|
6136
7124
|
protection.stop = () => {
|
|
6137
7125
|
loop?.stop();
|
|
7126
|
+
recovery?.stop();
|
|
7127
|
+
protection.uninstallEgress?.();
|
|
6138
7128
|
const outstanding = [firewallLog?.stop(), detections?.stop()].filter(
|
|
6139
7129
|
(wait) => wait && typeof wait.then === "function"
|
|
6140
7130
|
);
|
|
6141
7131
|
return Promise.all(outstanding).then(() => void 0);
|
|
6142
7132
|
};
|
|
6143
|
-
protection.stopRefresh =
|
|
7133
|
+
protection.stopRefresh = () => {
|
|
7134
|
+
loop?.stop();
|
|
7135
|
+
recovery?.stop();
|
|
7136
|
+
return Promise.resolve();
|
|
7137
|
+
};
|
|
7138
|
+
Object.defineProperty(protection, "ruleSource", {
|
|
7139
|
+
get: () => ({ ...ruleSource }),
|
|
7140
|
+
enumerable: true
|
|
7141
|
+
});
|
|
6144
7142
|
Object.defineProperty(protection, "detectionReporting", {
|
|
6145
7143
|
get: () => detectionReporting,
|
|
6146
7144
|
enumerable: true
|
|
@@ -6149,8 +7147,22 @@ async function createProtection(options = {}) {
|
|
|
6149
7147
|
get: () => detections ? () => detections.health() : void 0,
|
|
6150
7148
|
enumerable: true
|
|
6151
7149
|
});
|
|
7150
|
+
Object.defineProperty(protection, "blockLogHealth", {
|
|
7151
|
+
get: () => firewallLog ? () => firewallLog.health() : void 0,
|
|
7152
|
+
enumerable: true
|
|
7153
|
+
});
|
|
6152
7154
|
return protection;
|
|
6153
7155
|
}
|
|
7156
|
+
function accessorOf(obj, name) {
|
|
7157
|
+
for (let o = obj; o; o = Object.getPrototypeOf(o)) {
|
|
7158
|
+
const d = Object.getOwnPropertyDescriptor(o, name);
|
|
7159
|
+
if (d) return typeof d.get === "function" ? d.get : null;
|
|
7160
|
+
}
|
|
7161
|
+
return null;
|
|
7162
|
+
}
|
|
7163
|
+
function headersSentError(verb) {
|
|
7164
|
+
return Object.assign(new Error(`Cannot ${verb} headers after they are sent to the client`), { code: "ERR_HTTP_HEADERS_SENT" });
|
|
7165
|
+
}
|
|
6154
7166
|
function resolveMode(options, bundle) {
|
|
6155
7167
|
const env = typeof process !== "undefined" ? process.env?.PATCHSTACK_MODE : void 0;
|
|
6156
7168
|
if (env === "block" || env === "dry-run") return env;
|
|
@@ -6213,6 +7225,14 @@ function screenableContentType(ct) {
|
|
|
6213
7225
|
if (base === "application/octet-stream") return "sniff";
|
|
6214
7226
|
return "skip";
|
|
6215
7227
|
}
|
|
7228
|
+
function prefilterText(body, headers) {
|
|
7229
|
+
const values = [];
|
|
7230
|
+
for (const value of Object.values(headers ?? {})) {
|
|
7231
|
+
if (Array.isArray(value)) values.push(...value.map(String));
|
|
7232
|
+
else if (value !== void 0 && value !== null) values.push(String(value));
|
|
7233
|
+
}
|
|
7234
|
+
return [body, ...values].join("\n").toLowerCase();
|
|
7235
|
+
}
|
|
6216
7236
|
function looksBinary(bytes) {
|
|
6217
7237
|
const n = Math.min(bytes.length, 512);
|
|
6218
7238
|
let ctrl = 0;
|
|
@@ -6223,11 +7243,72 @@ function looksBinary(bytes) {
|
|
|
6223
7243
|
}
|
|
6224
7244
|
return n > 0 && ctrl / n > 0.1;
|
|
6225
7245
|
}
|
|
7246
|
+
function declaredCodings(value) {
|
|
7247
|
+
const raw = Array.isArray(value) ? value.join(",") : String(value ?? "");
|
|
7248
|
+
return raw.toLowerCase().split(",").map((c) => c.trim()).filter((c) => c !== "" && c !== "identity");
|
|
7249
|
+
}
|
|
7250
|
+
function stillEncoded(bytes) {
|
|
7251
|
+
if (looksBinary(bytes)) return true;
|
|
7252
|
+
try {
|
|
7253
|
+
new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
7254
|
+
return false;
|
|
7255
|
+
} catch {
|
|
7256
|
+
return true;
|
|
7257
|
+
}
|
|
7258
|
+
}
|
|
7259
|
+
var UTF8_CHARSETS = /* @__PURE__ */ new Set(["utf-8", "utf8", "unicode-1-1-utf-8", "us-ascii", "ascii"]);
|
|
7260
|
+
var WIDE_CHARSET = /^(?:utf-?(?:16|32)|ucs-?[24]|unicode|iso-10646)/;
|
|
7261
|
+
function declaredCharset(ct) {
|
|
7262
|
+
const m = /;\s*charset\s*=\s*(?:"([^"]*)"|([^;\s]*))/i.exec(String(ct ?? ""));
|
|
7263
|
+
const label = m ? (m[1] ?? m[2] ?? "").trim().toLowerCase() : "";
|
|
7264
|
+
return label || null;
|
|
7265
|
+
}
|
|
7266
|
+
function charsetReading(ct, bytes) {
|
|
7267
|
+
if (bytes.length >= 3 && bytes[0] === 239 && bytes[1] === 187 && bytes[2] === 191) return { ascii: false, bom: true };
|
|
7268
|
+
if (bytes.length >= 2 && bytes[0] === 255 && bytes[1] === 254) return { skip: "utf-16le" };
|
|
7269
|
+
if (bytes.length >= 2 && bytes[0] === 254 && bytes[1] === 255) return { skip: "utf-16be" };
|
|
7270
|
+
const charset = declaredCharset(ct);
|
|
7271
|
+
if (!charset || UTF8_CHARSETS.has(charset)) return { ascii: false };
|
|
7272
|
+
if (WIDE_CHARSET.test(charset)) return { skip: charset };
|
|
7273
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
7274
|
+
const b = bytes[i];
|
|
7275
|
+
if (b >= 128 || b === 0 || b === 27) return { skip: charset };
|
|
7276
|
+
}
|
|
7277
|
+
return { ascii: true };
|
|
7278
|
+
}
|
|
7279
|
+
function decodeForScreening(ct, bytes) {
|
|
7280
|
+
const reading = charsetReading(ct, bytes);
|
|
7281
|
+
if (reading.skip) return { skip: "unsupported-charset", charset: reading.skip };
|
|
7282
|
+
return { text: new TextDecoder().decode(bytes), ascii: reading.ascii, bom: reading.bom === true };
|
|
7283
|
+
}
|
|
7284
|
+
function withBom(bom, body) {
|
|
7285
|
+
return bom ? "\uFEFF" + body : body;
|
|
7286
|
+
}
|
|
7287
|
+
function outgrowsCharset(ascii, body) {
|
|
7288
|
+
return ascii === true && /[^\x00-\x7f]/.test(body);
|
|
7289
|
+
}
|
|
7290
|
+
var KEPT_ESCAPES = /* @__PURE__ */ new Set([34, 92, 60, 62, 38, 39]);
|
|
7291
|
+
function readJsonEscapes(text) {
|
|
7292
|
+
if (!text.includes("\\u") && !text.includes("\\/")) return text;
|
|
7293
|
+
try {
|
|
7294
|
+
JSON.parse(text);
|
|
7295
|
+
} catch {
|
|
7296
|
+
return text;
|
|
7297
|
+
}
|
|
7298
|
+
return text.replace(/\\(?:u([0-9a-fA-F]{4})|([\s\S]))/g, (whole, hex, ch, offset) => {
|
|
7299
|
+
if (hex === void 0) return ch === "/" && text[offset - 1] !== "<" ? "/" : whole;
|
|
7300
|
+
const code = parseInt(hex, 16);
|
|
7301
|
+
if (code < 32 || code > 126 || KEPT_ESCAPES.has(code)) return whole;
|
|
7302
|
+
return String.fromCharCode(code);
|
|
7303
|
+
});
|
|
7304
|
+
}
|
|
6226
7305
|
async function readTextResponse(response, cap = DEFAULT_SCREEN_CAP) {
|
|
6227
7306
|
if (!response || typeof response.clone !== "function") return { skip: "not-a-response" };
|
|
6228
7307
|
const ct = response.headers?.get?.("content-type") || "";
|
|
6229
7308
|
const kind = screenableContentType(ct);
|
|
6230
7309
|
if (kind === "skip") return { skip: baseContentType(ct) === "text/event-stream" ? "live-stream" : "non-text-content-type" };
|
|
7310
|
+
const codings = declaredCodings(response.headers?.get?.("content-encoding"));
|
|
7311
|
+
const encodedSkip = () => ({ skip: "encoded-body", encoding: codings.join(", ") });
|
|
6231
7312
|
const sniff = kind === "sniff";
|
|
6232
7313
|
const len = Number(response.headers?.get?.("content-length") || 0);
|
|
6233
7314
|
if (len && len > cap) return { skip: "body-cap" };
|
|
@@ -6271,13 +7352,22 @@ async function readTextResponse(response, cap = DEFAULT_SCREEN_CAP) {
|
|
|
6271
7352
|
} finally {
|
|
6272
7353
|
reader.releaseLock();
|
|
6273
7354
|
}
|
|
7355
|
+
const bytes = concatBytes(chunks, size);
|
|
7356
|
+
if (codings.length > 0 && stillEncoded(bytes)) return encodedSkip();
|
|
6274
7357
|
try {
|
|
6275
|
-
return
|
|
7358
|
+
return decodeForScreening(ct, bytes);
|
|
6276
7359
|
} catch {
|
|
6277
7360
|
return { skip: "decode-failed" };
|
|
6278
7361
|
}
|
|
6279
7362
|
}
|
|
6280
7363
|
try {
|
|
7364
|
+
if (codings.length > 0 || typeof clone.arrayBuffer === "function") {
|
|
7365
|
+
const bytes = new Uint8Array(await clone.arrayBuffer());
|
|
7366
|
+
if (bytes.byteLength > cap) return { skip: "body-cap" };
|
|
7367
|
+
if (codings.length > 0 && stillEncoded(bytes)) return encodedSkip();
|
|
7368
|
+
if (codings.length === 0 && sniff && looksBinary(bytes)) return { skip: "binary-body" };
|
|
7369
|
+
return decodeForScreening(ct, bytes);
|
|
7370
|
+
}
|
|
6281
7371
|
const text = await clone.text();
|
|
6282
7372
|
if (text.length > cap) return { skip: "body-cap" };
|
|
6283
7373
|
if (sniff && looksBinary(new TextEncoder().encode(text.slice(0, 512)))) return { skip: "binary-body" };
|
|
@@ -6307,8 +7397,9 @@ function headerObject(headers) {
|
|
|
6307
7397
|
function hasSpanMutations(rule) {
|
|
6308
7398
|
let found = false;
|
|
6309
7399
|
const walk = (conds) => {
|
|
6310
|
-
for (const c of conds
|
|
7400
|
+
for (const c of Array.isArray(conds) ? conds : []) {
|
|
6311
7401
|
if (found) return;
|
|
7402
|
+
if (!c || typeof c !== "object") continue;
|
|
6312
7403
|
if (Array.isArray(c.rules)) walk(c.rules);
|
|
6313
7404
|
const isSpan = c.match && (c.match.type === "regex" || c.match.type === "contains" || c.match.type === "stripos");
|
|
6314
7405
|
if (isSpan && Array.isArray(c.mutations) && c.mutations.length) found = true;
|
|
@@ -6317,10 +7408,34 @@ function hasSpanMutations(rule) {
|
|
|
6317
7408
|
walk(rule.rule_v2);
|
|
6318
7409
|
return found;
|
|
6319
7410
|
}
|
|
6320
|
-
function
|
|
7411
|
+
function redactorScope(parameter) {
|
|
7412
|
+
if (parameter === void 0 || parameter === null || parameterProblem(parameter) !== null) return null;
|
|
7413
|
+
const scope = { body: false, allHeaders: false, headers: /* @__PURE__ */ new Set() };
|
|
7414
|
+
for (const name of Array.isArray(parameter) ? parameter : [parameter]) {
|
|
7415
|
+
if (typeof name !== "string" || name === "") return null;
|
|
7416
|
+
if (name.startsWith("response.header.")) scope.headers.add(name.slice("response.header.".length).toLowerCase());
|
|
7417
|
+
else if (name === "response.headers") scope.allHeaders = true;
|
|
7418
|
+
else if (name === "response.body" || name === "response.status" || BROAD_SCOPE_SOURCES.has(name.split(".")[0])) {
|
|
7419
|
+
scope.body = true;
|
|
7420
|
+
scope.allHeaders = true;
|
|
7421
|
+
} else return null;
|
|
7422
|
+
}
|
|
7423
|
+
return scope;
|
|
7424
|
+
}
|
|
7425
|
+
var BROAD_SCOPE_SOURCES = /* @__PURE__ */ new Set(["get", "post", "request", "cookie", "files", "server", "raw", "all"]);
|
|
7426
|
+
function masksHeader(redactor, name) {
|
|
7427
|
+
return redactor.scope.allHeaders || redactor.scope.headers.has(name);
|
|
7428
|
+
}
|
|
7429
|
+
function extractRedactors(rule, onUnscoped) {
|
|
6321
7430
|
const out = [];
|
|
7431
|
+
const scoped = (c) => {
|
|
7432
|
+
const scope = redactorScope(c.parameter);
|
|
7433
|
+
if (scope === null) onUnscoped?.(c.parameter);
|
|
7434
|
+
return scope;
|
|
7435
|
+
};
|
|
6322
7436
|
const walk = (conds) => {
|
|
6323
|
-
for (const c of conds
|
|
7437
|
+
for (const c of Array.isArray(conds) ? conds : []) {
|
|
7438
|
+
if (!c || typeof c !== "object") continue;
|
|
6324
7439
|
if (Array.isArray(c.rules)) walk(c.rules);
|
|
6325
7440
|
const m = c.match;
|
|
6326
7441
|
if (!m) continue;
|
|
@@ -6328,16 +7443,21 @@ function extractRedactors(rule) {
|
|
|
6328
7443
|
const safe = safeRegExp(m.value);
|
|
6329
7444
|
if (safe) {
|
|
6330
7445
|
const flags = safe.flags.includes("g") ? safe.flags : safe.flags + "g";
|
|
7446
|
+
const scope = scoped(c);
|
|
6331
7447
|
try {
|
|
6332
|
-
out.push({ re: new RegExp(safe.source, flags) });
|
|
7448
|
+
if (scope) out.push({ re: new RegExp(safe.source, flags), scope });
|
|
6333
7449
|
} catch {
|
|
6334
7450
|
}
|
|
6335
7451
|
}
|
|
6336
7452
|
} else if ((m.type === "contains" || m.type === "stripos") && m.value != null) {
|
|
6337
|
-
|
|
7453
|
+
const scope = scoped(c);
|
|
7454
|
+
if (scope) out.push({ literal: String(m.value), scope });
|
|
6338
7455
|
} else if (m.type === "jwt_claim_equals" && typeof m.claim === "string") {
|
|
6339
|
-
|
|
6340
|
-
|
|
7456
|
+
const scope = scoped(c);
|
|
7457
|
+
if (scope) out.push({ jwtClaim: { claim: m.claim, value: String(m.value ?? "") }, scope });
|
|
7458
|
+
} else if (m.type === "array_key_value" && m.match && Array.isArray(c.parameter) && !readsBody(c.parameter) && redactorScope(c.parameter) === null) {
|
|
7459
|
+
onUnscoped?.(c.parameter);
|
|
7460
|
+
} else if (m.type === "array_key_value" && m.match && readsBody(c.parameter)) {
|
|
6341
7461
|
const keys = Array.isArray(m.key) ? m.key : [m.key];
|
|
6342
7462
|
for (const key of keys) {
|
|
6343
7463
|
out.push({ jsonPath: String(key).split("."), condition: m.match });
|
|
@@ -6351,6 +7471,110 @@ function extractRedactors(rule) {
|
|
|
6351
7471
|
function htmlEscape(str) {
|
|
6352
7472
|
return String(str).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
6353
7473
|
}
|
|
7474
|
+
function isJson(text) {
|
|
7475
|
+
try {
|
|
7476
|
+
JSON.parse(text);
|
|
7477
|
+
return true;
|
|
7478
|
+
} catch {
|
|
7479
|
+
return false;
|
|
7480
|
+
}
|
|
7481
|
+
}
|
|
7482
|
+
var JSON_STRING = 1;
|
|
7483
|
+
var JSON_NUMBER = 2;
|
|
7484
|
+
var JSON_LITERAL = 3;
|
|
7485
|
+
var JSON_NUMBER_TOKEN = /-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/y;
|
|
7486
|
+
function isHexCode(c) {
|
|
7487
|
+
return c >= 48 && c <= 57 || c >= 65 && c <= 70 || c >= 97 && c <= 102;
|
|
7488
|
+
}
|
|
7489
|
+
var PLAIN_STRING_SEGMENT = /^[^"\\\u0000-\u001f]*$/;
|
|
7490
|
+
function lexJson(text) {
|
|
7491
|
+
let kinds = new Int32Array(1024);
|
|
7492
|
+
let spans = new Int32Array(2048);
|
|
7493
|
+
let count = 0;
|
|
7494
|
+
const n = text.length;
|
|
7495
|
+
let i = 0;
|
|
7496
|
+
while (i < n) {
|
|
7497
|
+
const c = text.charCodeAt(i);
|
|
7498
|
+
if (c === 32 || c === 9 || c === 10 || c === 13) {
|
|
7499
|
+
i++;
|
|
7500
|
+
continue;
|
|
7501
|
+
}
|
|
7502
|
+
const start = i;
|
|
7503
|
+
let kind;
|
|
7504
|
+
if (c === 34) {
|
|
7505
|
+
i++;
|
|
7506
|
+
const close = text.indexOf('"', i);
|
|
7507
|
+
if (close === -1) return null;
|
|
7508
|
+
if (PLAIN_STRING_SEGMENT.test(text.slice(i, close))) {
|
|
7509
|
+
i = close;
|
|
7510
|
+
} else {
|
|
7511
|
+
for (; ; ) {
|
|
7512
|
+
if (i >= n) return null;
|
|
7513
|
+
const d = text.charCodeAt(i);
|
|
7514
|
+
if (d === 34) break;
|
|
7515
|
+
if (d < 32) return null;
|
|
7516
|
+
if (d !== 92) {
|
|
7517
|
+
i++;
|
|
7518
|
+
continue;
|
|
7519
|
+
}
|
|
7520
|
+
const e = text.charCodeAt(i + 1);
|
|
7521
|
+
if (e === 117) {
|
|
7522
|
+
for (let k = 2; k < 6; k++) if (!isHexCode(text.charCodeAt(i + k))) return null;
|
|
7523
|
+
i += 6;
|
|
7524
|
+
} else if (e === 34 || e === 92 || e === 47 || e === 98 || e === 102 || e === 110 || e === 114 || e === 116) {
|
|
7525
|
+
i += 2;
|
|
7526
|
+
} else {
|
|
7527
|
+
return null;
|
|
7528
|
+
}
|
|
7529
|
+
}
|
|
7530
|
+
}
|
|
7531
|
+
i++;
|
|
7532
|
+
kind = JSON_STRING;
|
|
7533
|
+
} else if (c === 123 || c === 125 || c === 91 || c === 93 || c === 58 || c === 44) {
|
|
7534
|
+
i++;
|
|
7535
|
+
kind = c;
|
|
7536
|
+
} else if (text.startsWith("true", i) || text.startsWith("null", i)) {
|
|
7537
|
+
i += 4;
|
|
7538
|
+
kind = JSON_LITERAL;
|
|
7539
|
+
} else if (text.startsWith("false", i)) {
|
|
7540
|
+
i += 5;
|
|
7541
|
+
kind = JSON_LITERAL;
|
|
7542
|
+
} else {
|
|
7543
|
+
JSON_NUMBER_TOKEN.lastIndex = i;
|
|
7544
|
+
if (!JSON_NUMBER_TOKEN.test(text)) return null;
|
|
7545
|
+
i = JSON_NUMBER_TOKEN.lastIndex;
|
|
7546
|
+
kind = JSON_NUMBER;
|
|
7547
|
+
}
|
|
7548
|
+
if (count === kinds.length) {
|
|
7549
|
+
const grownKinds = new Int32Array(count * 2);
|
|
7550
|
+
grownKinds.set(kinds);
|
|
7551
|
+
kinds = grownKinds;
|
|
7552
|
+
const grownSpans = new Int32Array(count * 4);
|
|
7553
|
+
grownSpans.set(spans);
|
|
7554
|
+
spans = grownSpans;
|
|
7555
|
+
}
|
|
7556
|
+
kinds[count] = kind;
|
|
7557
|
+
spans[2 * count] = start;
|
|
7558
|
+
spans[2 * count + 1] = i;
|
|
7559
|
+
count++;
|
|
7560
|
+
}
|
|
7561
|
+
return { kinds, spans, count };
|
|
7562
|
+
}
|
|
7563
|
+
function sameJsonStructure(beforeText, before, afterText, after) {
|
|
7564
|
+
if (!before || !after || before.count !== after.count) return false;
|
|
7565
|
+
const { kinds, spans, count } = before;
|
|
7566
|
+
for (let k = 0; k < count; k++) {
|
|
7567
|
+
const kind = kinds[k];
|
|
7568
|
+
if (after.kinds[k] !== kind) return false;
|
|
7569
|
+
if (kind > JSON_LITERAL) continue;
|
|
7570
|
+
if (kind === JSON_STRING && kinds[k + 1] !== 58) continue;
|
|
7571
|
+
const was = beforeText.slice(spans[2 * k], spans[2 * k + 1]);
|
|
7572
|
+
const now = afterText.slice(after.spans[2 * k], after.spans[2 * k + 1]);
|
|
7573
|
+
if (was === now) continue;
|
|
7574
|
+
if (kind !== JSON_STRING || JSON.parse(was) !== JSON.parse(now)) return false;
|
|
7575
|
+
}
|
|
7576
|
+
return true;
|
|
7577
|
+
}
|
|
6354
7578
|
function applyRedactors(body, redactors, mask, transform) {
|
|
6355
7579
|
let out = body;
|
|
6356
7580
|
for (const r of redactors) {
|
|
@@ -6366,6 +7590,11 @@ function applyRedactors(body, redactors, mask, transform) {
|
|
|
6366
7590
|
}
|
|
6367
7591
|
return out;
|
|
6368
7592
|
}
|
|
7593
|
+
function readsBody(parameter) {
|
|
7594
|
+
if (!Array.isArray(parameter)) return isBodyParam(parameter);
|
|
7595
|
+
const names = parameter.every((member) => typeof member === "string" && member !== "");
|
|
7596
|
+
return names && parameterProblem(parameter) === null && parameter.some((member) => isBodyParam(member));
|
|
7597
|
+
}
|
|
6369
7598
|
function isBodyParam(parameter) {
|
|
6370
7599
|
return parameter == null || parameter === "response.body" || parameter === "raw" || parameter === "response.raw";
|
|
6371
7600
|
}
|
|
@@ -6393,10 +7622,12 @@ function applyPathRedactors(text, pathRedactors, mask, cap, transform) {
|
|
|
6393
7622
|
if (!pathRedactors.length || typeof text !== "string" || text.length > cap) return text;
|
|
6394
7623
|
const head = text.trimStart()[0];
|
|
6395
7624
|
if (head !== "{" && head !== "[") return text;
|
|
6396
|
-
|
|
7625
|
+
let preserved;
|
|
6397
7626
|
let obj;
|
|
6398
7627
|
try {
|
|
6399
|
-
|
|
7628
|
+
JSON.parse(text);
|
|
7629
|
+
preserved = preserveNumbers(text, mask);
|
|
7630
|
+
obj = JSON.parse(preserved.text);
|
|
6400
7631
|
} catch {
|
|
6401
7632
|
return text;
|
|
6402
7633
|
}
|
|
@@ -6405,65 +7636,44 @@ function applyPathRedactors(text, pathRedactors, mask, cap, transform) {
|
|
|
6405
7636
|
const pred = conditionPredicate(r.condition);
|
|
6406
7637
|
walkLeaves(obj, r.jsonPath, (loc) => {
|
|
6407
7638
|
try {
|
|
6408
|
-
|
|
6409
|
-
|
|
7639
|
+
const number = preserved.numbers.get(loc.value);
|
|
7640
|
+
if (pred(number === void 0 ? loc.value : Number(number))) {
|
|
7641
|
+
setOwn(loc.parent, loc.key, transform ? transform(number ?? String(loc.value)) : mask);
|
|
6410
7642
|
changed = true;
|
|
6411
7643
|
}
|
|
6412
7644
|
} catch {
|
|
6413
7645
|
}
|
|
6414
7646
|
});
|
|
6415
7647
|
}
|
|
6416
|
-
return changed ?
|
|
6417
|
-
}
|
|
6418
|
-
var BIGINT_OPEN = "__PSBIGINT_9c2f__";
|
|
6419
|
-
var BIGINT_CLOSE = "__DNEGIB__";
|
|
6420
|
-
function preserveBigInts(text) {
|
|
6421
|
-
let out = "";
|
|
6422
|
-
let inStr = false;
|
|
6423
|
-
for (let i = 0; i < text.length; ) {
|
|
6424
|
-
const ch = text[i];
|
|
6425
|
-
if (inStr) {
|
|
6426
|
-
out += ch;
|
|
6427
|
-
if (ch === "\\") {
|
|
6428
|
-
out += text[i + 1] ?? "";
|
|
6429
|
-
i += 2;
|
|
6430
|
-
continue;
|
|
6431
|
-
}
|
|
6432
|
-
if (ch === '"') inStr = false;
|
|
6433
|
-
i++;
|
|
6434
|
-
continue;
|
|
6435
|
-
}
|
|
6436
|
-
if (ch === '"') {
|
|
6437
|
-
inStr = true;
|
|
6438
|
-
out += ch;
|
|
6439
|
-
i++;
|
|
6440
|
-
continue;
|
|
6441
|
-
}
|
|
6442
|
-
if (ch === "-" || ch >= "0" && ch <= "9") {
|
|
6443
|
-
let j = ch === "-" ? i + 1 : i;
|
|
6444
|
-
let digits = 0;
|
|
6445
|
-
while (j < text.length && text[j] >= "0" && text[j] <= "9") {
|
|
6446
|
-
digits++;
|
|
6447
|
-
j++;
|
|
6448
|
-
}
|
|
6449
|
-
const next = text[j];
|
|
6450
|
-
const isIntToken = digits > 0 && next !== "." && next !== "e" && next !== "E";
|
|
6451
|
-
if (isIntToken && digits >= 16) {
|
|
6452
|
-
out += `"${BIGINT_OPEN}${text.slice(i, j)}${BIGINT_CLOSE}"`;
|
|
6453
|
-
} else {
|
|
6454
|
-
out += text.slice(i, j || i + 1);
|
|
6455
|
-
}
|
|
6456
|
-
i = j > i ? j : i + 1;
|
|
6457
|
-
continue;
|
|
6458
|
-
}
|
|
6459
|
-
out += ch;
|
|
6460
|
-
i++;
|
|
6461
|
-
}
|
|
6462
|
-
return out;
|
|
7648
|
+
return changed ? restoreNumbers(JSON.stringify(obj), preserved.numbers) : text;
|
|
6463
7649
|
}
|
|
6464
|
-
function
|
|
6465
|
-
|
|
6466
|
-
|
|
7650
|
+
function preserveNumbers(text, mask) {
|
|
7651
|
+
const occupied = /* @__PURE__ */ new Set([mask]);
|
|
7652
|
+
const found = [];
|
|
7653
|
+
const tokens = /"(?:[^"\\]|\\[\s\S])*"|-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/g;
|
|
7654
|
+
for (const match of text.matchAll(tokens)) {
|
|
7655
|
+
const token = match[0];
|
|
7656
|
+
if (token[0] === '"') occupied.add(JSON.parse(token));
|
|
7657
|
+
else found.push({ start: match.index, token });
|
|
7658
|
+
}
|
|
7659
|
+
const numbers = /* @__PURE__ */ new Map();
|
|
7660
|
+
const chunks = [];
|
|
7661
|
+
let offset = 0;
|
|
7662
|
+
let index = 0;
|
|
7663
|
+
for (const { start, token } of found) {
|
|
7664
|
+
let marker;
|
|
7665
|
+
do {
|
|
7666
|
+
marker = "__PSNUMBER_" + index++ + "__";
|
|
7667
|
+
} while (occupied.has(marker));
|
|
7668
|
+
numbers.set(marker, token);
|
|
7669
|
+
chunks.push(text.slice(offset, start), JSON.stringify(marker));
|
|
7670
|
+
offset = start + token.length;
|
|
7671
|
+
}
|
|
7672
|
+
chunks.push(text.slice(offset));
|
|
7673
|
+
return { text: chunks.join(""), numbers };
|
|
7674
|
+
}
|
|
7675
|
+
function restoreNumbers(text, numbers) {
|
|
7676
|
+
return text.replace(/"(__PSNUMBER_\d+__)"/g, (token, marker) => numbers.get(marker) ?? token);
|
|
6467
7677
|
}
|
|
6468
7678
|
function isHeaderMutation(action) {
|
|
6469
7679
|
return action === "set-header" || action === "remove-header" || action === "harden-cookie";
|