@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.edge.js
CHANGED
|
@@ -426,18 +426,124 @@ function appendOwn(target, key, value) {
|
|
|
426
426
|
return setOwn(target, key, Array.isArray(previous) ? [...previous, value] : [previous, value]);
|
|
427
427
|
}
|
|
428
428
|
|
|
429
|
-
// src/protect/engine/
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
const
|
|
435
|
-
|
|
436
|
-
|
|
429
|
+
// src/protect/engine/cookies.js
|
|
430
|
+
function parseCookieHeader(header) {
|
|
431
|
+
const cookies = {};
|
|
432
|
+
if (typeof header !== "string" || header === "") return cookies;
|
|
433
|
+
const repeated = /* @__PURE__ */ new Map();
|
|
434
|
+
for (const pair of header.split(";")) {
|
|
435
|
+
const eq = pair.indexOf("=");
|
|
436
|
+
if (eq === -1) continue;
|
|
437
|
+
const name = pair.slice(0, eq).trim();
|
|
438
|
+
if (name === "") continue;
|
|
439
|
+
let value = pair.slice(eq + 1).trim();
|
|
440
|
+
if (value.length >= 2 && value[0] === '"' && value[value.length - 1] === '"') value = value.slice(1, -1);
|
|
441
|
+
if (!Object.hasOwn(cookies, name)) {
|
|
442
|
+
setOwn(cookies, name, value);
|
|
443
|
+
} else {
|
|
444
|
+
let values = repeated.get(name);
|
|
445
|
+
if (values === void 0) {
|
|
446
|
+
values = [cookies[name]];
|
|
447
|
+
repeated.set(name, values);
|
|
448
|
+
setOwn(cookies, name, values);
|
|
449
|
+
}
|
|
450
|
+
values.push(value);
|
|
451
|
+
}
|
|
437
452
|
}
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
453
|
+
return cookies;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// src/protect/engine/normalizer.js
|
|
457
|
+
var HTML_ENTITIES = {
|
|
458
|
+
"&": "&",
|
|
459
|
+
"<": "<",
|
|
460
|
+
">": ">",
|
|
461
|
+
""": '"',
|
|
462
|
+
"'": "'",
|
|
463
|
+
"'": "'",
|
|
464
|
+
" ": " ",
|
|
465
|
+
"'": "'",
|
|
466
|
+
""": '"',
|
|
467
|
+
"<": "<",
|
|
468
|
+
">": ">",
|
|
469
|
+
"&": "&",
|
|
470
|
+
""": '"',
|
|
471
|
+
"<": "<",
|
|
472
|
+
">": ">",
|
|
473
|
+
"&": "&"
|
|
474
|
+
};
|
|
475
|
+
var MAX_DECODE_ITERATIONS = 5;
|
|
476
|
+
function normalize(value, options = {}) {
|
|
477
|
+
if (typeof value !== "string") {
|
|
478
|
+
return value;
|
|
479
|
+
}
|
|
480
|
+
const opts = {
|
|
481
|
+
urlDecode: true,
|
|
482
|
+
htmlDecode: true,
|
|
483
|
+
sqlComments: true,
|
|
484
|
+
nullBytes: true,
|
|
485
|
+
whitespace: true,
|
|
486
|
+
...options
|
|
487
|
+
};
|
|
488
|
+
let result = value;
|
|
489
|
+
if (opts.nullBytes) {
|
|
490
|
+
result = removeNullBytes(result);
|
|
491
|
+
}
|
|
492
|
+
if (opts.urlDecode) {
|
|
493
|
+
result = urlDecode(result);
|
|
494
|
+
}
|
|
495
|
+
if (opts.htmlDecode) {
|
|
496
|
+
result = htmlEntityDecode(result);
|
|
497
|
+
}
|
|
498
|
+
if (opts.sqlComments) {
|
|
499
|
+
result = removeSqlComments(result);
|
|
500
|
+
}
|
|
501
|
+
if (opts.whitespace) {
|
|
502
|
+
result = normalizeWhitespace(result);
|
|
503
|
+
}
|
|
504
|
+
return result;
|
|
505
|
+
}
|
|
506
|
+
function urlDecode(value) {
|
|
507
|
+
if (typeof value !== "string") {
|
|
508
|
+
return value;
|
|
509
|
+
}
|
|
510
|
+
let result = value;
|
|
511
|
+
let previous = "";
|
|
512
|
+
let iterations = 0;
|
|
513
|
+
while (result !== previous && iterations < MAX_DECODE_ITERATIONS) {
|
|
514
|
+
previous = result;
|
|
515
|
+
iterations++;
|
|
516
|
+
result = safeUrlDecode(result);
|
|
517
|
+
}
|
|
518
|
+
return result;
|
|
519
|
+
}
|
|
520
|
+
var utf8 = new TextDecoder();
|
|
521
|
+
function safeUrlDecode(value) {
|
|
522
|
+
return value.replace(/(?:%[0-9A-Fa-f]{2})+/g, (run) => {
|
|
523
|
+
try {
|
|
524
|
+
return decodeURIComponent(run);
|
|
525
|
+
} catch {
|
|
526
|
+
const bytes = new Uint8Array(run.length / 3);
|
|
527
|
+
for (let i = 0; i < bytes.length; i++) bytes[i] = parseInt(run.slice(i * 3 + 1, i * 3 + 3), 16);
|
|
528
|
+
return utf8.decode(bytes);
|
|
529
|
+
}
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
function decodeQueryPlus(target) {
|
|
533
|
+
if (typeof target !== "string") return target;
|
|
534
|
+
const query = target.indexOf("?");
|
|
535
|
+
if (query === -1) return target;
|
|
536
|
+
return target.slice(0, query + 1) + target.slice(query + 1).replace(/\+/g, " ");
|
|
537
|
+
}
|
|
538
|
+
function htmlEntityDecode(value) {
|
|
539
|
+
if (typeof value !== "string") {
|
|
540
|
+
return value;
|
|
541
|
+
}
|
|
542
|
+
let result = value;
|
|
543
|
+
for (const [entity, char] of Object.entries(HTML_ENTITIES)) {
|
|
544
|
+
result = result.split(entity).join(char);
|
|
545
|
+
}
|
|
546
|
+
return decodeHtmlEntities(result);
|
|
441
547
|
}
|
|
442
548
|
var NAMED_ENTITIES = {
|
|
443
549
|
lt: "<",
|
|
@@ -445,7 +551,6 @@ var NAMED_ENTITIES = {
|
|
|
445
551
|
amp: "&",
|
|
446
552
|
quot: '"',
|
|
447
553
|
apos: "'",
|
|
448
|
-
"#39": "'",
|
|
449
554
|
nbsp: "\xA0",
|
|
450
555
|
sol: "/",
|
|
451
556
|
bsol: "\\",
|
|
@@ -464,7 +569,7 @@ var NAMED_ENTITIES = {
|
|
|
464
569
|
ast: "*"
|
|
465
570
|
};
|
|
466
571
|
function decodeHtmlEntities(input) {
|
|
467
|
-
return input.replace(/&(#[xX]
|
|
572
|
+
return input.replace(/&(#[xX][0-9a-fA-F]+|#\d+|[A-Za-z][A-Za-z0-9]*);?/g, (whole, body) => {
|
|
468
573
|
if (body[0] === "#") {
|
|
469
574
|
const hex = body[1] === "x" || body[1] === "X";
|
|
470
575
|
const code = Number.parseInt(hex ? body.slice(2) : body.slice(1), hex ? 16 : 10);
|
|
@@ -478,74 +583,359 @@ function decodeHtmlEntities(input) {
|
|
|
478
583
|
return Object.prototype.hasOwnProperty.call(NAMED_ENTITIES, body) ? NAMED_ENTITIES[body] : whole;
|
|
479
584
|
});
|
|
480
585
|
}
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
constructor(req) {
|
|
485
|
-
this.#req = req;
|
|
486
|
-
this.#cookies = null;
|
|
586
|
+
function removeSqlComments(value) {
|
|
587
|
+
if (typeof value !== "string") {
|
|
588
|
+
return value;
|
|
487
589
|
}
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
590
|
+
let result = value;
|
|
591
|
+
result = result.replace(/\/\*[\s\S]*?\*\//g, " ");
|
|
592
|
+
result = result.replace(/\/\*![\s\S]*?\*\//g, " ");
|
|
593
|
+
return result;
|
|
594
|
+
}
|
|
595
|
+
function removeNullBytes(value) {
|
|
596
|
+
if (typeof value !== "string") {
|
|
597
|
+
return value;
|
|
598
|
+
}
|
|
599
|
+
let result = value.replace(/\x00/g, "");
|
|
600
|
+
result = result.replace(/[\x01-\x08\x0B\x0C\x0E-\x1F]/g, "");
|
|
601
|
+
return result;
|
|
602
|
+
}
|
|
603
|
+
function normalizeWhitespace(value) {
|
|
604
|
+
if (typeof value !== "string") {
|
|
605
|
+
return value;
|
|
606
|
+
}
|
|
607
|
+
let result = value.replace(/[\t\r\n\f\v]+/g, " ");
|
|
608
|
+
result = result.replace(/ {2,}/g, " ");
|
|
609
|
+
return result;
|
|
610
|
+
}
|
|
611
|
+
var MAX_RAW_CHARS = 1024 * 1024;
|
|
612
|
+
var MAX_RAW_NODES = 25e3;
|
|
613
|
+
var MAX_RAW_DEPTH = 200;
|
|
614
|
+
var MAX_RAW_ENTRIES = 2e3;
|
|
615
|
+
function serializeForRawDetection(body) {
|
|
616
|
+
if (body === null || body === void 0) return "";
|
|
617
|
+
if (typeof body === "string") return body.slice(0, MAX_RAW_CHARS);
|
|
618
|
+
const visited = /* @__PURE__ */ new Set();
|
|
619
|
+
const output = [];
|
|
620
|
+
const stack = [{ kind: "value", value: body, depth: 0, root: true }];
|
|
621
|
+
let chars = 0;
|
|
622
|
+
let nodes = 0;
|
|
623
|
+
const append = (text) => {
|
|
624
|
+
if (chars >= MAX_RAW_CHARS) return;
|
|
625
|
+
const piece = String(text).slice(0, MAX_RAW_CHARS - chars);
|
|
626
|
+
output.push(piece);
|
|
627
|
+
chars += piece.length;
|
|
628
|
+
};
|
|
629
|
+
while (stack.length > 0 && chars < MAX_RAW_CHARS) {
|
|
630
|
+
const task = stack.pop();
|
|
631
|
+
if (task.kind === "text") {
|
|
632
|
+
append(task.text);
|
|
633
|
+
continue;
|
|
491
634
|
}
|
|
492
|
-
if (
|
|
493
|
-
|
|
635
|
+
if (task.kind === "key") {
|
|
636
|
+
append(boundedJsonString(task.value));
|
|
637
|
+
append(":");
|
|
638
|
+
continue;
|
|
494
639
|
}
|
|
495
|
-
|
|
496
|
-
|
|
640
|
+
nodes++;
|
|
641
|
+
if (nodes > MAX_RAW_NODES) {
|
|
642
|
+
append('"[NodeLimit]"');
|
|
643
|
+
continue;
|
|
497
644
|
}
|
|
498
|
-
|
|
499
|
-
|
|
645
|
+
const value = task.value;
|
|
646
|
+
if (value === null || value === void 0) {
|
|
647
|
+
append(task.root ? "" : "null");
|
|
648
|
+
continue;
|
|
500
649
|
}
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
650
|
+
if (typeof value === "string") {
|
|
651
|
+
append(task.root ? value.slice(0, MAX_RAW_CHARS) : boundedJsonString(value));
|
|
652
|
+
continue;
|
|
504
653
|
}
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
case "get":
|
|
509
|
-
return this.#resolveGet(key);
|
|
510
|
-
case "post":
|
|
511
|
-
return this.#resolvePost(key);
|
|
512
|
-
case "request":
|
|
513
|
-
return this.#resolveRequest(key);
|
|
514
|
-
case "cookie":
|
|
515
|
-
return this.#resolveCookie(key);
|
|
516
|
-
case "server":
|
|
517
|
-
return this.#resolveServer(key);
|
|
518
|
-
case "files":
|
|
519
|
-
return this.#resolveFiles(key);
|
|
520
|
-
case "response":
|
|
521
|
-
return this.#resolveResponse(key);
|
|
522
|
-
case "egress":
|
|
523
|
-
return this.#resolveEgress(key);
|
|
524
|
-
default:
|
|
525
|
-
return [];
|
|
654
|
+
if (typeof value !== "object") {
|
|
655
|
+
append(String(value).slice(0, MAX_RAW_CHARS));
|
|
656
|
+
continue;
|
|
526
657
|
}
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
#resolveResponse(key) {
|
|
531
|
-
const resp = this.#req._response;
|
|
532
|
-
if (!resp) {
|
|
533
|
-
return [];
|
|
658
|
+
if (visited.has(value)) {
|
|
659
|
+
append("[Circular]");
|
|
660
|
+
continue;
|
|
534
661
|
}
|
|
535
|
-
if (
|
|
536
|
-
|
|
662
|
+
if (task.depth >= MAX_RAW_DEPTH) {
|
|
663
|
+
append('"[DepthLimit]"');
|
|
664
|
+
continue;
|
|
537
665
|
}
|
|
538
|
-
|
|
539
|
-
|
|
666
|
+
visited.add(value);
|
|
667
|
+
if (Array.isArray(value)) {
|
|
668
|
+
const length2 = Math.min(value.length, MAX_RAW_ENTRIES);
|
|
669
|
+
const items = [];
|
|
670
|
+
for (let i = 0; i < length2; i++) {
|
|
671
|
+
if (i > 0) items.push({ kind: "text", text: "," });
|
|
672
|
+
items.push({ kind: "value", value: readOwnValue(value, String(i)), depth: task.depth + 1, root: false });
|
|
673
|
+
}
|
|
674
|
+
if (value.length > length2) {
|
|
675
|
+
if (length2 > 0) items.push({ kind: "text", text: "," });
|
|
676
|
+
items.push({ kind: "text", text: '"[EntryLimit]"' });
|
|
677
|
+
}
|
|
678
|
+
stack.push({ kind: "text", text: "]" });
|
|
679
|
+
for (let i = items.length - 1; i >= 0; i--) stack.push(items[i]);
|
|
680
|
+
stack.push({ kind: "text", text: "[" });
|
|
681
|
+
continue;
|
|
540
682
|
}
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
683
|
+
let keys;
|
|
684
|
+
try {
|
|
685
|
+
keys = Object.getOwnPropertyNames(value);
|
|
686
|
+
} catch {
|
|
687
|
+
append('"[Unreadable]"');
|
|
688
|
+
continue;
|
|
544
689
|
}
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
690
|
+
const length = Math.min(keys.length, MAX_RAW_ENTRIES);
|
|
691
|
+
const entries = [];
|
|
692
|
+
for (let i = 0; i < length; i++) {
|
|
693
|
+
const key = keys[i];
|
|
694
|
+
if (i > 0) entries.push({ kind: "text", text: "," });
|
|
695
|
+
entries.push({ kind: "key", value: key });
|
|
696
|
+
entries.push({ kind: "value", value: readOwnValue(value, key), depth: task.depth + 1, root: false });
|
|
697
|
+
}
|
|
698
|
+
if (keys.length > length) {
|
|
699
|
+
if (length > 0) entries.push({ kind: "text", text: "," });
|
|
700
|
+
entries.push({ kind: "text", text: '"[EntryLimit]":null' });
|
|
701
|
+
}
|
|
702
|
+
stack.push({ kind: "text", text: "}" });
|
|
703
|
+
for (let i = entries.length - 1; i >= 0; i--) stack.push(entries[i]);
|
|
704
|
+
stack.push({ kind: "text", text: "{" });
|
|
705
|
+
}
|
|
706
|
+
return output.join("");
|
|
707
|
+
}
|
|
708
|
+
function boundedJsonString(value) {
|
|
709
|
+
return JSON.stringify(String(value).slice(0, MAX_RAW_CHARS));
|
|
710
|
+
}
|
|
711
|
+
function readOwnValue(value, key) {
|
|
712
|
+
try {
|
|
713
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
714
|
+
return descriptor && Object.hasOwn(descriptor, "value") ? descriptor.value : "[Accessor]";
|
|
715
|
+
} catch {
|
|
716
|
+
return "[Unreadable]";
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
var INHERITED_ACCESSORS = /* @__PURE__ */ new Set(["headers", "query"]);
|
|
720
|
+
function requestField(req, key) {
|
|
721
|
+
if (req === null || typeof req !== "object") return void 0;
|
|
722
|
+
if (Object.hasOwn(req, key)) return req[key];
|
|
723
|
+
if (!INHERITED_ACCESSORS.has(key)) return void 0;
|
|
724
|
+
let holder = Object.getPrototypeOf(req);
|
|
725
|
+
while (holder !== null && !Object.hasOwn(holder, key)) holder = Object.getPrototypeOf(holder);
|
|
726
|
+
if (holder === null || holder === Object.prototype) return void 0;
|
|
727
|
+
const descriptor = Object.getOwnPropertyDescriptor(holder, key);
|
|
728
|
+
if (typeof descriptor?.get !== "function") return void 0;
|
|
729
|
+
return req[key];
|
|
730
|
+
}
|
|
731
|
+
function normalizeRequest(req, options = {}) {
|
|
732
|
+
const ownRaw = Object.hasOwn(req ?? {}, "_rawBody") && typeof req._rawBody === "string";
|
|
733
|
+
const body = requestField(req, "body");
|
|
734
|
+
const url = requestField(req, "url");
|
|
735
|
+
const rawBody = ownRaw ? req._rawBody : serializeForRawDetection(body ?? null);
|
|
736
|
+
const headers = requestField(req, "headers") || {};
|
|
737
|
+
return {
|
|
738
|
+
query: normalizeObject(requestField(req, "query") || {}, options),
|
|
739
|
+
body: normalizeObject(body || {}, options),
|
|
740
|
+
headers: normalizeObject(headers, options),
|
|
741
|
+
// Cookies are split into name/value pairs BEFORE their values are normalised, and normalised the
|
|
742
|
+
// same way whether a framework parsed them or they come from the header — so a rule on a cookie
|
|
743
|
+
// sees one value, independent of which cookie parser (if any) ran first.
|
|
744
|
+
cookies: normalizeObject(cookieValues(requestField(req, "cookies"), headers), options),
|
|
745
|
+
url: normalize(decodeQueryPlus(url || ""), options),
|
|
746
|
+
originalUrl: normalize(decodeQueryPlus(requestField(req, "originalUrl") || url || ""), options),
|
|
747
|
+
_rawBody: rawBody
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
function cookieValues(parsed, headers) {
|
|
751
|
+
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
|
|
752
|
+
return parseCookieHeader(headers?.cookie);
|
|
753
|
+
}
|
|
754
|
+
var MAX_NORMALIZE_DEPTH = 1e3;
|
|
755
|
+
function normalizeObject(value, options = {}, depth = 0) {
|
|
756
|
+
if (typeof value === "string") {
|
|
757
|
+
return normalize(value, options);
|
|
758
|
+
}
|
|
759
|
+
if (value === null || typeof value !== "object") {
|
|
760
|
+
return value;
|
|
761
|
+
}
|
|
762
|
+
if (depth >= MAX_NORMALIZE_DEPTH) {
|
|
763
|
+
options.onLimit?.();
|
|
764
|
+
return value;
|
|
765
|
+
}
|
|
766
|
+
const copies = /* @__PURE__ */ new Map();
|
|
767
|
+
const copyOf = (node) => {
|
|
768
|
+
const copy = Array.isArray(node) ? new Array(node.length) : {};
|
|
769
|
+
copies.set(node, copy);
|
|
770
|
+
return copy;
|
|
771
|
+
};
|
|
772
|
+
const top = copyOf(value);
|
|
773
|
+
const stack = [[value, top, depth]];
|
|
774
|
+
while (stack.length > 0) {
|
|
775
|
+
const [node, copy, level] = stack.pop();
|
|
776
|
+
for (const key of Object.keys(node)) {
|
|
777
|
+
const child = node[key];
|
|
778
|
+
if (typeof child === "string") {
|
|
779
|
+
setOwn(copy, key, normalize(child, options));
|
|
780
|
+
} else if (child === null || typeof child !== "object") {
|
|
781
|
+
setOwn(copy, key, child);
|
|
782
|
+
} else if (copies.has(child)) {
|
|
783
|
+
setOwn(copy, key, copies.get(child));
|
|
784
|
+
} else if (level + 1 >= MAX_NORMALIZE_DEPTH) {
|
|
785
|
+
setOwn(copy, key, child);
|
|
786
|
+
options.onLimit?.();
|
|
787
|
+
} else {
|
|
788
|
+
const childCopy = copyOf(child);
|
|
789
|
+
setOwn(copy, key, childCopy);
|
|
790
|
+
stack.push([child, childCopy, level + 1]);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
return top;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// src/protect/engine/request.js
|
|
798
|
+
var FILE_ATTRS = /* @__PURE__ */ new Set(["content", "filename", "type"]);
|
|
799
|
+
var ownValue = (obj, key) => obj !== null && typeof obj === "object" && Object.prototype.hasOwnProperty.call(obj, key) ? obj[key] : void 0;
|
|
800
|
+
function fieldSpellings(key) {
|
|
801
|
+
const spellings = [{ key, nested: true }];
|
|
802
|
+
if (key.includes("[")) {
|
|
803
|
+
const dotted = key.replace(/\[\]$/, "").replace(/\[([^\][]*)\]/g, ".$1");
|
|
804
|
+
if (dotted !== key && dotted !== "" && !dotted.includes("[") && !dotted.includes("]")) {
|
|
805
|
+
spellings.push({ key: dotted, nested: true });
|
|
806
|
+
}
|
|
807
|
+
return spellings;
|
|
808
|
+
}
|
|
809
|
+
const [head, ...rest] = key.split(".");
|
|
810
|
+
const bracketed = head + rest.map((part) => `[${part}]`).join("");
|
|
811
|
+
if (bracketed !== key) spellings.push({ key: bracketed, nested: false });
|
|
812
|
+
spellings.push({ key: `${bracketed}[]`, nested: false });
|
|
813
|
+
return spellings;
|
|
814
|
+
}
|
|
815
|
+
var fileFilename = (f) => f && typeof f === "object" ? f.filename : f;
|
|
816
|
+
var fileAttribute = (f, attr) => f && typeof f === "object" ? f[attr] : attr === "filename" ? f : void 0;
|
|
817
|
+
function base64DecodeUtf8(value) {
|
|
818
|
+
const str = String(value);
|
|
819
|
+
if (typeof Buffer !== "undefined") {
|
|
820
|
+
return Buffer.from(str, "base64").toString("utf-8");
|
|
821
|
+
}
|
|
822
|
+
const binary = atob(str);
|
|
823
|
+
const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
|
|
824
|
+
return new TextDecoder().decode(bytes);
|
|
825
|
+
}
|
|
826
|
+
var TEXT_MUTATIONS = /* @__PURE__ */ new Set(["base64_decode", "urldecode", "htmlentitydecode"]);
|
|
827
|
+
var MAX_MAPPED_NODES = 2e4;
|
|
828
|
+
var MAX_MAPPED_DEPTH = 1e3;
|
|
829
|
+
function mapStringLeaves(root, fn, onLimit) {
|
|
830
|
+
const copies = /* @__PURE__ */ new Map();
|
|
831
|
+
const copyOf = (node) => {
|
|
832
|
+
const copy = Array.isArray(node) ? [] : {};
|
|
833
|
+
copies.set(node, copy);
|
|
834
|
+
return copy;
|
|
835
|
+
};
|
|
836
|
+
const top = copyOf(root);
|
|
837
|
+
const stack = [[root, top, 0]];
|
|
838
|
+
let visited = 0;
|
|
839
|
+
while (stack.length) {
|
|
840
|
+
const [node, copy, depth] = stack.pop();
|
|
841
|
+
visited++;
|
|
842
|
+
for (const key of Object.keys(node)) {
|
|
843
|
+
const child = node[key];
|
|
844
|
+
if (typeof child === "string") {
|
|
845
|
+
setOwn(copy, key, fn(child));
|
|
846
|
+
} else if (child === null || typeof child !== "object") {
|
|
847
|
+
setOwn(copy, key, child);
|
|
848
|
+
} else if (copies.has(child)) {
|
|
849
|
+
setOwn(copy, key, copies.get(child));
|
|
850
|
+
} else if (depth + 1 >= MAX_MAPPED_DEPTH || visited + stack.length >= MAX_MAPPED_NODES) {
|
|
851
|
+
setOwn(copy, key, child);
|
|
852
|
+
onLimit();
|
|
853
|
+
} else {
|
|
854
|
+
const childCopy = copyOf(child);
|
|
855
|
+
setOwn(copy, key, childCopy);
|
|
856
|
+
stack.push([child, childCopy, depth + 1]);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
return top;
|
|
861
|
+
}
|
|
862
|
+
var RequestResolver = class {
|
|
863
|
+
#req;
|
|
864
|
+
#cookies;
|
|
865
|
+
#skips = /* @__PURE__ */ new Set();
|
|
866
|
+
constructor(req) {
|
|
867
|
+
this.#req = req;
|
|
868
|
+
this.#cookies = null;
|
|
869
|
+
}
|
|
870
|
+
/** Record an inspection limit this evaluation reached (reported once per reason). */
|
|
871
|
+
noteSkip(reason) {
|
|
872
|
+
this.#skips.add(reason);
|
|
873
|
+
}
|
|
874
|
+
/** The inspection limits recorded so far, in the order first reached. */
|
|
875
|
+
get skips() {
|
|
876
|
+
return [...this.#skips];
|
|
877
|
+
}
|
|
878
|
+
resolve(parameter) {
|
|
879
|
+
if (!parameter || parameter === "false") {
|
|
880
|
+
return [null];
|
|
881
|
+
}
|
|
882
|
+
if (parameter === "rules") {
|
|
883
|
+
return [null];
|
|
884
|
+
}
|
|
885
|
+
if (parameter === "raw") {
|
|
886
|
+
return this.#resolveRaw();
|
|
887
|
+
}
|
|
888
|
+
if (parameter === "all") {
|
|
889
|
+
return this.#resolveAll();
|
|
890
|
+
}
|
|
891
|
+
const dotIndex = parameter.indexOf(".");
|
|
892
|
+
if (dotIndex === -1) {
|
|
893
|
+
return [];
|
|
894
|
+
}
|
|
895
|
+
const source = parameter.substring(0, dotIndex);
|
|
896
|
+
const key = parameter.substring(dotIndex + 1);
|
|
897
|
+
switch (source) {
|
|
898
|
+
case "get":
|
|
899
|
+
return this.#resolveGet(key);
|
|
900
|
+
case "post":
|
|
901
|
+
return this.#resolvePost(key);
|
|
902
|
+
case "request":
|
|
903
|
+
return this.#resolveRequest(key);
|
|
904
|
+
case "cookie":
|
|
905
|
+
return this.#resolveCookie(key);
|
|
906
|
+
case "server":
|
|
907
|
+
return this.#resolveServer(key);
|
|
908
|
+
case "files":
|
|
909
|
+
return this.#resolveFiles(key);
|
|
910
|
+
case "response":
|
|
911
|
+
return this.#resolveResponse(key);
|
|
912
|
+
case "egress":
|
|
913
|
+
return this.#resolveEgress(key);
|
|
914
|
+
default:
|
|
915
|
+
return [];
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
// Response-phase sources (req._response = { status, headers, body }). Lets rules inspect
|
|
919
|
+
// what the app is about to SEND — e.g. a leaked secret in the body — regardless of route.
|
|
920
|
+
#resolveResponse(key) {
|
|
921
|
+
const resp = this.#req._response;
|
|
922
|
+
if (!resp) {
|
|
923
|
+
return [];
|
|
924
|
+
}
|
|
925
|
+
if (key === "status") {
|
|
926
|
+
return resp.status !== void 0 ? [String(resp.status)] : [];
|
|
927
|
+
}
|
|
928
|
+
if (key === "body") {
|
|
929
|
+
return resp.body != null && resp.body !== "" ? [resp.body] : [];
|
|
930
|
+
}
|
|
931
|
+
if (key === "headers") {
|
|
932
|
+
const headers = resp.headers ?? {};
|
|
933
|
+
return [Object.entries(headers).map(([k, v]) => `${k}: ${v}`).join("\n")];
|
|
934
|
+
}
|
|
935
|
+
if (key.startsWith("header.")) {
|
|
936
|
+
const name = key.slice("header.".length).toLowerCase();
|
|
937
|
+
const headers = resp.headers ?? {};
|
|
938
|
+
return Object.hasOwn(headers, name) ? [headers[name]] : [];
|
|
549
939
|
}
|
|
550
940
|
return [];
|
|
551
941
|
}
|
|
@@ -575,6 +965,9 @@ var RequestResolver = class {
|
|
|
575
965
|
if (value === null || value === void 0) {
|
|
576
966
|
return value;
|
|
577
967
|
}
|
|
968
|
+
if (typeof value === "object" && TEXT_MUTATIONS.has(mutation)) {
|
|
969
|
+
return mapStringLeaves(value, (leaf) => this.#applyMutation(mutation, leaf), () => this.noteSkip("container-cap"));
|
|
970
|
+
}
|
|
578
971
|
switch (mutation) {
|
|
579
972
|
case "base64_decode":
|
|
580
973
|
try {
|
|
@@ -595,11 +988,7 @@ var RequestResolver = class {
|
|
|
595
988
|
return value;
|
|
596
989
|
}
|
|
597
990
|
case "urldecode":
|
|
598
|
-
|
|
599
|
-
return decodeURIComponent(String(value));
|
|
600
|
-
} catch {
|
|
601
|
-
return value;
|
|
602
|
-
}
|
|
991
|
+
return safeUrlDecode(String(value).replace(/\+/g, " "));
|
|
603
992
|
case "htmlentitydecode":
|
|
604
993
|
return decodeHtmlEntities(String(value));
|
|
605
994
|
case "intval":
|
|
@@ -618,16 +1007,30 @@ var RequestResolver = class {
|
|
|
618
1007
|
if (key.endsWith("*")) {
|
|
619
1008
|
return this.#resolveWildcard(query, key);
|
|
620
1009
|
}
|
|
621
|
-
|
|
622
|
-
return value !== void 0 ? [value] : [];
|
|
1010
|
+
return this.#lookup(query, key);
|
|
623
1011
|
}
|
|
624
1012
|
#resolvePost(key) {
|
|
625
1013
|
const body = this.#req.body ?? {};
|
|
626
1014
|
if (key.endsWith("*")) {
|
|
627
1015
|
return this.#resolveWildcard(body, key);
|
|
628
1016
|
}
|
|
629
|
-
|
|
630
|
-
|
|
1017
|
+
return this.#lookup(body, key);
|
|
1018
|
+
}
|
|
1019
|
+
/**
|
|
1020
|
+
* Every value a field path names in `obj`, in either spelling a form or query field can take.
|
|
1021
|
+
*
|
|
1022
|
+
* A parser that expands brackets (`qs`, as Express uses) turns `user[name]=x` into `{ user: { name } }`
|
|
1023
|
+
* and `id[]=x` into `{ id: [x] }`; a flat parser (`URLSearchParams`, as the Fetch and Node adapters use)
|
|
1024
|
+
* keeps `user[name]` and `id[]` as literal keys. The rule names the field once, so both shapes answer
|
|
1025
|
+
* to both spellings: `user.name` and `user[name]`, `id` and `id[]`.
|
|
1026
|
+
*/
|
|
1027
|
+
#lookup(obj, key) {
|
|
1028
|
+
const values = [];
|
|
1029
|
+
for (const candidate of fieldSpellings(key)) {
|
|
1030
|
+
const value = candidate.nested ? this.#getNestedValue(obj, candidate.key) : ownValue(obj, candidate.key);
|
|
1031
|
+
if (value !== void 0) values.push(value);
|
|
1032
|
+
}
|
|
1033
|
+
return values;
|
|
631
1034
|
}
|
|
632
1035
|
#resolveRequest(key) {
|
|
633
1036
|
const query = this.#req.query ?? {};
|
|
@@ -640,8 +1043,11 @@ var RequestResolver = class {
|
|
|
640
1043
|
...this.#resolveWildcard(cookies, key)
|
|
641
1044
|
];
|
|
642
1045
|
}
|
|
643
|
-
const
|
|
644
|
-
|
|
1046
|
+
const fromQuery = this.#lookup(query, key);
|
|
1047
|
+
if (fromQuery.length > 0) return fromQuery;
|
|
1048
|
+
const fromBody = this.#lookup(body, key);
|
|
1049
|
+
if (fromBody.length > 0) return fromBody;
|
|
1050
|
+
return Object.hasOwn(cookies, key) ? [cookies[key]] : [];
|
|
645
1051
|
}
|
|
646
1052
|
#resolveCookie(key) {
|
|
647
1053
|
const cookies = this.#parseCookies();
|
|
@@ -718,422 +1124,144 @@ var RequestResolver = class {
|
|
|
718
1124
|
if (body === void 0 || body === null) {
|
|
719
1125
|
return [];
|
|
720
1126
|
}
|
|
721
|
-
if (typeof body === "string") {
|
|
722
|
-
return [body];
|
|
723
|
-
}
|
|
724
|
-
try {
|
|
725
|
-
return [JSON.stringify(body)];
|
|
726
|
-
} catch {
|
|
727
|
-
return [String(body)];
|
|
728
|
-
}
|
|
729
|
-
}
|
|
730
|
-
#resolveAll() {
|
|
731
|
-
const parts = [];
|
|
732
|
-
const uri = this.#req.originalUrl ?? this.#req.url ?? "/";
|
|
733
|
-
parts.push(uri);
|
|
734
|
-
const queryString = uri.includes("?") ? uri.split("?")[1] : "";
|
|
735
|
-
if (queryString) {
|
|
736
|
-
parts.push(queryString);
|
|
737
|
-
}
|
|
738
|
-
const body = this.#req.body;
|
|
739
|
-
if (body) {
|
|
740
|
-
parts.push(typeof body === "string" ? body : JSON.stringify(body));
|
|
741
|
-
}
|
|
742
|
-
if (typeof this.#req._rawBody === "string" && this.#req._rawBody) {
|
|
743
|
-
parts.push(this.#req._rawBody);
|
|
744
|
-
}
|
|
745
|
-
const headers = this.#req.headers ?? {};
|
|
746
|
-
const excludedHeaders = /* @__PURE__ */ new Set([
|
|
747
|
-
"host",
|
|
748
|
-
"connection",
|
|
749
|
-
"cache-control",
|
|
750
|
-
"accept",
|
|
751
|
-
"accept-encoding",
|
|
752
|
-
"accept-language",
|
|
753
|
-
"priority",
|
|
754
|
-
"sec-ch-ua",
|
|
755
|
-
"sec-ch-ua-mobile",
|
|
756
|
-
"sec-ch-ua-platform",
|
|
757
|
-
"sec-fetch-dest",
|
|
758
|
-
"sec-fetch-mode",
|
|
759
|
-
"sec-fetch-site",
|
|
760
|
-
"sec-fetch-user",
|
|
761
|
-
"upgrade-insecure-requests"
|
|
762
|
-
]);
|
|
763
|
-
for (const [name, value] of Object.entries(headers)) {
|
|
764
|
-
if (!excludedHeaders.has(name)) {
|
|
765
|
-
parts.push(`${name}: ${value}`);
|
|
766
|
-
}
|
|
767
|
-
}
|
|
768
|
-
const cookies = this.#parseCookies();
|
|
769
|
-
const cookieStr = Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join("; ");
|
|
770
|
-
if (cookieStr) {
|
|
771
|
-
parts.push(cookieStr);
|
|
772
|
-
}
|
|
773
|
-
return [parts.join(" ")];
|
|
774
|
-
}
|
|
775
|
-
#resolveWildcard(obj, pattern) {
|
|
776
|
-
if (typeof obj !== "object" || obj === null) {
|
|
777
|
-
return [];
|
|
778
|
-
}
|
|
779
|
-
const prefix = pattern.slice(0, -1);
|
|
780
|
-
const values = [];
|
|
781
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
782
|
-
if (key.startsWith(prefix)) {
|
|
783
|
-
values.push(value);
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
return values;
|
|
787
|
-
}
|
|
788
|
-
#getNestedValue(obj, key) {
|
|
789
|
-
if (typeof obj !== "object" || obj === null) {
|
|
790
|
-
return void 0;
|
|
791
|
-
}
|
|
792
|
-
const own = (o, k) => o !== null && typeof o === "object" && Object.prototype.hasOwnProperty.call(o, k);
|
|
793
|
-
if (own(obj, key)) {
|
|
794
|
-
return obj[key];
|
|
795
|
-
}
|
|
796
|
-
const parts = key.split(".");
|
|
797
|
-
let current = obj;
|
|
798
|
-
for (const part of parts) {
|
|
799
|
-
if (!own(current, part)) {
|
|
800
|
-
return void 0;
|
|
801
|
-
}
|
|
802
|
-
current = current[part];
|
|
803
|
-
}
|
|
804
|
-
return current;
|
|
805
|
-
}
|
|
806
|
-
#parseCookies() {
|
|
807
|
-
if (this.#cookies !== null) {
|
|
808
|
-
return this.#cookies;
|
|
809
|
-
}
|
|
810
|
-
if (this.#req.cookies) {
|
|
811
|
-
this.#cookies = this.#req.cookies;
|
|
812
|
-
return this.#cookies;
|
|
813
|
-
}
|
|
814
|
-
const header = this.#req.headers?.cookie;
|
|
815
|
-
if (!header) {
|
|
816
|
-
this.#cookies = {};
|
|
817
|
-
return this.#cookies;
|
|
818
|
-
}
|
|
819
|
-
const cookies = {};
|
|
820
|
-
for (const pair of header.split(";")) {
|
|
821
|
-
const eqIndex = pair.indexOf("=");
|
|
822
|
-
if (eqIndex === -1) {
|
|
823
|
-
continue;
|
|
824
|
-
}
|
|
825
|
-
const name = pair.substring(0, eqIndex).trim();
|
|
826
|
-
const value = pair.substring(eqIndex + 1).trim();
|
|
827
|
-
setOwn(cookies, name, value);
|
|
828
|
-
}
|
|
829
|
-
this.#cookies = cookies;
|
|
830
|
-
return this.#cookies;
|
|
831
|
-
}
|
|
832
|
-
};
|
|
833
|
-
|
|
834
|
-
// src/protect/notify.js
|
|
835
|
-
var reported = /* @__PURE__ */ new Set();
|
|
836
|
-
function notify(fn, arg, label) {
|
|
837
|
-
if (typeof fn !== "function") return false;
|
|
838
|
-
try {
|
|
839
|
-
const result = fn(arg);
|
|
840
|
-
if (result !== null && typeof result === "object" && typeof result.then === "function") {
|
|
841
|
-
try {
|
|
842
|
-
result.then(void 0, (err) => warnOnce2(label, err));
|
|
843
|
-
} catch {
|
|
844
|
-
}
|
|
845
|
-
}
|
|
846
|
-
return true;
|
|
847
|
-
} catch (err) {
|
|
848
|
-
warnOnce2(label, err);
|
|
849
|
-
return false;
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
function warnOnce2(label, err) {
|
|
853
|
-
if (reported.has(label)) return;
|
|
854
|
-
reported.add(label);
|
|
855
|
-
try {
|
|
856
|
-
console.warn(
|
|
857
|
-
`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)}`
|
|
858
|
-
);
|
|
859
|
-
} catch {
|
|
860
|
-
}
|
|
861
|
-
}
|
|
862
|
-
|
|
863
|
-
// src/protect/engine/normalizer.js
|
|
864
|
-
var HTML_ENTITIES = {
|
|
865
|
-
"&": "&",
|
|
866
|
-
"<": "<",
|
|
867
|
-
">": ">",
|
|
868
|
-
""": '"',
|
|
869
|
-
"'": "'",
|
|
870
|
-
"'": "'",
|
|
871
|
-
" ": " ",
|
|
872
|
-
"'": "'",
|
|
873
|
-
""": '"',
|
|
874
|
-
"<": "<",
|
|
875
|
-
">": ">",
|
|
876
|
-
"&": "&",
|
|
877
|
-
""": '"',
|
|
878
|
-
"<": "<",
|
|
879
|
-
">": ">",
|
|
880
|
-
"&": "&"
|
|
881
|
-
};
|
|
882
|
-
var MAX_DECODE_ITERATIONS = 5;
|
|
883
|
-
function normalize(value, options = {}) {
|
|
884
|
-
if (typeof value !== "string") {
|
|
885
|
-
return value;
|
|
886
|
-
}
|
|
887
|
-
const opts = {
|
|
888
|
-
urlDecode: true,
|
|
889
|
-
htmlDecode: true,
|
|
890
|
-
sqlComments: true,
|
|
891
|
-
nullBytes: true,
|
|
892
|
-
whitespace: true,
|
|
893
|
-
...options
|
|
894
|
-
};
|
|
895
|
-
let result = value;
|
|
896
|
-
if (opts.nullBytes) {
|
|
897
|
-
result = removeNullBytes(result);
|
|
898
|
-
}
|
|
899
|
-
if (opts.urlDecode) {
|
|
900
|
-
result = urlDecode(result);
|
|
901
|
-
}
|
|
902
|
-
if (opts.htmlDecode) {
|
|
903
|
-
result = htmlEntityDecode(result);
|
|
904
|
-
}
|
|
905
|
-
if (opts.sqlComments) {
|
|
906
|
-
result = removeSqlComments(result);
|
|
907
|
-
}
|
|
908
|
-
if (opts.whitespace) {
|
|
909
|
-
result = normalizeWhitespace(result);
|
|
910
|
-
}
|
|
911
|
-
return result;
|
|
912
|
-
}
|
|
913
|
-
function urlDecode(value) {
|
|
914
|
-
if (typeof value !== "string") {
|
|
915
|
-
return value;
|
|
916
|
-
}
|
|
917
|
-
let result = value;
|
|
918
|
-
let previous = "";
|
|
919
|
-
let iterations = 0;
|
|
920
|
-
while (result !== previous && iterations < MAX_DECODE_ITERATIONS) {
|
|
921
|
-
previous = result;
|
|
922
|
-
iterations++;
|
|
923
|
-
try {
|
|
924
|
-
result = decodeURIComponent(result);
|
|
925
|
-
} catch {
|
|
926
|
-
result = safeUrlDecode(result);
|
|
927
|
-
break;
|
|
928
|
-
}
|
|
929
|
-
}
|
|
930
|
-
return result;
|
|
931
|
-
}
|
|
932
|
-
function safeUrlDecode(value) {
|
|
933
|
-
return value.replace(/%([0-9A-Fa-f]{2})/g, (match, hex) => {
|
|
934
|
-
try {
|
|
935
|
-
return String.fromCharCode(parseInt(hex, 16));
|
|
936
|
-
} catch {
|
|
937
|
-
return match;
|
|
938
|
-
}
|
|
939
|
-
});
|
|
940
|
-
}
|
|
941
|
-
function htmlEntityDecode(value) {
|
|
942
|
-
if (typeof value !== "string") {
|
|
943
|
-
return value;
|
|
944
|
-
}
|
|
945
|
-
let result = value;
|
|
946
|
-
for (const [entity, char] of Object.entries(HTML_ENTITIES)) {
|
|
947
|
-
result = result.split(entity).join(char);
|
|
948
|
-
}
|
|
949
|
-
result = result.replace(/&#(\d+);/g, (match, code) => {
|
|
950
|
-
const num = parseInt(code, 10);
|
|
951
|
-
return num > 0 && num < 65536 ? String.fromCharCode(num) : match;
|
|
952
|
-
});
|
|
953
|
-
result = result.replace(/&#x([0-9A-Fa-f]+);/g, (match, hex) => {
|
|
954
|
-
const num = parseInt(hex, 16);
|
|
955
|
-
return num > 0 && num < 65536 ? String.fromCharCode(num) : match;
|
|
956
|
-
});
|
|
957
|
-
return result;
|
|
958
|
-
}
|
|
959
|
-
function removeSqlComments(value) {
|
|
960
|
-
if (typeof value !== "string") {
|
|
961
|
-
return value;
|
|
962
|
-
}
|
|
963
|
-
let result = value;
|
|
964
|
-
result = result.replace(/\/\*[\s\S]*?\*\//g, " ");
|
|
965
|
-
result = result.replace(/\/\*![\s\S]*?\*\//g, " ");
|
|
966
|
-
return result;
|
|
967
|
-
}
|
|
968
|
-
function removeNullBytes(value) {
|
|
969
|
-
if (typeof value !== "string") {
|
|
970
|
-
return value;
|
|
971
|
-
}
|
|
972
|
-
let result = value.replace(/\x00/g, "");
|
|
973
|
-
result = result.replace(/[\x01-\x08\x0B\x0C\x0E-\x1F]/g, "");
|
|
974
|
-
return result;
|
|
975
|
-
}
|
|
976
|
-
function normalizeWhitespace(value) {
|
|
977
|
-
if (typeof value !== "string") {
|
|
978
|
-
return value;
|
|
979
|
-
}
|
|
980
|
-
let result = value.replace(/[\t\r\n\f\v]+/g, " ");
|
|
981
|
-
result = result.replace(/ {2,}/g, " ");
|
|
982
|
-
return result;
|
|
983
|
-
}
|
|
984
|
-
var MAX_RAW_CHARS = 1024 * 1024;
|
|
985
|
-
var MAX_RAW_NODES = 25e3;
|
|
986
|
-
var MAX_RAW_DEPTH = 200;
|
|
987
|
-
var MAX_RAW_ENTRIES = 2e3;
|
|
988
|
-
function serializeForRawDetection(body) {
|
|
989
|
-
if (body === null || body === void 0) return "";
|
|
990
|
-
if (typeof body === "string") return body.slice(0, MAX_RAW_CHARS);
|
|
991
|
-
const visited = /* @__PURE__ */ new Set();
|
|
992
|
-
const output = [];
|
|
993
|
-
const stack = [{ kind: "value", value: body, depth: 0, root: true }];
|
|
994
|
-
let chars = 0;
|
|
995
|
-
let nodes = 0;
|
|
996
|
-
const append = (text) => {
|
|
997
|
-
if (chars >= MAX_RAW_CHARS) return;
|
|
998
|
-
const piece = String(text).slice(0, MAX_RAW_CHARS - chars);
|
|
999
|
-
output.push(piece);
|
|
1000
|
-
chars += piece.length;
|
|
1001
|
-
};
|
|
1002
|
-
while (stack.length > 0 && chars < MAX_RAW_CHARS) {
|
|
1003
|
-
const task = stack.pop();
|
|
1004
|
-
if (task.kind === "text") {
|
|
1005
|
-
append(task.text);
|
|
1006
|
-
continue;
|
|
1007
|
-
}
|
|
1008
|
-
if (task.kind === "key") {
|
|
1009
|
-
append(boundedJsonString(task.value));
|
|
1010
|
-
append(":");
|
|
1011
|
-
continue;
|
|
1012
|
-
}
|
|
1013
|
-
nodes++;
|
|
1014
|
-
if (nodes > MAX_RAW_NODES) {
|
|
1015
|
-
append('"[NodeLimit]"');
|
|
1016
|
-
continue;
|
|
1017
|
-
}
|
|
1018
|
-
const value = task.value;
|
|
1019
|
-
if (value === null || value === void 0) {
|
|
1020
|
-
append(task.root ? "" : "null");
|
|
1021
|
-
continue;
|
|
1127
|
+
if (typeof body === "string") {
|
|
1128
|
+
return [body];
|
|
1022
1129
|
}
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1130
|
+
try {
|
|
1131
|
+
return [JSON.stringify(body)];
|
|
1132
|
+
} catch {
|
|
1133
|
+
return [String(body)];
|
|
1026
1134
|
}
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1135
|
+
}
|
|
1136
|
+
#resolveAll() {
|
|
1137
|
+
const parts = [];
|
|
1138
|
+
const uri = this.#req.originalUrl ?? this.#req.url ?? "/";
|
|
1139
|
+
parts.push(uri);
|
|
1140
|
+
const queryString = uri.includes("?") ? uri.split("?")[1] : "";
|
|
1141
|
+
if (queryString) {
|
|
1142
|
+
parts.push(queryString);
|
|
1030
1143
|
}
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1144
|
+
const body = this.#req.body;
|
|
1145
|
+
if (body) {
|
|
1146
|
+
parts.push(typeof body === "string" ? body : JSON.stringify(body));
|
|
1034
1147
|
}
|
|
1035
|
-
if (
|
|
1036
|
-
|
|
1037
|
-
continue;
|
|
1148
|
+
if (typeof this.#req._rawBody === "string" && this.#req._rawBody) {
|
|
1149
|
+
parts.push(this.#req._rawBody);
|
|
1038
1150
|
}
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1151
|
+
const headers = this.#req.headers ?? {};
|
|
1152
|
+
const excludedHeaders = /* @__PURE__ */ new Set([
|
|
1153
|
+
"host",
|
|
1154
|
+
"connection",
|
|
1155
|
+
"cache-control",
|
|
1156
|
+
"accept",
|
|
1157
|
+
"accept-encoding",
|
|
1158
|
+
"accept-language",
|
|
1159
|
+
"priority",
|
|
1160
|
+
"sec-ch-ua",
|
|
1161
|
+
"sec-ch-ua-mobile",
|
|
1162
|
+
"sec-ch-ua-platform",
|
|
1163
|
+
"sec-fetch-dest",
|
|
1164
|
+
"sec-fetch-mode",
|
|
1165
|
+
"sec-fetch-site",
|
|
1166
|
+
"sec-fetch-user",
|
|
1167
|
+
"upgrade-insecure-requests"
|
|
1168
|
+
]);
|
|
1169
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
1170
|
+
if (!excludedHeaders.has(name)) {
|
|
1171
|
+
parts.push(`${name}: ${value}`);
|
|
1046
1172
|
}
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1173
|
+
}
|
|
1174
|
+
const cookies = this.#parseCookies();
|
|
1175
|
+
const cookieStr = Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join("; ");
|
|
1176
|
+
if (cookieStr) {
|
|
1177
|
+
parts.push(cookieStr);
|
|
1178
|
+
}
|
|
1179
|
+
return [parts.join(" ")];
|
|
1180
|
+
}
|
|
1181
|
+
#resolveWildcard(obj, pattern) {
|
|
1182
|
+
if (typeof obj !== "object" || obj === null) {
|
|
1183
|
+
return [];
|
|
1184
|
+
}
|
|
1185
|
+
const prefix = pattern.slice(0, -1);
|
|
1186
|
+
const values = [];
|
|
1187
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
1188
|
+
if (key.startsWith(prefix)) {
|
|
1189
|
+
values.push(value);
|
|
1050
1190
|
}
|
|
1051
|
-
stack.push({ kind: "text", text: "]" });
|
|
1052
|
-
for (let i = items.length - 1; i >= 0; i--) stack.push(items[i]);
|
|
1053
|
-
stack.push({ kind: "text", text: "[" });
|
|
1054
|
-
continue;
|
|
1055
1191
|
}
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
continue;
|
|
1192
|
+
return values;
|
|
1193
|
+
}
|
|
1194
|
+
#getNestedValue(obj, key) {
|
|
1195
|
+
if (typeof obj !== "object" || obj === null) {
|
|
1196
|
+
return void 0;
|
|
1062
1197
|
}
|
|
1063
|
-
const
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
const key = keys[i];
|
|
1067
|
-
if (i > 0) entries.push({ kind: "text", text: "," });
|
|
1068
|
-
entries.push({ kind: "key", value: key });
|
|
1069
|
-
entries.push({ kind: "value", value: readOwnValue(value, key), depth: task.depth + 1, root: false });
|
|
1198
|
+
const own = (o, k) => o !== null && typeof o === "object" && Object.prototype.hasOwnProperty.call(o, k);
|
|
1199
|
+
if (own(obj, key)) {
|
|
1200
|
+
return obj[key];
|
|
1070
1201
|
}
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1202
|
+
const parts = key.split(".");
|
|
1203
|
+
let current = obj;
|
|
1204
|
+
for (const part of parts) {
|
|
1205
|
+
if (!own(current, part)) {
|
|
1206
|
+
return void 0;
|
|
1207
|
+
}
|
|
1208
|
+
current = current[part];
|
|
1074
1209
|
}
|
|
1075
|
-
|
|
1076
|
-
for (let i = entries.length - 1; i >= 0; i--) stack.push(entries[i]);
|
|
1077
|
-
stack.push({ kind: "text", text: "{" });
|
|
1210
|
+
return current;
|
|
1078
1211
|
}
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1212
|
+
#parseCookies() {
|
|
1213
|
+
if (this.#cookies !== null) {
|
|
1214
|
+
return this.#cookies;
|
|
1215
|
+
}
|
|
1216
|
+
if (this.#req.cookies) {
|
|
1217
|
+
this.#cookies = this.#req.cookies;
|
|
1218
|
+
return this.#cookies;
|
|
1219
|
+
}
|
|
1220
|
+
this.#cookies = parseCookieHeader(this.#req.headers?.cookie);
|
|
1221
|
+
return this.#cookies;
|
|
1222
|
+
}
|
|
1223
|
+
};
|
|
1224
|
+
|
|
1225
|
+
// src/protect/notify.js
|
|
1226
|
+
var reported = /* @__PURE__ */ new WeakMap();
|
|
1227
|
+
var warnings = 0;
|
|
1228
|
+
var MAX_WARNINGS = 20;
|
|
1229
|
+
function notify(fn, arg, label) {
|
|
1230
|
+
if (typeof fn !== "function") return false;
|
|
1085
1231
|
try {
|
|
1086
|
-
const
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1232
|
+
const result = fn(arg);
|
|
1233
|
+
if (result !== null && typeof result === "object" && typeof result.then === "function") {
|
|
1234
|
+
try {
|
|
1235
|
+
result.then(void 0, (err) => warnOnce2(fn, label, err));
|
|
1236
|
+
} catch {
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
return true;
|
|
1240
|
+
} catch (err) {
|
|
1241
|
+
warnOnce2(fn, label, err);
|
|
1242
|
+
return false;
|
|
1090
1243
|
}
|
|
1091
1244
|
}
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
if (
|
|
1095
|
-
if (
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
while (holder !== null && !Object.hasOwn(holder, key)) holder = Object.getPrototypeOf(holder);
|
|
1099
|
-
if (holder === null || holder === Object.prototype) return void 0;
|
|
1100
|
-
const descriptor = Object.getOwnPropertyDescriptor(holder, key);
|
|
1101
|
-
if (typeof descriptor?.get !== "function") return void 0;
|
|
1102
|
-
return req[key];
|
|
1103
|
-
}
|
|
1104
|
-
function normalizeRequest(req, options = {}) {
|
|
1105
|
-
const ownRaw = Object.hasOwn(req ?? {}, "_rawBody") && typeof req._rawBody === "string";
|
|
1106
|
-
const body = requestField(req, "body");
|
|
1107
|
-
const url = requestField(req, "url");
|
|
1108
|
-
const rawBody = ownRaw ? req._rawBody : serializeForRawDetection(body ?? null);
|
|
1109
|
-
return {
|
|
1110
|
-
query: normalizeObject(requestField(req, "query") || {}, options),
|
|
1111
|
-
body: normalizeObject(body || {}, options),
|
|
1112
|
-
headers: normalizeObject(requestField(req, "headers") || {}, options),
|
|
1113
|
-
url: normalize(url || "", options),
|
|
1114
|
-
originalUrl: normalize(requestField(req, "originalUrl") || url || "", options),
|
|
1115
|
-
_rawBody: rawBody
|
|
1116
|
-
};
|
|
1117
|
-
}
|
|
1118
|
-
var MAX_NORMALIZE_DEPTH = 200;
|
|
1119
|
-
function normalizeObject(value, options = {}, depth = 0) {
|
|
1120
|
-
if (typeof value === "string") {
|
|
1121
|
-
return normalize(value, options);
|
|
1122
|
-
}
|
|
1123
|
-
if (depth >= MAX_NORMALIZE_DEPTH) {
|
|
1124
|
-
return value;
|
|
1125
|
-
}
|
|
1126
|
-
if (Array.isArray(value)) {
|
|
1127
|
-
return value.map((item) => normalizeObject(item, options, depth + 1));
|
|
1245
|
+
function warnOnce2(fn, label, err) {
|
|
1246
|
+
let labels = reported.get(fn);
|
|
1247
|
+
if (labels?.has(label)) return;
|
|
1248
|
+
if (!labels) {
|
|
1249
|
+
labels = /* @__PURE__ */ new Set();
|
|
1250
|
+
reported.set(fn, labels);
|
|
1128
1251
|
}
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1252
|
+
labels.add(label);
|
|
1253
|
+
if (warnings > MAX_WARNINGS) return;
|
|
1254
|
+
warnings++;
|
|
1255
|
+
try {
|
|
1256
|
+
if (warnings > MAX_WARNINGS) {
|
|
1257
|
+
console.warn("Patchstack: further failing callbacks passed to createProtection are not reported in this process.");
|
|
1258
|
+
return;
|
|
1133
1259
|
}
|
|
1134
|
-
|
|
1260
|
+
console.warn(
|
|
1261
|
+
`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)}`
|
|
1262
|
+
);
|
|
1263
|
+
} catch {
|
|
1135
1264
|
}
|
|
1136
|
-
return value;
|
|
1137
1265
|
}
|
|
1138
1266
|
|
|
1139
1267
|
// src/protect/engine/engine.js
|
|
@@ -1235,6 +1363,184 @@ function hasAdjacentUnbounded(body, flags) {
|
|
|
1235
1363
|
}
|
|
1236
1364
|
return false;
|
|
1237
1365
|
}
|
|
1366
|
+
function parseRepetition(body) {
|
|
1367
|
+
let i = 0;
|
|
1368
|
+
const parseAlternation = () => {
|
|
1369
|
+
const branches = [parseSequence()];
|
|
1370
|
+
while (body[i] === "|") {
|
|
1371
|
+
i++;
|
|
1372
|
+
branches.push(parseSequence());
|
|
1373
|
+
}
|
|
1374
|
+
return branches.length === 1 ? branches[0] : { type: "alt", branches };
|
|
1375
|
+
};
|
|
1376
|
+
const parseSequence = () => {
|
|
1377
|
+
const items = [];
|
|
1378
|
+
while (i < body.length && body[i] !== "|" && body[i] !== ")") {
|
|
1379
|
+
const atom = parseAtom();
|
|
1380
|
+
if (atom === null) return null;
|
|
1381
|
+
const quantified = parseQuantifier(atom);
|
|
1382
|
+
if (quantified === null) return null;
|
|
1383
|
+
items.push(quantified);
|
|
1384
|
+
}
|
|
1385
|
+
return { type: "seq", items };
|
|
1386
|
+
};
|
|
1387
|
+
const parseAtom = () => {
|
|
1388
|
+
const ch = body[i];
|
|
1389
|
+
if (ch === "(") {
|
|
1390
|
+
const start2 = i;
|
|
1391
|
+
i++;
|
|
1392
|
+
let kind = "capture";
|
|
1393
|
+
if (body.startsWith("?:", i)) {
|
|
1394
|
+
kind = "group";
|
|
1395
|
+
i += 2;
|
|
1396
|
+
} else if (body.startsWith("?=", i) || body.startsWith("?!", i)) {
|
|
1397
|
+
kind = "look";
|
|
1398
|
+
i += 2;
|
|
1399
|
+
} else if (body.startsWith("?<=", i) || body.startsWith("?<!", i)) {
|
|
1400
|
+
kind = "look";
|
|
1401
|
+
i += 3;
|
|
1402
|
+
} else if (body.startsWith("?<", i)) {
|
|
1403
|
+
const close = body.indexOf(">", i + 2);
|
|
1404
|
+
if (close === -1) return null;
|
|
1405
|
+
i = close + 1;
|
|
1406
|
+
} else if (body[i] === "?") {
|
|
1407
|
+
return null;
|
|
1408
|
+
}
|
|
1409
|
+
const inner = parseAlternation();
|
|
1410
|
+
if (inner === null || body[i] !== ")") return null;
|
|
1411
|
+
i++;
|
|
1412
|
+
return { type: "group", kind, body: inner, source: body.slice(start2, i) };
|
|
1413
|
+
}
|
|
1414
|
+
const start = i;
|
|
1415
|
+
if (ch === "[") {
|
|
1416
|
+
i++;
|
|
1417
|
+
if (body[i] === "^") i++;
|
|
1418
|
+
if (body[i] === "]") i++;
|
|
1419
|
+
while (i < body.length && body[i] !== "]") i += body[i] === "\\" ? 2 : 1;
|
|
1420
|
+
if (body[i] !== "]") return null;
|
|
1421
|
+
i++;
|
|
1422
|
+
return { type: "atom", source: body.slice(start, i) };
|
|
1423
|
+
}
|
|
1424
|
+
if (ch === "\\") {
|
|
1425
|
+
const next = body[i + 1];
|
|
1426
|
+
if (next === void 0) return null;
|
|
1427
|
+
if ((next === "p" || next === "P" || next === "u") && body[i + 2] === "{") {
|
|
1428
|
+
const close = body.indexOf("}", i + 3);
|
|
1429
|
+
if (close === -1) return null;
|
|
1430
|
+
i = close + 1;
|
|
1431
|
+
} else if (next === "k" && body[i + 2] === "<") {
|
|
1432
|
+
const close = body.indexOf(">", i + 3);
|
|
1433
|
+
if (close === -1) return null;
|
|
1434
|
+
i = close + 1;
|
|
1435
|
+
} else {
|
|
1436
|
+
i += 2;
|
|
1437
|
+
}
|
|
1438
|
+
return { type: /[bB]/.test(next) ? "assert" : "atom", source: body.slice(start, i) };
|
|
1439
|
+
}
|
|
1440
|
+
if (ch === "^" || ch === "$") {
|
|
1441
|
+
i++;
|
|
1442
|
+
return { type: "assert" };
|
|
1443
|
+
}
|
|
1444
|
+
if (ch === "*" || ch === "+" || ch === "?") return null;
|
|
1445
|
+
i++;
|
|
1446
|
+
return { type: "atom", source: body.slice(start, i) };
|
|
1447
|
+
};
|
|
1448
|
+
const parseQuantifier = (atom) => {
|
|
1449
|
+
let min;
|
|
1450
|
+
let max;
|
|
1451
|
+
const ch = body[i];
|
|
1452
|
+
if (ch === "*") [min, max, i] = [0, Infinity, i + 1];
|
|
1453
|
+
else if (ch === "+") [min, max, i] = [1, Infinity, i + 1];
|
|
1454
|
+
else if (ch === "?") [min, max, i] = [0, 1, i + 1];
|
|
1455
|
+
else if (ch === "{") {
|
|
1456
|
+
const bounds = /^\{(\d+)(,(\d*))?\}/.exec(body.slice(i));
|
|
1457
|
+
if (!bounds) return atom;
|
|
1458
|
+
min = Number(bounds[1]);
|
|
1459
|
+
max = bounds[2] === void 0 ? min : bounds[3] === "" ? Infinity : Number(bounds[3]);
|
|
1460
|
+
i += bounds[0].length;
|
|
1461
|
+
} else {
|
|
1462
|
+
return atom;
|
|
1463
|
+
}
|
|
1464
|
+
if (body[i] === "?") i++;
|
|
1465
|
+
if (body[i] === "*" || body[i] === "+" || body[i] === "?" || body[i] === "{") return null;
|
|
1466
|
+
return { type: "quant", min, max, body: atom };
|
|
1467
|
+
};
|
|
1468
|
+
const root = parseAlternation();
|
|
1469
|
+
return root !== null && i === body.length ? root : null;
|
|
1470
|
+
}
|
|
1471
|
+
function* nodesOf(node) {
|
|
1472
|
+
if (!node) return;
|
|
1473
|
+
yield node;
|
|
1474
|
+
if (node.type === "alt") for (const branch of node.branches) yield* nodesOf(branch);
|
|
1475
|
+
else if (node.type === "seq") for (const item of node.items) yield* nodesOf(item);
|
|
1476
|
+
else if (node.type === "group" || node.type === "quant") yield* nodesOf(node.body);
|
|
1477
|
+
}
|
|
1478
|
+
var BOUNDED_NESTING_LIMIT = 100;
|
|
1479
|
+
var BOUNDED_ALTERNATION_LIMIT = 16;
|
|
1480
|
+
var OPTIONAL_REPEAT_LIMIT = 8;
|
|
1481
|
+
function hasFencedRepetitions(quant, flags) {
|
|
1482
|
+
let body = quant.body;
|
|
1483
|
+
if (body.type === "group" && body.kind !== "look") body = body.body;
|
|
1484
|
+
if (body.type !== "seq" || body.items.length < 2) return false;
|
|
1485
|
+
const variable = [...nodesOf(body)].filter((n) => n.type === "quant" && (n.max > 1 || n.min < n.max));
|
|
1486
|
+
if (variable.some((n) => n.body.type !== "atom" || n.body.source === void 0)) return false;
|
|
1487
|
+
const atomFlags = flags.replace(/[^i]/g, "");
|
|
1488
|
+
const unicode = /[uv]/.test(flags);
|
|
1489
|
+
let insides;
|
|
1490
|
+
try {
|
|
1491
|
+
insides = variable.map((n) => new RegExp(`^(?:${n.body.source})$`, `${atomFlags}${unicode ? "u" : ""}`));
|
|
1492
|
+
} catch {
|
|
1493
|
+
return false;
|
|
1494
|
+
}
|
|
1495
|
+
const literalOf = (source) => {
|
|
1496
|
+
if (source.length === 1 && !".[]()|^$\\".includes(source)) return source;
|
|
1497
|
+
if (source.length === 2 && source[0] === "\\" && /[^\w\s]/.test(source[1])) return source[1];
|
|
1498
|
+
return null;
|
|
1499
|
+
};
|
|
1500
|
+
const disjoint = (edge) => {
|
|
1501
|
+
if (edge?.type !== "atom" || edge.source === void 0) return false;
|
|
1502
|
+
const literal = literalOf(edge.source);
|
|
1503
|
+
if (literal !== null) return insides.every((re) => !re.test(literal));
|
|
1504
|
+
if (unicode) return false;
|
|
1505
|
+
let fence;
|
|
1506
|
+
try {
|
|
1507
|
+
fence = new RegExp(`^(?:${edge.source})$`, atomFlags);
|
|
1508
|
+
} catch {
|
|
1509
|
+
return false;
|
|
1510
|
+
}
|
|
1511
|
+
for (let unit = 0; unit <= 65535; unit++) {
|
|
1512
|
+
const ch = String.fromCharCode(unit);
|
|
1513
|
+
if (fence.test(ch) && insides.some((re) => re.test(ch))) return false;
|
|
1514
|
+
}
|
|
1515
|
+
return true;
|
|
1516
|
+
};
|
|
1517
|
+
return disjoint(body.items[0]) || disjoint(body.items[body.items.length - 1]);
|
|
1518
|
+
}
|
|
1519
|
+
function hasCatastrophicShape(body, flags) {
|
|
1520
|
+
const root = parseRepetition(body);
|
|
1521
|
+
if (root === null) return null;
|
|
1522
|
+
for (const node of nodesOf(root)) {
|
|
1523
|
+
if (node.type === "group" && node.kind === "look" && hasAdjacentUnbounded(lookaroundBody(node.source), flags)) return true;
|
|
1524
|
+
if (node.type !== "quant" || node.max <= 1) continue;
|
|
1525
|
+
const fenced = hasFencedRepetitions(node, flags);
|
|
1526
|
+
for (const inner of nodesOf(node.body)) {
|
|
1527
|
+
if (inner === node.body && inner.type === "quant") continue;
|
|
1528
|
+
if (inner.type === "quant" && inner.max > 1 && !fenced) {
|
|
1529
|
+
if (node.max === Infinity || inner.max === Infinity || node.max * inner.max > BOUNDED_NESTING_LIMIT) return true;
|
|
1530
|
+
}
|
|
1531
|
+
if (inner.type === "quant" && inner.min === 0 && inner.max <= 1 && !fenced && (node.max === Infinity || node.max > OPTIONAL_REPEAT_LIMIT)) {
|
|
1532
|
+
return true;
|
|
1533
|
+
}
|
|
1534
|
+
if (inner.type === "alt" && inner.branches.length > 1 && (node.max === Infinity || node.max > BOUNDED_ALTERNATION_LIMIT)) {
|
|
1535
|
+
return true;
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
return false;
|
|
1540
|
+
}
|
|
1541
|
+
function lookaroundBody(source) {
|
|
1542
|
+
return source.slice(source.startsWith("(?<") ? 4 : 3, -1);
|
|
1543
|
+
}
|
|
1238
1544
|
function groupEnd(body, start) {
|
|
1239
1545
|
let depth = 0;
|
|
1240
1546
|
let inClass = false;
|
|
@@ -1261,6 +1567,8 @@ function warnRejectedPatternOnce(pattern) {
|
|
|
1261
1567
|
);
|
|
1262
1568
|
}
|
|
1263
1569
|
var MAX_PATTERN_LENGTH = 1e3;
|
|
1570
|
+
var regexVerdicts = /* @__PURE__ */ new Map();
|
|
1571
|
+
var REGEX_VERDICT_LIMIT = 2048;
|
|
1264
1572
|
function safeRegExp(pattern) {
|
|
1265
1573
|
if (!pattern) {
|
|
1266
1574
|
return null;
|
|
@@ -1272,16 +1580,32 @@ function safeRegExp(pattern) {
|
|
|
1272
1580
|
if (!match) {
|
|
1273
1581
|
return null;
|
|
1274
1582
|
}
|
|
1275
|
-
|
|
1276
|
-
|
|
1583
|
+
let safe = regexVerdicts.get(pattern);
|
|
1584
|
+
if (safe === void 0) {
|
|
1585
|
+
safe = isSafeBody(match[1], match[2]);
|
|
1586
|
+
if (regexVerdicts.size >= REGEX_VERDICT_LIMIT) regexVerdicts.clear();
|
|
1587
|
+
regexVerdicts.set(pattern, safe);
|
|
1277
1588
|
}
|
|
1278
|
-
if (
|
|
1589
|
+
if (!safe) return null;
|
|
1279
1590
|
try {
|
|
1280
1591
|
return new RegExp(match[1], match[2]);
|
|
1281
1592
|
} catch {
|
|
1282
1593
|
return null;
|
|
1283
1594
|
}
|
|
1284
1595
|
}
|
|
1596
|
+
function isSafeBody(body, flags) {
|
|
1597
|
+
for (const dangerous of REDOS_PATTERNS) {
|
|
1598
|
+
if (dangerous.test(body)) return false;
|
|
1599
|
+
}
|
|
1600
|
+
if (hasAdjacentUnbounded(body, flags)) return false;
|
|
1601
|
+
if (hasCatastrophicShape(body, flags) === true) return false;
|
|
1602
|
+
try {
|
|
1603
|
+
new RegExp(body, flags);
|
|
1604
|
+
} catch {
|
|
1605
|
+
return false;
|
|
1606
|
+
}
|
|
1607
|
+
return true;
|
|
1608
|
+
}
|
|
1285
1609
|
function ctypeResult(strValue, isClass, matchVal) {
|
|
1286
1610
|
if (strValue === "") {
|
|
1287
1611
|
return false;
|
|
@@ -1325,17 +1649,21 @@ function arrayKeyValue(value, matchObj) {
|
|
|
1325
1649
|
}
|
|
1326
1650
|
var WHOLE_VALUE_MATCH_TYPES = /* @__PURE__ */ new Set(["isset", "array_in_array", "array_key_value"]);
|
|
1327
1651
|
function collectLeafValues(root, nodeCap = 2e4, maxDepth = 1e3) {
|
|
1328
|
-
const
|
|
1652
|
+
const leaves = [];
|
|
1329
1653
|
const stack = [[root, 0]];
|
|
1330
1654
|
let visited = 0;
|
|
1655
|
+
let truncated = false;
|
|
1331
1656
|
while (stack.length) {
|
|
1332
1657
|
const [node, depth] = stack.pop();
|
|
1333
1658
|
if (node === null || node === void 0) continue;
|
|
1334
1659
|
if (typeof node !== "object") {
|
|
1335
|
-
|
|
1660
|
+
leaves.push(node);
|
|
1661
|
+
continue;
|
|
1662
|
+
}
|
|
1663
|
+
if (depth >= maxDepth || visited >= nodeCap) {
|
|
1664
|
+
truncated = true;
|
|
1336
1665
|
continue;
|
|
1337
1666
|
}
|
|
1338
|
-
if (depth >= maxDepth || visited >= nodeCap) continue;
|
|
1339
1667
|
visited++;
|
|
1340
1668
|
if (Array.isArray(node)) {
|
|
1341
1669
|
for (let i = node.length - 1; i >= 0; i--) stack.push([node[i], depth + 1]);
|
|
@@ -1343,7 +1671,18 @@ function collectLeafValues(root, nodeCap = 2e4, maxDepth = 1e3) {
|
|
|
1343
1671
|
for (const k of Object.keys(node)) stack.push([node[k], depth + 1]);
|
|
1344
1672
|
}
|
|
1345
1673
|
}
|
|
1346
|
-
return
|
|
1674
|
+
return { leaves, truncated };
|
|
1675
|
+
}
|
|
1676
|
+
function skipsOf(resolver) {
|
|
1677
|
+
const skips = resolver?.skips;
|
|
1678
|
+
return skips && skips.length > 0 ? { skips } : {};
|
|
1679
|
+
}
|
|
1680
|
+
function serialisedValue(value) {
|
|
1681
|
+
try {
|
|
1682
|
+
return JSON.stringify(value);
|
|
1683
|
+
} catch {
|
|
1684
|
+
return void 0;
|
|
1685
|
+
}
|
|
1347
1686
|
}
|
|
1348
1687
|
var warnedKeys = /* @__PURE__ */ new Set();
|
|
1349
1688
|
function warnOnce3(key, message) {
|
|
@@ -1361,12 +1700,22 @@ function warnUnsupportedMatchType(type) {
|
|
|
1361
1700
|
`[patchstack] Unsupported rule_v2 match type "${type}" \u2014 condition treated as no-match. Rules relying on it are not enforced by this engine.`
|
|
1362
1701
|
);
|
|
1363
1702
|
}
|
|
1703
|
+
var SPECIAL_SCHEMES = /* @__PURE__ */ new Set(["http", "https", "ws", "wss", "ftp", "file"]);
|
|
1704
|
+
function trimControls(text) {
|
|
1705
|
+
let start = 0;
|
|
1706
|
+
let end = text.length;
|
|
1707
|
+
while (start < end && text.charCodeAt(start) <= 32) start++;
|
|
1708
|
+
while (end > start && text.charCodeAt(end - 1) <= 32) end--;
|
|
1709
|
+
return text.slice(start, end);
|
|
1710
|
+
}
|
|
1364
1711
|
function hostFromValue(value) {
|
|
1365
|
-
const raw = String(value ?? "").trim();
|
|
1712
|
+
const raw = trimControls(String(value ?? "").replace(/[\t\n\r]/g, "")).trim();
|
|
1366
1713
|
if (raw === "") return "";
|
|
1367
|
-
|
|
1714
|
+
const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(raw)?.[1]?.toLowerCase();
|
|
1715
|
+
const relative = /^[\\/]{2}/.test(raw);
|
|
1716
|
+
if (relative || /^[a-z][a-z0-9+.-]*:\/\//i.test(raw) || scheme !== void 0 && SPECIAL_SCHEMES.has(scheme)) {
|
|
1368
1717
|
try {
|
|
1369
|
-
return new URL(
|
|
1718
|
+
return new URL(relative ? `http:${raw}` : raw).hostname;
|
|
1370
1719
|
} catch {
|
|
1371
1720
|
return raw;
|
|
1372
1721
|
}
|
|
@@ -1394,10 +1743,18 @@ function isInternalHost(hostname) {
|
|
|
1394
1743
|
const allZeroHi = g[0] === 0 && g[1] === 0 && g[2] === 0 && g[3] === 0 && g[4] === 0;
|
|
1395
1744
|
if (allZeroHi && g[5] === 0 && g[6] === 0 && (g[7] === 0 || g[7] === 1)) return true;
|
|
1396
1745
|
if ((g[0] & 65472) === 65152) return true;
|
|
1746
|
+
if ((g[0] & 65472) === 65216) return true;
|
|
1397
1747
|
if ((g[0] & 65024) === 64512) return true;
|
|
1748
|
+
if ((g[0] & 65280) === 65280) return true;
|
|
1749
|
+
const low32 = (g[6] << 16 >>> 0 | g[7]) >>> 0;
|
|
1398
1750
|
if (allZeroHi && (g[5] === 65535 || g[5] === 0)) {
|
|
1399
|
-
return isPrivateV4Int(
|
|
1751
|
+
return isPrivateV4Int(low32);
|
|
1400
1752
|
}
|
|
1753
|
+
if (g[0] === 100 && g[1] === 65435 && g[2] === 0 && g[3] === 0 && g[4] === 0 && g[5] === 0) {
|
|
1754
|
+
return isPrivateV4Int(low32);
|
|
1755
|
+
}
|
|
1756
|
+
if (g[0] === 100 && g[1] === 65435 && g[2] === 1) return true;
|
|
1757
|
+
if (g[0] === 8194) return isPrivateV4Int((g[1] << 16 >>> 0 | g[2]) >>> 0);
|
|
1401
1758
|
return false;
|
|
1402
1759
|
}
|
|
1403
1760
|
const v4 = parseIPv4ToInt(host);
|
|
@@ -1407,6 +1764,10 @@ function isInternalHost(hostname) {
|
|
|
1407
1764
|
function isPrivateV4Int(n) {
|
|
1408
1765
|
const a = n >>> 24 & 255;
|
|
1409
1766
|
const b = n >>> 16 & 255;
|
|
1767
|
+
const c = n >>> 8 & 255;
|
|
1768
|
+
if (a >= 224) return true;
|
|
1769
|
+
if (a === 192 && b === 0 && c === 0) return true;
|
|
1770
|
+
if (a === 198 && (b === 18 || b === 19)) return true;
|
|
1410
1771
|
if (a === 127 || a === 10 || a === 0) return true;
|
|
1411
1772
|
if (a === 169 && b === 254) return true;
|
|
1412
1773
|
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
@@ -1755,8 +2116,12 @@ var RuleEngine = class {
|
|
|
1755
2116
|
let normalizedReq;
|
|
1756
2117
|
let resolver;
|
|
1757
2118
|
try {
|
|
1758
|
-
|
|
2119
|
+
let limited = false;
|
|
2120
|
+
normalizedReq = { ...req, ...normalizeRequest(req, { onLimit: () => {
|
|
2121
|
+
limited = true;
|
|
2122
|
+
} }) };
|
|
1759
2123
|
resolver = new RequestResolver(normalizedReq);
|
|
2124
|
+
if (limited) resolver.noteSkip("container-cap");
|
|
1760
2125
|
} catch (err) {
|
|
1761
2126
|
this.#reportError(err);
|
|
1762
2127
|
return { blocked: false, rule: null, message: null };
|
|
@@ -1782,14 +2147,15 @@ var RuleEngine = class {
|
|
|
1782
2147
|
// that re-read the request instead would be reading it a second time: a getter, a stream or
|
|
1783
2148
|
// anything else that answers once can give a different value, and evidence that disagrees
|
|
1784
2149
|
// with the match it belongs to is worse than none.
|
|
1785
|
-
resolver
|
|
2150
|
+
resolver,
|
|
2151
|
+
...skipsOf(resolver)
|
|
1786
2152
|
};
|
|
1787
2153
|
}
|
|
1788
2154
|
} catch (err) {
|
|
1789
2155
|
this.#reportError(err);
|
|
1790
2156
|
}
|
|
1791
2157
|
}
|
|
1792
|
-
return { blocked: false, rule: null, message: null };
|
|
2158
|
+
return { blocked: false, rule: null, message: null, ...skipsOf(resolver) };
|
|
1793
2159
|
}
|
|
1794
2160
|
#evaluateRule(conditions, resolver) {
|
|
1795
2161
|
const inclusiveConditions = conditions.filter((c) => c.inclusive);
|
|
@@ -1845,9 +2211,15 @@ var RuleEngine = class {
|
|
|
1845
2211
|
if (WHOLE_VALUE_MATCH_TYPES.has(match.type)) {
|
|
1846
2212
|
if (matchValue(match.type, value, match.value, match)) return true;
|
|
1847
2213
|
} else {
|
|
1848
|
-
|
|
2214
|
+
const { leaves, truncated } = collectLeafValues(value);
|
|
2215
|
+
for (const leaf of leaves) {
|
|
1849
2216
|
if (matchValue(match.type, leaf, match.value, match)) return true;
|
|
1850
2217
|
}
|
|
2218
|
+
if (truncated) {
|
|
2219
|
+
resolver.noteSkip("container-cap");
|
|
2220
|
+
const text = serialisedValue(value);
|
|
2221
|
+
if (text !== void 0 && matchValue(match.type, text, match.value, match)) return true;
|
|
2222
|
+
}
|
|
1851
2223
|
}
|
|
1852
2224
|
continue;
|
|
1853
2225
|
}
|
|
@@ -2234,6 +2606,9 @@ function conditionShapeProblem(condition) {
|
|
|
2234
2606
|
if (nested && !named) {
|
|
2235
2607
|
return `condition carries nested rules but its parameter is ${JSON.stringify(condition.parameter)}; a group must be {"parameter":"${GROUP_PARAMETER}"}`;
|
|
2236
2608
|
}
|
|
2609
|
+
if (condition.inclusive !== void 0 && typeof condition.inclusive !== "boolean") {
|
|
2610
|
+
return "inclusive must be true or false";
|
|
2611
|
+
}
|
|
2237
2612
|
if (named && nested) {
|
|
2238
2613
|
if (condition.match !== void 0) return "a group carries no match of its own; the engine ignores it";
|
|
2239
2614
|
if (condition.mutations !== void 0) return "a group carries no mutations of its own; the engine ignores them";
|
|
@@ -3176,7 +3551,7 @@ async function fromFetchRequest(request, options = {}) {
|
|
|
3176
3551
|
// that guessed differently would attribute one request to two addresses.
|
|
3177
3552
|
ip: client.ip ?? "",
|
|
3178
3553
|
_clientIp: client,
|
|
3179
|
-
cookies:
|
|
3554
|
+
cookies: parseCookieHeader(headers.cookie),
|
|
3180
3555
|
// Verbatim body text: preserves literal keys (e.g. `__proto__`) that JSON.stringify
|
|
3181
3556
|
// drops, so prototype-pollution rules on `raw` are robust.
|
|
3182
3557
|
_rawBody: rawBody,
|
|
@@ -3238,7 +3613,7 @@ function concatChunks(chunks, total) {
|
|
|
3238
3613
|
}
|
|
3239
3614
|
function parseBody(rawBody, contentType) {
|
|
3240
3615
|
const ct = String(contentType || "").toLowerCase();
|
|
3241
|
-
const
|
|
3616
|
+
const isJson2 = ct.includes("application/json") || /\+json\b/.test(ct);
|
|
3242
3617
|
const isForm = ct.includes("application/x-www-form-urlencoded");
|
|
3243
3618
|
const isMultipart = ct.includes("multipart/form-data");
|
|
3244
3619
|
const isAmbiguous = ct === "" || ct.startsWith("text/plain") || ct.includes("csp-report") || ct.includes("/json");
|
|
@@ -3252,7 +3627,7 @@ function parseBody(rawBody, contentType) {
|
|
|
3252
3627
|
for (const [k, v] of new URLSearchParams(rawBody)) appendOwn(body, k, v);
|
|
3253
3628
|
return { body, files: void 0 };
|
|
3254
3629
|
}
|
|
3255
|
-
if (
|
|
3630
|
+
if (isJson2 || isAmbiguous) {
|
|
3256
3631
|
try {
|
|
3257
3632
|
const parsed = JSON.parse(rawBody);
|
|
3258
3633
|
if (parsed && typeof parsed === "object") return { body: parsed, files: void 0 };
|
|
@@ -3268,38 +3643,115 @@ function parseMultipart(rawBody, boundary) {
|
|
|
3268
3643
|
const sep = /\r?\n\r?\n/.exec(part);
|
|
3269
3644
|
if (!sep) continue;
|
|
3270
3645
|
const rawHeaders = part.slice(0, sep.index);
|
|
3271
|
-
const disposition = /content-disposition:[^\r\n]
|
|
3272
|
-
if (
|
|
3273
|
-
const
|
|
3274
|
-
if (
|
|
3646
|
+
const disposition = /content-disposition:([^\r\n]*)/i.exec(rawHeaders)?.[1];
|
|
3647
|
+
if (disposition === void 0) continue;
|
|
3648
|
+
const { names, filename } = dispositionFields(disposition);
|
|
3649
|
+
if (names.length === 0) continue;
|
|
3275
3650
|
const content = part.slice(sep.index + sep[0].length).replace(/\r?\n$/, "");
|
|
3276
|
-
const
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
appendOwn(files, name, file);
|
|
3281
|
-
} else {
|
|
3282
|
-
appendOwn(body, name, content);
|
|
3651
|
+
const file = filename === void 0 ? void 0 : { filename, type: /content-type:\s*([^\r\n;]+)/i.exec(rawHeaders)?.[1]?.trim() || "", content };
|
|
3652
|
+
for (const name of names) {
|
|
3653
|
+
if (file) appendOwn(files, name, file);
|
|
3654
|
+
else appendOwn(body, name, content);
|
|
3283
3655
|
}
|
|
3284
3656
|
}
|
|
3285
3657
|
return { body, files };
|
|
3286
3658
|
}
|
|
3287
|
-
function
|
|
3288
|
-
const
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3659
|
+
function dispositionFields(value) {
|
|
3660
|
+
const text = String(value);
|
|
3661
|
+
const names = /* @__PURE__ */ new Set();
|
|
3662
|
+
let filename;
|
|
3663
|
+
let extendedFilename;
|
|
3664
|
+
let i = text.indexOf(";");
|
|
3665
|
+
while (i !== -1 && i < text.length) {
|
|
3666
|
+
i++;
|
|
3667
|
+
while (text[i] === " " || text[i] === " ") i++;
|
|
3668
|
+
let key = "";
|
|
3669
|
+
while (i < text.length && text[i] !== "=" && text[i] !== ";") key += text[i++];
|
|
3670
|
+
key = key.trim().toLowerCase();
|
|
3671
|
+
let val = null;
|
|
3672
|
+
if (text[i] === "=") {
|
|
3673
|
+
i++;
|
|
3674
|
+
while (text[i] === " " || text[i] === " ") i++;
|
|
3675
|
+
if (text[i] === '"') {
|
|
3676
|
+
i++;
|
|
3677
|
+
val = "";
|
|
3678
|
+
while (i < text.length && text[i] !== '"') {
|
|
3679
|
+
if (text[i] === "\\" && i + 1 < text.length) i++;
|
|
3680
|
+
val += text[i++];
|
|
3681
|
+
}
|
|
3682
|
+
i++;
|
|
3683
|
+
while (i < text.length && text[i] !== ";") i++;
|
|
3684
|
+
} else {
|
|
3685
|
+
let bare = "";
|
|
3686
|
+
while (i < text.length && text[i] !== ";") bare += text[i++];
|
|
3687
|
+
val = bare.trim();
|
|
3688
|
+
}
|
|
3689
|
+
}
|
|
3690
|
+
if (val !== null) {
|
|
3691
|
+
if (key === "name") names.add(val);
|
|
3692
|
+
else if (key === "filename") filename = val;
|
|
3693
|
+
else if (key === "filename*") extendedFilename = decodeExtendedValue(val);
|
|
3296
3694
|
}
|
|
3297
|
-
|
|
3695
|
+
i = i < text.length && text[i] === ";" ? i : -1;
|
|
3696
|
+
}
|
|
3697
|
+
return { names: [...names], filename: extendedFilename ?? filename };
|
|
3698
|
+
}
|
|
3699
|
+
function decodeExtendedValue(value) {
|
|
3700
|
+
const match = /^([^']*)'[^']*'(.*)$/.exec(value);
|
|
3701
|
+
if (!match) return value;
|
|
3702
|
+
try {
|
|
3703
|
+
return decodeURIComponent(match[2]);
|
|
3704
|
+
} catch {
|
|
3705
|
+
return match[2];
|
|
3298
3706
|
}
|
|
3299
|
-
return cookies;
|
|
3300
3707
|
}
|
|
3301
3708
|
|
|
3302
3709
|
// src/protect/engine/node.js
|
|
3710
|
+
function readBodyPrefix(req, maxBytes, done) {
|
|
3711
|
+
const chunks = [];
|
|
3712
|
+
let retained = 0;
|
|
3713
|
+
let size = 0;
|
|
3714
|
+
let failed = false;
|
|
3715
|
+
let finished = false;
|
|
3716
|
+
const finish = (error, read) => {
|
|
3717
|
+
if (finished) return;
|
|
3718
|
+
finished = true;
|
|
3719
|
+
done(error, read);
|
|
3720
|
+
};
|
|
3721
|
+
req.on("data", (chunk) => {
|
|
3722
|
+
if (failed) return;
|
|
3723
|
+
try {
|
|
3724
|
+
const bytes = typeof chunk === "string" ? Buffer.from(chunk, req.readableEncoding || "utf8") : chunk;
|
|
3725
|
+
size += bytes.length;
|
|
3726
|
+
const take = Math.min(bytes.length, maxBytes - retained);
|
|
3727
|
+
if (take <= 0) return;
|
|
3728
|
+
chunks.push(take === bytes.length ? bytes : bytes.subarray(0, take));
|
|
3729
|
+
retained += take;
|
|
3730
|
+
} catch {
|
|
3731
|
+
failed = true;
|
|
3732
|
+
chunks.length = 0;
|
|
3733
|
+
}
|
|
3734
|
+
});
|
|
3735
|
+
req.on("error", (error) => finish(error));
|
|
3736
|
+
req.on("end", () => {
|
|
3737
|
+
if (failed) {
|
|
3738
|
+
finish(null, { text: "", overflow: false, size, failed: true });
|
|
3739
|
+
return;
|
|
3740
|
+
}
|
|
3741
|
+
const prefix = Buffer.concat(chunks);
|
|
3742
|
+
const overflow = size > maxBytes;
|
|
3743
|
+
const text = (overflow ? prefix.subarray(0, completeUtf8Length(prefix)) : prefix).toString("utf8");
|
|
3744
|
+
finish(null, { text, overflow, size, failed: false });
|
|
3745
|
+
});
|
|
3746
|
+
}
|
|
3747
|
+
function completeUtf8Length(bytes) {
|
|
3748
|
+
let start = bytes.length - 1;
|
|
3749
|
+
while (start >= 0 && bytes.length - start <= 3 && (bytes[start] & 192) === 128) start--;
|
|
3750
|
+
if (start < 0) return bytes.length;
|
|
3751
|
+
const lead = bytes[start];
|
|
3752
|
+
const needed = lead >= 240 ? 4 : lead >= 224 ? 3 : lead >= 192 ? 2 : 1;
|
|
3753
|
+
return bytes.length - start < needed ? start : bytes.length;
|
|
3754
|
+
}
|
|
3303
3755
|
function fromNodeRequest(req, rawBody = "", options = {}) {
|
|
3304
3756
|
const method = (req.method || "GET").toUpperCase();
|
|
3305
3757
|
const headers = {};
|
|
@@ -3345,29 +3797,41 @@ function fromNodeRequest(req, rawBody = "", options = {}) {
|
|
|
3345
3797
|
headers,
|
|
3346
3798
|
ip: client.ip ?? "",
|
|
3347
3799
|
_clientIp: client,
|
|
3348
|
-
cookies:
|
|
3800
|
+
cookies: parseCookieHeader(headers.cookie),
|
|
3349
3801
|
// Verbatim body text: preserves literal keys (e.g. `__proto__`) that JSON.stringify drops.
|
|
3350
3802
|
_rawBody: rawBody
|
|
3351
3803
|
};
|
|
3352
3804
|
}
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3805
|
+
|
|
3806
|
+
// src/protect/egress.js
|
|
3807
|
+
var REGISTRY = /* @__PURE__ */ Symbol.for("patchstack.connect.egress-guard");
|
|
3808
|
+
function egressRegistry() {
|
|
3809
|
+
const existing = globalThis[REGISTRY];
|
|
3810
|
+
if (existing && existing.screens instanceof Set && existing.surfaces instanceof Map) return existing;
|
|
3811
|
+
const created = { screens: /* @__PURE__ */ new Set(), surfaces: /* @__PURE__ */ new Map() };
|
|
3812
|
+
Object.defineProperty(globalThis, REGISTRY, { value: created, configurable: true, writable: true });
|
|
3813
|
+
return created;
|
|
3814
|
+
}
|
|
3815
|
+
var refusal = (host) => new Error(`Patchstack blocked an outbound request to a disallowed address: ${host}`);
|
|
3816
|
+
function readableDestination(input, init) {
|
|
3817
|
+
try {
|
|
3818
|
+
const raw = typeof input === "string" || input instanceof URL ? String(input) : input?.url ?? input?.href;
|
|
3819
|
+
if (typeof raw !== "string" && !(raw instanceof URL)) return null;
|
|
3820
|
+
const url = new URL(String(raw)).href;
|
|
3821
|
+
const method = String(init?.method ?? input?.method ?? "GET").toUpperCase();
|
|
3822
|
+
return { url, method };
|
|
3823
|
+
} catch {
|
|
3824
|
+
return null;
|
|
3357
3825
|
}
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
setOwn(cookies, pair.slice(0, idx).trim(), pair.slice(idx + 1).trim());
|
|
3826
|
+
}
|
|
3827
|
+
function anyRefuses(screens, url, host, method) {
|
|
3828
|
+
let refused = false;
|
|
3829
|
+
for (const screen of screens) {
|
|
3830
|
+
if (screen.block(url, host, method)) refused = true;
|
|
3364
3831
|
}
|
|
3365
|
-
return
|
|
3832
|
+
return refused;
|
|
3366
3833
|
}
|
|
3367
|
-
|
|
3368
|
-
// src/protect/egress.js
|
|
3369
3834
|
async function installEgressGuard({ shouldBlock, onBlock, onSkip, dnsScreen = true, lookup, allowHosts } = {}) {
|
|
3370
|
-
const restores = [];
|
|
3371
3835
|
if (typeof shouldBlock !== "function") return () => {
|
|
3372
3836
|
};
|
|
3373
3837
|
const exempt = new Set((allowHosts ?? []).map((h) => String(h).toLowerCase()));
|
|
@@ -3412,6 +3876,9 @@ async function installEgressGuard({ shouldBlock, onBlock, onSkip, dnsScreen = tr
|
|
|
3412
3876
|
resolve(false);
|
|
3413
3877
|
}
|
|
3414
3878
|
});
|
|
3879
|
+
const registry = egressRegistry();
|
|
3880
|
+
const own = { block, prescreen: resolvesToDisallowed, dns: screen, skip };
|
|
3881
|
+
registry.screens.add(own);
|
|
3415
3882
|
const originalFetch = globalThis.fetch;
|
|
3416
3883
|
if (typeof originalFetch === "function" && !originalFetch.__patchstackGuarded) {
|
|
3417
3884
|
const REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
|
|
@@ -3484,15 +3951,19 @@ async function installEgressGuard({ shouldBlock, onBlock, onSkip, dnsScreen = tr
|
|
|
3484
3951
|
} catch {
|
|
3485
3952
|
host = null;
|
|
3486
3953
|
}
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3954
|
+
const screens = [...registry.screens];
|
|
3955
|
+
if (anyRefuses(screens, u, host, method)) throw refusal(host ?? u);
|
|
3956
|
+
const resolved = await Promise.all(screens.map((each) => each.prescreen(u, host, method)));
|
|
3957
|
+
if (resolved.includes(true)) throw refusal(host ?? u);
|
|
3490
3958
|
};
|
|
3491
3959
|
const guarded = async (input, init) => {
|
|
3492
3960
|
let cur;
|
|
3493
3961
|
try {
|
|
3494
3962
|
cur = new Request(input, { ...init || {}, redirect: "manual" });
|
|
3495
3963
|
} catch {
|
|
3964
|
+
const destination = readableDestination(input, init);
|
|
3965
|
+
if (destination) await screenUrl(destination.url, destination.method);
|
|
3966
|
+
else for (const each of registry.screens) each.skip("unrecognised-request", {});
|
|
3496
3967
|
return originalFetch(input, init);
|
|
3497
3968
|
}
|
|
3498
3969
|
const callerRedirect = init && init.redirect || input && input.redirect || "follow";
|
|
@@ -3503,13 +3974,12 @@ async function installEgressGuard({ shouldBlock, onBlock, onSkip, dnsScreen = tr
|
|
|
3503
3974
|
const headers = new Headers(cur.headers);
|
|
3504
3975
|
const signal = cur.signal;
|
|
3505
3976
|
const replay = captureReplayBody(cur);
|
|
3977
|
+
const { body: _callerBody, ...callerOptions } = init || {};
|
|
3506
3978
|
let body;
|
|
3507
3979
|
for (let hop = 0; ; hop++) {
|
|
3508
3980
|
let resp;
|
|
3509
3981
|
try {
|
|
3510
|
-
resp = await originalFetch(
|
|
3511
|
-
hop === 0 ? cur : new Request(url, { method, headers, body, redirect: "manual", signal })
|
|
3512
|
-
);
|
|
3982
|
+
resp = hop === 0 ? await originalFetch(cur, { ...callerOptions, redirect: "manual" }) : await originalFetch(url, { ...callerOptions, method, headers, body, redirect: "manual", signal });
|
|
3513
3983
|
} catch (error) {
|
|
3514
3984
|
replay.cancel();
|
|
3515
3985
|
throw error;
|
|
@@ -3517,7 +3987,7 @@ async function installEgressGuard({ shouldBlock, onBlock, onSkip, dnsScreen = tr
|
|
|
3517
3987
|
const location = REDIRECT_STATUSES.has(resp.status) ? resp.headers.get("location") : null;
|
|
3518
3988
|
if (!location) {
|
|
3519
3989
|
replay.cancel();
|
|
3520
|
-
return resp;
|
|
3990
|
+
return hop === 0 ? resp : markRedirected(resp);
|
|
3521
3991
|
}
|
|
3522
3992
|
await discardResponseBody(resp);
|
|
3523
3993
|
if (hop >= MAX_REDIRECTS) {
|
|
@@ -3550,43 +4020,84 @@ async function installEgressGuard({ shouldBlock, onBlock, onSkip, dnsScreen = tr
|
|
|
3550
4020
|
};
|
|
3551
4021
|
guarded.__patchstackGuarded = true;
|
|
3552
4022
|
globalThis.fetch = guarded;
|
|
3553
|
-
|
|
3554
|
-
|
|
4023
|
+
registry.surfaces.set(guarded, {
|
|
4024
|
+
release() {
|
|
4025
|
+
if (globalThis.fetch !== guarded) return false;
|
|
4026
|
+
globalThis.fetch = originalFetch;
|
|
4027
|
+
return true;
|
|
4028
|
+
}
|
|
3555
4029
|
});
|
|
3556
4030
|
}
|
|
4031
|
+
let syncBuiltins = () => {
|
|
4032
|
+
};
|
|
4033
|
+
try {
|
|
4034
|
+
const { syncBuiltinESMExports } = await Promise.resolve().then(() => __toESM(require_ps_edge_stub(), 1));
|
|
4035
|
+
if (typeof syncBuiltinESMExports === "function") syncBuiltins = syncBuiltinESMExports;
|
|
4036
|
+
} catch {
|
|
4037
|
+
}
|
|
4038
|
+
let patchedAny = false;
|
|
3557
4039
|
for (const moduleName of ["node:http", "node:https"]) {
|
|
3558
4040
|
try {
|
|
3559
4041
|
const mod = await import(moduleName);
|
|
3560
|
-
|
|
3561
|
-
|
|
4042
|
+
if (registry.surfaces.has(moduleName)) continue;
|
|
4043
|
+
const release = patchHttpModule(mod.default ?? mod, registry);
|
|
4044
|
+
if (release) {
|
|
4045
|
+
patchedAny = true;
|
|
4046
|
+
registry.surfaces.set(moduleName, {
|
|
4047
|
+
release() {
|
|
4048
|
+
if (!release()) return false;
|
|
4049
|
+
syncBuiltins();
|
|
4050
|
+
return true;
|
|
4051
|
+
}
|
|
4052
|
+
});
|
|
4053
|
+
}
|
|
3562
4054
|
} catch {
|
|
3563
4055
|
}
|
|
3564
4056
|
}
|
|
4057
|
+
if (patchedAny) syncBuiltins();
|
|
3565
4058
|
return () => {
|
|
3566
|
-
|
|
4059
|
+
registry.screens.delete(own);
|
|
4060
|
+
if (registry.screens.size > 0) return;
|
|
4061
|
+
for (const [name, surface] of registry.surfaces) {
|
|
3567
4062
|
try {
|
|
3568
|
-
|
|
4063
|
+
if (surface.release()) registry.surfaces.delete(name);
|
|
3569
4064
|
} catch {
|
|
3570
4065
|
}
|
|
3571
4066
|
}
|
|
3572
4067
|
};
|
|
3573
4068
|
}
|
|
3574
|
-
function
|
|
4069
|
+
function markRedirected(response) {
|
|
4070
|
+
try {
|
|
4071
|
+
Object.defineProperty(response, "redirected", { value: true, configurable: true });
|
|
4072
|
+
} catch {
|
|
4073
|
+
}
|
|
4074
|
+
return response;
|
|
4075
|
+
}
|
|
4076
|
+
function patchHttpModule(http, registry) {
|
|
3575
4077
|
if (!http || typeof http.request !== "function" || http.__patchstackGuarded) return null;
|
|
3576
4078
|
const originalRequest = http.request;
|
|
3577
4079
|
const originalGet = http.get;
|
|
3578
|
-
const
|
|
4080
|
+
const OriginalClientRequest = typeof http.ClientRequest === "function" ? http.ClientRequest : null;
|
|
4081
|
+
const guardArgs = (args) => {
|
|
3579
4082
|
const target = extractHttpTarget(args);
|
|
3580
|
-
if (target
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
4083
|
+
if (!target) return args;
|
|
4084
|
+
const screens = [...registry.screens];
|
|
4085
|
+
if (anyRefuses(screens, target.url, target.host, target.method)) throw refusal(target.host ?? target.url);
|
|
4086
|
+
const resolving = target.host ? screens.filter((each) => each.dns && each.dns.isIP(target.host) === 0 && !each.dns.isExempt(target.host)) : [];
|
|
4087
|
+
if (resolving.length > 0) {
|
|
4088
|
+
const block = (url, host, method) => anyRefuses(resolving, url, host, method);
|
|
4089
|
+
const skip = (reason, detail) => {
|
|
4090
|
+
for (const each of resolving) each.skip(reason, detail);
|
|
4091
|
+
};
|
|
3584
4092
|
try {
|
|
3585
|
-
|
|
4093
|
+
return withScreeningLookup(args, target, block, resolving[0].dns.lookup, skip);
|
|
3586
4094
|
} catch {
|
|
3587
4095
|
}
|
|
3588
4096
|
}
|
|
3589
|
-
return
|
|
4097
|
+
return args;
|
|
4098
|
+
};
|
|
4099
|
+
const wrap = (original) => function(...args) {
|
|
4100
|
+
return original.apply(this, guardArgs(args));
|
|
3590
4101
|
};
|
|
3591
4102
|
const guardedRequest = wrap(originalRequest);
|
|
3592
4103
|
http.request = guardedRequest;
|
|
@@ -3595,11 +4106,26 @@ function patchHttpModule(http, block, screen, skip) {
|
|
|
3595
4106
|
guardedGet = wrap(originalGet);
|
|
3596
4107
|
http.get = guardedGet;
|
|
3597
4108
|
}
|
|
4109
|
+
let GuardedClientRequest;
|
|
4110
|
+
if (OriginalClientRequest) {
|
|
4111
|
+
GuardedClientRequest = function ClientRequest(...args) {
|
|
4112
|
+
const screened = guardArgs(args);
|
|
4113
|
+
return new.target ? Reflect.construct(OriginalClientRequest, screened, new.target) : Reflect.construct(OriginalClientRequest, screened);
|
|
4114
|
+
};
|
|
4115
|
+
GuardedClientRequest.prototype = OriginalClientRequest.prototype;
|
|
4116
|
+
Object.setPrototypeOf(GuardedClientRequest, OriginalClientRequest);
|
|
4117
|
+
http.ClientRequest = GuardedClientRequest;
|
|
4118
|
+
}
|
|
3598
4119
|
http.__patchstackGuarded = true;
|
|
3599
4120
|
return () => {
|
|
3600
|
-
if (http.request
|
|
3601
|
-
if (guardedGet && http.get
|
|
4121
|
+
if (http.request !== guardedRequest) return false;
|
|
4122
|
+
if (guardedGet && http.get !== guardedGet) return false;
|
|
4123
|
+
if (GuardedClientRequest && http.ClientRequest !== GuardedClientRequest) return false;
|
|
4124
|
+
http.request = originalRequest;
|
|
4125
|
+
if (guardedGet) http.get = originalGet;
|
|
4126
|
+
if (GuardedClientRequest) http.ClientRequest = OriginalClientRequest;
|
|
3602
4127
|
delete http.__patchstackGuarded;
|
|
4128
|
+
return true;
|
|
3603
4129
|
};
|
|
3604
4130
|
}
|
|
3605
4131
|
function extractHttpTarget(args) {
|
|
@@ -3608,19 +4134,27 @@ function extractHttpTarget(args) {
|
|
|
3608
4134
|
if (typeof first === "string" || first instanceof URL) {
|
|
3609
4135
|
const url = new URL(String(first));
|
|
3610
4136
|
const opts = args.find((a) => a && typeof a === "object" && !(a instanceof URL));
|
|
3611
|
-
return { url
|
|
3612
|
-
}
|
|
3613
|
-
if (first && typeof first === "object") {
|
|
3614
|
-
const host = normalizeHost(first.hostname || first.host) || "localhost";
|
|
3615
|
-
const protocol = first.protocol || "http:";
|
|
3616
|
-
const port = first.port ? `:${first.port}` : "";
|
|
3617
|
-
const path = first.path || "/";
|
|
3618
|
-
return { url: `${protocol}//${host}${port}${path}`, host, method: first.method || "GET" };
|
|
4137
|
+
return targetOf({ ...optionsFromUrl(url), ...opts ?? {} });
|
|
3619
4138
|
}
|
|
4139
|
+
if (first && typeof first === "object") return targetOf(first);
|
|
3620
4140
|
} catch {
|
|
3621
4141
|
}
|
|
3622
4142
|
return null;
|
|
3623
4143
|
}
|
|
4144
|
+
function optionsFromUrl(url) {
|
|
4145
|
+
const hostname = url.hostname.startsWith("[") ? url.hostname.slice(1, -1) : url.hostname;
|
|
4146
|
+
const options = { protocol: url.protocol, hostname, path: `${url.pathname || "/"}${url.search}` };
|
|
4147
|
+
if (url.port !== "") options.port = Number(url.port);
|
|
4148
|
+
return options;
|
|
4149
|
+
}
|
|
4150
|
+
function targetOf(options) {
|
|
4151
|
+
const host = normalizeHost(options.hostname || options.host) || "localhost";
|
|
4152
|
+
const protocol = options.protocol || "http:";
|
|
4153
|
+
const port = options.port ? `:${options.port}` : "";
|
|
4154
|
+
const path = options.path || "/";
|
|
4155
|
+
const authority = host.includes(":") ? `[${host}]` : host;
|
|
4156
|
+
return { url: `${protocol}//${authority}${port}${path}`, host, method: options.method || "GET" };
|
|
4157
|
+
}
|
|
3624
4158
|
function normalizeHost(raw) {
|
|
3625
4159
|
const host = String(raw || "").trim();
|
|
3626
4160
|
const bracketed = /^\[([^\]]+)\]/.exec(host);
|
|
@@ -4001,19 +4535,49 @@ async function loadFs() {
|
|
|
4001
4535
|
}
|
|
4002
4536
|
return fsMod;
|
|
4003
4537
|
}
|
|
4538
|
+
async function sourceIdentity(options = {}) {
|
|
4539
|
+
if (typeof options.siteUuid === "string" && options.siteUuid.trim() !== "") {
|
|
4540
|
+
const endpoint = typeof options.pulseRulesUrl === "string" ? options.pulseRulesUrl : "";
|
|
4541
|
+
return `site:${options.siteUuid.trim().toLowerCase()}@${endpoint}`;
|
|
4542
|
+
}
|
|
4543
|
+
if (typeof options.token === "string" && options.token !== "") {
|
|
4544
|
+
const digest = await sha256Hex(options.token);
|
|
4545
|
+
if (digest === null) return null;
|
|
4546
|
+
const endpoint = typeof options.baseUrl === "string" ? options.baseUrl : "";
|
|
4547
|
+
return `token:${digest}@${endpoint}`;
|
|
4548
|
+
}
|
|
4549
|
+
return null;
|
|
4550
|
+
}
|
|
4551
|
+
async function sha256Hex(text) {
|
|
4552
|
+
try {
|
|
4553
|
+
const subtle = globalThis.crypto?.subtle;
|
|
4554
|
+
if (!subtle || typeof TextEncoder === "undefined") return null;
|
|
4555
|
+
const digest = await subtle.digest("SHA-256", new TextEncoder().encode(text));
|
|
4556
|
+
return Array.from(new Uint8Array(digest), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
4557
|
+
} catch {
|
|
4558
|
+
return null;
|
|
4559
|
+
}
|
|
4560
|
+
}
|
|
4004
4561
|
function makeStore(options = {}) {
|
|
4005
4562
|
let mem = null;
|
|
4006
4563
|
const durable = durableTier(options);
|
|
4564
|
+
let identity;
|
|
4565
|
+
const identityOf = () => identity ??= sourceIdentity(options);
|
|
4007
4566
|
return {
|
|
4008
4567
|
async read() {
|
|
4009
4568
|
if (mem) return mem;
|
|
4569
|
+
const source = await identityOf();
|
|
4570
|
+
if (source === null) return null;
|
|
4010
4571
|
const env = await durable.read();
|
|
4011
|
-
if (env)
|
|
4572
|
+
if (!env || env.source !== source) return null;
|
|
4573
|
+
mem = env;
|
|
4012
4574
|
return env;
|
|
4013
4575
|
},
|
|
4014
4576
|
async write(env) {
|
|
4015
|
-
if (env)
|
|
4016
|
-
await
|
|
4577
|
+
if (!env) return;
|
|
4578
|
+
const stamped = { ...env, source: await identityOf() };
|
|
4579
|
+
mem = stamped;
|
|
4580
|
+
await durable.write(stamped);
|
|
4017
4581
|
}
|
|
4018
4582
|
};
|
|
4019
4583
|
}
|
|
@@ -4135,11 +4699,12 @@ function toEnvelope(value) {
|
|
|
4135
4699
|
buildId,
|
|
4136
4700
|
// A confirmation belongs to the presentation stored beside it. A crossed or partially written
|
|
4137
4701
|
// envelope confirms nothing, even when one of its fields happens to name the current map.
|
|
4138
|
-
matchedBuildId: buildId !== null && matched === buildId ? matched : null
|
|
4702
|
+
matchedBuildId: buildId !== null && matched === buildId ? matched : null,
|
|
4703
|
+
source: typeof value.source === "string" ? value.source : null
|
|
4139
4704
|
};
|
|
4140
4705
|
}
|
|
4141
4706
|
if (Array.isArray(value.firewall) || Array.isArray(value.whitelists)) {
|
|
4142
|
-
return { bundle: value, etag: null, buildId: null, matchedBuildId: null };
|
|
4707
|
+
return { bundle: value, etag: null, buildId: null, matchedBuildId: null, source: null };
|
|
4143
4708
|
}
|
|
4144
4709
|
return null;
|
|
4145
4710
|
}
|
|
@@ -4206,13 +4771,17 @@ function withScopedDryRun(bundle, confirmed, reason, options = {}) {
|
|
|
4206
4771
|
...unusable > 0 ? [`${unusable} rule(s) carry an unusable ${BUILD_SCOPE_PROPERTY}`] : [],
|
|
4207
4772
|
...held > unusable ? [reason] : []
|
|
4208
4773
|
];
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
"onError"
|
|
4213
|
-
);
|
|
4774
|
+
const message = `${held} build-scoped rule(s) are detecting only, not blocking: ${explanations.join("; ")}`;
|
|
4775
|
+
if (typeof options.onError === "function") notify(options.onError, new Error(message), "onError");
|
|
4776
|
+
else warnHeldOnce(message);
|
|
4214
4777
|
return { ...bundle, firewall };
|
|
4215
4778
|
}
|
|
4779
|
+
var warnedHeld = /* @__PURE__ */ new Set();
|
|
4780
|
+
function warnHeldOnce(message) {
|
|
4781
|
+
if (warnedHeld.has(message)) return;
|
|
4782
|
+
warnedHeld.add(message);
|
|
4783
|
+
console.warn(`[patchstack] ${message}. Pass { onError } to handle this yourself.`);
|
|
4784
|
+
}
|
|
4216
4785
|
function confirmedBy(res, presented) {
|
|
4217
4786
|
if (presented === null) return null;
|
|
4218
4787
|
const verdict = res?.build;
|
|
@@ -4387,6 +4956,60 @@ function startRefresh(tick, { refreshMs, onError } = {}) {
|
|
|
4387
4956
|
}
|
|
4388
4957
|
};
|
|
4389
4958
|
}
|
|
4959
|
+
var RECOVERY_DELAYS_MS = [5e3, 15e3, 45e3, 12e4, 3e5, 6e5];
|
|
4960
|
+
function startRecovery(tick, { onError } = {}) {
|
|
4961
|
+
let stopped = false;
|
|
4962
|
+
let attempt = 0;
|
|
4963
|
+
let timer = null;
|
|
4964
|
+
const schedule = () => {
|
|
4965
|
+
if (stopped) return;
|
|
4966
|
+
const delay = RECOVERY_DELAYS_MS[Math.min(attempt, RECOVERY_DELAYS_MS.length - 1)];
|
|
4967
|
+
timer = setTimeout(run, delay * (1 - Math.random() * JITTER_FRACTION3));
|
|
4968
|
+
timer?.unref?.();
|
|
4969
|
+
};
|
|
4970
|
+
const run = async () => {
|
|
4971
|
+
if (stopped) return;
|
|
4972
|
+
attempt++;
|
|
4973
|
+
try {
|
|
4974
|
+
const status = await tick();
|
|
4975
|
+
if (!status || status.ok !== false) {
|
|
4976
|
+
stopped = true;
|
|
4977
|
+
return;
|
|
4978
|
+
}
|
|
4979
|
+
} catch (err) {
|
|
4980
|
+
notify(onError, err, "onError");
|
|
4981
|
+
}
|
|
4982
|
+
schedule();
|
|
4983
|
+
};
|
|
4984
|
+
schedule();
|
|
4985
|
+
return {
|
|
4986
|
+
stop() {
|
|
4987
|
+
stopped = true;
|
|
4988
|
+
if (timer) clearTimeout(timer);
|
|
4989
|
+
}
|
|
4990
|
+
};
|
|
4991
|
+
}
|
|
4992
|
+
function serialise(tick) {
|
|
4993
|
+
let running = null;
|
|
4994
|
+
let next = null;
|
|
4995
|
+
const run = () => {
|
|
4996
|
+
if (!running) {
|
|
4997
|
+
running = Promise.resolve().then(() => tick()).finally(() => {
|
|
4998
|
+
running = null;
|
|
4999
|
+
});
|
|
5000
|
+
return running;
|
|
5001
|
+
}
|
|
5002
|
+
if (!next) {
|
|
5003
|
+
next = running.catch(() => {
|
|
5004
|
+
}).then(() => {
|
|
5005
|
+
next = null;
|
|
5006
|
+
return run();
|
|
5007
|
+
});
|
|
5008
|
+
}
|
|
5009
|
+
return next;
|
|
5010
|
+
};
|
|
5011
|
+
return run;
|
|
5012
|
+
}
|
|
4390
5013
|
function makeRefreshHandler(tick, secret) {
|
|
4391
5014
|
let inflight2 = null;
|
|
4392
5015
|
const runOnce = () => {
|
|
@@ -4401,7 +5024,7 @@ function makeRefreshHandler(tick, secret) {
|
|
|
4401
5024
|
return async (request) => {
|
|
4402
5025
|
if (!secret) return new Response("not found", { status: 404 });
|
|
4403
5026
|
const provided = request?.headers?.get?.("x-patchstack-refresh") ?? null;
|
|
4404
|
-
if (provided
|
|
5027
|
+
if (!await sameSecret(provided, secret)) return new Response("forbidden", { status: 403 });
|
|
4405
5028
|
let refreshed = true;
|
|
4406
5029
|
try {
|
|
4407
5030
|
const status = await runOnce();
|
|
@@ -4412,6 +5035,33 @@ function makeRefreshHandler(tick, secret) {
|
|
|
4412
5035
|
return new Response(JSON.stringify({ refreshed }), { status: 200, headers: { "content-type": "application/json" } });
|
|
4413
5036
|
};
|
|
4414
5037
|
}
|
|
5038
|
+
async function sameSecret(provided, secret) {
|
|
5039
|
+
if (typeof provided !== "string" || typeof secret !== "string") return false;
|
|
5040
|
+
const subtle = globalThis.crypto?.subtle;
|
|
5041
|
+
if (subtle && typeof TextEncoder === "function") {
|
|
5042
|
+
try {
|
|
5043
|
+
const encoder2 = new TextEncoder();
|
|
5044
|
+
const [a, b] = await Promise.all([
|
|
5045
|
+
subtle.digest("SHA-256", encoder2.encode(provided)),
|
|
5046
|
+
subtle.digest("SHA-256", encoder2.encode(secret))
|
|
5047
|
+
]);
|
|
5048
|
+
return equalBytes(new Uint8Array(a), new Uint8Array(b));
|
|
5049
|
+
} catch {
|
|
5050
|
+
}
|
|
5051
|
+
}
|
|
5052
|
+
let diff = provided.length ^ secret.length;
|
|
5053
|
+
const length = Math.max(provided.length, secret.length);
|
|
5054
|
+
for (let i = 0; i < length; i++) {
|
|
5055
|
+
diff |= (provided.charCodeAt(i) || 0) ^ (secret.charCodeAt(i) || 0);
|
|
5056
|
+
}
|
|
5057
|
+
return diff === 0;
|
|
5058
|
+
}
|
|
5059
|
+
function equalBytes(a, b) {
|
|
5060
|
+
if (a.length !== b.length) return false;
|
|
5061
|
+
let diff = 0;
|
|
5062
|
+
for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i];
|
|
5063
|
+
return diff === 0;
|
|
5064
|
+
}
|
|
4415
5065
|
|
|
4416
5066
|
// src/protect/detections.js
|
|
4417
5067
|
var DEFAULT_BASE_URL3 = "https://api.patchstack.com/monitor/pulse";
|
|
@@ -5145,9 +5795,180 @@ function hardensWithoutBody(rule) {
|
|
|
5145
5795
|
if (!rule || !HEADER_ACTIONS.includes(rule.action)) return false;
|
|
5146
5796
|
const { parameters, complete } = readRuleParameters(rule);
|
|
5147
5797
|
if (!complete) return false;
|
|
5148
|
-
return parameters.every(
|
|
5149
|
-
|
|
5798
|
+
return parameters.every(bodyIndependent);
|
|
5799
|
+
}
|
|
5800
|
+
function readsOnlyResponseHeaders(rule) {
|
|
5801
|
+
if (!rule) return false;
|
|
5802
|
+
const { parameters, complete } = readRuleParameters(rule);
|
|
5803
|
+
if (!complete || !parameters.some(isHeaderParameter)) return false;
|
|
5804
|
+
return parameters.every(bodyIndependent);
|
|
5805
|
+
}
|
|
5806
|
+
function isHeaderParameter(parameter) {
|
|
5807
|
+
return parameter === "response.headers" || parameter.startsWith(BODY_INDEPENDENT_PREFIX);
|
|
5808
|
+
}
|
|
5809
|
+
function bodyIndependent(parameter) {
|
|
5810
|
+
return BODY_INDEPENDENT.includes(parameter) || parameter.startsWith(BODY_INDEPENDENT_PREFIX);
|
|
5811
|
+
}
|
|
5812
|
+
|
|
5813
|
+
// src/protect/supabase-guard.js
|
|
5814
|
+
var GUARD_PATH = "/_patchstack/guard";
|
|
5815
|
+
var DEFAULT_MAX_BODY_BYTES = 1024 * 1024;
|
|
5816
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
5817
|
+
var REQUEST_HEADER_DENYLIST = /* @__PURE__ */ new Set([
|
|
5818
|
+
"connection",
|
|
5819
|
+
"cookie",
|
|
5820
|
+
"cookie2",
|
|
5821
|
+
"forwarded",
|
|
5822
|
+
"host",
|
|
5823
|
+
"keep-alive",
|
|
5824
|
+
"proxy-authenticate",
|
|
5825
|
+
"proxy-authorization",
|
|
5826
|
+
"te",
|
|
5827
|
+
"trailer",
|
|
5828
|
+
"transfer-encoding",
|
|
5829
|
+
"upgrade",
|
|
5830
|
+
"x-forwarded-for",
|
|
5831
|
+
"x-forwarded-host",
|
|
5832
|
+
"x-forwarded-port",
|
|
5833
|
+
"x-forwarded-proto",
|
|
5834
|
+
"x-ps-target",
|
|
5835
|
+
"x-real-ip"
|
|
5836
|
+
]);
|
|
5837
|
+
var RESPONSE_HEADER_DENYLIST = /* @__PURE__ */ new Set([
|
|
5838
|
+
"connection",
|
|
5839
|
+
"content-encoding",
|
|
5840
|
+
"content-length",
|
|
5841
|
+
"keep-alive",
|
|
5842
|
+
"proxy-authenticate",
|
|
5843
|
+
"proxy-authorization",
|
|
5844
|
+
"set-cookie",
|
|
5845
|
+
"set-cookie2",
|
|
5846
|
+
"te",
|
|
5847
|
+
"trailer",
|
|
5848
|
+
"transfer-encoding",
|
|
5849
|
+
"upgrade"
|
|
5850
|
+
]);
|
|
5851
|
+
var BodyTooLargeError = class extends Error {
|
|
5852
|
+
};
|
|
5853
|
+
async function boundedBody(request, maxBytes) {
|
|
5854
|
+
const stated = Number(request.headers.get("content-length"));
|
|
5855
|
+
if (Number.isFinite(stated) && stated > maxBytes) throw new BodyTooLargeError();
|
|
5856
|
+
if (!request.body) return void 0;
|
|
5857
|
+
const reader = request.body.getReader();
|
|
5858
|
+
const chunks = [];
|
|
5859
|
+
let total = 0;
|
|
5860
|
+
try {
|
|
5861
|
+
for (; ; ) {
|
|
5862
|
+
const { done, value } = await reader.read();
|
|
5863
|
+
if (done) break;
|
|
5864
|
+
total += value.byteLength;
|
|
5865
|
+
if (total > maxBytes) {
|
|
5866
|
+
void reader.cancel().catch(() => {
|
|
5867
|
+
});
|
|
5868
|
+
throw new BodyTooLargeError();
|
|
5869
|
+
}
|
|
5870
|
+
chunks.push(value);
|
|
5871
|
+
}
|
|
5872
|
+
} finally {
|
|
5873
|
+
reader.releaseLock();
|
|
5874
|
+
}
|
|
5875
|
+
const body = new Uint8Array(total);
|
|
5876
|
+
let offset = 0;
|
|
5877
|
+
for (const chunk of chunks) {
|
|
5878
|
+
body.set(chunk, offset);
|
|
5879
|
+
offset += chunk.byteLength;
|
|
5880
|
+
}
|
|
5881
|
+
return body;
|
|
5882
|
+
}
|
|
5883
|
+
function requestHeaders(input) {
|
|
5884
|
+
const output = new Headers();
|
|
5885
|
+
const connectionTokens = new Set(
|
|
5886
|
+
(input.get("connection") ?? "").split(",").map((value) => value.trim().toLowerCase()).filter(Boolean)
|
|
5887
|
+
);
|
|
5888
|
+
input.forEach((value, key) => {
|
|
5889
|
+
const lower = key.toLowerCase();
|
|
5890
|
+
if (!REQUEST_HEADER_DENYLIST.has(lower) && !connectionTokens.has(lower)) output.append(key, value);
|
|
5891
|
+
});
|
|
5892
|
+
return output;
|
|
5893
|
+
}
|
|
5894
|
+
function responseHeaders(input) {
|
|
5895
|
+
const output = new Headers();
|
|
5896
|
+
const connectionTokens = new Set(
|
|
5897
|
+
(input.get("connection") ?? "").split(",").map((value) => value.trim().toLowerCase()).filter(Boolean)
|
|
5150
5898
|
);
|
|
5899
|
+
input.forEach((value, key) => {
|
|
5900
|
+
const lower = key.toLowerCase();
|
|
5901
|
+
if (!RESPONSE_HEADER_DENYLIST.has(lower) && !connectionTokens.has(lower)) output.append(key, value);
|
|
5902
|
+
});
|
|
5903
|
+
return output;
|
|
5904
|
+
}
|
|
5905
|
+
var SOURCE_REQUEST = /* @__PURE__ */ Symbol.for("@patchstack/connect.sourceRequest");
|
|
5906
|
+
function createSupabaseGuard({
|
|
5907
|
+
protection,
|
|
5908
|
+
supabaseUrl,
|
|
5909
|
+
fetchImpl = fetch,
|
|
5910
|
+
maxBodyBytes = DEFAULT_MAX_BODY_BYTES,
|
|
5911
|
+
timeoutMs = DEFAULT_TIMEOUT_MS
|
|
5912
|
+
}) {
|
|
5913
|
+
const guard = protection.fetchGuard();
|
|
5914
|
+
const allowedOrigin = supabaseUrl ? new URL(supabaseUrl).origin : null;
|
|
5915
|
+
const bodyLimit = Number.isFinite(maxBodyBytes) && maxBodyBytes > 0 ? maxBodyBytes : DEFAULT_MAX_BODY_BYTES;
|
|
5916
|
+
const upstreamTimeout = Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : DEFAULT_TIMEOUT_MS;
|
|
5917
|
+
return async function handleGuardRequest(request, ...hostArgs) {
|
|
5918
|
+
const target = request.headers.get("x-ps-target");
|
|
5919
|
+
if (!target) return new Response("patchstack: missing x-ps-target", { status: 400 });
|
|
5920
|
+
let targetUrl;
|
|
5921
|
+
try {
|
|
5922
|
+
targetUrl = new URL(target);
|
|
5923
|
+
} catch {
|
|
5924
|
+
return new Response("patchstack: invalid target", { status: 400 });
|
|
5925
|
+
}
|
|
5926
|
+
if (!allowedOrigin || targetUrl.protocol !== "https:" || targetUrl.origin !== allowedOrigin) {
|
|
5927
|
+
return new Response("patchstack: target not allowed", { status: 403 });
|
|
5928
|
+
}
|
|
5929
|
+
const hasBody = request.method !== "GET" && request.method !== "HEAD";
|
|
5930
|
+
let body;
|
|
5931
|
+
try {
|
|
5932
|
+
body = hasBody ? await boundedBody(request, bodyLimit) : void 0;
|
|
5933
|
+
} catch (error) {
|
|
5934
|
+
if (error instanceof BodyTooLargeError) {
|
|
5935
|
+
return new Response("patchstack: request body too large", { status: 413 });
|
|
5936
|
+
}
|
|
5937
|
+
throw error;
|
|
5938
|
+
}
|
|
5939
|
+
const forwardHeaders = requestHeaders(request.headers);
|
|
5940
|
+
const evalReq = new Request(targetUrl.toString(), {
|
|
5941
|
+
method: request.method,
|
|
5942
|
+
// Inspect what the browser sent, including app-origin cookies; only the forwarded copy is
|
|
5943
|
+
// sanitized at the cross-origin boundary below.
|
|
5944
|
+
headers: request.headers,
|
|
5945
|
+
body
|
|
5946
|
+
});
|
|
5947
|
+
Object.defineProperty(evalReq, SOURCE_REQUEST, { value: request });
|
|
5948
|
+
const blocked = await guard(evalReq, ...hostArgs);
|
|
5949
|
+
if (blocked) return blocked;
|
|
5950
|
+
let upstream;
|
|
5951
|
+
try {
|
|
5952
|
+
upstream = await fetchImpl(targetUrl.toString(), {
|
|
5953
|
+
method: request.method,
|
|
5954
|
+
headers: forwardHeaders,
|
|
5955
|
+
body,
|
|
5956
|
+
redirect: "manual",
|
|
5957
|
+
signal: AbortSignal.timeout(upstreamTimeout)
|
|
5958
|
+
});
|
|
5959
|
+
} catch (error) {
|
|
5960
|
+
if (error?.name === "TimeoutError" || error?.name === "AbortError") {
|
|
5961
|
+
return new Response("patchstack: upstream timed out", { status: 504 });
|
|
5962
|
+
}
|
|
5963
|
+
throw error;
|
|
5964
|
+
}
|
|
5965
|
+
const forwarded = new Response(upstream.body, {
|
|
5966
|
+
status: upstream.status,
|
|
5967
|
+
statusText: upstream.statusText,
|
|
5968
|
+
headers: responseHeaders(upstream.headers)
|
|
5969
|
+
});
|
|
5970
|
+
return protection.screenResponse ? protection.screenResponse(forwarded, evalReq) : forwarded;
|
|
5971
|
+
};
|
|
5151
5972
|
}
|
|
5152
5973
|
|
|
5153
5974
|
// src/protect/firewall-log.js
|
|
@@ -5196,8 +6017,13 @@ function resolveApiBase(pulseOrManifestUrl) {
|
|
|
5196
6017
|
function createFirewallLogReporter(opts) {
|
|
5197
6018
|
const creds = parseApiKey(opts.apiKey);
|
|
5198
6019
|
if (!creds) {
|
|
5199
|
-
return {
|
|
5200
|
-
|
|
6020
|
+
return {
|
|
6021
|
+
record() {
|
|
6022
|
+
},
|
|
6023
|
+
flush: () => Promise.resolve(),
|
|
6024
|
+
stop: () => Promise.resolve(),
|
|
6025
|
+
health: () => ({ recorded: 0, delivered: 0, failed: 0, dropped: 0, queued: 0 })
|
|
6026
|
+
};
|
|
5201
6027
|
}
|
|
5202
6028
|
const apiBase = safeBaseUrl(opts.apiBase, DEFAULT_API_BASE, "block-log API").replace(/\/$/, "");
|
|
5203
6029
|
const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
@@ -5210,6 +6036,19 @@ function createFirewallLogReporter(opts) {
|
|
|
5210
6036
|
let drainPromise = null;
|
|
5211
6037
|
let activeController = null;
|
|
5212
6038
|
let ended = false;
|
|
6039
|
+
let recorded = 0;
|
|
6040
|
+
let delivered = 0;
|
|
6041
|
+
let failed = 0;
|
|
6042
|
+
let dropped = 0;
|
|
6043
|
+
let activeBatch = null;
|
|
6044
|
+
const settle = (sent, outcome) => {
|
|
6045
|
+
if (sent.counted) return;
|
|
6046
|
+
sent.counted = true;
|
|
6047
|
+
if (outcome === "delivered") delivered += sent.size;
|
|
6048
|
+
else if (outcome === "failed") failed += sent.size;
|
|
6049
|
+
else dropped += sent.size;
|
|
6050
|
+
if (activeBatch === sent) activeBatch = null;
|
|
6051
|
+
};
|
|
5213
6052
|
let cachedToken = null;
|
|
5214
6053
|
let tokenInflight = null;
|
|
5215
6054
|
const fetchAccessToken = async (signal) => {
|
|
@@ -5263,10 +6102,16 @@ function createFirewallLogReporter(opts) {
|
|
|
5263
6102
|
const attemptTimer = setTimeout(() => controller?.abort(), ATTEMPT_TIMEOUT_MS2);
|
|
5264
6103
|
attemptTimer?.unref?.();
|
|
5265
6104
|
let entry;
|
|
6105
|
+
const sent = { size: batch.length, counted: false };
|
|
6106
|
+
activeBatch = sent;
|
|
5266
6107
|
entry = (async () => {
|
|
5267
6108
|
try {
|
|
5268
6109
|
const token = await fetchAccessToken(controller?.signal);
|
|
5269
|
-
if (
|
|
6110
|
+
if (ended) return;
|
|
6111
|
+
if (!token) {
|
|
6112
|
+
settle(sent, "failed");
|
|
6113
|
+
return;
|
|
6114
|
+
}
|
|
5270
6115
|
const body = new URLSearchParams();
|
|
5271
6116
|
body.set("type", "firewall");
|
|
5272
6117
|
body.set("logs", JSON.stringify(batch));
|
|
@@ -5283,10 +6128,11 @@ function createFirewallLogReporter(opts) {
|
|
|
5283
6128
|
// Both phases carry the attempt controller, so slow transports cannot accumulate work.
|
|
5284
6129
|
...controller ? { signal: controller.signal } : {}
|
|
5285
6130
|
});
|
|
5286
|
-
|
|
5287
|
-
|
|
6131
|
+
const res = p && typeof p.then === "function" ? await p.catch(() => null) : p;
|
|
6132
|
+
settle(sent, res && res.ok ? "delivered" : "failed");
|
|
5288
6133
|
} catch {
|
|
5289
6134
|
} finally {
|
|
6135
|
+
settle(sent, "failed");
|
|
5290
6136
|
clearTimeout(attemptTimer);
|
|
5291
6137
|
if (activeController === controller) activeController = null;
|
|
5292
6138
|
if (inFlight === entry) inFlight = null;
|
|
@@ -5313,234 +6159,85 @@ function createFirewallLogReporter(opts) {
|
|
|
5313
6159
|
if (stopped) return;
|
|
5314
6160
|
const fid = event?.rule?.id;
|
|
5315
6161
|
if (fid === void 0 || fid === null || fid === "") return;
|
|
5316
|
-
if (queue.length >= MAX_QUEUE2)
|
|
5317
|
-
|
|
5318
|
-
fid,
|
|
5319
|
-
method: event.method ?? null,
|
|
5320
|
-
request_uri: event.path ?? null,
|
|
5321
|
-
ip: event.ip ?? null,
|
|
5322
|
-
user_agent: event.userAgent ?? null,
|
|
5323
|
-
log_date: (/* @__PURE__ */ new Date()).toISOString()
|
|
5324
|
-
});
|
|
5325
|
-
if (queue.length >= MAX_BATCH2) {
|
|
5326
|
-
flush();
|
|
6162
|
+
if (queue.length >= MAX_QUEUE2) {
|
|
6163
|
+
dropped++;
|
|
5327
6164
|
return;
|
|
5328
6165
|
}
|
|
5329
|
-
|
|
5330
|
-
|
|
5331
|
-
|
|
5332
|
-
|
|
5333
|
-
|
|
5334
|
-
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
* spinning would be worse than leaving the records where they are.
|
|
5338
|
-
*/
|
|
5339
|
-
stop() {
|
|
5340
|
-
if (drainPromise) return drainPromise;
|
|
5341
|
-
stopped = true;
|
|
5342
|
-
if (timer) {
|
|
5343
|
-
clearTimeout(timer);
|
|
5344
|
-
timer = null;
|
|
5345
|
-
}
|
|
5346
|
-
const terminate = () => {
|
|
5347
|
-
ended = true;
|
|
5348
|
-
activeController?.abort();
|
|
5349
|
-
activeController = null;
|
|
5350
|
-
queue = [];
|
|
5351
|
-
inFlight = null;
|
|
5352
|
-
};
|
|
5353
|
-
let budget = null;
|
|
5354
|
-
const spent = new Promise((resolve) => {
|
|
5355
|
-
budget = setTimeout(() => {
|
|
5356
|
-
terminate();
|
|
5357
|
-
resolve();
|
|
5358
|
-
}, STOP_BUDGET_MS2);
|
|
5359
|
-
if (typeof budget.unref === "function") budget.unref();
|
|
5360
|
-
});
|
|
5361
|
-
const work = (async () => {
|
|
5362
|
-
while (!ended) {
|
|
5363
|
-
if (inFlight) {
|
|
5364
|
-
await inFlight;
|
|
5365
|
-
continue;
|
|
5366
|
-
}
|
|
5367
|
-
if (queue.length === 0) break;
|
|
5368
|
-
const before = queue.length;
|
|
5369
|
-
await flush();
|
|
5370
|
-
if (queue.length >= before) break;
|
|
5371
|
-
}
|
|
5372
|
-
})();
|
|
5373
|
-
drainPromise = Promise.race([work, spent]).then(() => {
|
|
5374
|
-
if (budget) clearTimeout(budget);
|
|
5375
|
-
});
|
|
5376
|
-
return drainPromise;
|
|
5377
|
-
}
|
|
5378
|
-
};
|
|
5379
|
-
}
|
|
5380
|
-
function telemetryEnabled() {
|
|
5381
|
-
const v = typeof process !== "undefined" ? process.env?.PATCHSTACK_TELEMETRY : void 0;
|
|
5382
|
-
if (v === void 0 || v === "") return true;
|
|
5383
|
-
return !/^(0|false|off|no)$/i.test(String(v));
|
|
5384
|
-
}
|
|
5385
|
-
|
|
5386
|
-
// src/protect/supabase-guard.js
|
|
5387
|
-
var GUARD_PATH = "/_patchstack/guard";
|
|
5388
|
-
var DEFAULT_MAX_BODY_BYTES = 1024 * 1024;
|
|
5389
|
-
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
5390
|
-
var REQUEST_HEADER_DENYLIST = /* @__PURE__ */ new Set([
|
|
5391
|
-
"connection",
|
|
5392
|
-
"cookie",
|
|
5393
|
-
"cookie2",
|
|
5394
|
-
"forwarded",
|
|
5395
|
-
"host",
|
|
5396
|
-
"keep-alive",
|
|
5397
|
-
"proxy-authenticate",
|
|
5398
|
-
"proxy-authorization",
|
|
5399
|
-
"te",
|
|
5400
|
-
"trailer",
|
|
5401
|
-
"transfer-encoding",
|
|
5402
|
-
"upgrade",
|
|
5403
|
-
"x-forwarded-for",
|
|
5404
|
-
"x-forwarded-host",
|
|
5405
|
-
"x-forwarded-port",
|
|
5406
|
-
"x-forwarded-proto",
|
|
5407
|
-
"x-ps-target",
|
|
5408
|
-
"x-real-ip"
|
|
5409
|
-
]);
|
|
5410
|
-
var RESPONSE_HEADER_DENYLIST = /* @__PURE__ */ new Set([
|
|
5411
|
-
"connection",
|
|
5412
|
-
"content-encoding",
|
|
5413
|
-
"content-length",
|
|
5414
|
-
"keep-alive",
|
|
5415
|
-
"proxy-authenticate",
|
|
5416
|
-
"proxy-authorization",
|
|
5417
|
-
"set-cookie",
|
|
5418
|
-
"set-cookie2",
|
|
5419
|
-
"te",
|
|
5420
|
-
"trailer",
|
|
5421
|
-
"transfer-encoding",
|
|
5422
|
-
"upgrade"
|
|
5423
|
-
]);
|
|
5424
|
-
var BodyTooLargeError = class extends Error {
|
|
5425
|
-
};
|
|
5426
|
-
async function boundedBody(request, maxBytes) {
|
|
5427
|
-
const stated = Number(request.headers.get("content-length"));
|
|
5428
|
-
if (Number.isFinite(stated) && stated > maxBytes) throw new BodyTooLargeError();
|
|
5429
|
-
if (!request.body) return void 0;
|
|
5430
|
-
const reader = request.body.getReader();
|
|
5431
|
-
const chunks = [];
|
|
5432
|
-
let total = 0;
|
|
5433
|
-
try {
|
|
5434
|
-
for (; ; ) {
|
|
5435
|
-
const { done, value } = await reader.read();
|
|
5436
|
-
if (done) break;
|
|
5437
|
-
total += value.byteLength;
|
|
5438
|
-
if (total > maxBytes) {
|
|
5439
|
-
void reader.cancel().catch(() => {
|
|
5440
|
-
});
|
|
5441
|
-
throw new BodyTooLargeError();
|
|
5442
|
-
}
|
|
5443
|
-
chunks.push(value);
|
|
5444
|
-
}
|
|
5445
|
-
} finally {
|
|
5446
|
-
reader.releaseLock();
|
|
5447
|
-
}
|
|
5448
|
-
const body = new Uint8Array(total);
|
|
5449
|
-
let offset = 0;
|
|
5450
|
-
for (const chunk of chunks) {
|
|
5451
|
-
body.set(chunk, offset);
|
|
5452
|
-
offset += chunk.byteLength;
|
|
5453
|
-
}
|
|
5454
|
-
return body;
|
|
5455
|
-
}
|
|
5456
|
-
function requestHeaders(input) {
|
|
5457
|
-
const output = new Headers();
|
|
5458
|
-
const connectionTokens = new Set(
|
|
5459
|
-
(input.get("connection") ?? "").split(",").map((value) => value.trim().toLowerCase()).filter(Boolean)
|
|
5460
|
-
);
|
|
5461
|
-
input.forEach((value, key) => {
|
|
5462
|
-
const lower = key.toLowerCase();
|
|
5463
|
-
if (!REQUEST_HEADER_DENYLIST.has(lower) && !connectionTokens.has(lower)) output.append(key, value);
|
|
5464
|
-
});
|
|
5465
|
-
return output;
|
|
5466
|
-
}
|
|
5467
|
-
function responseHeaders(input) {
|
|
5468
|
-
const output = new Headers();
|
|
5469
|
-
const connectionTokens = new Set(
|
|
5470
|
-
(input.get("connection") ?? "").split(",").map((value) => value.trim().toLowerCase()).filter(Boolean)
|
|
5471
|
-
);
|
|
5472
|
-
input.forEach((value, key) => {
|
|
5473
|
-
const lower = key.toLowerCase();
|
|
5474
|
-
if (!RESPONSE_HEADER_DENYLIST.has(lower) && !connectionTokens.has(lower)) output.append(key, value);
|
|
5475
|
-
});
|
|
5476
|
-
return output;
|
|
5477
|
-
}
|
|
5478
|
-
function createSupabaseGuard({
|
|
5479
|
-
protection,
|
|
5480
|
-
supabaseUrl,
|
|
5481
|
-
fetchImpl = fetch,
|
|
5482
|
-
maxBodyBytes = DEFAULT_MAX_BODY_BYTES,
|
|
5483
|
-
timeoutMs = DEFAULT_TIMEOUT_MS
|
|
5484
|
-
}) {
|
|
5485
|
-
const guard = protection.fetchGuard();
|
|
5486
|
-
const allowedOrigin = supabaseUrl ? new URL(supabaseUrl).origin : null;
|
|
5487
|
-
const bodyLimit = Number.isFinite(maxBodyBytes) && maxBodyBytes > 0 ? maxBodyBytes : DEFAULT_MAX_BODY_BYTES;
|
|
5488
|
-
const upstreamTimeout = Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : DEFAULT_TIMEOUT_MS;
|
|
5489
|
-
return async function handleGuardRequest(request) {
|
|
5490
|
-
const target = request.headers.get("x-ps-target");
|
|
5491
|
-
if (!target) return new Response("patchstack: missing x-ps-target", { status: 400 });
|
|
5492
|
-
let targetUrl;
|
|
5493
|
-
try {
|
|
5494
|
-
targetUrl = new URL(target);
|
|
5495
|
-
} catch {
|
|
5496
|
-
return new Response("patchstack: invalid target", { status: 400 });
|
|
5497
|
-
}
|
|
5498
|
-
if (!allowedOrigin || targetUrl.protocol !== "https:" || targetUrl.origin !== allowedOrigin) {
|
|
5499
|
-
return new Response("patchstack: target not allowed", { status: 403 });
|
|
5500
|
-
}
|
|
5501
|
-
const hasBody = request.method !== "GET" && request.method !== "HEAD";
|
|
5502
|
-
let body;
|
|
5503
|
-
try {
|
|
5504
|
-
body = hasBody ? await boundedBody(request, bodyLimit) : void 0;
|
|
5505
|
-
} catch (error) {
|
|
5506
|
-
if (error instanceof BodyTooLargeError) {
|
|
5507
|
-
return new Response("patchstack: request body too large", { status: 413 });
|
|
5508
|
-
}
|
|
5509
|
-
throw error;
|
|
5510
|
-
}
|
|
5511
|
-
const forwardHeaders = requestHeaders(request.headers);
|
|
5512
|
-
const evalReq = new Request(targetUrl.toString(), {
|
|
5513
|
-
method: request.method,
|
|
5514
|
-
// Inspect what the browser sent, including app-origin cookies; only the forwarded copy is
|
|
5515
|
-
// sanitized at the cross-origin boundary below.
|
|
5516
|
-
headers: request.headers,
|
|
5517
|
-
body
|
|
5518
|
-
});
|
|
5519
|
-
const blocked = await guard(evalReq);
|
|
5520
|
-
if (blocked) return blocked;
|
|
5521
|
-
let upstream;
|
|
5522
|
-
try {
|
|
5523
|
-
upstream = await fetchImpl(targetUrl.toString(), {
|
|
5524
|
-
method: request.method,
|
|
5525
|
-
headers: forwardHeaders,
|
|
5526
|
-
body,
|
|
5527
|
-
redirect: "manual",
|
|
5528
|
-
signal: AbortSignal.timeout(upstreamTimeout)
|
|
6166
|
+
recorded++;
|
|
6167
|
+
queue.push({
|
|
6168
|
+
fid,
|
|
6169
|
+
method: event.method ?? null,
|
|
6170
|
+
request_uri: event.path ?? null,
|
|
6171
|
+
ip: event.ip ?? null,
|
|
6172
|
+
user_agent: event.userAgent ?? null,
|
|
6173
|
+
log_date: (/* @__PURE__ */ new Date()).toISOString()
|
|
5529
6174
|
});
|
|
5530
|
-
|
|
5531
|
-
|
|
5532
|
-
return
|
|
6175
|
+
if (queue.length >= MAX_BATCH2) {
|
|
6176
|
+
flush();
|
|
6177
|
+
return;
|
|
5533
6178
|
}
|
|
5534
|
-
|
|
6179
|
+
if (!timer) timer = setTimeout(flush, flushMs);
|
|
6180
|
+
},
|
|
6181
|
+
flush,
|
|
6182
|
+
/** Where the records went so far; see the counters above. `queued` is what is waiting now. */
|
|
6183
|
+
health() {
|
|
6184
|
+
return { recorded, delivered, failed, dropped, queued: queue.length };
|
|
6185
|
+
},
|
|
6186
|
+
/**
|
|
6187
|
+
* Stop, and hand back a wait for what was outstanding.
|
|
6188
|
+
*
|
|
6189
|
+
* One flush sends at most a batch, so the queue is drained a batch at a time. The loop stops as soon
|
|
6190
|
+
* as a pass cannot shrink the queue — with no usable transport there is nothing to wait for, and
|
|
6191
|
+
* spinning would be worse than leaving the records where they are.
|
|
6192
|
+
*/
|
|
6193
|
+
stop() {
|
|
6194
|
+
if (drainPromise) return drainPromise;
|
|
6195
|
+
stopped = true;
|
|
6196
|
+
if (timer) {
|
|
6197
|
+
clearTimeout(timer);
|
|
6198
|
+
timer = null;
|
|
6199
|
+
}
|
|
6200
|
+
const terminate = () => {
|
|
6201
|
+
ended = true;
|
|
6202
|
+
activeController?.abort();
|
|
6203
|
+
activeController = null;
|
|
6204
|
+
if (activeBatch) settle(activeBatch, "dropped");
|
|
6205
|
+
dropped += queue.length;
|
|
6206
|
+
queue = [];
|
|
6207
|
+
inFlight = null;
|
|
6208
|
+
};
|
|
6209
|
+
let budget = null;
|
|
6210
|
+
const spent = new Promise((resolve) => {
|
|
6211
|
+
budget = setTimeout(() => {
|
|
6212
|
+
terminate();
|
|
6213
|
+
resolve();
|
|
6214
|
+
}, STOP_BUDGET_MS2);
|
|
6215
|
+
if (typeof budget.unref === "function") budget.unref();
|
|
6216
|
+
});
|
|
6217
|
+
const work = (async () => {
|
|
6218
|
+
while (!ended) {
|
|
6219
|
+
if (inFlight) {
|
|
6220
|
+
await inFlight;
|
|
6221
|
+
continue;
|
|
6222
|
+
}
|
|
6223
|
+
if (queue.length === 0) break;
|
|
6224
|
+
const before = queue.length;
|
|
6225
|
+
await flush();
|
|
6226
|
+
if (queue.length >= before) break;
|
|
6227
|
+
}
|
|
6228
|
+
})();
|
|
6229
|
+
drainPromise = Promise.race([work, spent]).then(() => {
|
|
6230
|
+
if (budget) clearTimeout(budget);
|
|
6231
|
+
});
|
|
6232
|
+
return drainPromise;
|
|
5535
6233
|
}
|
|
5536
|
-
const forwarded = new Response(upstream.body, {
|
|
5537
|
-
status: upstream.status,
|
|
5538
|
-
statusText: upstream.statusText,
|
|
5539
|
-
headers: responseHeaders(upstream.headers)
|
|
5540
|
-
});
|
|
5541
|
-
return protection.screenResponse ? protection.screenResponse(forwarded) : forwarded;
|
|
5542
6234
|
};
|
|
5543
6235
|
}
|
|
6236
|
+
function telemetryEnabled() {
|
|
6237
|
+
const v = typeof process !== "undefined" ? process.env?.PATCHSTACK_TELEMETRY : void 0;
|
|
6238
|
+
if (v === void 0 || v === "") return true;
|
|
6239
|
+
return !/^(0|false|off|no)$/i.test(String(v));
|
|
6240
|
+
}
|
|
5544
6241
|
|
|
5545
6242
|
// src/protect/verify-sentinel.js
|
|
5546
6243
|
var VERIFY_HEADER = "x-patchstack-verify";
|
|
@@ -5632,6 +6329,7 @@ async function createProtection(options = {}) {
|
|
|
5632
6329
|
"onDetect"
|
|
5633
6330
|
);
|
|
5634
6331
|
};
|
|
6332
|
+
const nodeHttp = await Promise.resolve().then(() => __toESM(require_ps_edge_stub(), 1)).then((m) => m.default ?? m, () => null);
|
|
5635
6333
|
const store = makeStore(options);
|
|
5636
6334
|
const bootTimeoutMs = Number(options.bootTimeoutMs) > 0 ? Number(options.bootTimeoutMs) : 5e3;
|
|
5637
6335
|
const pulseAuth = await resolvePulseAuth(options);
|
|
@@ -5657,6 +6355,21 @@ async function createProtection(options = {}) {
|
|
|
5657
6355
|
pulseAuth,
|
|
5658
6356
|
detectionState: preFetchState
|
|
5659
6357
|
});
|
|
6358
|
+
let ruleSource = { ...bundle.source ?? { ok: true, origin: "empty" } };
|
|
6359
|
+
const warnedSources = /* @__PURE__ */ new Set();
|
|
6360
|
+
const hasLiveSource = Boolean(options.siteUuid || options.token);
|
|
6361
|
+
const noteRuleSource = (source) => {
|
|
6362
|
+
ruleSource = { ...source ?? { ok: true, origin: ruleSource.origin } };
|
|
6363
|
+
if (!hasLiveSource || typeof onError === "function" || ruleSource.ok !== false) return;
|
|
6364
|
+
const cause = `${ruleSource.origin}:${ruleSource.reason ?? ""}`;
|
|
6365
|
+
if (warnedSources.has(cause)) return;
|
|
6366
|
+
warnedSources.add(cause);
|
|
6367
|
+
const running = ruleSource.origin === "cache" ? "the last rules it received" : ruleSource.origin === "bundled" ? "its bundled fallback rules" : "no rules at all";
|
|
6368
|
+
console.warn(
|
|
6369
|
+
`[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.`
|
|
6370
|
+
);
|
|
6371
|
+
};
|
|
6372
|
+
noteRuleSource(bundle.source);
|
|
5660
6373
|
let detectionReporting = "not-enrolled";
|
|
5661
6374
|
let currentOrigin;
|
|
5662
6375
|
const applyReportingState = async (origin) => {
|
|
@@ -5712,20 +6425,47 @@ async function createProtection(options = {}) {
|
|
|
5712
6425
|
whitelist_keys: delivered.whitelist_keys,
|
|
5713
6426
|
onError
|
|
5714
6427
|
});
|
|
5715
|
-
responseRuleSet = responseRules.map((rule) =>
|
|
5716
|
-
|
|
5717
|
-
|
|
5718
|
-
|
|
5719
|
-
|
|
5720
|
-
|
|
5721
|
-
|
|
5722
|
-
|
|
5723
|
-
|
|
5724
|
-
|
|
5725
|
-
|
|
5726
|
-
|
|
5727
|
-
|
|
5728
|
-
|
|
6428
|
+
responseRuleSet = responseRules.map((rule) => {
|
|
6429
|
+
const unscoped = [];
|
|
6430
|
+
const rewrites = rule.action === "redact" || rule.action === "encode";
|
|
6431
|
+
let derived;
|
|
6432
|
+
try {
|
|
6433
|
+
const redactors = rewrites ? extractRedactors(rule, (parameter) => unscoped.push(parameter)) : null;
|
|
6434
|
+
const mutatedSpan = rewrites && hasSpanMutations(rule);
|
|
6435
|
+
derived = {
|
|
6436
|
+
redactors,
|
|
6437
|
+
mutatedSpan,
|
|
6438
|
+
// A redaction that reads response headers only, and masks spans in them, and an explicit block
|
|
6439
|
+
// that reads response headers only, are decided and carried out without the body — so they
|
|
6440
|
+
// still apply to a response whose body was not screened.
|
|
6441
|
+
redactsHeaders: rule.action === "redact" && !mutatedSpan && (redactors ?? []).some((r) => !r.jsonPath) && readsOnlyResponseHeaders(rule),
|
|
6442
|
+
blocksOnHeaders: rule.action === "block" && readsOnlyResponseHeaders(rule)
|
|
6443
|
+
};
|
|
6444
|
+
} catch (err) {
|
|
6445
|
+
notify(onError, err, "onError");
|
|
6446
|
+
derived = { redactors: null, mutatedSpan: false, redactsHeaders: false, blocksOnHeaders: false };
|
|
6447
|
+
}
|
|
6448
|
+
if (unscoped.length) {
|
|
6449
|
+
notify(
|
|
6450
|
+
onError,
|
|
6451
|
+
new Error(
|
|
6452
|
+
`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`
|
|
6453
|
+
),
|
|
6454
|
+
"onError"
|
|
6455
|
+
);
|
|
6456
|
+
}
|
|
6457
|
+
return {
|
|
6458
|
+
rule,
|
|
6459
|
+
engine: new RuleEngine({ firewall: [rule], onError }),
|
|
6460
|
+
...derived,
|
|
6461
|
+
// Optional cheap pre-filter: literal anchor(s) that MUST appear for the (expensive) regex to
|
|
6462
|
+
// have any chance of matching. Lets screenText skip the full scan on responses with no candidate
|
|
6463
|
+
// — the common case — cutting CPU/latency and shrinking the regex/ReDoS surface.
|
|
6464
|
+
// Case-insensitive, and checked against the header values as well as the body, since a rule may
|
|
6465
|
+
// read either.
|
|
6466
|
+
prefilter: Array.isArray(rule.prefilter) && rule.prefilter.length ? rule.prefilter.map((s) => String(s).toLowerCase()) : null
|
|
6467
|
+
};
|
|
6468
|
+
});
|
|
5729
6469
|
egressRuleSet = egressRules.map((rule) => ({
|
|
5730
6470
|
rule,
|
|
5731
6471
|
engine: new RuleEngine({ firewall: [rule], onError })
|
|
@@ -5739,6 +6479,9 @@ async function createProtection(options = {}) {
|
|
|
5739
6479
|
skipCounts[key] = (skipCounts[key] ?? 0) + 1;
|
|
5740
6480
|
notify(onSkip, { phase, reason, detail, count: skipCounts[key] }, "onSkip");
|
|
5741
6481
|
};
|
|
6482
|
+
const recordEvaluationSkips = (result) => {
|
|
6483
|
+
for (const reason of result?.skips ?? []) recordSkip("request", reason);
|
|
6484
|
+
};
|
|
5742
6485
|
const maskFn = typeof options.maskWith === "function" ? options.maskWith : () => typeof options.maskWith === "string" ? options.maskWith : "[REDACTED]";
|
|
5743
6486
|
const ruleMode = (rule) => rule?.enforcement === "dry-run" ? "dry-run" : mode;
|
|
5744
6487
|
const planCache = createPlanCache();
|
|
@@ -5781,15 +6524,19 @@ async function createProtection(options = {}) {
|
|
|
5781
6524
|
});
|
|
5782
6525
|
return effectiveMode === "block" ? block() : allow2();
|
|
5783
6526
|
};
|
|
6527
|
+
const decidedByHeaders = (_rule, entry) => Boolean(entry?.redactsHeaders || entry?.blocksOnHeaders);
|
|
6528
|
+
const decidedWithoutBody = (rule, entry) => hardensWithoutBody(rule) || decidedByHeaders(rule, entry);
|
|
5784
6529
|
const screenText = (text, meta, reqCtx, only) => {
|
|
5785
6530
|
let blockRule = null;
|
|
5786
6531
|
const redactions = [];
|
|
5787
6532
|
const headerMutations = [];
|
|
5788
6533
|
let lowerText = null;
|
|
5789
|
-
|
|
5790
|
-
|
|
6534
|
+
const responseSkips = /* @__PURE__ */ new Set();
|
|
6535
|
+
for (const entry of responseRuleSet) {
|
|
6536
|
+
const { rule, engine: re, redactors, prefilter, mutatedSpan } = entry;
|
|
6537
|
+
if (only && !only(rule, entry)) continue;
|
|
5791
6538
|
if (prefilter) {
|
|
5792
|
-
if (lowerText === null) lowerText = text.
|
|
6539
|
+
if (lowerText === null) lowerText = prefilterText(text, meta.headers);
|
|
5793
6540
|
if (!prefilter.some((p) => lowerText.includes(p))) continue;
|
|
5794
6541
|
}
|
|
5795
6542
|
let result;
|
|
@@ -5799,6 +6546,11 @@ async function createProtection(options = {}) {
|
|
|
5799
6546
|
notify(onError, err, "onError");
|
|
5800
6547
|
continue;
|
|
5801
6548
|
}
|
|
6549
|
+
for (const reason of result.skips ?? []) {
|
|
6550
|
+
if (responseSkips.has(reason)) continue;
|
|
6551
|
+
responseSkips.add(reason);
|
|
6552
|
+
recordSkip("response", reason);
|
|
6553
|
+
}
|
|
5802
6554
|
if (!result.blocked) continue;
|
|
5803
6555
|
const responseMode = ruleMode(rule);
|
|
5804
6556
|
onDetect({
|
|
@@ -5828,6 +6580,8 @@ async function createProtection(options = {}) {
|
|
|
5828
6580
|
if (blockRule) return { verdict: "block" };
|
|
5829
6581
|
let body = text;
|
|
5830
6582
|
const headers = { ...meta.headers || {} };
|
|
6583
|
+
let responseIsJson;
|
|
6584
|
+
let structure;
|
|
5831
6585
|
for (const { rule, redactors } of redactions) {
|
|
5832
6586
|
const mask = maskFn(rule.category);
|
|
5833
6587
|
const transform = rule.action === "encode" ? htmlEscape : null;
|
|
@@ -5835,17 +6589,29 @@ async function createProtection(options = {}) {
|
|
|
5835
6589
|
const spanRedactors = redactors.filter((r) => !r.jsonPath);
|
|
5836
6590
|
if (pathRedactors.length) body = applyPathRedactors(body, pathRedactors, mask, screenCap, transform);
|
|
5837
6591
|
if (!spanRedactors.length) continue;
|
|
5838
|
-
|
|
6592
|
+
const beforeSpan = body;
|
|
6593
|
+
const bodySpans = spanRedactors.filter((r) => r.scope.body === true);
|
|
6594
|
+
if (bodySpans.length) body = applyRedactors(body, bodySpans, mask, transform);
|
|
6595
|
+
if (body !== beforeSpan) {
|
|
6596
|
+
responseIsJson ??= isJson(text);
|
|
6597
|
+
if (responseIsJson) {
|
|
6598
|
+
if (structure?.text !== beforeSpan) structure = { text: beforeSpan, tokens: lexJson(beforeSpan) };
|
|
6599
|
+
const tokens = lexJson(body);
|
|
6600
|
+
if (!sameJsonStructure(structure.text, structure.tokens, body, tokens)) return { verdict: "block" };
|
|
6601
|
+
structure = { text: body, tokens };
|
|
6602
|
+
}
|
|
6603
|
+
}
|
|
5839
6604
|
if (transform) continue;
|
|
5840
6605
|
for (const name of Object.keys(headers)) {
|
|
5841
6606
|
const value = headers[name];
|
|
6607
|
+
const headerSpans = spanRedactors.filter((r) => masksHeader(r, name));
|
|
5842
6608
|
if (typeof value === "string") {
|
|
5843
|
-
setOwn(headers, name, applyRedactors(value,
|
|
6609
|
+
setOwn(headers, name, applyRedactors(value, headerSpans, mask));
|
|
5844
6610
|
} else if (Array.isArray(value)) {
|
|
5845
6611
|
setOwn(
|
|
5846
6612
|
headers,
|
|
5847
6613
|
name,
|
|
5848
|
-
value.map((item) => typeof item === "string" ? applyRedactors(item,
|
|
6614
|
+
value.map((item) => typeof item === "string" ? applyRedactors(item, headerSpans, mask) : item)
|
|
5849
6615
|
);
|
|
5850
6616
|
}
|
|
5851
6617
|
}
|
|
@@ -5892,18 +6658,53 @@ async function createProtection(options = {}) {
|
|
|
5892
6658
|
}
|
|
5893
6659
|
};
|
|
5894
6660
|
};
|
|
5895
|
-
const
|
|
6661
|
+
const peerOf = (request, hostArgs) => {
|
|
6662
|
+
if (typeof options.peerAddress !== "function") return void 0;
|
|
6663
|
+
try {
|
|
6664
|
+
return options.peerAddress(request?.[SOURCE_REQUEST] ?? request, ...hostArgs);
|
|
6665
|
+
} catch (err) {
|
|
6666
|
+
notify(onError, err, "onError");
|
|
6667
|
+
return void 0;
|
|
6668
|
+
}
|
|
6669
|
+
};
|
|
6670
|
+
let warnedNoPeer = false;
|
|
6671
|
+
const warnNoPeer = () => {
|
|
6672
|
+
if (warnedNoPeer) return;
|
|
6673
|
+
warnedNoPeer = true;
|
|
6674
|
+
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.";
|
|
6675
|
+
if (typeof onError === "function") notify(onError, new Error(message), "onError");
|
|
6676
|
+
else console.warn(message);
|
|
6677
|
+
};
|
|
6678
|
+
const clientByRequest = /* @__PURE__ */ new WeakMap();
|
|
6679
|
+
const clientForResponse = (request, hostArgs) => {
|
|
6680
|
+
const known = clientByRequest.get(request);
|
|
6681
|
+
if (known) return known;
|
|
6682
|
+
const client = resolveClientIp({
|
|
6683
|
+
peer: peerOf(request, hostArgs),
|
|
6684
|
+
headers: headerObject(request.headers),
|
|
6685
|
+
trustedProxy: options.trustedProxy
|
|
6686
|
+
});
|
|
6687
|
+
if (options.trustedProxy !== void 0 && client.source === "unavailable") warnNoPeer();
|
|
6688
|
+
return client;
|
|
6689
|
+
};
|
|
6690
|
+
const screenFetchRequest = async (request, hostArgs = []) => {
|
|
5896
6691
|
let result;
|
|
5897
6692
|
let shaped;
|
|
5898
6693
|
try {
|
|
5899
6694
|
shaped = await fromFetchRequest(request, {
|
|
6695
|
+
peer: peerOf(request, hostArgs),
|
|
5900
6696
|
trustedProxy: options.trustedProxy,
|
|
5901
6697
|
maxBodyBytes: options.maxBodyBytes
|
|
5902
6698
|
});
|
|
6699
|
+
if (shaped?._clientIp) {
|
|
6700
|
+
clientByRequest.set(request, shaped._clientIp);
|
|
6701
|
+
if (options.trustedProxy !== void 0 && shaped._clientIp.source === "unavailable") warnNoPeer();
|
|
6702
|
+
}
|
|
5903
6703
|
if (shaped?._bodyInspectionSkip) {
|
|
5904
6704
|
recordSkip("request", shaped._bodyInspectionSkip, { limit: options.maxBodyBytes ?? 1024 * 1024 });
|
|
5905
6705
|
}
|
|
5906
6706
|
result = engine.evaluate(shaped);
|
|
6707
|
+
recordEvaluationSkips(result);
|
|
5907
6708
|
} catch (err) {
|
|
5908
6709
|
notify(onError, err, "onError");
|
|
5909
6710
|
return { blocked: null, client: void 0 };
|
|
@@ -5952,14 +6753,20 @@ async function createProtection(options = {}) {
|
|
|
5952
6753
|
const screenResp = async (response, reqCtx) => {
|
|
5953
6754
|
const read = await readTextResponse(response, screenCap);
|
|
5954
6755
|
if (read.skip) {
|
|
5955
|
-
if (read.skip !== "not-a-response")
|
|
6756
|
+
if (read.skip !== "not-a-response") {
|
|
6757
|
+
recordSkip("response", read.skip, {
|
|
6758
|
+
status: response?.status,
|
|
6759
|
+
...read.encoding ? { encoding: read.encoding } : {},
|
|
6760
|
+
...read.charset ? { charset: read.charset } : {}
|
|
6761
|
+
});
|
|
6762
|
+
}
|
|
5956
6763
|
if (read.skip === "not-a-response") return response;
|
|
5957
6764
|
return hardenHeadersOnly(response, reqCtx);
|
|
5958
6765
|
}
|
|
5959
|
-
const text = read.text;
|
|
6766
|
+
const text = readJsonEscapes(read.text);
|
|
5960
6767
|
const r = screenText(text, { status: response.status, headers: headerObject(response.headers) }, reqCtx);
|
|
5961
|
-
if (r.verdict === "block") return leakResponse();
|
|
5962
|
-
if (r.verdict === "redact") return rebuildResponse(response, r.body, r.headers);
|
|
6768
|
+
if (r.verdict === "block" || r.verdict === "redact" && outgrowsCharset(read.ascii, r.body)) return leakResponse();
|
|
6769
|
+
if (r.verdict === "redact") return rebuildResponse(response, withBom(read.bom, r.body), r.headers);
|
|
5963
6770
|
return response;
|
|
5964
6771
|
};
|
|
5965
6772
|
const headerValueChanged = (was, value) => {
|
|
@@ -6021,7 +6828,8 @@ async function createProtection(options = {}) {
|
|
|
6021
6828
|
const hardenHeadersOnly = (response, reqCtx) => {
|
|
6022
6829
|
try {
|
|
6023
6830
|
const meta = { status: response.status, headers: headerObject(response.headers) };
|
|
6024
|
-
const r = screenText("", meta, reqCtx,
|
|
6831
|
+
const r = screenText("", meta, reqCtx, decidedWithoutBody);
|
|
6832
|
+
if (r.verdict === "block") return leakResponse();
|
|
6025
6833
|
if (r.verdict !== "redact" || !r.headers) return response;
|
|
6026
6834
|
if (!headersChanged(meta.headers, r.headers)) return response;
|
|
6027
6835
|
return rebuildResponse(response, response.body, r.headers);
|
|
@@ -6033,9 +6841,16 @@ async function createProtection(options = {}) {
|
|
|
6033
6841
|
const wrapNodeResponse = (res, reqCtx) => {
|
|
6034
6842
|
const origWrite = res.write.bind(res);
|
|
6035
6843
|
const origEnd = res.end.bind(res);
|
|
6844
|
+
const setHeaderNow = typeof res.setHeader === "function" ? res.setHeader.bind(res) : null;
|
|
6845
|
+
const removeHeaderNow = typeof res.removeHeader === "function" ? res.removeHeader.bind(res) : null;
|
|
6846
|
+
const sentAccessor = accessorOf(res, "headersSent");
|
|
6847
|
+
let sentValue = sentAccessor ? void 0 : res.headersSent;
|
|
6848
|
+
const reallySent = () => Boolean(sentAccessor ? sentAccessor.call(res) : sentValue);
|
|
6036
6849
|
const chunks = [];
|
|
6037
6850
|
let size = 0;
|
|
6038
6851
|
let overflow = false;
|
|
6852
|
+
let withheld = false;
|
|
6853
|
+
let headScreened = false;
|
|
6039
6854
|
const MAX = screenCap;
|
|
6040
6855
|
let hardened = false;
|
|
6041
6856
|
let answeredWithoutBody = false;
|
|
@@ -6043,7 +6858,7 @@ async function createProtection(options = {}) {
|
|
|
6043
6858
|
if (hardened) return null;
|
|
6044
6859
|
hardened = true;
|
|
6045
6860
|
try {
|
|
6046
|
-
if (
|
|
6861
|
+
if (reallySent() || !setHeaderNow) return null;
|
|
6047
6862
|
const set = typeof res.getHeaders === "function" ? res.getHeaders() : {};
|
|
6048
6863
|
const headers = effective && effective.headers ? { ...set, ...effective.headers } : set;
|
|
6049
6864
|
const status = effective && effective.status !== void 0 ? effective.status : res.statusCode;
|
|
@@ -6055,8 +6870,8 @@ async function createProtection(options = {}) {
|
|
|
6055
6870
|
if (!headerValueChanged(headers[name], value)) continue;
|
|
6056
6871
|
changed.set(name.toLowerCase(), value);
|
|
6057
6872
|
try {
|
|
6058
|
-
if (value === null || value === void 0)
|
|
6059
|
-
else
|
|
6873
|
+
if (value === null || value === void 0) removeHeaderNow?.(name);
|
|
6874
|
+
else setHeaderNow(name, value);
|
|
6060
6875
|
} catch {
|
|
6061
6876
|
}
|
|
6062
6877
|
}
|
|
@@ -6067,26 +6882,187 @@ async function createProtection(options = {}) {
|
|
|
6067
6882
|
}
|
|
6068
6883
|
};
|
|
6069
6884
|
const stillToAnswer = (rule) => !hardensWithoutBody(rule);
|
|
6070
|
-
|
|
6071
|
-
|
|
6885
|
+
const notYetAnswered = (rule, entry) => (!answeredWithoutBody || stillToAnswer(rule)) && !(headScreened && decidedByHeaders(rule, entry));
|
|
6886
|
+
let pendingHead = null;
|
|
6887
|
+
let sending = false;
|
|
6888
|
+
const origWriteHead = typeof res.writeHead === "function" ? res.writeHead.bind(res) : null;
|
|
6889
|
+
if (origWriteHead) {
|
|
6072
6890
|
res.writeHead = function(...args) {
|
|
6073
|
-
|
|
6074
|
-
|
|
6075
|
-
const
|
|
6076
|
-
|
|
6077
|
-
|
|
6078
|
-
|
|
6079
|
-
|
|
6080
|
-
|
|
6081
|
-
|
|
6082
|
-
|
|
6891
|
+
if (sending || reallySent()) return origWriteHead(...args);
|
|
6892
|
+
if (pendingHead) throw headersSentError("write");
|
|
6893
|
+
const judged = judgeWriteHead(args);
|
|
6894
|
+
if (judged) {
|
|
6895
|
+
res.statusCode = judged.statusCode;
|
|
6896
|
+
res.statusMessage = judged.statusMessage;
|
|
6897
|
+
mirrorHeaders(judged.headers);
|
|
6898
|
+
pendingHead = args;
|
|
6899
|
+
} else {
|
|
6900
|
+
pendingHead = args;
|
|
6901
|
+
if (typeof args[0] === "number") res.statusCode = args[0];
|
|
6902
|
+
if (typeof args[1] === "string") res.statusMessage = args[1];
|
|
6903
|
+
}
|
|
6904
|
+
return res;
|
|
6905
|
+
};
|
|
6906
|
+
}
|
|
6907
|
+
function judgeWriteHead(args) {
|
|
6908
|
+
const ServerResponse = nodeHttp?.ServerResponse;
|
|
6909
|
+
if (typeof ServerResponse !== "function") return null;
|
|
6910
|
+
const detached = new ServerResponse({ method: res.req?.method ?? "GET", httpVersionMajor: 1, httpVersionMinor: 1, headers: {} });
|
|
6911
|
+
if (res.statusMessage !== void 0) detached.statusMessage = res.statusMessage;
|
|
6912
|
+
for (const [name, value] of headerEntries(res)) detached.setHeader(name, value);
|
|
6913
|
+
detached.writeHead(...args);
|
|
6914
|
+
return { statusCode: detached.statusCode, statusMessage: detached.statusMessage, headers: headerEntries(detached) };
|
|
6915
|
+
}
|
|
6916
|
+
function headerEntries(target) {
|
|
6917
|
+
const names = typeof target.getRawHeaderNames === "function" ? target.getRawHeaderNames() : typeof target.getHeaderNames === "function" ? target.getHeaderNames() : [];
|
|
6918
|
+
return names.map((name) => [name, target.getHeader(name)]);
|
|
6919
|
+
}
|
|
6920
|
+
function mirrorHeaders(entries) {
|
|
6921
|
+
if (!setHeaderNow) return;
|
|
6922
|
+
for (const [name, value] of entries) {
|
|
6923
|
+
const current = res.getHeader?.(name);
|
|
6924
|
+
if (current === void 0 || headerValueChanged(current, value)) setHeaderNow(name, value);
|
|
6925
|
+
}
|
|
6926
|
+
}
|
|
6927
|
+
Object.defineProperty(res, "headersSent", {
|
|
6928
|
+
configurable: true,
|
|
6929
|
+
enumerable: true,
|
|
6930
|
+
get: () => pendingHead !== null || reallySent(),
|
|
6931
|
+
// A response-like object that tracks the flag itself keeps doing so.
|
|
6932
|
+
set: (value) => {
|
|
6933
|
+
if (!sentAccessor) sentValue = value;
|
|
6934
|
+
}
|
|
6935
|
+
});
|
|
6936
|
+
for (const [method, verb] of [["setHeader", "set"], ["removeHeader", "remove"], ["appendHeader", "append"]]) {
|
|
6937
|
+
const original = typeof res[method] === "function" ? res[method].bind(res) : null;
|
|
6938
|
+
if (!original) continue;
|
|
6939
|
+
res[method] = function(...args) {
|
|
6940
|
+
if (pendingHead) throw headersSentError(verb);
|
|
6941
|
+
return original(...args);
|
|
6942
|
+
};
|
|
6943
|
+
}
|
|
6944
|
+
if (typeof res.flushHeaders === "function") {
|
|
6945
|
+
const origFlushHeaders = res.flushHeaders.bind(res);
|
|
6946
|
+
res.flushHeaders = function() {
|
|
6947
|
+
const unread = screenUnreadHead();
|
|
6948
|
+
if (unread?.withhold) {
|
|
6949
|
+
sendWithheld();
|
|
6950
|
+
return void 0;
|
|
6951
|
+
}
|
|
6952
|
+
sendHead(unread);
|
|
6953
|
+
return origFlushHeaders();
|
|
6083
6954
|
};
|
|
6084
6955
|
}
|
|
6956
|
+
let hardenDone = false;
|
|
6957
|
+
let hardenedValues = null;
|
|
6958
|
+
const hardenOnce = () => {
|
|
6959
|
+
if (hardenDone) return hardenedValues;
|
|
6960
|
+
hardenDone = true;
|
|
6961
|
+
if (!pendingHead) {
|
|
6962
|
+
hardenedValues = hardenBeforeFlush();
|
|
6963
|
+
return hardenedValues;
|
|
6964
|
+
}
|
|
6965
|
+
const at = writeHeadHeaderIndex(pendingHead);
|
|
6966
|
+
hardenedValues = hardenBeforeFlush({
|
|
6967
|
+
status: typeof pendingHead[0] === "number" ? pendingHead[0] : void 0,
|
|
6968
|
+
headers: at === -1 ? null : writeHeadHeaderObject(pendingHead[at])
|
|
6969
|
+
});
|
|
6970
|
+
return hardenedValues;
|
|
6971
|
+
};
|
|
6972
|
+
const effectiveHead = () => {
|
|
6973
|
+
const set = typeof res.getHeaders === "function" ? { ...res.getHeaders() } : {};
|
|
6974
|
+
if (typeof res.getHeaders !== "function" && typeof res.getHeader === "function") {
|
|
6975
|
+
for (const name of ["content-type", "content-length"]) {
|
|
6976
|
+
const value = res.getHeader(name);
|
|
6977
|
+
if (value !== void 0) set[name] = value;
|
|
6978
|
+
}
|
|
6979
|
+
}
|
|
6980
|
+
if (!pendingHead) return { status: res.statusCode, headers: set };
|
|
6981
|
+
const at = writeHeadHeaderIndex(pendingHead);
|
|
6982
|
+
return {
|
|
6983
|
+
status: typeof pendingHead[0] === "number" ? pendingHead[0] : res.statusCode,
|
|
6984
|
+
headers: at === -1 ? set : { ...set, ...writeHeadHeaderObject(pendingHead[at]) }
|
|
6985
|
+
};
|
|
6986
|
+
};
|
|
6987
|
+
const screenUnreadHead = () => {
|
|
6988
|
+
if (headScreened) return void 0;
|
|
6989
|
+
headScreened = true;
|
|
6990
|
+
try {
|
|
6991
|
+
const head = effectiveHead();
|
|
6992
|
+
const r = screenText("", head, reqCtx, decidedByHeaders);
|
|
6993
|
+
if (r.verdict === "block") {
|
|
6994
|
+
if (!reallySent()) return { withhold: true };
|
|
6995
|
+
recordSkip("response", "headers-sent", { action: "block" });
|
|
6996
|
+
return void 0;
|
|
6997
|
+
}
|
|
6998
|
+
if (r.verdict !== "redact" || !r.headers) return void 0;
|
|
6999
|
+
const changed = /* @__PURE__ */ new Map();
|
|
7000
|
+
for (const [name, value] of Object.entries(r.headers)) {
|
|
7001
|
+
if (headerValueChanged(head.headers[name], value)) changed.set(name.toLowerCase(), value);
|
|
7002
|
+
}
|
|
7003
|
+
if (changed.size && reallySent()) {
|
|
7004
|
+
recordSkip("response", "headers-sent", { headers: [...changed.keys()] });
|
|
7005
|
+
return void 0;
|
|
7006
|
+
}
|
|
7007
|
+
return changed.size ? { headers: changed } : void 0;
|
|
7008
|
+
} catch (err) {
|
|
7009
|
+
notify(onError, err, "onError");
|
|
7010
|
+
return void 0;
|
|
7011
|
+
}
|
|
7012
|
+
};
|
|
7013
|
+
const sendHead = (changes) => {
|
|
7014
|
+
if (sending) return;
|
|
7015
|
+
const hardened2 = hardenOnce();
|
|
7016
|
+
sending = true;
|
|
7017
|
+
if (!pendingHead) {
|
|
7018
|
+
if (changes) applyToResponse(changes);
|
|
7019
|
+
return;
|
|
7020
|
+
}
|
|
7021
|
+
const args = [...pendingHead];
|
|
7022
|
+
pendingHead = null;
|
|
7023
|
+
const merged = new Map(hardened2 ?? []);
|
|
7024
|
+
for (const [name, value] of changes?.headers ?? []) merged.set(name, value);
|
|
7025
|
+
if (changes?.status !== void 0) {
|
|
7026
|
+
args[0] = changes.status;
|
|
7027
|
+
if (typeof args[1] === "string") args.splice(1, 1);
|
|
7028
|
+
}
|
|
7029
|
+
const headerAt = writeHeadHeaderIndex(args);
|
|
7030
|
+
if (headerAt !== -1 && merged.size) args[headerAt] = rewriteWriteHeadHeaders(args[headerAt], merged);
|
|
7031
|
+
applyToResponse({ headers: merged });
|
|
7032
|
+
origWriteHead(...args);
|
|
7033
|
+
};
|
|
7034
|
+
const sendWithheld = (cb) => {
|
|
7035
|
+
const body = JSON.stringify({ error: "Response withheld by Patchstack (sensitive data detected)" });
|
|
7036
|
+
const headers = /* @__PURE__ */ new Map();
|
|
7037
|
+
for (const name of Object.keys(effectiveHead().headers)) headers.set(name.toLowerCase(), null);
|
|
7038
|
+
headers.set("content-type", "application/json");
|
|
7039
|
+
headers.set("content-length", String(Buffer.byteLength(body)));
|
|
7040
|
+
headers.set("transfer-encoding", null);
|
|
7041
|
+
withheld = true;
|
|
7042
|
+
sendHead({ status: 500, headers });
|
|
7043
|
+
return origEnd(body, cb);
|
|
7044
|
+
};
|
|
7045
|
+
const applyToResponse = (changes) => {
|
|
7046
|
+
if (changes.status !== void 0) res.statusCode = changes.status;
|
|
7047
|
+
for (const [name, value] of changes.headers ?? []) {
|
|
7048
|
+
try {
|
|
7049
|
+
if (value === null || value === void 0) removeHeaderNow?.(name);
|
|
7050
|
+
else setHeaderNow?.(name, value);
|
|
7051
|
+
} catch {
|
|
7052
|
+
}
|
|
7053
|
+
}
|
|
7054
|
+
};
|
|
6085
7055
|
const collect = (chunk, enc) => {
|
|
6086
7056
|
if (chunk == null) return;
|
|
6087
7057
|
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, typeof enc === "string" ? enc : "utf8");
|
|
6088
7058
|
size += buf.length;
|
|
6089
7059
|
if (size > MAX) {
|
|
7060
|
+
const unread = screenUnreadHead();
|
|
7061
|
+
if (unread?.withhold) {
|
|
7062
|
+
sendWithheld();
|
|
7063
|
+
return;
|
|
7064
|
+
}
|
|
7065
|
+
sendHead(unread);
|
|
6090
7066
|
for (const c of chunks) origWrite(c);
|
|
6091
7067
|
chunks.length = 0;
|
|
6092
7068
|
origWrite(buf);
|
|
@@ -6097,7 +7073,11 @@ async function createProtection(options = {}) {
|
|
|
6097
7073
|
chunks.push(buf);
|
|
6098
7074
|
};
|
|
6099
7075
|
res.write = function(chunk, enc, cb) {
|
|
6100
|
-
|
|
7076
|
+
if (withheld) {
|
|
7077
|
+
if (typeof enc === "function") enc();
|
|
7078
|
+
else if (typeof cb === "function") cb();
|
|
7079
|
+
return true;
|
|
7080
|
+
}
|
|
6101
7081
|
if (overflow) return origWrite(chunk, enc, cb);
|
|
6102
7082
|
collect(chunk, enc);
|
|
6103
7083
|
if (typeof enc === "function") enc();
|
|
@@ -6105,7 +7085,6 @@ async function createProtection(options = {}) {
|
|
|
6105
7085
|
return true;
|
|
6106
7086
|
};
|
|
6107
7087
|
res.end = function(chunk, enc, cb) {
|
|
6108
|
-
hardenBeforeFlush();
|
|
6109
7088
|
if (typeof chunk === "function") {
|
|
6110
7089
|
cb = chunk;
|
|
6111
7090
|
chunk = void 0;
|
|
@@ -6114,74 +7093,90 @@ async function createProtection(options = {}) {
|
|
|
6114
7093
|
cb = enc;
|
|
6115
7094
|
enc = void 0;
|
|
6116
7095
|
}
|
|
7096
|
+
if (withheld) {
|
|
7097
|
+
if (typeof cb === "function") cb();
|
|
7098
|
+
return res;
|
|
7099
|
+
}
|
|
6117
7100
|
if (overflow) {
|
|
6118
7101
|
if (chunk != null) origWrite(chunk, enc);
|
|
6119
7102
|
return origEnd(cb);
|
|
6120
7103
|
}
|
|
6121
7104
|
collect(chunk, enc);
|
|
7105
|
+
if (withheld) {
|
|
7106
|
+
if (typeof cb === "function") cb();
|
|
7107
|
+
return res;
|
|
7108
|
+
}
|
|
6122
7109
|
if (overflow) return origEnd(cb);
|
|
7110
|
+
hardenOnce();
|
|
7111
|
+
const passThrough = (changes) => {
|
|
7112
|
+
if (changes?.withhold) return sendWithheld(cb);
|
|
7113
|
+
sendHead(changes);
|
|
7114
|
+
for (const c of chunks) origWrite(c);
|
|
7115
|
+
return origEnd(cb);
|
|
7116
|
+
};
|
|
6123
7117
|
const buffer = Buffer.concat(chunks);
|
|
6124
|
-
|
|
7118
|
+
const head = effectiveHead();
|
|
7119
|
+
let ct = head.headers["content-type"];
|
|
6125
7120
|
if (Array.isArray(ct)) ct = ct[0];
|
|
6126
7121
|
const kind = screenableContentType(ct);
|
|
6127
7122
|
if (kind === "skip" || kind === "sniff" && looksBinary(buffer)) {
|
|
6128
7123
|
recordSkip("response", kind === "skip" ? baseContentType(ct) === "text/event-stream" ? "live-stream" : "non-text-content-type" : "binary-body");
|
|
6129
|
-
|
|
6130
|
-
|
|
7124
|
+
return passThrough(screenUnreadHead());
|
|
7125
|
+
}
|
|
7126
|
+
const codings = declaredCodings(head.headers["content-encoding"]);
|
|
7127
|
+
if (codings.length > 0 && stillEncoded(buffer)) {
|
|
7128
|
+
recordSkip("response", "encoded-body", { encoding: codings.join(", ") });
|
|
7129
|
+
return passThrough(screenUnreadHead());
|
|
7130
|
+
}
|
|
7131
|
+
const reading = charsetReading(ct, buffer);
|
|
7132
|
+
if (reading.skip) {
|
|
7133
|
+
recordSkip("response", "unsupported-charset", { charset: reading.skip });
|
|
7134
|
+
return passThrough(screenUnreadHead());
|
|
6131
7135
|
}
|
|
6132
|
-
const text = buffer.toString("utf8");
|
|
7136
|
+
const text = readJsonEscapes(buffer.toString("utf8", reading.bom ? 3 : 0));
|
|
6133
7137
|
let r;
|
|
6134
7138
|
try {
|
|
6135
|
-
r = screenText(
|
|
6136
|
-
text,
|
|
6137
|
-
{ status: res.statusCode, headers: res.getHeaders ? res.getHeaders() : {} },
|
|
6138
|
-
reqCtx,
|
|
6139
|
-
answeredWithoutBody ? stillToAnswer : void 0
|
|
6140
|
-
);
|
|
7139
|
+
r = screenText(text, head, reqCtx, notYetAnswered);
|
|
6141
7140
|
} catch (err) {
|
|
6142
7141
|
notify(onError, err, "onError");
|
|
6143
|
-
|
|
6144
|
-
|
|
7142
|
+
return passThrough();
|
|
7143
|
+
}
|
|
7144
|
+
if (r.verdict === "redact" && outgrowsCharset(reading.ascii, r.body)) r = { verdict: "block" };
|
|
7145
|
+
if (r.verdict === "redact") r = { ...r, body: withBom(reading.bom, r.body) };
|
|
7146
|
+
if (r.verdict !== "block" && r.verdict !== "redact") return passThrough();
|
|
7147
|
+
if (reallySent() && res.chunkedEncoding !== true) {
|
|
7148
|
+
notify(onError, new Error("Patchstack: response withheld \u2014 its headers were sent before the body could be screened"), "onError");
|
|
7149
|
+
res.destroy?.();
|
|
7150
|
+
return res;
|
|
6145
7151
|
}
|
|
6146
7152
|
if (r.verdict === "block") {
|
|
6147
|
-
|
|
6148
|
-
|
|
6149
|
-
res
|
|
6150
|
-
} catch {
|
|
7153
|
+
if (reallySent()) {
|
|
7154
|
+
res.destroy?.();
|
|
7155
|
+
return res;
|
|
6151
7156
|
}
|
|
6152
|
-
return
|
|
7157
|
+
return sendWithheld(cb);
|
|
6153
7158
|
}
|
|
6154
|
-
|
|
6155
|
-
|
|
6156
|
-
|
|
6157
|
-
|
|
7159
|
+
const changed = /* @__PURE__ */ new Map();
|
|
7160
|
+
if (r.headers) {
|
|
7161
|
+
for (const [name, value] of Object.entries(r.headers)) {
|
|
7162
|
+
if (name.toLowerCase() === "content-length") continue;
|
|
7163
|
+
if (!headerValueChanged(head.headers[name], value)) continue;
|
|
7164
|
+
changed.set(name.toLowerCase(), value);
|
|
6158
7165
|
}
|
|
6159
|
-
|
|
6160
|
-
|
|
6161
|
-
|
|
6162
|
-
|
|
6163
|
-
|
|
6164
|
-
|
|
6165
|
-
|
|
6166
|
-
|
|
6167
|
-
}
|
|
6168
|
-
} else if (Array.isArray(value)) {
|
|
6169
|
-
try {
|
|
6170
|
-
res.setHeader(name, value);
|
|
6171
|
-
} catch {
|
|
6172
|
-
}
|
|
6173
|
-
} else if (typeof value === "string" && current[name] !== value) {
|
|
6174
|
-
try {
|
|
6175
|
-
res.setHeader(name, value);
|
|
6176
|
-
} catch {
|
|
6177
|
-
}
|
|
6178
|
-
}
|
|
6179
|
-
}
|
|
7166
|
+
}
|
|
7167
|
+
if (!reallySent()) {
|
|
7168
|
+
const declared = head.headers["content-length"];
|
|
7169
|
+
if (declared === void 0) {
|
|
7170
|
+
changed.set("content-length", null);
|
|
7171
|
+
} else {
|
|
7172
|
+
changed.set("content-length", String(Buffer.byteLength(r.body)));
|
|
7173
|
+
changed.set("transfer-encoding", null);
|
|
6180
7174
|
}
|
|
6181
|
-
|
|
7175
|
+
sendHead({ headers: changed });
|
|
7176
|
+
} else if (changed.size) {
|
|
7177
|
+
recordSkip("response", "headers-sent", { headers: [...changed.keys()] });
|
|
6182
7178
|
}
|
|
6183
|
-
|
|
6184
|
-
return origEnd(cb);
|
|
7179
|
+
return origEnd(r.body, cb);
|
|
6185
7180
|
};
|
|
6186
7181
|
};
|
|
6187
7182
|
const allow = new Set(allowHosts.map((h) => String(h).toLowerCase()));
|
|
@@ -6255,21 +7250,15 @@ async function createProtection(options = {}) {
|
|
|
6255
7250
|
// .fetch(), and by the Supabase guard on its forwarded upstream response.
|
|
6256
7251
|
// A standalone response screen with no request phase of its own — the Supabase guard's forwarded
|
|
6257
7252
|
// upstream response. It resolves once here, which is the only resolution for this call.
|
|
6258
|
-
screenResponse: (response, request) => screenResp(
|
|
6259
|
-
response,
|
|
6260
|
-
request ? reqContextFromFetch(
|
|
6261
|
-
request,
|
|
6262
|
-
resolveClientIp({ headers: headerObject(request.headers), trustedProxy: options.trustedProxy })
|
|
6263
|
-
) : void 0
|
|
6264
|
-
),
|
|
7253
|
+
screenResponse: (response, request, ...hostArgs) => screenResp(response, request ? reqContextFromFetch(request, clientForResponse(request, hostArgs)) : void 0),
|
|
6265
7254
|
// (request) => Response | null (null = allow, caller proceeds). Request phase only.
|
|
6266
7255
|
fetchGuard() {
|
|
6267
|
-
return async (request) => (await screenFetchRequest(request)).blocked;
|
|
7256
|
+
return async (request, ...hostArgs) => (await screenFetchRequest(request, hostArgs)).blocked;
|
|
6268
7257
|
},
|
|
6269
7258
|
// Wrap a fetch handler: screens the request, then the response (redact/block).
|
|
6270
7259
|
fetch(handler) {
|
|
6271
7260
|
return async (request, ...rest) => {
|
|
6272
|
-
const { blocked, client } = await screenFetchRequest(request);
|
|
7261
|
+
const { blocked, client } = await screenFetchRequest(request, rest);
|
|
6273
7262
|
if (blocked) return blocked;
|
|
6274
7263
|
const response = await handler(request, ...rest);
|
|
6275
7264
|
return screenResp(response, reqContextFromFetch(request, client));
|
|
@@ -6283,6 +7272,7 @@ async function createProtection(options = {}) {
|
|
|
6283
7272
|
const { shaped, client } = shapeExpressRequest(req);
|
|
6284
7273
|
try {
|
|
6285
7274
|
result = engine.evaluate(shaped);
|
|
7275
|
+
recordEvaluationSkips(result);
|
|
6286
7276
|
} catch (err) {
|
|
6287
7277
|
notify(onError, err, "onError");
|
|
6288
7278
|
if (exprOptions.screenResponses) wrapNodeResponse(res, reqContextFromNode(req, client));
|
|
@@ -6317,33 +7307,25 @@ async function createProtection(options = {}) {
|
|
|
6317
7307
|
screenNodeRequest(req, res, next, "", req.body);
|
|
6318
7308
|
return;
|
|
6319
7309
|
}
|
|
6320
|
-
|
|
6321
|
-
|
|
6322
|
-
|
|
6323
|
-
|
|
6324
|
-
size += chunk.length;
|
|
6325
|
-
if (size > maxBytes) {
|
|
6326
|
-
overflow = true;
|
|
7310
|
+
readBodyPrefix(req, maxBytes, (err, read) => {
|
|
7311
|
+
if (err) {
|
|
7312
|
+
notify(onError, err, "onError");
|
|
7313
|
+
next();
|
|
6327
7314
|
return;
|
|
6328
7315
|
}
|
|
6329
|
-
|
|
6330
|
-
|
|
6331
|
-
|
|
6332
|
-
notify(onError, err, "onError");
|
|
6333
|
-
next();
|
|
6334
|
-
});
|
|
6335
|
-
req.on("end", () => {
|
|
6336
|
-
if (overflow) recordSkip("request", "body-cap", { bytes: size, limit: maxBytes });
|
|
6337
|
-
screenNodeRequest(req, res, next, overflow ? "" : Buffer.concat(chunks).toString("utf8"));
|
|
7316
|
+
if (read.overflow) recordSkip("request", "body-cap", { bytes: read.size, limit: maxBytes });
|
|
7317
|
+
if (read.failed) recordSkip("request", "read-failed", { bytes: read.size });
|
|
7318
|
+
screenNodeRequest(req, res, next, read.text, void 0, read.overflow || read.failed);
|
|
6338
7319
|
});
|
|
6339
7320
|
};
|
|
6340
|
-
function screenNodeRequest(req, res, next, rawBody, parsedBody) {
|
|
7321
|
+
function screenNodeRequest(req, res, next, rawBody, parsedBody, truncated = false) {
|
|
6341
7322
|
let shaped;
|
|
6342
7323
|
let result;
|
|
6343
7324
|
try {
|
|
6344
7325
|
shaped = fromNodeRequest(req, rawBody, { trustedProxy: options.trustedProxy });
|
|
6345
7326
|
if (parsedBody !== void 0 && parsedBody !== null) shaped.body = parsedBody;
|
|
6346
7327
|
result = engine.evaluate(shaped);
|
|
7328
|
+
recordEvaluationSkips(result);
|
|
6347
7329
|
} catch (err) {
|
|
6348
7330
|
notify(onError, err, "onError");
|
|
6349
7331
|
return next();
|
|
@@ -6362,7 +7344,7 @@ async function createProtection(options = {}) {
|
|
|
6362
7344
|
}
|
|
6363
7345
|
},
|
|
6364
7346
|
() => {
|
|
6365
|
-
if (req.body === void 0) req.body = shaped.body;
|
|
7347
|
+
if (req.body === void 0 && !truncated) req.body = shaped.body;
|
|
6366
7348
|
if (nodeOptions.screenResponses) wrapNodeResponse(res, reqContextFromNode(req, shaped?._clientIp));
|
|
6367
7349
|
next();
|
|
6368
7350
|
},
|
|
@@ -6394,6 +7376,7 @@ async function createProtection(options = {}) {
|
|
|
6394
7376
|
notify(onError, err, "onError");
|
|
6395
7377
|
}
|
|
6396
7378
|
}
|
|
7379
|
+
let recovery = null;
|
|
6397
7380
|
const runRefreshTick = async () => {
|
|
6398
7381
|
if (reporter) {
|
|
6399
7382
|
try {
|
|
@@ -6410,23 +7393,38 @@ async function createProtection(options = {}) {
|
|
|
6410
7393
|
});
|
|
6411
7394
|
mode = resolveMode(options, next);
|
|
6412
7395
|
applyBundle(next);
|
|
7396
|
+
noteRuleSource(next.source);
|
|
7397
|
+
if (next.source?.ok !== false) recovery?.stop();
|
|
6413
7398
|
await applyAndAcknowledge(next.source?.origin, declaredState);
|
|
6414
7399
|
if (detections) detections.setRulesEtag((await store.read())?.etag ?? null);
|
|
6415
7400
|
return next.source ?? { ok: true };
|
|
6416
7401
|
};
|
|
7402
|
+
const refreshTick = serialise(runRefreshTick);
|
|
6417
7403
|
if (live) {
|
|
6418
|
-
protection.refresh = () =>
|
|
6419
|
-
protection.refreshHandler = () => makeRefreshHandler(
|
|
7404
|
+
protection.refresh = () => refreshTick();
|
|
7405
|
+
protection.refreshHandler = () => makeRefreshHandler(refreshTick, refreshSecret);
|
|
6420
7406
|
}
|
|
6421
|
-
const loop = options.refreshMs > 0 && live ? startRefresh(
|
|
7407
|
+
const loop = options.refreshMs > 0 && live ? startRefresh(refreshTick, { refreshMs: options.refreshMs, onError }) : null;
|
|
7408
|
+
const canAsk = Boolean(options.token || pulseAuth);
|
|
7409
|
+
recovery = live && canAsk && !loop && ruleSource.ok === false ? startRecovery(refreshTick, { onError }) : null;
|
|
6422
7410
|
protection.stop = () => {
|
|
6423
7411
|
loop?.stop();
|
|
7412
|
+
recovery?.stop();
|
|
7413
|
+
protection.uninstallEgress?.();
|
|
6424
7414
|
const outstanding = [firewallLog?.stop(), detections?.stop()].filter(
|
|
6425
7415
|
(wait) => wait && typeof wait.then === "function"
|
|
6426
7416
|
);
|
|
6427
7417
|
return Promise.all(outstanding).then(() => void 0);
|
|
6428
7418
|
};
|
|
6429
|
-
protection.stopRefresh =
|
|
7419
|
+
protection.stopRefresh = () => {
|
|
7420
|
+
loop?.stop();
|
|
7421
|
+
recovery?.stop();
|
|
7422
|
+
return Promise.resolve();
|
|
7423
|
+
};
|
|
7424
|
+
Object.defineProperty(protection, "ruleSource", {
|
|
7425
|
+
get: () => ({ ...ruleSource }),
|
|
7426
|
+
enumerable: true
|
|
7427
|
+
});
|
|
6430
7428
|
Object.defineProperty(protection, "detectionReporting", {
|
|
6431
7429
|
get: () => detectionReporting,
|
|
6432
7430
|
enumerable: true
|
|
@@ -6435,8 +7433,22 @@ async function createProtection(options = {}) {
|
|
|
6435
7433
|
get: () => detections ? () => detections.health() : void 0,
|
|
6436
7434
|
enumerable: true
|
|
6437
7435
|
});
|
|
7436
|
+
Object.defineProperty(protection, "blockLogHealth", {
|
|
7437
|
+
get: () => firewallLog ? () => firewallLog.health() : void 0,
|
|
7438
|
+
enumerable: true
|
|
7439
|
+
});
|
|
6438
7440
|
return protection;
|
|
6439
7441
|
}
|
|
7442
|
+
function accessorOf(obj, name) {
|
|
7443
|
+
for (let o = obj; o; o = Object.getPrototypeOf(o)) {
|
|
7444
|
+
const d = Object.getOwnPropertyDescriptor(o, name);
|
|
7445
|
+
if (d) return typeof d.get === "function" ? d.get : null;
|
|
7446
|
+
}
|
|
7447
|
+
return null;
|
|
7448
|
+
}
|
|
7449
|
+
function headersSentError(verb) {
|
|
7450
|
+
return Object.assign(new Error(`Cannot ${verb} headers after they are sent to the client`), { code: "ERR_HTTP_HEADERS_SENT" });
|
|
7451
|
+
}
|
|
6440
7452
|
function resolveMode(options, bundle) {
|
|
6441
7453
|
const env = typeof process !== "undefined" ? process.env?.PATCHSTACK_MODE : void 0;
|
|
6442
7454
|
if (env === "block" || env === "dry-run") return env;
|
|
@@ -6499,6 +7511,14 @@ function screenableContentType(ct) {
|
|
|
6499
7511
|
if (base === "application/octet-stream") return "sniff";
|
|
6500
7512
|
return "skip";
|
|
6501
7513
|
}
|
|
7514
|
+
function prefilterText(body, headers) {
|
|
7515
|
+
const values = [];
|
|
7516
|
+
for (const value of Object.values(headers ?? {})) {
|
|
7517
|
+
if (Array.isArray(value)) values.push(...value.map(String));
|
|
7518
|
+
else if (value !== void 0 && value !== null) values.push(String(value));
|
|
7519
|
+
}
|
|
7520
|
+
return [body, ...values].join("\n").toLowerCase();
|
|
7521
|
+
}
|
|
6502
7522
|
function looksBinary(bytes) {
|
|
6503
7523
|
const n = Math.min(bytes.length, 512);
|
|
6504
7524
|
let ctrl = 0;
|
|
@@ -6509,11 +7529,72 @@ function looksBinary(bytes) {
|
|
|
6509
7529
|
}
|
|
6510
7530
|
return n > 0 && ctrl / n > 0.1;
|
|
6511
7531
|
}
|
|
7532
|
+
function declaredCodings(value) {
|
|
7533
|
+
const raw = Array.isArray(value) ? value.join(",") : String(value ?? "");
|
|
7534
|
+
return raw.toLowerCase().split(",").map((c) => c.trim()).filter((c) => c !== "" && c !== "identity");
|
|
7535
|
+
}
|
|
7536
|
+
function stillEncoded(bytes) {
|
|
7537
|
+
if (looksBinary(bytes)) return true;
|
|
7538
|
+
try {
|
|
7539
|
+
new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
7540
|
+
return false;
|
|
7541
|
+
} catch {
|
|
7542
|
+
return true;
|
|
7543
|
+
}
|
|
7544
|
+
}
|
|
7545
|
+
var UTF8_CHARSETS = /* @__PURE__ */ new Set(["utf-8", "utf8", "unicode-1-1-utf-8", "us-ascii", "ascii"]);
|
|
7546
|
+
var WIDE_CHARSET = /^(?:utf-?(?:16|32)|ucs-?[24]|unicode|iso-10646)/;
|
|
7547
|
+
function declaredCharset(ct) {
|
|
7548
|
+
const m = /;\s*charset\s*=\s*(?:"([^"]*)"|([^;\s]*))/i.exec(String(ct ?? ""));
|
|
7549
|
+
const label = m ? (m[1] ?? m[2] ?? "").trim().toLowerCase() : "";
|
|
7550
|
+
return label || null;
|
|
7551
|
+
}
|
|
7552
|
+
function charsetReading(ct, bytes) {
|
|
7553
|
+
if (bytes.length >= 3 && bytes[0] === 239 && bytes[1] === 187 && bytes[2] === 191) return { ascii: false, bom: true };
|
|
7554
|
+
if (bytes.length >= 2 && bytes[0] === 255 && bytes[1] === 254) return { skip: "utf-16le" };
|
|
7555
|
+
if (bytes.length >= 2 && bytes[0] === 254 && bytes[1] === 255) return { skip: "utf-16be" };
|
|
7556
|
+
const charset = declaredCharset(ct);
|
|
7557
|
+
if (!charset || UTF8_CHARSETS.has(charset)) return { ascii: false };
|
|
7558
|
+
if (WIDE_CHARSET.test(charset)) return { skip: charset };
|
|
7559
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
7560
|
+
const b = bytes[i];
|
|
7561
|
+
if (b >= 128 || b === 0 || b === 27) return { skip: charset };
|
|
7562
|
+
}
|
|
7563
|
+
return { ascii: true };
|
|
7564
|
+
}
|
|
7565
|
+
function decodeForScreening(ct, bytes) {
|
|
7566
|
+
const reading = charsetReading(ct, bytes);
|
|
7567
|
+
if (reading.skip) return { skip: "unsupported-charset", charset: reading.skip };
|
|
7568
|
+
return { text: new TextDecoder().decode(bytes), ascii: reading.ascii, bom: reading.bom === true };
|
|
7569
|
+
}
|
|
7570
|
+
function withBom(bom, body) {
|
|
7571
|
+
return bom ? "\uFEFF" + body : body;
|
|
7572
|
+
}
|
|
7573
|
+
function outgrowsCharset(ascii, body) {
|
|
7574
|
+
return ascii === true && /[^\x00-\x7f]/.test(body);
|
|
7575
|
+
}
|
|
7576
|
+
var KEPT_ESCAPES = /* @__PURE__ */ new Set([34, 92, 60, 62, 38, 39]);
|
|
7577
|
+
function readJsonEscapes(text) {
|
|
7578
|
+
if (!text.includes("\\u") && !text.includes("\\/")) return text;
|
|
7579
|
+
try {
|
|
7580
|
+
JSON.parse(text);
|
|
7581
|
+
} catch {
|
|
7582
|
+
return text;
|
|
7583
|
+
}
|
|
7584
|
+
return text.replace(/\\(?:u([0-9a-fA-F]{4})|([\s\S]))/g, (whole, hex, ch, offset) => {
|
|
7585
|
+
if (hex === void 0) return ch === "/" && text[offset - 1] !== "<" ? "/" : whole;
|
|
7586
|
+
const code = parseInt(hex, 16);
|
|
7587
|
+
if (code < 32 || code > 126 || KEPT_ESCAPES.has(code)) return whole;
|
|
7588
|
+
return String.fromCharCode(code);
|
|
7589
|
+
});
|
|
7590
|
+
}
|
|
6512
7591
|
async function readTextResponse(response, cap = DEFAULT_SCREEN_CAP) {
|
|
6513
7592
|
if (!response || typeof response.clone !== "function") return { skip: "not-a-response" };
|
|
6514
7593
|
const ct = response.headers?.get?.("content-type") || "";
|
|
6515
7594
|
const kind = screenableContentType(ct);
|
|
6516
7595
|
if (kind === "skip") return { skip: baseContentType(ct) === "text/event-stream" ? "live-stream" : "non-text-content-type" };
|
|
7596
|
+
const codings = declaredCodings(response.headers?.get?.("content-encoding"));
|
|
7597
|
+
const encodedSkip = () => ({ skip: "encoded-body", encoding: codings.join(", ") });
|
|
6517
7598
|
const sniff = kind === "sniff";
|
|
6518
7599
|
const len = Number(response.headers?.get?.("content-length") || 0);
|
|
6519
7600
|
if (len && len > cap) return { skip: "body-cap" };
|
|
@@ -6557,13 +7638,22 @@ async function readTextResponse(response, cap = DEFAULT_SCREEN_CAP) {
|
|
|
6557
7638
|
} finally {
|
|
6558
7639
|
reader.releaseLock();
|
|
6559
7640
|
}
|
|
7641
|
+
const bytes = concatBytes(chunks, size);
|
|
7642
|
+
if (codings.length > 0 && stillEncoded(bytes)) return encodedSkip();
|
|
6560
7643
|
try {
|
|
6561
|
-
return
|
|
7644
|
+
return decodeForScreening(ct, bytes);
|
|
6562
7645
|
} catch {
|
|
6563
7646
|
return { skip: "decode-failed" };
|
|
6564
7647
|
}
|
|
6565
7648
|
}
|
|
6566
7649
|
try {
|
|
7650
|
+
if (codings.length > 0 || typeof clone.arrayBuffer === "function") {
|
|
7651
|
+
const bytes = new Uint8Array(await clone.arrayBuffer());
|
|
7652
|
+
if (bytes.byteLength > cap) return { skip: "body-cap" };
|
|
7653
|
+
if (codings.length > 0 && stillEncoded(bytes)) return encodedSkip();
|
|
7654
|
+
if (codings.length === 0 && sniff && looksBinary(bytes)) return { skip: "binary-body" };
|
|
7655
|
+
return decodeForScreening(ct, bytes);
|
|
7656
|
+
}
|
|
6567
7657
|
const text = await clone.text();
|
|
6568
7658
|
if (text.length > cap) return { skip: "body-cap" };
|
|
6569
7659
|
if (sniff && looksBinary(new TextEncoder().encode(text.slice(0, 512)))) return { skip: "binary-body" };
|
|
@@ -6593,8 +7683,9 @@ function headerObject(headers) {
|
|
|
6593
7683
|
function hasSpanMutations(rule) {
|
|
6594
7684
|
let found = false;
|
|
6595
7685
|
const walk = (conds) => {
|
|
6596
|
-
for (const c of conds
|
|
7686
|
+
for (const c of Array.isArray(conds) ? conds : []) {
|
|
6597
7687
|
if (found) return;
|
|
7688
|
+
if (!c || typeof c !== "object") continue;
|
|
6598
7689
|
if (Array.isArray(c.rules)) walk(c.rules);
|
|
6599
7690
|
const isSpan = c.match && (c.match.type === "regex" || c.match.type === "contains" || c.match.type === "stripos");
|
|
6600
7691
|
if (isSpan && Array.isArray(c.mutations) && c.mutations.length) found = true;
|
|
@@ -6603,10 +7694,34 @@ function hasSpanMutations(rule) {
|
|
|
6603
7694
|
walk(rule.rule_v2);
|
|
6604
7695
|
return found;
|
|
6605
7696
|
}
|
|
6606
|
-
function
|
|
7697
|
+
function redactorScope(parameter) {
|
|
7698
|
+
if (parameter === void 0 || parameter === null || parameterProblem(parameter) !== null) return null;
|
|
7699
|
+
const scope = { body: false, allHeaders: false, headers: /* @__PURE__ */ new Set() };
|
|
7700
|
+
for (const name of Array.isArray(parameter) ? parameter : [parameter]) {
|
|
7701
|
+
if (typeof name !== "string" || name === "") return null;
|
|
7702
|
+
if (name.startsWith("response.header.")) scope.headers.add(name.slice("response.header.".length).toLowerCase());
|
|
7703
|
+
else if (name === "response.headers") scope.allHeaders = true;
|
|
7704
|
+
else if (name === "response.body" || name === "response.status" || BROAD_SCOPE_SOURCES.has(name.split(".")[0])) {
|
|
7705
|
+
scope.body = true;
|
|
7706
|
+
scope.allHeaders = true;
|
|
7707
|
+
} else return null;
|
|
7708
|
+
}
|
|
7709
|
+
return scope;
|
|
7710
|
+
}
|
|
7711
|
+
var BROAD_SCOPE_SOURCES = /* @__PURE__ */ new Set(["get", "post", "request", "cookie", "files", "server", "raw", "all"]);
|
|
7712
|
+
function masksHeader(redactor, name) {
|
|
7713
|
+
return redactor.scope.allHeaders || redactor.scope.headers.has(name);
|
|
7714
|
+
}
|
|
7715
|
+
function extractRedactors(rule, onUnscoped) {
|
|
6607
7716
|
const out = [];
|
|
7717
|
+
const scoped = (c) => {
|
|
7718
|
+
const scope = redactorScope(c.parameter);
|
|
7719
|
+
if (scope === null) onUnscoped?.(c.parameter);
|
|
7720
|
+
return scope;
|
|
7721
|
+
};
|
|
6608
7722
|
const walk = (conds) => {
|
|
6609
|
-
for (const c of conds
|
|
7723
|
+
for (const c of Array.isArray(conds) ? conds : []) {
|
|
7724
|
+
if (!c || typeof c !== "object") continue;
|
|
6610
7725
|
if (Array.isArray(c.rules)) walk(c.rules);
|
|
6611
7726
|
const m = c.match;
|
|
6612
7727
|
if (!m) continue;
|
|
@@ -6614,16 +7729,21 @@ function extractRedactors(rule) {
|
|
|
6614
7729
|
const safe = safeRegExp(m.value);
|
|
6615
7730
|
if (safe) {
|
|
6616
7731
|
const flags = safe.flags.includes("g") ? safe.flags : safe.flags + "g";
|
|
7732
|
+
const scope = scoped(c);
|
|
6617
7733
|
try {
|
|
6618
|
-
out.push({ re: new RegExp(safe.source, flags) });
|
|
7734
|
+
if (scope) out.push({ re: new RegExp(safe.source, flags), scope });
|
|
6619
7735
|
} catch {
|
|
6620
7736
|
}
|
|
6621
7737
|
}
|
|
6622
7738
|
} else if ((m.type === "contains" || m.type === "stripos") && m.value != null) {
|
|
6623
|
-
|
|
7739
|
+
const scope = scoped(c);
|
|
7740
|
+
if (scope) out.push({ literal: String(m.value), scope });
|
|
6624
7741
|
} else if (m.type === "jwt_claim_equals" && typeof m.claim === "string") {
|
|
6625
|
-
|
|
6626
|
-
|
|
7742
|
+
const scope = scoped(c);
|
|
7743
|
+
if (scope) out.push({ jwtClaim: { claim: m.claim, value: String(m.value ?? "") }, scope });
|
|
7744
|
+
} else if (m.type === "array_key_value" && m.match && Array.isArray(c.parameter) && !readsBody(c.parameter) && redactorScope(c.parameter) === null) {
|
|
7745
|
+
onUnscoped?.(c.parameter);
|
|
7746
|
+
} else if (m.type === "array_key_value" && m.match && readsBody(c.parameter)) {
|
|
6627
7747
|
const keys = Array.isArray(m.key) ? m.key : [m.key];
|
|
6628
7748
|
for (const key of keys) {
|
|
6629
7749
|
out.push({ jsonPath: String(key).split("."), condition: m.match });
|
|
@@ -6637,6 +7757,110 @@ function extractRedactors(rule) {
|
|
|
6637
7757
|
function htmlEscape(str) {
|
|
6638
7758
|
return String(str).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
6639
7759
|
}
|
|
7760
|
+
function isJson(text) {
|
|
7761
|
+
try {
|
|
7762
|
+
JSON.parse(text);
|
|
7763
|
+
return true;
|
|
7764
|
+
} catch {
|
|
7765
|
+
return false;
|
|
7766
|
+
}
|
|
7767
|
+
}
|
|
7768
|
+
var JSON_STRING = 1;
|
|
7769
|
+
var JSON_NUMBER = 2;
|
|
7770
|
+
var JSON_LITERAL = 3;
|
|
7771
|
+
var JSON_NUMBER_TOKEN = /-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/y;
|
|
7772
|
+
function isHexCode(c) {
|
|
7773
|
+
return c >= 48 && c <= 57 || c >= 65 && c <= 70 || c >= 97 && c <= 102;
|
|
7774
|
+
}
|
|
7775
|
+
var PLAIN_STRING_SEGMENT = /^[^"\\\u0000-\u001f]*$/;
|
|
7776
|
+
function lexJson(text) {
|
|
7777
|
+
let kinds = new Int32Array(1024);
|
|
7778
|
+
let spans = new Int32Array(2048);
|
|
7779
|
+
let count = 0;
|
|
7780
|
+
const n = text.length;
|
|
7781
|
+
let i = 0;
|
|
7782
|
+
while (i < n) {
|
|
7783
|
+
const c = text.charCodeAt(i);
|
|
7784
|
+
if (c === 32 || c === 9 || c === 10 || c === 13) {
|
|
7785
|
+
i++;
|
|
7786
|
+
continue;
|
|
7787
|
+
}
|
|
7788
|
+
const start = i;
|
|
7789
|
+
let kind;
|
|
7790
|
+
if (c === 34) {
|
|
7791
|
+
i++;
|
|
7792
|
+
const close = text.indexOf('"', i);
|
|
7793
|
+
if (close === -1) return null;
|
|
7794
|
+
if (PLAIN_STRING_SEGMENT.test(text.slice(i, close))) {
|
|
7795
|
+
i = close;
|
|
7796
|
+
} else {
|
|
7797
|
+
for (; ; ) {
|
|
7798
|
+
if (i >= n) return null;
|
|
7799
|
+
const d = text.charCodeAt(i);
|
|
7800
|
+
if (d === 34) break;
|
|
7801
|
+
if (d < 32) return null;
|
|
7802
|
+
if (d !== 92) {
|
|
7803
|
+
i++;
|
|
7804
|
+
continue;
|
|
7805
|
+
}
|
|
7806
|
+
const e = text.charCodeAt(i + 1);
|
|
7807
|
+
if (e === 117) {
|
|
7808
|
+
for (let k = 2; k < 6; k++) if (!isHexCode(text.charCodeAt(i + k))) return null;
|
|
7809
|
+
i += 6;
|
|
7810
|
+
} else if (e === 34 || e === 92 || e === 47 || e === 98 || e === 102 || e === 110 || e === 114 || e === 116) {
|
|
7811
|
+
i += 2;
|
|
7812
|
+
} else {
|
|
7813
|
+
return null;
|
|
7814
|
+
}
|
|
7815
|
+
}
|
|
7816
|
+
}
|
|
7817
|
+
i++;
|
|
7818
|
+
kind = JSON_STRING;
|
|
7819
|
+
} else if (c === 123 || c === 125 || c === 91 || c === 93 || c === 58 || c === 44) {
|
|
7820
|
+
i++;
|
|
7821
|
+
kind = c;
|
|
7822
|
+
} else if (text.startsWith("true", i) || text.startsWith("null", i)) {
|
|
7823
|
+
i += 4;
|
|
7824
|
+
kind = JSON_LITERAL;
|
|
7825
|
+
} else if (text.startsWith("false", i)) {
|
|
7826
|
+
i += 5;
|
|
7827
|
+
kind = JSON_LITERAL;
|
|
7828
|
+
} else {
|
|
7829
|
+
JSON_NUMBER_TOKEN.lastIndex = i;
|
|
7830
|
+
if (!JSON_NUMBER_TOKEN.test(text)) return null;
|
|
7831
|
+
i = JSON_NUMBER_TOKEN.lastIndex;
|
|
7832
|
+
kind = JSON_NUMBER;
|
|
7833
|
+
}
|
|
7834
|
+
if (count === kinds.length) {
|
|
7835
|
+
const grownKinds = new Int32Array(count * 2);
|
|
7836
|
+
grownKinds.set(kinds);
|
|
7837
|
+
kinds = grownKinds;
|
|
7838
|
+
const grownSpans = new Int32Array(count * 4);
|
|
7839
|
+
grownSpans.set(spans);
|
|
7840
|
+
spans = grownSpans;
|
|
7841
|
+
}
|
|
7842
|
+
kinds[count] = kind;
|
|
7843
|
+
spans[2 * count] = start;
|
|
7844
|
+
spans[2 * count + 1] = i;
|
|
7845
|
+
count++;
|
|
7846
|
+
}
|
|
7847
|
+
return { kinds, spans, count };
|
|
7848
|
+
}
|
|
7849
|
+
function sameJsonStructure(beforeText, before, afterText, after) {
|
|
7850
|
+
if (!before || !after || before.count !== after.count) return false;
|
|
7851
|
+
const { kinds, spans, count } = before;
|
|
7852
|
+
for (let k = 0; k < count; k++) {
|
|
7853
|
+
const kind = kinds[k];
|
|
7854
|
+
if (after.kinds[k] !== kind) return false;
|
|
7855
|
+
if (kind > JSON_LITERAL) continue;
|
|
7856
|
+
if (kind === JSON_STRING && kinds[k + 1] !== 58) continue;
|
|
7857
|
+
const was = beforeText.slice(spans[2 * k], spans[2 * k + 1]);
|
|
7858
|
+
const now = afterText.slice(after.spans[2 * k], after.spans[2 * k + 1]);
|
|
7859
|
+
if (was === now) continue;
|
|
7860
|
+
if (kind !== JSON_STRING || JSON.parse(was) !== JSON.parse(now)) return false;
|
|
7861
|
+
}
|
|
7862
|
+
return true;
|
|
7863
|
+
}
|
|
6640
7864
|
function applyRedactors(body, redactors, mask, transform) {
|
|
6641
7865
|
let out = body;
|
|
6642
7866
|
for (const r of redactors) {
|
|
@@ -6652,6 +7876,11 @@ function applyRedactors(body, redactors, mask, transform) {
|
|
|
6652
7876
|
}
|
|
6653
7877
|
return out;
|
|
6654
7878
|
}
|
|
7879
|
+
function readsBody(parameter) {
|
|
7880
|
+
if (!Array.isArray(parameter)) return isBodyParam(parameter);
|
|
7881
|
+
const names = parameter.every((member) => typeof member === "string" && member !== "");
|
|
7882
|
+
return names && parameterProblem(parameter) === null && parameter.some((member) => isBodyParam(member));
|
|
7883
|
+
}
|
|
6655
7884
|
function isBodyParam(parameter) {
|
|
6656
7885
|
return parameter == null || parameter === "response.body" || parameter === "raw" || parameter === "response.raw";
|
|
6657
7886
|
}
|
|
@@ -6679,10 +7908,12 @@ function applyPathRedactors(text, pathRedactors, mask, cap, transform) {
|
|
|
6679
7908
|
if (!pathRedactors.length || typeof text !== "string" || text.length > cap) return text;
|
|
6680
7909
|
const head = text.trimStart()[0];
|
|
6681
7910
|
if (head !== "{" && head !== "[") return text;
|
|
6682
|
-
|
|
7911
|
+
let preserved;
|
|
6683
7912
|
let obj;
|
|
6684
7913
|
try {
|
|
6685
|
-
|
|
7914
|
+
JSON.parse(text);
|
|
7915
|
+
preserved = preserveNumbers(text, mask);
|
|
7916
|
+
obj = JSON.parse(preserved.text);
|
|
6686
7917
|
} catch {
|
|
6687
7918
|
return text;
|
|
6688
7919
|
}
|
|
@@ -6691,65 +7922,44 @@ function applyPathRedactors(text, pathRedactors, mask, cap, transform) {
|
|
|
6691
7922
|
const pred = conditionPredicate(r.condition);
|
|
6692
7923
|
walkLeaves(obj, r.jsonPath, (loc) => {
|
|
6693
7924
|
try {
|
|
6694
|
-
|
|
6695
|
-
|
|
7925
|
+
const number = preserved.numbers.get(loc.value);
|
|
7926
|
+
if (pred(number === void 0 ? loc.value : Number(number))) {
|
|
7927
|
+
setOwn(loc.parent, loc.key, transform ? transform(number ?? String(loc.value)) : mask);
|
|
6696
7928
|
changed = true;
|
|
6697
7929
|
}
|
|
6698
7930
|
} catch {
|
|
6699
7931
|
}
|
|
6700
7932
|
});
|
|
6701
7933
|
}
|
|
6702
|
-
return changed ?
|
|
6703
|
-
}
|
|
6704
|
-
var BIGINT_OPEN = "__PSBIGINT_9c2f__";
|
|
6705
|
-
var BIGINT_CLOSE = "__DNEGIB__";
|
|
6706
|
-
function preserveBigInts(text) {
|
|
6707
|
-
let out = "";
|
|
6708
|
-
let inStr = false;
|
|
6709
|
-
for (let i = 0; i < text.length; ) {
|
|
6710
|
-
const ch = text[i];
|
|
6711
|
-
if (inStr) {
|
|
6712
|
-
out += ch;
|
|
6713
|
-
if (ch === "\\") {
|
|
6714
|
-
out += text[i + 1] ?? "";
|
|
6715
|
-
i += 2;
|
|
6716
|
-
continue;
|
|
6717
|
-
}
|
|
6718
|
-
if (ch === '"') inStr = false;
|
|
6719
|
-
i++;
|
|
6720
|
-
continue;
|
|
6721
|
-
}
|
|
6722
|
-
if (ch === '"') {
|
|
6723
|
-
inStr = true;
|
|
6724
|
-
out += ch;
|
|
6725
|
-
i++;
|
|
6726
|
-
continue;
|
|
6727
|
-
}
|
|
6728
|
-
if (ch === "-" || ch >= "0" && ch <= "9") {
|
|
6729
|
-
let j = ch === "-" ? i + 1 : i;
|
|
6730
|
-
let digits = 0;
|
|
6731
|
-
while (j < text.length && text[j] >= "0" && text[j] <= "9") {
|
|
6732
|
-
digits++;
|
|
6733
|
-
j++;
|
|
6734
|
-
}
|
|
6735
|
-
const next = text[j];
|
|
6736
|
-
const isIntToken = digits > 0 && next !== "." && next !== "e" && next !== "E";
|
|
6737
|
-
if (isIntToken && digits >= 16) {
|
|
6738
|
-
out += `"${BIGINT_OPEN}${text.slice(i, j)}${BIGINT_CLOSE}"`;
|
|
6739
|
-
} else {
|
|
6740
|
-
out += text.slice(i, j || i + 1);
|
|
6741
|
-
}
|
|
6742
|
-
i = j > i ? j : i + 1;
|
|
6743
|
-
continue;
|
|
6744
|
-
}
|
|
6745
|
-
out += ch;
|
|
6746
|
-
i++;
|
|
6747
|
-
}
|
|
6748
|
-
return out;
|
|
7934
|
+
return changed ? restoreNumbers(JSON.stringify(obj), preserved.numbers) : text;
|
|
6749
7935
|
}
|
|
6750
|
-
function
|
|
6751
|
-
|
|
6752
|
-
|
|
7936
|
+
function preserveNumbers(text, mask) {
|
|
7937
|
+
const occupied = /* @__PURE__ */ new Set([mask]);
|
|
7938
|
+
const found = [];
|
|
7939
|
+
const tokens = /"(?:[^"\\]|\\[\s\S])*"|-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/g;
|
|
7940
|
+
for (const match of text.matchAll(tokens)) {
|
|
7941
|
+
const token = match[0];
|
|
7942
|
+
if (token[0] === '"') occupied.add(JSON.parse(token));
|
|
7943
|
+
else found.push({ start: match.index, token });
|
|
7944
|
+
}
|
|
7945
|
+
const numbers = /* @__PURE__ */ new Map();
|
|
7946
|
+
const chunks = [];
|
|
7947
|
+
let offset = 0;
|
|
7948
|
+
let index = 0;
|
|
7949
|
+
for (const { start, token } of found) {
|
|
7950
|
+
let marker;
|
|
7951
|
+
do {
|
|
7952
|
+
marker = "__PSNUMBER_" + index++ + "__";
|
|
7953
|
+
} while (occupied.has(marker));
|
|
7954
|
+
numbers.set(marker, token);
|
|
7955
|
+
chunks.push(text.slice(offset, start), JSON.stringify(marker));
|
|
7956
|
+
offset = start + token.length;
|
|
7957
|
+
}
|
|
7958
|
+
chunks.push(text.slice(offset));
|
|
7959
|
+
return { text: chunks.join(""), numbers };
|
|
7960
|
+
}
|
|
7961
|
+
function restoreNumbers(text, numbers) {
|
|
7962
|
+
return text.replace(/"(__PSNUMBER_\d+__)"/g, (token, marker) => numbers.get(marker) ?? token);
|
|
6753
7963
|
}
|
|
6754
7964
|
function isHeaderMutation(action) {
|
|
6755
7965
|
return action === "set-header" || action === "remove-header" || action === "harden-cookie";
|