@patchstack/connect 0.3.16 → 0.3.18
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 +3 -1
- package/README.md +36 -5
- package/dist/cli.js +1064 -94
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/protect/templates/astro-middleware.ts +35 -0
- package/dist/protect/templates/demo-rules.json +238 -0
- package/dist/protect/templates/express-guard.cjs +31 -0
- package/dist/protect/templates/express-guard.js +30 -0
- package/dist/protect/templates/express-guard.ts +32 -0
- package/dist/protect/templates/fastify-plugin.ts +48 -0
- package/dist/protect/templates/generic-guard.ts +51 -0
- package/dist/protect/templates/guard.ts +14 -0
- package/dist/protect/templates/next-middleware.ts +37 -0
- package/dist/protect/templates/sveltekit-hooks.ts +35 -0
- package/dist/protect.cjs +451 -78
- package/dist/protect.cjs.map +1 -1
- package/dist/protect.d.ts +9 -1
- package/dist/protect.js +451 -78
- package/dist/protect.js.map +1 -1
- package/package.json +3 -2
package/dist/protect.js
CHANGED
|
@@ -1,38 +1,48 @@
|
|
|
1
1
|
// src/protect/engine/client.js
|
|
2
2
|
var DEFAULT_BASE_URL = "https://api.patchstack.com";
|
|
3
3
|
var DEFAULT_CACHE_TTL = 3e5;
|
|
4
|
+
var JITTER_FRACTION = 0.1;
|
|
4
5
|
var PatchstackRuleClient = class {
|
|
5
6
|
#token;
|
|
6
7
|
#baseUrl;
|
|
7
8
|
#cacheTtl;
|
|
8
9
|
#cache = null;
|
|
9
10
|
#cacheTime = null;
|
|
10
|
-
|
|
11
|
+
#ttlEffective = 0;
|
|
12
|
+
#etag;
|
|
13
|
+
constructor({ token, baseUrl, cacheTtl, etag } = {}) {
|
|
11
14
|
this.#token = token ?? process.env.PATCHSTACK_WAF_TOKEN;
|
|
12
15
|
this.#baseUrl = baseUrl ?? process.env.PATCHSTACK_WAF_API_URL ?? DEFAULT_BASE_URL;
|
|
13
16
|
this.#cacheTtl = cacheTtl ?? DEFAULT_CACHE_TTL;
|
|
17
|
+
this.#etag = etag ?? null;
|
|
14
18
|
if (!this.#token) {
|
|
15
19
|
throw new Error("Patchstack WAF token is required. Pass { token } or set PATCHSTACK_WAF_TOKEN env var.");
|
|
16
20
|
}
|
|
17
21
|
}
|
|
18
22
|
async getRules() {
|
|
19
23
|
const now = Date.now();
|
|
20
|
-
if (this.#cache && this.#cacheTime && now - this.#cacheTime < this.#
|
|
24
|
+
if (this.#cache && this.#cacheTime !== null && now - this.#cacheTime < this.#ttlEffective) {
|
|
21
25
|
return this.#cache;
|
|
22
26
|
}
|
|
23
27
|
const url = `${this.#baseUrl}/api/get-rules/3`;
|
|
24
28
|
try {
|
|
29
|
+
const headers = {
|
|
30
|
+
"Content-Type": "application/json",
|
|
31
|
+
"Authorization": `Bearer ${this.#token}`,
|
|
32
|
+
"LicenseID": "1"
|
|
33
|
+
// Hard-coded, is never actually used but needed by the API
|
|
34
|
+
};
|
|
35
|
+
if (this.#etag) headers["If-None-Match"] = this.#etag;
|
|
25
36
|
const response = await fetch(url, {
|
|
26
37
|
method: "POST",
|
|
27
|
-
headers
|
|
28
|
-
"Content-Type": "application/json",
|
|
29
|
-
"Authorization": `Bearer ${this.#token}`,
|
|
30
|
-
"LicenseID": "1"
|
|
31
|
-
// Hard-coded, is never actually used but needed by the API
|
|
32
|
-
},
|
|
38
|
+
headers,
|
|
33
39
|
body: JSON.stringify({}),
|
|
34
40
|
signal: AbortSignal.timeout(3e4)
|
|
35
41
|
});
|
|
42
|
+
if (response.status === 304) {
|
|
43
|
+
this.#touch(now);
|
|
44
|
+
return this.#cache ?? { success: true, notModified: true, etag: this.#etag, firewall: [], whitelists: [], whitelist_keys: {} };
|
|
45
|
+
}
|
|
36
46
|
if (!response.ok) {
|
|
37
47
|
return {
|
|
38
48
|
success: false,
|
|
@@ -45,12 +55,15 @@ var PatchstackRuleClient = class {
|
|
|
45
55
|
const data = await response.json();
|
|
46
56
|
const result = {
|
|
47
57
|
success: true,
|
|
58
|
+
notModified: false,
|
|
59
|
+
etag: response.headers?.get?.("etag") ?? null,
|
|
48
60
|
firewall: Array.isArray(data.firewall) ? data.firewall : [],
|
|
49
61
|
whitelists: Array.isArray(data.whitelists) ? data.whitelists : [],
|
|
50
62
|
whitelist_keys: data.whitelist_keys ?? {}
|
|
51
63
|
};
|
|
52
64
|
this.#cache = result;
|
|
53
|
-
this.#
|
|
65
|
+
this.#etag = result.etag;
|
|
66
|
+
this.#touch(now);
|
|
54
67
|
return result;
|
|
55
68
|
} catch (err) {
|
|
56
69
|
return {
|
|
@@ -62,9 +75,14 @@ var PatchstackRuleClient = class {
|
|
|
62
75
|
};
|
|
63
76
|
}
|
|
64
77
|
}
|
|
78
|
+
#touch(now) {
|
|
79
|
+
this.#cacheTime = now;
|
|
80
|
+
this.#ttlEffective = this.#cacheTtl - Math.floor(Math.random() * this.#cacheTtl * JITTER_FRACTION);
|
|
81
|
+
}
|
|
65
82
|
clearCache() {
|
|
66
83
|
this.#cache = null;
|
|
67
84
|
this.#cacheTime = null;
|
|
85
|
+
this.#etag = null;
|
|
68
86
|
}
|
|
69
87
|
};
|
|
70
88
|
|
|
@@ -902,44 +920,53 @@ var RuleEngine = class {
|
|
|
902
920
|
// src/protect/engine/pulse-client.js
|
|
903
921
|
var DEFAULT_BASE_URL2 = "https://api.patchstack.com/monitor/pulse";
|
|
904
922
|
var DEFAULT_CACHE_TTL2 = 3e5;
|
|
923
|
+
var JITTER_FRACTION2 = 0.1;
|
|
905
924
|
var PulseRuleClient = class {
|
|
906
925
|
#siteUuid;
|
|
907
926
|
#baseUrl;
|
|
908
927
|
#cacheTtl;
|
|
909
928
|
#cache = null;
|
|
910
929
|
#cacheTime = null;
|
|
911
|
-
|
|
930
|
+
#ttlEffective = 0;
|
|
931
|
+
#etag;
|
|
932
|
+
constructor({ siteUuid, baseUrl, cacheTtl, etag } = {}) {
|
|
912
933
|
this.#siteUuid = siteUuid ?? process.env.PATCHSTACK_SITE_UUID;
|
|
913
934
|
this.#baseUrl = baseUrl ?? process.env.PATCHSTACK_PULSE_RULES_URL ?? DEFAULT_BASE_URL2;
|
|
914
935
|
this.#cacheTtl = cacheTtl ?? DEFAULT_CACHE_TTL2;
|
|
936
|
+
this.#etag = etag ?? null;
|
|
915
937
|
if (!this.#siteUuid) {
|
|
916
938
|
throw new Error("Patchstack site UUID is required. Pass { siteUuid } or set PATCHSTACK_SITE_UUID.");
|
|
917
939
|
}
|
|
918
940
|
}
|
|
919
941
|
async getRules() {
|
|
920
942
|
const now = Date.now();
|
|
921
|
-
if (this.#cache && this.#cacheTime && now - this.#cacheTime < this.#
|
|
943
|
+
if (this.#cache && this.#cacheTime !== null && now - this.#cacheTime < this.#ttlEffective) {
|
|
922
944
|
return this.#cache;
|
|
923
945
|
}
|
|
924
946
|
const url = `${this.#baseUrl}/rules/${encodeURIComponent(this.#siteUuid)}`;
|
|
925
947
|
try {
|
|
926
|
-
const
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
948
|
+
const headers = { Accept: "application/json" };
|
|
949
|
+
if (this.#etag) headers["If-None-Match"] = this.#etag;
|
|
950
|
+
const response = await fetch(url, { method: "GET", headers, signal: AbortSignal.timeout(3e4) });
|
|
951
|
+
if (response.status === 304) {
|
|
952
|
+
this.#touch(now);
|
|
953
|
+
return this.#cache ?? { success: true, notModified: true, etag: this.#etag, firewall: [], whitelists: [], whitelist_keys: {} };
|
|
954
|
+
}
|
|
931
955
|
if (!response.ok) {
|
|
932
956
|
return { success: false, error: `API returned ${response.status}`, firewall: [], whitelists: [], whitelist_keys: {} };
|
|
933
957
|
}
|
|
934
958
|
const data = await response.json();
|
|
935
959
|
const result = {
|
|
936
960
|
success: true,
|
|
961
|
+
notModified: false,
|
|
962
|
+
etag: response.headers?.get?.("etag") ?? null,
|
|
937
963
|
firewall: Array.isArray(data.firewall) ? data.firewall : [],
|
|
938
964
|
whitelists: Array.isArray(data.whitelists) ? data.whitelists : [],
|
|
939
965
|
whitelist_keys: data.whitelist_keys ?? {}
|
|
940
966
|
};
|
|
941
967
|
this.#cache = result;
|
|
942
|
-
this.#
|
|
968
|
+
this.#etag = result.etag;
|
|
969
|
+
this.#touch(now);
|
|
943
970
|
return result;
|
|
944
971
|
} catch (err) {
|
|
945
972
|
return {
|
|
@@ -951,14 +978,21 @@ var PulseRuleClient = class {
|
|
|
951
978
|
};
|
|
952
979
|
}
|
|
953
980
|
}
|
|
981
|
+
#touch(now) {
|
|
982
|
+
this.#cacheTime = now;
|
|
983
|
+
this.#ttlEffective = this.#cacheTtl - Math.floor(Math.random() * this.#cacheTtl * JITTER_FRACTION2);
|
|
984
|
+
}
|
|
954
985
|
clearCache() {
|
|
955
986
|
this.#cache = null;
|
|
956
987
|
this.#cacheTime = null;
|
|
988
|
+
this.#etag = null;
|
|
957
989
|
}
|
|
958
990
|
};
|
|
959
991
|
|
|
960
992
|
// src/protect/engine/fetch.js
|
|
961
|
-
|
|
993
|
+
var MAX_BODY_BYTES = 1024 * 1024;
|
|
994
|
+
async function fromFetchRequest(request, options = {}) {
|
|
995
|
+
const maxBodyBytes = options.maxBodyBytes ?? MAX_BODY_BYTES;
|
|
962
996
|
const url = new URL(request.url);
|
|
963
997
|
const method = (request.method || "GET").toUpperCase();
|
|
964
998
|
const headers = {};
|
|
@@ -975,14 +1009,11 @@ async function fromFetchRequest(request) {
|
|
|
975
1009
|
}
|
|
976
1010
|
let rawBody = "";
|
|
977
1011
|
if (method !== "GET" && method !== "HEAD") {
|
|
978
|
-
|
|
979
|
-
rawBody = await request.clone().text();
|
|
980
|
-
} catch {
|
|
981
|
-
rawBody = "";
|
|
982
|
-
}
|
|
1012
|
+
rawBody = await readCappedText(request, maxBodyBytes);
|
|
983
1013
|
}
|
|
984
1014
|
const contentType = headers["content-type"] || "";
|
|
985
1015
|
let body = {};
|
|
1016
|
+
let files;
|
|
986
1017
|
if (rawBody) {
|
|
987
1018
|
if (contentType.includes("application/json")) {
|
|
988
1019
|
try {
|
|
@@ -995,6 +1026,13 @@ async function fromFetchRequest(request) {
|
|
|
995
1026
|
for (const [k, v] of new URLSearchParams(rawBody)) {
|
|
996
1027
|
body[k] = k in body ? [].concat(body[k], v) : v;
|
|
997
1028
|
}
|
|
1029
|
+
} else if (contentType.includes("multipart/form-data")) {
|
|
1030
|
+
const boundary = /boundary=("?)([^";]+)\1/i.exec(contentType)?.[2];
|
|
1031
|
+
if (boundary) {
|
|
1032
|
+
const parsed = parseMultipart(rawBody, boundary);
|
|
1033
|
+
body = parsed.body;
|
|
1034
|
+
files = parsed.files;
|
|
1035
|
+
}
|
|
998
1036
|
}
|
|
999
1037
|
}
|
|
1000
1038
|
const uri = url.pathname + url.search;
|
|
@@ -1005,6 +1043,7 @@ async function fromFetchRequest(request) {
|
|
|
1005
1043
|
originalUrl: uri,
|
|
1006
1044
|
query,
|
|
1007
1045
|
body,
|
|
1046
|
+
files,
|
|
1008
1047
|
headers,
|
|
1009
1048
|
ip: forwarded.split(",")[0].trim(),
|
|
1010
1049
|
cookies: parseCookies(headers.cookie),
|
|
@@ -1013,6 +1052,44 @@ async function fromFetchRequest(request) {
|
|
|
1013
1052
|
_rawBody: rawBody
|
|
1014
1053
|
};
|
|
1015
1054
|
}
|
|
1055
|
+
async function readCappedText(request, max) {
|
|
1056
|
+
const ceiling = Math.max(max, max * 4);
|
|
1057
|
+
const declared = Number(request.headers?.get?.("content-length") || 0);
|
|
1058
|
+
if (declared && declared > ceiling) return "";
|
|
1059
|
+
let clone;
|
|
1060
|
+
try {
|
|
1061
|
+
clone = request.clone();
|
|
1062
|
+
} catch {
|
|
1063
|
+
return "";
|
|
1064
|
+
}
|
|
1065
|
+
try {
|
|
1066
|
+
const text = await clone.text();
|
|
1067
|
+
return text.length > max ? text.slice(0, max) : text;
|
|
1068
|
+
} catch {
|
|
1069
|
+
return "";
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
function parseMultipart(rawBody, boundary) {
|
|
1073
|
+
const body = {};
|
|
1074
|
+
const files = {};
|
|
1075
|
+
for (const part of rawBody.split("--" + boundary)) {
|
|
1076
|
+
const sep = /\r?\n\r?\n/.exec(part);
|
|
1077
|
+
if (!sep) continue;
|
|
1078
|
+
const rawHeaders = part.slice(0, sep.index);
|
|
1079
|
+
const disposition = /content-disposition:[^\r\n]*/i.exec(rawHeaders)?.[0];
|
|
1080
|
+
if (!disposition) continue;
|
|
1081
|
+
const name = /name="([^"]*)"/i.exec(disposition)?.[1];
|
|
1082
|
+
if (name == null) continue;
|
|
1083
|
+
const content = part.slice(sep.index + sep[0].length).replace(/\r?\n$/, "");
|
|
1084
|
+
const filename = /filename="([^"]*)"/i.exec(disposition)?.[1];
|
|
1085
|
+
if (filename !== void 0) {
|
|
1086
|
+
files[name] = name in files ? [].concat(files[name], filename) : filename;
|
|
1087
|
+
} else {
|
|
1088
|
+
body[name] = name in body ? [].concat(body[name], content) : content;
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
return { body, files };
|
|
1092
|
+
}
|
|
1016
1093
|
function parseCookies(header) {
|
|
1017
1094
|
const cookies = {};
|
|
1018
1095
|
if (!header) {
|
|
@@ -1092,35 +1169,124 @@ function parseCookies2(header) {
|
|
|
1092
1169
|
}
|
|
1093
1170
|
|
|
1094
1171
|
// src/protect/egress.js
|
|
1095
|
-
function installEgressGuard({ shouldBlock, onBlock } = {}) {
|
|
1096
|
-
const
|
|
1097
|
-
if (typeof
|
|
1098
|
-
|
|
1172
|
+
async function installEgressGuard({ shouldBlock, onBlock } = {}) {
|
|
1173
|
+
const restores = [];
|
|
1174
|
+
if (typeof shouldBlock !== "function") return () => {
|
|
1175
|
+
};
|
|
1176
|
+
const block = (url, host, method) => {
|
|
1177
|
+
if (!shouldBlock(url, host, method)) return false;
|
|
1178
|
+
onBlock?.({ url, host, method });
|
|
1179
|
+
return true;
|
|
1180
|
+
};
|
|
1181
|
+
const originalFetch = globalThis.fetch;
|
|
1182
|
+
if (typeof originalFetch === "function" && !originalFetch.__patchstackGuarded) {
|
|
1183
|
+
const guarded = async (input, init) => {
|
|
1184
|
+
const url = typeof input === "string" ? input : input && input.url || String(input);
|
|
1185
|
+
let host = null;
|
|
1186
|
+
try {
|
|
1187
|
+
host = new URL(url).hostname;
|
|
1188
|
+
} catch {
|
|
1189
|
+
host = null;
|
|
1190
|
+
}
|
|
1191
|
+
const method = init && init.method || input && input.method || "GET";
|
|
1192
|
+
if (block(url, host, method)) {
|
|
1193
|
+
throw new Error(`Patchstack blocked an outbound request to a disallowed address: ${host ?? url}`);
|
|
1194
|
+
}
|
|
1195
|
+
return originalFetch(input, init);
|
|
1099
1196
|
};
|
|
1197
|
+
guarded.__patchstackGuarded = true;
|
|
1198
|
+
globalThis.fetch = guarded;
|
|
1199
|
+
restores.push(() => {
|
|
1200
|
+
if (globalThis.fetch === guarded) globalThis.fetch = originalFetch;
|
|
1201
|
+
});
|
|
1100
1202
|
}
|
|
1101
|
-
const
|
|
1102
|
-
const url = typeof input === "string" ? input : input && input.url || String(input);
|
|
1103
|
-
let host = null;
|
|
1203
|
+
for (const moduleName of ["node:http", "node:https"]) {
|
|
1104
1204
|
try {
|
|
1105
|
-
|
|
1205
|
+
const mod = await import(moduleName);
|
|
1206
|
+
const restore = patchHttpModule(mod.default ?? mod, block);
|
|
1207
|
+
if (restore) restores.push(restore);
|
|
1106
1208
|
} catch {
|
|
1107
|
-
host = null;
|
|
1108
1209
|
}
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1210
|
+
}
|
|
1211
|
+
const OriginalWS = globalThis.WebSocket;
|
|
1212
|
+
if (typeof OriginalWS === "function" && !OriginalWS.__patchstackGuarded) {
|
|
1213
|
+
const GuardedWS = new Proxy(OriginalWS, {
|
|
1214
|
+
construct(target, args, newTarget) {
|
|
1215
|
+
const url = String(args?.[0] ?? "");
|
|
1216
|
+
let host = null;
|
|
1217
|
+
try {
|
|
1218
|
+
host = new URL(url).hostname;
|
|
1219
|
+
} catch {
|
|
1220
|
+
host = null;
|
|
1221
|
+
}
|
|
1222
|
+
if (block(url, host, "WEBSOCKET")) {
|
|
1223
|
+
throw new Error(`Patchstack blocked an outbound WebSocket to a disallowed address: ${host ?? url}`);
|
|
1224
|
+
}
|
|
1225
|
+
return Reflect.construct(target, args, newTarget);
|
|
1226
|
+
}
|
|
1227
|
+
});
|
|
1228
|
+
OriginalWS.__patchstackGuarded = true;
|
|
1229
|
+
globalThis.WebSocket = GuardedWS;
|
|
1230
|
+
restores.push(() => {
|
|
1231
|
+
if (globalThis.WebSocket === GuardedWS) globalThis.WebSocket = OriginalWS;
|
|
1232
|
+
delete OriginalWS.__patchstackGuarded;
|
|
1233
|
+
});
|
|
1234
|
+
}
|
|
1235
|
+
return () => {
|
|
1236
|
+
for (const restore of restores) {
|
|
1237
|
+
try {
|
|
1238
|
+
restore();
|
|
1239
|
+
} catch {
|
|
1240
|
+
}
|
|
1113
1241
|
}
|
|
1114
|
-
return original(input, init);
|
|
1115
1242
|
};
|
|
1116
|
-
|
|
1117
|
-
|
|
1243
|
+
}
|
|
1244
|
+
function patchHttpModule(http, block) {
|
|
1245
|
+
if (!http || typeof http.request !== "function" || http.__patchstackGuarded) return null;
|
|
1246
|
+
const originalRequest = http.request;
|
|
1247
|
+
const originalGet = http.get;
|
|
1248
|
+
const wrap = (original) => function(...args) {
|
|
1249
|
+
const target = extractHttpTarget(args);
|
|
1250
|
+
if (target && block(target.url, target.host, target.method)) {
|
|
1251
|
+
throw new Error(`Patchstack blocked an outbound request to a disallowed address: ${target.host ?? target.url}`);
|
|
1252
|
+
}
|
|
1253
|
+
return original.apply(this, args);
|
|
1254
|
+
};
|
|
1255
|
+
http.request = wrap(originalRequest);
|
|
1256
|
+
if (typeof originalGet === "function") http.get = wrap(originalGet);
|
|
1257
|
+
http.__patchstackGuarded = true;
|
|
1118
1258
|
return () => {
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1259
|
+
http.request = originalRequest;
|
|
1260
|
+
if (typeof originalGet === "function") http.get = originalGet;
|
|
1261
|
+
delete http.__patchstackGuarded;
|
|
1122
1262
|
};
|
|
1123
1263
|
}
|
|
1264
|
+
function extractHttpTarget(args) {
|
|
1265
|
+
const first = args[0];
|
|
1266
|
+
try {
|
|
1267
|
+
if (typeof first === "string" || first instanceof URL) {
|
|
1268
|
+
const url = new URL(String(first));
|
|
1269
|
+
const opts = args.find((a) => a && typeof a === "object" && !(a instanceof URL));
|
|
1270
|
+
return { url: url.href, host: url.hostname, method: opts && opts.method || "GET" };
|
|
1271
|
+
}
|
|
1272
|
+
if (first && typeof first === "object") {
|
|
1273
|
+
const host = normalizeHost(first.hostname || first.host);
|
|
1274
|
+
const protocol = first.protocol || "http:";
|
|
1275
|
+
const port = first.port ? `:${first.port}` : "";
|
|
1276
|
+
const path = first.path || "/";
|
|
1277
|
+
return { url: `${protocol}//${host}${port}${path}`, host, method: first.method || "GET" };
|
|
1278
|
+
}
|
|
1279
|
+
} catch {
|
|
1280
|
+
}
|
|
1281
|
+
return null;
|
|
1282
|
+
}
|
|
1283
|
+
function normalizeHost(raw) {
|
|
1284
|
+
const host = String(raw || "").trim();
|
|
1285
|
+
const bracketed = /^\[([^\]]+)\]/.exec(host);
|
|
1286
|
+
if (bracketed) return bracketed[1];
|
|
1287
|
+
if ((host.match(/:/g) || []).length > 1) return host;
|
|
1288
|
+
return host.split(":")[0];
|
|
1289
|
+
}
|
|
1124
1290
|
|
|
1125
1291
|
// src/protect/defaults.js
|
|
1126
1292
|
var DEFAULT_RESPONSE_RULES = [
|
|
@@ -1171,6 +1337,25 @@ var DEFAULT_RESPONSE_RULES = [
|
|
|
1171
1337
|
category: "info-exposure",
|
|
1172
1338
|
action: "redact",
|
|
1173
1339
|
rule_v2: [{ parameter: "response.body", match: { type: "regex", value: "/\\n\\s+at\\s+.+\\(.+:\\d+:\\d+\\)/" } }]
|
|
1340
|
+
},
|
|
1341
|
+
{
|
|
1342
|
+
id: "resp-sql-error",
|
|
1343
|
+
title: "SQL / ORM error disclosure in response body",
|
|
1344
|
+
phase: "response",
|
|
1345
|
+
category: "info-exposure",
|
|
1346
|
+
action: "redact",
|
|
1347
|
+
rule_v2: [{ parameter: "response.body", match: { type: "regex", value: "/(SQLSTATE\\[[0-9A-Z]+\\]|SequelizeDatabaseError|ER_[A-Z_]+|ORA-\\d{5}|PG::[A-Za-z]+Error|SQLITE_ERROR|You have an error in your SQL syntax)/i" } }]
|
|
1348
|
+
},
|
|
1349
|
+
{
|
|
1350
|
+
id: "resp-exception-trace",
|
|
1351
|
+
title: "Backend exception / stack trace disclosure in response body",
|
|
1352
|
+
phase: "response",
|
|
1353
|
+
category: "info-exposure",
|
|
1354
|
+
action: "redact",
|
|
1355
|
+
// Multi-language exception/traceback signatures a normal API response never carries:
|
|
1356
|
+
// Python traceback, Java "Exception in thread", .NET System.*Exception, JVM stack frames,
|
|
1357
|
+
// Go goroutine dumps. (Node `at fn (file:line:col)` frames are handled by resp-stack-trace.)
|
|
1358
|
+
rule_v2: [{ parameter: "response.body", match: { type: "regex", value: '/(Traceback \\(most recent call last\\)|Exception in thread "|System\\.[A-Za-z.]+Exception|\\bat [\\w.$]+\\([\\w]+\\.(?:java|kt|scala|rb|py|cs):\\d+\\)|goroutine \\d+ \\[)/' } }]
|
|
1174
1359
|
}
|
|
1175
1360
|
];
|
|
1176
1361
|
var DEFAULT_EGRESS_RULES = [
|
|
@@ -1183,6 +1368,89 @@ var DEFAULT_EGRESS_RULES = [
|
|
|
1183
1368
|
}
|
|
1184
1369
|
];
|
|
1185
1370
|
|
|
1371
|
+
// src/protect/block-page.js
|
|
1372
|
+
function escapeHtml(s) {
|
|
1373
|
+
return String(s).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
1374
|
+
}
|
|
1375
|
+
function renderBlockPage({ url = "", code = "patchstack" } = {}) {
|
|
1376
|
+
const safeUrl = escapeHtml(url);
|
|
1377
|
+
const safeCode = escapeHtml(code || "patchstack");
|
|
1378
|
+
return `<!DOCTYPE html>
|
|
1379
|
+
<html lang="en">
|
|
1380
|
+
<head>
|
|
1381
|
+
<meta charset="UTF-8">
|
|
1382
|
+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
1383
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1384
|
+
<style type="text/css">
|
|
1385
|
+
.access-denied,.site-url-text{letter-spacing:.01em;color:#fff}body{background:#111216}.main-container{display:flex;justify-content:center;flex-direction:column;align-items:center}.container{width:688px;height:456px;background:#1c1e22;border-radius:16px;margin-top:120px}.content{height:330px;margin:64px}.access-denied{font-family:Verdana;font-style:normal;font-weight:600;font-size:24px;line-height:32px;display:flex;align-items:center}.descp-text,.site-url{display:flex;align-items:center}.site-url{margin-top:32px;width:560px;height:64px;background:#26282c;border-radius:4px}.reason-code-text,.site-url-text{font-family:Verdana;font-style:normal;border:none;background:#26282c}.site-url-text{text-overflow: ellipsis;font-weight:400;font-size:16px;line-height:24px;margin-left:16px;width:85%}.copy-icon{width:19px;height:22px;margin-left:20px}.copy-icon:hover{cursor:pointer}.reason-code-text{font-weight:600;font-size:24px;line-height:32px;letter-spacing:.01em;color:#AFE614;margin-left:16px;width:85%}.descp-text,.return-text{font-size:16px;line-height:24px;letter-spacing:.01em;font-weight:400}.return-text:hover{color: #AFE614}.descp-text,.logoText,.return-text,.secured-text{font-family:Verdana;font-style:normal;color:#fff}.descp{margin-top:32px;width:560px}.bottom-second-containers,.bottom-text-containers{align-items:center;display:flex}.bottom-container{margin-top:40px;display:flex;width:688px}.bottom-second-containers{margin-left:auto}.arrow-vector{margin-left:20px}.return-text{text-align:center;margin-left:12.59px;text-decoration:none}.secured-text{font-weight:400;font-size:11.2px;line-height:16px;display:flex;align-items:center;letter-spacing:.02em;margin-right:8px}.logoText{font-weight:600;font-size:16px;line-height:24px;letter-spacing:.01em;flex:none;order:1;flex-grow:0}.patch-logo{width:120px}.mobile-bottom-container{margin-top:40px;width:372px;display:none}.mobile-bottom-second-containers{margin-top:64px;display:flex;margin-left:16.5px}@media only screen and (max-width:900px){.container{background:#111216;width:370px;height:auto;margin-top:0}.content{margin-left:16.5px}.descp,.site-url{width:340px}.site-url-text{font-weight:400;font-size:14.4px;line-height:20px;letter-spacing:.02em;width:80%}.copy-icon{margin-left:20px}.reason-code-text{font-size:16px;line-height:32px;letter-spacing:.02em;margin-left:16px;width:80%}.descp-text,.return-text{font-size:14.4px;line-height:20px}.descp-text{display:flex;align-items:center;letter-spacing:.02em}.bottom-container{display:none}.mobile-bottom-container{display:block}.return-text{text-align:center;letter-spacing:.01em;color:#fff;margin-left:9.73px}}
|
|
1386
|
+
</style>
|
|
1387
|
+
<title>Access Denied</title>
|
|
1388
|
+
</head>
|
|
1389
|
+
<body>
|
|
1390
|
+
<div class="main-container">
|
|
1391
|
+
<div class="container">
|
|
1392
|
+
<div class="content">
|
|
1393
|
+
<span class="access-denied">Access Denied</span>
|
|
1394
|
+
<div class="site-url">
|
|
1395
|
+
<input id="url" class="site-url-text" type="text"
|
|
1396
|
+
value="${safeUrl}" readonly>
|
|
1397
|
+
<img class="copy-icon" src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTkiIGhlaWdodD0iMjMiIHZpZXdCb3g9IjAgMCAxOSAyMyIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE0IDAuMDIzNDM3NUgwVjE2LjAyMzRIMlYyLjAyMzQ0SDE0VjAuMDIzNDM3NVpNMTkgNC4wMjM0NEg0VjIyLjAyMzRIMTlWNC4wMjM0NFpNMTcgMjAuMDIzNEg2VjYuMDIzNDRIMTdWMjAuMDIzNFoiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo=" alt="Copy Text" onmouseover="hover(this);" onmouseout="unhover(this);" onclick="copyText(id='url')">
|
|
1398
|
+
</div>
|
|
1399
|
+
<div class="site-url">
|
|
1400
|
+
<input id="response" class="reason-code-text" type="text" value="${safeCode}" readonly>
|
|
1401
|
+
<img class="copy-icon" src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTkiIGhlaWdodD0iMjMiIHZpZXdCb3g9IjAgMCAxOSAyMyIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE0IDAuMDIzNDM3NUgwVjE2LjAyMzRIMlYyLjAyMzQ0SDE0VjAuMDIzNDM3NVpNMTkgNC4wMjM0NEg0VjIyLjAyMzRIMTlWNC4wMjM0NFpNMTcgMjAuMDIzNEg2VjYuMDIzNDRIMTdWMjAuMDIzNFoiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo=" alt="Copy Text" onmouseover="hover(this);" onmouseout="unhover(this);" onclick="copyText(id='response')">
|
|
1402
|
+
</div>
|
|
1403
|
+
<div class="descp">
|
|
1404
|
+
<span class="descp-text">This request has been blocked by Patchstack. If this message persists and you are a legitimate user, contact the site administrator with the reference above.</span>
|
|
1405
|
+
</div>
|
|
1406
|
+
</div>
|
|
1407
|
+
</div>
|
|
1408
|
+
<div class="bottom-container">
|
|
1409
|
+
<div class="bottom-text-containers">
|
|
1410
|
+
<img class="arrow-vector" src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iOCIgaGVpZ2h0PSIxMyIgdmlld0JveD0iMCAwIDggMTMiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik03LjQxIDEuNDMzNDRMNiAwLjAyMzQzNzVMMCA2LjAyMzQ0TDYgMTIuMDIzNEw3LjQxIDEwLjYxMzRMMi44MyA2LjAyMzQ0TDcuNDEgMS40MzM0NFoiIGZpbGw9IiNBRkU2MTQiLz4KPC9zdmc+Cg==" alt="Arrow Left">
|
|
1411
|
+
<a href="/" class="return-text">Return to homepage</a>
|
|
1412
|
+
</div>
|
|
1413
|
+
<div class="bottom-second-containers">
|
|
1414
|
+
<span class="secured-text">This website is secured by</span>
|
|
1415
|
+
<a href="https://patchstack.com" class="logoText">
|
|
1416
|
+
<img class="patch-logo" src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTg0IiBoZWlnaHQ9IjIyIiB2aWV3Qm94PSIwIDAgMTg0IDIyIiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTUwLjQwMDEgMTEuNTAwOEM1MC40MDAxIDkuMTAwNzggNDkuMzAwMSA4LjEwMDc4IDQ2LjYwMDEgOC4xMDA3OEM0NC4wMDAxIDguMTAwNzggNDIuNzAwMSA5LjIwMDc4IDQyLjcwMDEgMTEuNTAwOEM0Mi43MDAxIDEzLjgwMDggNDQuMDAwMSAxNC45MDA4IDQ2LjYwMDEgMTQuOTAwOEM0OS4zMDAxIDE0LjkwMDggNTAuNDAwMSAxMy44MDA4IDUwLjQwMDEgMTEuNTAwOFpNNTMuNDAwMSAxMS41MDA4QzUzLjQwMDEgMTUuMzAwOCA1MS4zMDAxIDE3LjYwMDggNDcuNDAwMSAxNy42MDA4QzQ1LjQwMDEgMTcuNjAwOCA0My42MDAxIDE2LjUwMDggNDIuNzAwMSAxNC41MDA4VjIxLjUwMDhIMzkuNjAwMVY1LjYwMDc4SDQyLjcwMDFWOC41MDA3OEM0My42MDAxIDYuNTAwNzggNDUuNDAwMSA1LjMwMDc4IDQ3LjUwMDEgNS4zMDA3OEM1MS40MDAxIDUuMzAwNzggNTMuNDAwMSA3LjcwMDc4IDUzLjQwMDEgMTEuNTAwOFoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNNjYuNDAwMSAxMS40OTg0QzY2LjQwMDEgOS4xOTg0NCA2NS4xMDAxIDguMDk4NDQgNjIuNTAwMSA4LjA5ODQ0QzU5LjgwMDEgOC4wOTg0NCA1OC43MDAxIDkuMDk4NDQgNTguNzAwMSAxMS40OTg0QzU4LjcwMDEgMTMuNzk4NCA1OS44MDAxIDE0Ljg5ODQgNjIuNTAwMSAxNC44OTg0QzY1LjEwMDEgMTQuODk4NCA2Ni40MDAxIDEzLjc5ODQgNjYuNDAwMSAxMS40OTg0Wk02Ni4zMDAxIDUuNTk4NDRINjkuNDAwMVYxNy4zOTg0SDY2LjMwMDFWMTQuNDk4NEM2NS40MDAxIDE2LjQ5ODQgNjMuNzAwMSAxNy42OTg0IDYxLjYwMDEgMTcuNjk4NEM1Ny43MDAxIDE3LjY5ODQgNTUuNjAwMSAxNS4yOTg0IDU1LjYwMDEgMTEuNTk4NEM1NS42MDAxIDcuNzk4NDQgNTcuNjAwMSA1LjM5ODQ0IDYxLjQwMDEgNS4zOTg0NEM2My41MDAxIDUuMzk4NDQgNjUuNDAwMSA2LjU5ODQ0IDY2LjIwMDEgOC41OTg0NFY1LjU5ODQ0SDY2LjMwMDFaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTgxLjM5OTkgOC4zSDc3LjM5OTlWMTQuN0g4MS4wOTk5VjE3LjRINzcuOTk5OUM3NS43OTk5IDE3LjQgNzQuMjk5OSAxNS45IDc0LjI5OTkgMTMuN1Y4LjNINzEuMzk5OVY1LjZINzQuMjk5OVYySDc3LjM5OTlWNS41SDgxLjM5OTlWOC4zWiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik05My42OTk5IDEyLjIwMDhMOTYuNjk5OSAxMi45MDA4Qzk1Ljk5OTkgMTYuMDAwOCA5My42OTk5IDE3LjYwMDggODkuOTk5OSAxNy42MDA4Qzg3Ljc5OTkgMTcuNjAwOCA4Ni4wOTk5IDE3LjAwMDggODQuNzk5OSAxNS45MDA4QzgzLjQ5OTkgMTQuODAwOCA4Mi44OTk5IDEzLjMwMDggODIuODk5OSAxMS41MDA4QzgyLjg5OTkgOS42MDA3OCA4My40OTk5IDguMTAwNzggODQuNzk5OSA3LjAwMDc4Qzg2LjA5OTkgNS45MDA3OCA4Ny43OTk5IDUuMzAwNzggODkuOTk5OSA1LjMwMDc4QzkzLjY5OTkgNS4zMDA3OCA5NS45OTk5IDYuOTAwNzggOTYuNjk5OSAxMC4wMDA4TDkzLjY5OTkgMTAuNzAwOEM5My4yOTk5IDguODAwNzggOTIuMjk5OSA4LjEwMDc4IDg5Ljg5OTkgOC4xMDA3OEM4Ni45OTk5IDguMTAwNzggODUuODk5OSA5LjIwMDc4IDg1Ljg5OTkgMTEuNTAwOEM4NS44OTk5IDEzLjgwMDggODYuOTk5OSAxNC45MDA4IDg5Ljg5OTkgMTQuOTAwOEM5Mi4yOTk5IDE0LjkwMDggOTMuMjk5OSAxNC4yMDA4IDkzLjY5OTkgMTIuMjAwOFoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTExLjggMTAuNlYxNy40SDEwOC43VjExLjJDMTA4LjcgOSAxMDcuOCA4LjEgMTA1LjkgOC4xQzEwMy42IDguMSAxMDIuNCA5LjMgMTAyLjQgMTEuN1YxNy4zSDk5LjI5OThWMC41SDEwMi40VjguM0MxMDMuMSA2LjMgMTA0LjYgNS4zIDEwNyA1LjNDMTEwLjIgNS4zIDExMS44IDcuMSAxMTEuOCAxMC42WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xMTQuMSAxNC4wOTg0TDExNy4xIDEzLjI5ODRDMTE3LjQgMTQuNzk4NCAxMTguMyAxNS4yOTg0IDEyMC4yIDE1LjI5ODRDMTIxLjggMTUuMjk4NCAxMjIuNyAxNC44OTg0IDEyMi43IDE0LjA5ODRDMTIyLjcgMTMuMjk4NCAxMjIuMiAxMi45OTg0IDEyMC42IDEyLjc5ODRMMTE4LjIgMTIuNDk4NEMxMTUuOSAxMi4xOTg0IDExNC41IDEwLjg5ODQgMTE0LjUgOC45OTg0NEMxMTQuNSA2Ljc5ODQ0IDExNi41IDUuMzk4NDQgMTE5LjggNS4zOTg0NEMxMjMuMSA1LjM5ODQ0IDEyNSA2LjY5ODQ0IDEyNS41IDguODk4NDRMMTIyLjUgOS41OTg0NEMxMjIuMyA4LjA5ODQ0IDEyMS41IDcuNjk4NDQgMTE5LjkgNy42OTg0NEMxMTguNCA3LjY5ODQ0IDExNy42IDguMDk4NDQgMTE3LjYgOC44OTg0NEMxMTcuNiA5LjU5ODQ0IDExOC4xIDkuOTk4NDQgMTE5LjMgMTAuMDk4NEwxMjIuMiAxMC40OTg0QzEyNC40IDEwLjc5ODQgMTI1LjggMTEuOTk4NCAxMjUuOCAxMy44OTg0QzEyNS44IDE2LjI5ODQgMTIzLjcgMTcuNzk4NCAxMjAuMSAxNy43OTg0QzExNi43IDE3LjU5ODQgMTE0LjUgMTYuMTk4NCAxMTQuMSAxNC4wOTg0WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xMzYuNyA4LjNIMTMyLjdWMTQuN0gxMzYuNFYxNy40SDEzMy4zQzEzMS4xIDE3LjQgMTI5LjYgMTUuOSAxMjkuNiAxMy43VjguM0gxMjYuN1Y1LjZIMTI5LjZWMkgxMzIuN1Y1LjVIMTM2LjdWOC4zWiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xNDkuMiAxMS40OTg0QzE0OS4yIDkuMTk4NDQgMTQ3LjkgOC4wOTg0NCAxNDUuMyA4LjA5ODQ0QzE0Mi42IDguMDk4NDQgMTQxLjUgOS4wOTg0NCAxNDEuNSAxMS40OTg0QzE0MS41IDEzLjc5ODQgMTQyLjYgMTQuODk4NCAxNDUuMyAxNC44OTg0QzE0Ny45IDE0Ljg5ODQgMTQ5LjIgMTMuNzk4NCAxNDkuMiAxMS40OTg0Wk0xNDkuMSA1LjU5ODQ0SDE1Mi4yVjE3LjM5ODRIMTQ5LjFWMTQuNDk4NEMxNDguMiAxNi40OTg0IDE0Ni41IDE3LjY5ODQgMTQ0LjQgMTcuNjk4NEMxNDAuNSAxNy42OTg0IDEzOC40IDE1LjI5ODQgMTM4LjQgMTEuNTk4NEMxMzguNCA3Ljc5ODQ0IDE0MC40IDUuMzk4NDQgMTQ0LjIgNS4zOTg0NEMxNDYuMyA1LjM5ODQ0IDE0OC4yIDYuNTk4NDQgMTQ5IDguNTk4NDRWNS41OTg0NEgxNDkuMVoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTY1LjYgMTIuMjAwOEwxNjguNiAxMi45MDA4QzE2Ny45IDE2LjAwMDggMTY1LjYgMTcuNjAwOCAxNjEuOSAxNy42MDA4QzE1OS43IDE3LjYwMDggMTU4IDE3LjAwMDggMTU2LjcgMTUuOTAwOEMxNTUuNCAxNC44MDA4IDE1NC44IDEzLjMwMDggMTU0LjggMTEuNTAwOEMxNTQuOCA5LjYwMDc4IDE1NS40IDguMTAwNzggMTU2LjcgNy4wMDA3OEMxNTggNS45MDA3OCAxNTkuNyA1LjMwMDc4IDE2MS45IDUuMzAwNzhDMTY1LjYgNS4zMDA3OCAxNjcuOSA2LjkwMDc4IDE2OC42IDEwLjAwMDhMMTY1LjYgMTAuNzAwOEMxNjUuMiA4LjgwMDc4IDE2NC4yIDguMTAwNzggMTYxLjggOC4xMDA3OEMxNTguOSA4LjEwMDc4IDE1Ny44IDkuMjAwNzggMTU3LjggMTEuNTAwOEMxNTcuOCAxMy44MDA4IDE1OC45IDE0LjkwMDggMTYxLjggMTQuOTAwOEMxNjQuMSAxNC45MDA4IDE2NS4xIDE0LjIwMDggMTY1LjYgMTIuMjAwOFoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTgwLjI1NyAxNy40TDE3Ni42NTcgMTIuM0wxNzQuMTU3IDE0LjZWMTcuNEgxNzEuMTU3VjAuNUgxNzQuMTU3VjExLjFMMTc5Ljg1NyA1LjZIMTgzLjc1N0wxNzguNTU3IDEwLjVMMTgzLjk1NyAxNy40SDE4MC4yNTdaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTAgMjEuNTAwOEg3LjNWMTYuMzAwOEgwVjIxLjUwMDhaIiBmaWxsPSIjQUZFNjE0Ii8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNNy4yOTk4IDIxLjUwMDhIMTQuNTk5OFYxNi4zMDA4SDcuMjk5OFYyMS41MDA4WiIgZmlsbD0iIzZFQkUwMCIvPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTE0LjYwMDEgMTYuM0gyOS4yMDAxVjAuNUgxNC42MDAxVjE2LjNaIiBmaWxsPSIjQUZFNjE0Ii8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMCAxMUgxNC42VjAuNUgwVjExWiIgZmlsbD0iIzZFQkUwMCIvPgo8L3N2Zz4K" alt="Patchstack Logo">
|
|
1417
|
+
</a>
|
|
1418
|
+
</div>
|
|
1419
|
+
</div>
|
|
1420
|
+
|
|
1421
|
+
<div class="mobile-bottom-container">
|
|
1422
|
+
<div class="mobile-bottom-text-containers">
|
|
1423
|
+
<img class="arrow-vector" src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iOCIgaGVpZ2h0PSIxMyIgdmlld0JveD0iMCAwIDggMTMiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik03LjQxIDEuNDMzNDRMNiAwLjAyMzQzNzVMMCA2LjAyMzQ0TDYgMTIuMDIzNEw3LjQxIDEwLjYxMzRMMi44MyA2LjAyMzQ0TDcuNDEgMS40MzM0NFoiIGZpbGw9IiNBRkU2MTQiLz4KPC9zdmc+Cg==" alt="Arrow Left">
|
|
1424
|
+
<a href="/" class="return-text">Return to homepage</a>
|
|
1425
|
+
</div>
|
|
1426
|
+
<div class="mobile-bottom-second-containers">
|
|
1427
|
+
<span class="secured-text">This website is secured by</span>
|
|
1428
|
+
<a href="https://patchstack.com" class="logoText">
|
|
1429
|
+
<img class="patch-logo" src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTg0IiBoZWlnaHQ9IjIyIiB2aWV3Qm94PSIwIDAgMTg0IDIyIiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTUwLjQwMDEgMTEuNTAwOEM1MC40MDAxIDkuMTAwNzggNDkuMzAwMSA4LjEwMDc4IDQ2LjYwMDEgOC4xMDA3OEM0NC4wMDAxIDguMTAwNzggNDIuNzAwMSA5LjIwMDc4IDQyLjcwMDEgMTEuNTAwOEM0Mi43MDAxIDEzLjgwMDggNDQuMDAwMSAxNC45MDA4IDQ2LjYwMDEgMTQuOTAwOEM0OS4zMDAxIDE0LjkwMDggNTAuNDAwMSAxMy44MDA4IDUwLjQwMDEgMTEuNTAwOFpNNTMuNDAwMSAxMS41MDA4QzUzLjQwMDEgMTUuMzAwOCA1MS4zMDAxIDE3LjYwMDggNDcuNDAwMSAxNy42MDA4QzQ1LjQwMDEgMTcuNjAwOCA0My42MDAxIDE2LjUwMDggNDIuNzAwMSAxNC41MDA4VjIxLjUwMDhIMzkuNjAwMVY1LjYwMDc4SDQyLjcwMDFWOC41MDA3OEM0My42MDAxIDYuNTAwNzggNDUuNDAwMSA1LjMwMDc4IDQ3LjUwMDEgNS4zMDA3OEM1MS40MDAxIDUuMzAwNzggNTMuNDAwMSA3LjcwMDc4IDUzLjQwMDEgMTEuNTAwOFoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNNjYuNDAwMSAxMS40OTg0QzY2LjQwMDEgOS4xOTg0NCA2NS4xMDAxIDguMDk4NDQgNjIuNTAwMSA4LjA5ODQ0QzU5LjgwMDEgOC4wOTg0NCA1OC43MDAxIDkuMDk4NDQgNTguNzAwMSAxMS40OTg0QzU4LjcwMDEgMTMuNzk4NCA1OS44MDAxIDE0Ljg5ODQgNjIuNTAwMSAxNC44OTg0QzY1LjEwMDEgMTQuODk4NCA2Ni40MDAxIDEzLjc5ODQgNjYuNDAwMSAxMS40OTg0Wk02Ni4zMDAxIDUuNTk4NDRINjkuNDAwMVYxNy4zOTg0SDY2LjMwMDFWMTQuNDk4NEM2NS40MDAxIDE2LjQ5ODQgNjMuNzAwMSAxNy42OTg0IDYxLjYwMDEgMTcuNjk4NEM1Ny43MDAxIDE3LjY5ODQgNTUuNjAwMSAxNS4yOTg0IDU1LjYwMDEgMTEuNTk4NEM1NS42MDAxIDcuNzk4NDQgNTcuNjAwMSA1LjM5ODQ0IDYxLjQwMDEgNS4zOTg0NEM2My41MDAxIDUuMzk4NDQgNjUuNDAwMSA2LjU5ODQ0IDY2LjIwMDEgOC41OTg0NFY1LjU5ODQ0SDY2LjMwMDFaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTgxLjM5OTkgOC4zSDc3LjM5OTlWMTQuN0g4MS4wOTk5VjE3LjRINzcuOTk5OUM3NS43OTk5IDE3LjQgNzQuMjk5OSAxNS45IDc0LjI5OTkgMTMuN1Y4LjNINzEuMzk5OVY1LjZINzQuMjk5OVYySDc3LjM5OTlWNS41SDgxLjM5OTlWOC4zWiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik05My42OTk5IDEyLjIwMDhMOTYuNjk5OSAxMi45MDA4Qzk1Ljk5OTkgMTYuMDAwOCA5My42OTk5IDE3LjYwMDggODkuOTk5OSAxNy42MDA4Qzg3Ljc5OTkgMTcuNjAwOCA4Ni4wOTk5IDE3LjAwMDggODQuNzk5OSAxNS45MDA4QzgzLjQ5OTkgMTQuODAwOCA4Mi44OTk5IDEzLjMwMDggODIuODk5OSAxMS41MDA4QzgyLjg5OTkgOS42MDA3OCA4My40OTk5IDguMTAwNzggODQuNzk5OSA3LjAwMDc4Qzg2LjA5OTkgNS45MDA3OCA4Ny43OTk5IDUuMzAwNzggODkuOTk5OSA1LjMwMDc4QzkzLjY5OTkgNS4zMDA3OCA5NS45OTk5IDYuOTAwNzggOTYuNjk5OSAxMC4wMDA4TDkzLjY5OTkgMTAuNzAwOEM5My4yOTk5IDguODAwNzggOTIuMjk5OSA4LjEwMDc4IDg5Ljg5OTkgOC4xMDA3OEM4Ni45OTk5IDguMTAwNzggODUuODk5OSA5LjIwMDc4IDg1Ljg5OTkgMTEuNTAwOEM4NS44OTk5IDEzLjgwMDggODYuOTk5OSAxNC45MDA4IDg5Ljg5OTkgMTQuOTAwOEM5Mi4yOTk5IDE0LjkwMDggOTMuMjk5OSAxNC4yMDA4IDkzLjY5OTkgMTIuMjAwOFoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTExLjggMTAuNlYxNy40SDEwOC43VjExLjJDMTA4LjcgOSAxMDcuOCA4LjEgMTA1LjkgOC4xQzEwMy42IDguMSAxMDIuNCA5LjMgMTAyLjQgMTEuN1YxNy4zSDk5LjI5OThWMC41SDEwMi40VjguM0MxMDMuMSA2LjMgMTA0LjYgNS4zIDEwNyA1LjNDMTEwLjIgNS4zIDExMS44IDcuMSAxMTEuOCAxMC42WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xMTQuMSAxNC4wOTg0TDExNy4xIDEzLjI5ODRDMTE3LjQgMTQuNzk4NCAxMTguMyAxNS4yOTg0IDEyMC4yIDE1LjI5ODRDMTIxLjggMTUuMjk4NCAxMjIuNyAxNC44OTg0IDEyMi43IDE0LjA5ODRDMTIyLjcgMTMuMjk4NCAxMjIuMiAxMi45OTg0IDEyMC42IDEyLjc5ODRMMTE4LjIgMTIuNDk4NEMxMTUuOSAxMi4xOTg0IDExNC41IDEwLjg5ODQgMTE0LjUgOC45OTg0NEMxMTQuNSA2Ljc5ODQ0IDExNi41IDUuMzk4NDQgMTE5LjggNS4zOTg0NEMxMjMuMSA1LjM5ODQ0IDEyNSA2LjY5ODQ0IDEyNS41IDguODk4NDRMMTIyLjUgOS41OTg0NEMxMjIuMyA4LjA5ODQ0IDEyMS41IDcuNjk4NDQgMTE5LjkgNy42OTg0NEMxMTguNCA3LjY5ODQ0IDExNy42IDguMDk4NDQgMTE3LjYgOC44OTg0NEMxMTcuNiA5LjU5ODQ0IDExOC4xIDkuOTk4NDQgMTE5LjMgMTAuMDk4NEwxMjIuMiAxMC40OTg0QzEyNC40IDEwLjc5ODQgMTI1LjggMTEuOTk4NCAxMjUuOCAxMy44OTg0QzEyNS44IDE2LjI5ODQgMTIzLjcgMTcuNzk4NCAxMjAuMSAxNy43OTg0QzExNi43IDE3LjU5ODQgMTE0LjUgMTYuMTk4NCAxMTQuMSAxNC4wOTg0WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xMzYuNyA4LjNIMTMyLjdWMTQuN0gxMzYuNFYxNy40SDEzMy4zQzEzMS4xIDE3LjQgMTI5LjYgMTUuOSAxMjkuNiAxMy43VjguM0gxMjYuN1Y1LjZIMTI5LjZWMkgxMzIuN1Y1LjVIMTM2LjdWOC4zWiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xNDkuMiAxMS40OTg0QzE0OS4yIDkuMTk4NDQgMTQ3LjkgOC4wOTg0NCAxNDUuMyA4LjA5ODQ0QzE0Mi42IDguMDk4NDQgMTQxLjUgOS4wOTg0NCAxNDEuNSAxMS40OTg0QzE0MS41IDEzLjc5ODQgMTQyLjYgMTQuODk4NCAxNDUuMyAxNC44OTg0QzE0Ny45IDE0Ljg5ODQgMTQ5LjIgMTMuNzk4NCAxNDkuMiAxMS40OTg0Wk0xNDkuMSA1LjU5ODQ0SDE1Mi4yVjE3LjM5ODRIMTQ5LjFWMTQuNDk4NEMxNDguMiAxNi40OTg0IDE0Ni41IDE3LjY5ODQgMTQ0LjQgMTcuNjk4NEMxNDAuNSAxNy42OTg0IDEzOC40IDE1LjI5ODQgMTM4LjQgMTEuNTk4NEMxMzguNCA3Ljc5ODQ0IDE0MC40IDUuMzk4NDQgMTQ0LjIgNS4zOTg0NEMxNDYuMyA1LjM5ODQ0IDE0OC4yIDYuNTk4NDQgMTQ5IDguNTk4NDRWNS41OTg0NEgxNDkuMVoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTY1LjYgMTIuMjAwOEwxNjguNiAxMi45MDA4QzE2Ny45IDE2LjAwMDggMTY1LjYgMTcuNjAwOCAxNjEuOSAxNy42MDA4QzE1OS43IDE3LjYwMDggMTU4IDE3LjAwMDggMTU2LjcgMTUuOTAwOEMxNTUuNCAxNC44MDA4IDE1NC44IDEzLjMwMDggMTU0LjggMTEuNTAwOEMxNTQuOCA5LjYwMDc4IDE1NS40IDguMTAwNzggMTU2LjcgNy4wMDA3OEMxNTggNS45MDA3OCAxNTkuNyA1LjMwMDc4IDE2MS45IDUuMzAwNzhDMTY1LjYgNS4zMDA3OCAxNjcuOSA2LjkwMDc4IDE2OC42IDEwLjAwMDhMMTY1LjYgMTAuNzAwOEMxNjUuMiA4LjgwMDc4IDE2NC4yIDguMTAwNzggMTYxLjggOC4xMDA3OEMxNTguOSA4LjEwMDc4IDE1Ny44IDkuMjAwNzggMTU3LjggMTEuNTAwOEMxNTcuOCAxMy44MDA4IDE1OC45IDE0LjkwMDggMTYxLjggMTQuOTAwOEMxNjQuMSAxNC45MDA4IDE2NS4xIDE0LjIwMDggMTY1LjYgMTIuMjAwOFoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTgwLjI1NyAxNy40TDE3Ni42NTcgMTIuM0wxNzQuMTU3IDE0LjZWMTcuNEgxNzEuMTU3VjAuNUgxNzQuMTU3VjExLjFMMTc5Ljg1NyA1LjZIMTgzLjc1N0wxNzguNTU3IDEwLjVMMTgzLjk1NyAxNy40SDE4MC4yNTdaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTAgMjEuNTAwOEg3LjNWMTYuMzAwOEgwVjIxLjUwMDhaIiBmaWxsPSIjQUZFNjE0Ii8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNNy4yOTk4IDIxLjUwMDhIMTQuNTk5OFYxNi4zMDA4SDcuMjk5OFYyMS41MDA4WiIgZmlsbD0iIzZFQkUwMCIvPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTE0LjYwMDEgMTYuM0gyOS4yMDAxVjAuNUgxNC42MDAxVjE2LjNaIiBmaWxsPSIjQUZFNjE0Ii8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMCAxMUgxNC42VjAuNUgwVjExWiIgZmlsbD0iIzZFQkUwMCIvPgo8L3N2Zz4K" alt="Patchstack Logo">
|
|
1430
|
+
</a>
|
|
1431
|
+
</div>
|
|
1432
|
+
</div>
|
|
1433
|
+
</div>
|
|
1434
|
+
</body>
|
|
1435
|
+
<script>
|
|
1436
|
+
function copyText(id) {
|
|
1437
|
+
var copyText = document.getElementById(id)
|
|
1438
|
+
copyText.select();
|
|
1439
|
+
copyText.setSelectionRange(0, 99999);
|
|
1440
|
+
navigator.clipboard.writeText(copyText.value);
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
function hover(element) {
|
|
1444
|
+
element.setAttribute('src', 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTkiIGhlaWdodD0iMjMiIHZpZXdCb3g9IjAgMCAxOSAyMyIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE0IDAuMDIzNDM3NUgwVjE2LjAyMzRIMlYyLjAyMzQ0SDE0VjAuMDIzNDM3NVpNMTkgNC4wMjM0NEg0VjIyLjAyMzRIMTlWNC4wMjM0NFpNMTcgMjAuMDIzNEg2VjYuMDIzNDRIMTdWMjAuMDIzNFoiIGZpbGw9IiNBRkU2MTQiLz4KPC9zdmc+Cg==');
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
function unhover(element) {
|
|
1448
|
+
element.setAttribute('src', 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTkiIGhlaWdodD0iMjMiIHZpZXdCb3g9IjAgMCAxOSAyMyIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE0IDAuMDIzNDM3NUgwVjE2LjAyMzRIMlYyLjAyMzQ0SDE0VjAuMDIzNDM3NVpNMTkgNC4wMjM0NEg0VjIyLjAyMzRIMTlWNC4wMjM0NFpNMTcgMjAuMDIzNEg2VjYuMDIzNDRIMTdWMjAuMDIzNFoiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo=');
|
|
1449
|
+
}
|
|
1450
|
+
</script>
|
|
1451
|
+
</html>`;
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1186
1454
|
// src/protect/runtime.js
|
|
1187
1455
|
import { readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
1188
1456
|
import { join } from "path";
|
|
@@ -1308,15 +1576,27 @@ async function createProtection(options = {}) {
|
|
|
1308
1576
|
if (mode !== "block" || !blockRule && !redactions.length) return { verdict: "pass" };
|
|
1309
1577
|
if (blockRule) return { verdict: "block" };
|
|
1310
1578
|
let body = text;
|
|
1311
|
-
|
|
1312
|
-
|
|
1579
|
+
const headers = { ...meta.headers || {} };
|
|
1580
|
+
for (const { rule, redactors } of redactions) {
|
|
1581
|
+
const mask = maskFn(rule.category);
|
|
1582
|
+
body = applyRedactors(body, redactors, mask);
|
|
1583
|
+
for (const name of Object.keys(headers)) {
|
|
1584
|
+
const value = headers[name];
|
|
1585
|
+
if (typeof value === "string") {
|
|
1586
|
+
headers[name] = applyRedactors(value, redactors, mask);
|
|
1587
|
+
} else if (Array.isArray(value)) {
|
|
1588
|
+
headers[name] = value.map((item) => typeof item === "string" ? applyRedactors(item, redactors, mask) : item);
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
return { verdict: "redact", body, headers };
|
|
1313
1593
|
};
|
|
1314
1594
|
const screenResp = async (response) => {
|
|
1315
1595
|
const text = await readTextResponse(response);
|
|
1316
1596
|
if (text == null) return response;
|
|
1317
1597
|
const r = screenText(text, { status: response.status, headers: headerObject(response.headers) });
|
|
1318
1598
|
if (r.verdict === "block") return leakResponse();
|
|
1319
|
-
if (r.verdict === "redact") return rebuildResponse(response, r.body);
|
|
1599
|
+
if (r.verdict === "redact") return rebuildResponse(response, r.body, r.headers);
|
|
1320
1600
|
return response;
|
|
1321
1601
|
};
|
|
1322
1602
|
const wrapNodeResponse = (res) => {
|
|
@@ -1366,7 +1646,7 @@ async function createProtection(options = {}) {
|
|
|
1366
1646
|
}
|
|
1367
1647
|
let r;
|
|
1368
1648
|
try {
|
|
1369
|
-
r = screenText(text, { status: res.statusCode, headers: {} });
|
|
1649
|
+
r = screenText(text, { status: res.statusCode, headers: res.getHeaders ? res.getHeaders() : {} });
|
|
1370
1650
|
} catch (err) {
|
|
1371
1651
|
onError?.(err);
|
|
1372
1652
|
for (const c of chunks) origWrite(c);
|
|
@@ -1385,6 +1665,22 @@ async function createProtection(options = {}) {
|
|
|
1385
1665
|
res.removeHeader && res.removeHeader("content-length");
|
|
1386
1666
|
} catch {
|
|
1387
1667
|
}
|
|
1668
|
+
if (r.headers && res.setHeader) {
|
|
1669
|
+
const current = res.getHeaders ? res.getHeaders() : {};
|
|
1670
|
+
for (const [name, value] of Object.entries(r.headers)) {
|
|
1671
|
+
if (Array.isArray(value)) {
|
|
1672
|
+
try {
|
|
1673
|
+
res.setHeader(name, value);
|
|
1674
|
+
} catch {
|
|
1675
|
+
}
|
|
1676
|
+
} else if (typeof value === "string" && current[name] !== value) {
|
|
1677
|
+
try {
|
|
1678
|
+
res.setHeader(name, value);
|
|
1679
|
+
} catch {
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1388
1684
|
return origEnd(r.body, cb);
|
|
1389
1685
|
}
|
|
1390
1686
|
for (const c of chunks) origWrite(c);
|
|
@@ -1421,7 +1717,7 @@ async function createProtection(options = {}) {
|
|
|
1421
1717
|
onError?.(err);
|
|
1422
1718
|
return null;
|
|
1423
1719
|
}
|
|
1424
|
-
return decide("request", result, () => blockResponse(result), () => null);
|
|
1720
|
+
return decide("request", result, () => blockResponse(result, request), () => null);
|
|
1425
1721
|
};
|
|
1426
1722
|
},
|
|
1427
1723
|
// Wrap a fetch handler: screens the request, then the response (redact/block).
|
|
@@ -1449,7 +1745,13 @@ async function createProtection(options = {}) {
|
|
|
1449
1745
|
decide(
|
|
1450
1746
|
"request",
|
|
1451
1747
|
result,
|
|
1452
|
-
() =>
|
|
1748
|
+
() => {
|
|
1749
|
+
if (isDocumentNavigation((n) => req.headers?.[n])) {
|
|
1750
|
+
res.status(403).type("html").send(renderBlockPage({ url: req.originalUrl || req.url || "/", code: result?.rule?.id }));
|
|
1751
|
+
} else {
|
|
1752
|
+
res.status(403).json(blockBody(result));
|
|
1753
|
+
}
|
|
1754
|
+
},
|
|
1453
1755
|
() => {
|
|
1454
1756
|
if (exprOptions.screenResponses) wrapNodeResponse(res);
|
|
1455
1757
|
next();
|
|
@@ -1493,8 +1795,13 @@ async function createProtection(options = {}) {
|
|
|
1493
1795
|
result,
|
|
1494
1796
|
() => {
|
|
1495
1797
|
res.statusCode = 403;
|
|
1496
|
-
|
|
1497
|
-
|
|
1798
|
+
if (isDocumentNavigation((n) => req.headers?.[n])) {
|
|
1799
|
+
res.setHeader("content-type", "text/html; charset=utf-8");
|
|
1800
|
+
res.end(renderBlockPage({ url: req.url || "/", code: result?.rule?.id }));
|
|
1801
|
+
} else {
|
|
1802
|
+
res.setHeader("content-type", "application/json");
|
|
1803
|
+
res.end(JSON.stringify(blockBody(result)));
|
|
1804
|
+
}
|
|
1498
1805
|
},
|
|
1499
1806
|
() => {
|
|
1500
1807
|
if (req.body === void 0) req.body = shaped.body;
|
|
@@ -1507,7 +1814,7 @@ async function createProtection(options = {}) {
|
|
|
1507
1814
|
}
|
|
1508
1815
|
};
|
|
1509
1816
|
if (options.egress) {
|
|
1510
|
-
protection.uninstallEgress = installEgressGuard({ shouldBlock: egressShouldBlock, onBlock: options.onEgressBlock });
|
|
1817
|
+
protection.uninstallEgress = await installEgressGuard({ shouldBlock: egressShouldBlock, onBlock: options.onEgressBlock });
|
|
1511
1818
|
}
|
|
1512
1819
|
return protection;
|
|
1513
1820
|
}
|
|
@@ -1533,6 +1840,8 @@ function headerObject(headers) {
|
|
|
1533
1840
|
headers?.forEach?.((v, k) => {
|
|
1534
1841
|
out[k.toLowerCase()] = v;
|
|
1535
1842
|
});
|
|
1843
|
+
const setCookies = headers?.getSetCookie?.();
|
|
1844
|
+
if (setCookies && setCookies.length) out["set-cookie"] = setCookies;
|
|
1536
1845
|
return out;
|
|
1537
1846
|
}
|
|
1538
1847
|
function extractRedactors(rule) {
|
|
@@ -1567,9 +1876,27 @@ function applyRedactors(body, redactors, mask) {
|
|
|
1567
1876
|
}
|
|
1568
1877
|
return out;
|
|
1569
1878
|
}
|
|
1570
|
-
function rebuildResponse(response, body) {
|
|
1879
|
+
function rebuildResponse(response, body, redactedHeaders) {
|
|
1571
1880
|
const headers = new Headers(response.headers);
|
|
1572
1881
|
headers.delete("content-length");
|
|
1882
|
+
if (redactedHeaders) {
|
|
1883
|
+
for (const [name, value] of Object.entries(redactedHeaders)) {
|
|
1884
|
+
if (typeof value === "string") {
|
|
1885
|
+
if (headers.get(name) !== value) {
|
|
1886
|
+
try {
|
|
1887
|
+
headers.set(name, value);
|
|
1888
|
+
} catch {
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
} else if (Array.isArray(value)) {
|
|
1892
|
+
try {
|
|
1893
|
+
headers.delete(name);
|
|
1894
|
+
for (const item of value) headers.append(name, String(item));
|
|
1895
|
+
} catch {
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1573
1900
|
return new Response(body, { status: response.status, statusText: response.statusText, headers });
|
|
1574
1901
|
}
|
|
1575
1902
|
function leakResponse() {
|
|
@@ -1579,40 +1906,43 @@ function leakResponse() {
|
|
|
1579
1906
|
});
|
|
1580
1907
|
}
|
|
1581
1908
|
async function resolveRules(options) {
|
|
1909
|
+
const cache = makeCache(options);
|
|
1582
1910
|
if (options.siteUuid) {
|
|
1583
|
-
const
|
|
1911
|
+
const prior = await cache.read();
|
|
1912
|
+
const client = new PulseRuleClient({ siteUuid: options.siteUuid, baseUrl: options.pulseRulesUrl, etag: prior?.etag });
|
|
1584
1913
|
const res = await client.getRules();
|
|
1585
|
-
if (res.success)
|
|
1914
|
+
if (res.success && res.notModified && prior?.bundle) return normalizeBundle(prior.bundle);
|
|
1915
|
+
if (res.success && !res.notModified) {
|
|
1586
1916
|
const bundle = normalizeBundle(res);
|
|
1587
|
-
|
|
1917
|
+
await cache.write({ bundle, etag: res.etag ?? null });
|
|
1588
1918
|
return bundle;
|
|
1589
1919
|
}
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
return normalizeBundle(cached);
|
|
1920
|
+
if (prior?.bundle) {
|
|
1921
|
+
options.onError?.(new Error(`pulse rule fetch failed (${res.error ?? "no usable response"}); using cached bundle`));
|
|
1922
|
+
return normalizeBundle(prior.bundle);
|
|
1594
1923
|
}
|
|
1595
1924
|
if (options.rules) {
|
|
1596
|
-
options.onError?.(new Error(`pulse rule fetch failed (${res.error}); using bundled fallback`));
|
|
1925
|
+
options.onError?.(new Error(`pulse rule fetch failed (${res.error ?? "no usable response"}); using bundled fallback`));
|
|
1597
1926
|
return normalizeBundle(options.rules);
|
|
1598
1927
|
}
|
|
1599
|
-
options.onError?.(new Error(`pulse rule fetch failed (${res.error}); no cache \u2014 running with no rules`));
|
|
1928
|
+
options.onError?.(new Error(`pulse rule fetch failed (${res.error ?? "no usable response"}); no cache \u2014 running with no rules`));
|
|
1600
1929
|
return emptyBundle();
|
|
1601
1930
|
}
|
|
1602
1931
|
if (options.token) {
|
|
1603
|
-
const
|
|
1932
|
+
const prior = await cache.read();
|
|
1933
|
+
const client = new PatchstackRuleClient({ token: options.token, baseUrl: options.baseUrl, etag: prior?.etag });
|
|
1604
1934
|
const res = await client.getRules();
|
|
1605
|
-
if (res.success)
|
|
1935
|
+
if (res.success && res.notModified && prior?.bundle) return normalizeBundle(prior.bundle);
|
|
1936
|
+
if (res.success && !res.notModified) {
|
|
1606
1937
|
const bundle = normalizeBundle(res);
|
|
1607
|
-
|
|
1938
|
+
await cache.write({ bundle, etag: res.etag ?? null });
|
|
1608
1939
|
return bundle;
|
|
1609
1940
|
}
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
return normalizeBundle(cached);
|
|
1941
|
+
if (prior?.bundle) {
|
|
1942
|
+
options.onError?.(new Error(`rule fetch failed (${res.error ?? "no usable response"}); using cached bundle`));
|
|
1943
|
+
return normalizeBundle(prior.bundle);
|
|
1614
1944
|
}
|
|
1615
|
-
options.onError?.(new Error(`rule fetch failed (${res.error}); no cache \u2014 running with no rules`));
|
|
1945
|
+
options.onError?.(new Error(`rule fetch failed (${res.error ?? "no usable response"}); no cache \u2014 running with no rules`));
|
|
1616
1946
|
return emptyBundle();
|
|
1617
1947
|
}
|
|
1618
1948
|
if (options.rules) {
|
|
@@ -1630,33 +1960,76 @@ function normalizeBundle(b) {
|
|
|
1630
1960
|
function emptyBundle() {
|
|
1631
1961
|
return { firewall: [], whitelists: [], whitelist_keys: {} };
|
|
1632
1962
|
}
|
|
1963
|
+
function makeCache(options) {
|
|
1964
|
+
const adapter = options.ruleCache;
|
|
1965
|
+
if (adapter && typeof adapter.read === "function" && typeof adapter.write === "function") {
|
|
1966
|
+
return {
|
|
1967
|
+
read: async () => {
|
|
1968
|
+
try {
|
|
1969
|
+
return toEnvelope(await adapter.read());
|
|
1970
|
+
} catch {
|
|
1971
|
+
return null;
|
|
1972
|
+
}
|
|
1973
|
+
},
|
|
1974
|
+
write: async (env) => {
|
|
1975
|
+
try {
|
|
1976
|
+
await adapter.write(env);
|
|
1977
|
+
} catch {
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
};
|
|
1981
|
+
}
|
|
1982
|
+
const dir = options.cacheDir;
|
|
1983
|
+
return {
|
|
1984
|
+
read: async () => cacheRead(dir),
|
|
1985
|
+
write: async (env) => cacheWrite(dir, env)
|
|
1986
|
+
};
|
|
1987
|
+
}
|
|
1633
1988
|
function cachePath(dir) {
|
|
1634
1989
|
return join(dir, "patchstack-rules.json");
|
|
1635
1990
|
}
|
|
1636
|
-
function cacheWrite(dir,
|
|
1991
|
+
function cacheWrite(dir, env) {
|
|
1637
1992
|
if (!dir) return;
|
|
1638
1993
|
try {
|
|
1639
1994
|
mkdirSync(dir, { recursive: true });
|
|
1640
|
-
writeFileSync(cachePath(dir), JSON.stringify(
|
|
1995
|
+
writeFileSync(cachePath(dir), JSON.stringify(env));
|
|
1641
1996
|
} catch {
|
|
1642
1997
|
}
|
|
1643
1998
|
}
|
|
1644
1999
|
function cacheRead(dir) {
|
|
1645
2000
|
if (!dir) return null;
|
|
1646
2001
|
try {
|
|
1647
|
-
return JSON.parse(readFileSync(cachePath(dir), "utf8"));
|
|
2002
|
+
return toEnvelope(JSON.parse(readFileSync(cachePath(dir), "utf8")));
|
|
1648
2003
|
} catch {
|
|
1649
2004
|
return null;
|
|
1650
2005
|
}
|
|
1651
2006
|
}
|
|
2007
|
+
function toEnvelope(value) {
|
|
2008
|
+
if (!value || typeof value !== "object") return null;
|
|
2009
|
+
if (value.bundle && typeof value.bundle === "object") {
|
|
2010
|
+
return { bundle: value.bundle, etag: value.etag ?? null };
|
|
2011
|
+
}
|
|
2012
|
+
if (Array.isArray(value.firewall) || Array.isArray(value.whitelists)) {
|
|
2013
|
+
return { bundle: value, etag: null };
|
|
2014
|
+
}
|
|
2015
|
+
return null;
|
|
2016
|
+
}
|
|
2017
|
+
var BLOCK_MESSAGE = "This request has been blocked by Patchstack.";
|
|
1652
2018
|
function blockBody(result) {
|
|
1653
|
-
return {
|
|
1654
|
-
error: "Blocked by Patchstack",
|
|
1655
|
-
rule: result.rule?.id,
|
|
1656
|
-
message: result.message
|
|
1657
|
-
};
|
|
2019
|
+
return { error: BLOCK_MESSAGE, message: BLOCK_MESSAGE, rule: result?.rule?.id };
|
|
1658
2020
|
}
|
|
1659
|
-
function
|
|
2021
|
+
function isDocumentNavigation(getHeader) {
|
|
2022
|
+
const dest = getHeader("sec-fetch-dest");
|
|
2023
|
+
if (dest) return dest === "document";
|
|
2024
|
+
return (getHeader("accept") || "").includes("text/html");
|
|
2025
|
+
}
|
|
2026
|
+
function blockResponse(result, request) {
|
|
2027
|
+
if (request && isDocumentNavigation((n) => request.headers.get(n))) {
|
|
2028
|
+
return new Response(renderBlockPage({ url: request.url, code: result?.rule?.id }), {
|
|
2029
|
+
status: 403,
|
|
2030
|
+
headers: { "content-type": "text/html; charset=utf-8" }
|
|
2031
|
+
});
|
|
2032
|
+
}
|
|
1660
2033
|
return new Response(JSON.stringify(blockBody(result)), {
|
|
1661
2034
|
status: 403,
|
|
1662
2035
|
headers: { "content-type": "application/json" }
|