@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.cjs
CHANGED
|
@@ -31,38 +31,48 @@ module.exports = __toCommonJS(runtime_exports);
|
|
|
31
31
|
// src/protect/engine/client.js
|
|
32
32
|
var DEFAULT_BASE_URL = "https://api.patchstack.com";
|
|
33
33
|
var DEFAULT_CACHE_TTL = 3e5;
|
|
34
|
+
var JITTER_FRACTION = 0.1;
|
|
34
35
|
var PatchstackRuleClient = class {
|
|
35
36
|
#token;
|
|
36
37
|
#baseUrl;
|
|
37
38
|
#cacheTtl;
|
|
38
39
|
#cache = null;
|
|
39
40
|
#cacheTime = null;
|
|
40
|
-
|
|
41
|
+
#ttlEffective = 0;
|
|
42
|
+
#etag;
|
|
43
|
+
constructor({ token, baseUrl, cacheTtl, etag } = {}) {
|
|
41
44
|
this.#token = token ?? process.env.PATCHSTACK_WAF_TOKEN;
|
|
42
45
|
this.#baseUrl = baseUrl ?? process.env.PATCHSTACK_WAF_API_URL ?? DEFAULT_BASE_URL;
|
|
43
46
|
this.#cacheTtl = cacheTtl ?? DEFAULT_CACHE_TTL;
|
|
47
|
+
this.#etag = etag ?? null;
|
|
44
48
|
if (!this.#token) {
|
|
45
49
|
throw new Error("Patchstack WAF token is required. Pass { token } or set PATCHSTACK_WAF_TOKEN env var.");
|
|
46
50
|
}
|
|
47
51
|
}
|
|
48
52
|
async getRules() {
|
|
49
53
|
const now = Date.now();
|
|
50
|
-
if (this.#cache && this.#cacheTime && now - this.#cacheTime < this.#
|
|
54
|
+
if (this.#cache && this.#cacheTime !== null && now - this.#cacheTime < this.#ttlEffective) {
|
|
51
55
|
return this.#cache;
|
|
52
56
|
}
|
|
53
57
|
const url = `${this.#baseUrl}/api/get-rules/3`;
|
|
54
58
|
try {
|
|
59
|
+
const headers = {
|
|
60
|
+
"Content-Type": "application/json",
|
|
61
|
+
"Authorization": `Bearer ${this.#token}`,
|
|
62
|
+
"LicenseID": "1"
|
|
63
|
+
// Hard-coded, is never actually used but needed by the API
|
|
64
|
+
};
|
|
65
|
+
if (this.#etag) headers["If-None-Match"] = this.#etag;
|
|
55
66
|
const response = await fetch(url, {
|
|
56
67
|
method: "POST",
|
|
57
|
-
headers
|
|
58
|
-
"Content-Type": "application/json",
|
|
59
|
-
"Authorization": `Bearer ${this.#token}`,
|
|
60
|
-
"LicenseID": "1"
|
|
61
|
-
// Hard-coded, is never actually used but needed by the API
|
|
62
|
-
},
|
|
68
|
+
headers,
|
|
63
69
|
body: JSON.stringify({}),
|
|
64
70
|
signal: AbortSignal.timeout(3e4)
|
|
65
71
|
});
|
|
72
|
+
if (response.status === 304) {
|
|
73
|
+
this.#touch(now);
|
|
74
|
+
return this.#cache ?? { success: true, notModified: true, etag: this.#etag, firewall: [], whitelists: [], whitelist_keys: {} };
|
|
75
|
+
}
|
|
66
76
|
if (!response.ok) {
|
|
67
77
|
return {
|
|
68
78
|
success: false,
|
|
@@ -75,12 +85,15 @@ var PatchstackRuleClient = class {
|
|
|
75
85
|
const data = await response.json();
|
|
76
86
|
const result = {
|
|
77
87
|
success: true,
|
|
88
|
+
notModified: false,
|
|
89
|
+
etag: response.headers?.get?.("etag") ?? null,
|
|
78
90
|
firewall: Array.isArray(data.firewall) ? data.firewall : [],
|
|
79
91
|
whitelists: Array.isArray(data.whitelists) ? data.whitelists : [],
|
|
80
92
|
whitelist_keys: data.whitelist_keys ?? {}
|
|
81
93
|
};
|
|
82
94
|
this.#cache = result;
|
|
83
|
-
this.#
|
|
95
|
+
this.#etag = result.etag;
|
|
96
|
+
this.#touch(now);
|
|
84
97
|
return result;
|
|
85
98
|
} catch (err) {
|
|
86
99
|
return {
|
|
@@ -92,9 +105,14 @@ var PatchstackRuleClient = class {
|
|
|
92
105
|
};
|
|
93
106
|
}
|
|
94
107
|
}
|
|
108
|
+
#touch(now) {
|
|
109
|
+
this.#cacheTime = now;
|
|
110
|
+
this.#ttlEffective = this.#cacheTtl - Math.floor(Math.random() * this.#cacheTtl * JITTER_FRACTION);
|
|
111
|
+
}
|
|
95
112
|
clearCache() {
|
|
96
113
|
this.#cache = null;
|
|
97
114
|
this.#cacheTime = null;
|
|
115
|
+
this.#etag = null;
|
|
98
116
|
}
|
|
99
117
|
};
|
|
100
118
|
|
|
@@ -932,44 +950,53 @@ var RuleEngine = class {
|
|
|
932
950
|
// src/protect/engine/pulse-client.js
|
|
933
951
|
var DEFAULT_BASE_URL2 = "https://api.patchstack.com/monitor/pulse";
|
|
934
952
|
var DEFAULT_CACHE_TTL2 = 3e5;
|
|
953
|
+
var JITTER_FRACTION2 = 0.1;
|
|
935
954
|
var PulseRuleClient = class {
|
|
936
955
|
#siteUuid;
|
|
937
956
|
#baseUrl;
|
|
938
957
|
#cacheTtl;
|
|
939
958
|
#cache = null;
|
|
940
959
|
#cacheTime = null;
|
|
941
|
-
|
|
960
|
+
#ttlEffective = 0;
|
|
961
|
+
#etag;
|
|
962
|
+
constructor({ siteUuid, baseUrl, cacheTtl, etag } = {}) {
|
|
942
963
|
this.#siteUuid = siteUuid ?? process.env.PATCHSTACK_SITE_UUID;
|
|
943
964
|
this.#baseUrl = baseUrl ?? process.env.PATCHSTACK_PULSE_RULES_URL ?? DEFAULT_BASE_URL2;
|
|
944
965
|
this.#cacheTtl = cacheTtl ?? DEFAULT_CACHE_TTL2;
|
|
966
|
+
this.#etag = etag ?? null;
|
|
945
967
|
if (!this.#siteUuid) {
|
|
946
968
|
throw new Error("Patchstack site UUID is required. Pass { siteUuid } or set PATCHSTACK_SITE_UUID.");
|
|
947
969
|
}
|
|
948
970
|
}
|
|
949
971
|
async getRules() {
|
|
950
972
|
const now = Date.now();
|
|
951
|
-
if (this.#cache && this.#cacheTime && now - this.#cacheTime < this.#
|
|
973
|
+
if (this.#cache && this.#cacheTime !== null && now - this.#cacheTime < this.#ttlEffective) {
|
|
952
974
|
return this.#cache;
|
|
953
975
|
}
|
|
954
976
|
const url = `${this.#baseUrl}/rules/${encodeURIComponent(this.#siteUuid)}`;
|
|
955
977
|
try {
|
|
956
|
-
const
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
978
|
+
const headers = { Accept: "application/json" };
|
|
979
|
+
if (this.#etag) headers["If-None-Match"] = this.#etag;
|
|
980
|
+
const response = await fetch(url, { method: "GET", headers, signal: AbortSignal.timeout(3e4) });
|
|
981
|
+
if (response.status === 304) {
|
|
982
|
+
this.#touch(now);
|
|
983
|
+
return this.#cache ?? { success: true, notModified: true, etag: this.#etag, firewall: [], whitelists: [], whitelist_keys: {} };
|
|
984
|
+
}
|
|
961
985
|
if (!response.ok) {
|
|
962
986
|
return { success: false, error: `API returned ${response.status}`, firewall: [], whitelists: [], whitelist_keys: {} };
|
|
963
987
|
}
|
|
964
988
|
const data = await response.json();
|
|
965
989
|
const result = {
|
|
966
990
|
success: true,
|
|
991
|
+
notModified: false,
|
|
992
|
+
etag: response.headers?.get?.("etag") ?? null,
|
|
967
993
|
firewall: Array.isArray(data.firewall) ? data.firewall : [],
|
|
968
994
|
whitelists: Array.isArray(data.whitelists) ? data.whitelists : [],
|
|
969
995
|
whitelist_keys: data.whitelist_keys ?? {}
|
|
970
996
|
};
|
|
971
997
|
this.#cache = result;
|
|
972
|
-
this.#
|
|
998
|
+
this.#etag = result.etag;
|
|
999
|
+
this.#touch(now);
|
|
973
1000
|
return result;
|
|
974
1001
|
} catch (err) {
|
|
975
1002
|
return {
|
|
@@ -981,14 +1008,21 @@ var PulseRuleClient = class {
|
|
|
981
1008
|
};
|
|
982
1009
|
}
|
|
983
1010
|
}
|
|
1011
|
+
#touch(now) {
|
|
1012
|
+
this.#cacheTime = now;
|
|
1013
|
+
this.#ttlEffective = this.#cacheTtl - Math.floor(Math.random() * this.#cacheTtl * JITTER_FRACTION2);
|
|
1014
|
+
}
|
|
984
1015
|
clearCache() {
|
|
985
1016
|
this.#cache = null;
|
|
986
1017
|
this.#cacheTime = null;
|
|
1018
|
+
this.#etag = null;
|
|
987
1019
|
}
|
|
988
1020
|
};
|
|
989
1021
|
|
|
990
1022
|
// src/protect/engine/fetch.js
|
|
991
|
-
|
|
1023
|
+
var MAX_BODY_BYTES = 1024 * 1024;
|
|
1024
|
+
async function fromFetchRequest(request, options = {}) {
|
|
1025
|
+
const maxBodyBytes = options.maxBodyBytes ?? MAX_BODY_BYTES;
|
|
992
1026
|
const url = new URL(request.url);
|
|
993
1027
|
const method = (request.method || "GET").toUpperCase();
|
|
994
1028
|
const headers = {};
|
|
@@ -1005,14 +1039,11 @@ async function fromFetchRequest(request) {
|
|
|
1005
1039
|
}
|
|
1006
1040
|
let rawBody = "";
|
|
1007
1041
|
if (method !== "GET" && method !== "HEAD") {
|
|
1008
|
-
|
|
1009
|
-
rawBody = await request.clone().text();
|
|
1010
|
-
} catch {
|
|
1011
|
-
rawBody = "";
|
|
1012
|
-
}
|
|
1042
|
+
rawBody = await readCappedText(request, maxBodyBytes);
|
|
1013
1043
|
}
|
|
1014
1044
|
const contentType = headers["content-type"] || "";
|
|
1015
1045
|
let body = {};
|
|
1046
|
+
let files;
|
|
1016
1047
|
if (rawBody) {
|
|
1017
1048
|
if (contentType.includes("application/json")) {
|
|
1018
1049
|
try {
|
|
@@ -1025,6 +1056,13 @@ async function fromFetchRequest(request) {
|
|
|
1025
1056
|
for (const [k, v] of new URLSearchParams(rawBody)) {
|
|
1026
1057
|
body[k] = k in body ? [].concat(body[k], v) : v;
|
|
1027
1058
|
}
|
|
1059
|
+
} else if (contentType.includes("multipart/form-data")) {
|
|
1060
|
+
const boundary = /boundary=("?)([^";]+)\1/i.exec(contentType)?.[2];
|
|
1061
|
+
if (boundary) {
|
|
1062
|
+
const parsed = parseMultipart(rawBody, boundary);
|
|
1063
|
+
body = parsed.body;
|
|
1064
|
+
files = parsed.files;
|
|
1065
|
+
}
|
|
1028
1066
|
}
|
|
1029
1067
|
}
|
|
1030
1068
|
const uri = url.pathname + url.search;
|
|
@@ -1035,6 +1073,7 @@ async function fromFetchRequest(request) {
|
|
|
1035
1073
|
originalUrl: uri,
|
|
1036
1074
|
query,
|
|
1037
1075
|
body,
|
|
1076
|
+
files,
|
|
1038
1077
|
headers,
|
|
1039
1078
|
ip: forwarded.split(",")[0].trim(),
|
|
1040
1079
|
cookies: parseCookies(headers.cookie),
|
|
@@ -1043,6 +1082,44 @@ async function fromFetchRequest(request) {
|
|
|
1043
1082
|
_rawBody: rawBody
|
|
1044
1083
|
};
|
|
1045
1084
|
}
|
|
1085
|
+
async function readCappedText(request, max) {
|
|
1086
|
+
const ceiling = Math.max(max, max * 4);
|
|
1087
|
+
const declared = Number(request.headers?.get?.("content-length") || 0);
|
|
1088
|
+
if (declared && declared > ceiling) return "";
|
|
1089
|
+
let clone;
|
|
1090
|
+
try {
|
|
1091
|
+
clone = request.clone();
|
|
1092
|
+
} catch {
|
|
1093
|
+
return "";
|
|
1094
|
+
}
|
|
1095
|
+
try {
|
|
1096
|
+
const text = await clone.text();
|
|
1097
|
+
return text.length > max ? text.slice(0, max) : text;
|
|
1098
|
+
} catch {
|
|
1099
|
+
return "";
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
function parseMultipart(rawBody, boundary) {
|
|
1103
|
+
const body = {};
|
|
1104
|
+
const files = {};
|
|
1105
|
+
for (const part of rawBody.split("--" + boundary)) {
|
|
1106
|
+
const sep = /\r?\n\r?\n/.exec(part);
|
|
1107
|
+
if (!sep) continue;
|
|
1108
|
+
const rawHeaders = part.slice(0, sep.index);
|
|
1109
|
+
const disposition = /content-disposition:[^\r\n]*/i.exec(rawHeaders)?.[0];
|
|
1110
|
+
if (!disposition) continue;
|
|
1111
|
+
const name = /name="([^"]*)"/i.exec(disposition)?.[1];
|
|
1112
|
+
if (name == null) continue;
|
|
1113
|
+
const content = part.slice(sep.index + sep[0].length).replace(/\r?\n$/, "");
|
|
1114
|
+
const filename = /filename="([^"]*)"/i.exec(disposition)?.[1];
|
|
1115
|
+
if (filename !== void 0) {
|
|
1116
|
+
files[name] = name in files ? [].concat(files[name], filename) : filename;
|
|
1117
|
+
} else {
|
|
1118
|
+
body[name] = name in body ? [].concat(body[name], content) : content;
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
return { body, files };
|
|
1122
|
+
}
|
|
1046
1123
|
function parseCookies(header) {
|
|
1047
1124
|
const cookies = {};
|
|
1048
1125
|
if (!header) {
|
|
@@ -1122,35 +1199,124 @@ function parseCookies2(header) {
|
|
|
1122
1199
|
}
|
|
1123
1200
|
|
|
1124
1201
|
// src/protect/egress.js
|
|
1125
|
-
function installEgressGuard({ shouldBlock, onBlock } = {}) {
|
|
1126
|
-
const
|
|
1127
|
-
if (typeof
|
|
1128
|
-
|
|
1202
|
+
async function installEgressGuard({ shouldBlock, onBlock } = {}) {
|
|
1203
|
+
const restores = [];
|
|
1204
|
+
if (typeof shouldBlock !== "function") return () => {
|
|
1205
|
+
};
|
|
1206
|
+
const block = (url, host, method) => {
|
|
1207
|
+
if (!shouldBlock(url, host, method)) return false;
|
|
1208
|
+
onBlock?.({ url, host, method });
|
|
1209
|
+
return true;
|
|
1210
|
+
};
|
|
1211
|
+
const originalFetch = globalThis.fetch;
|
|
1212
|
+
if (typeof originalFetch === "function" && !originalFetch.__patchstackGuarded) {
|
|
1213
|
+
const guarded = async (input, init) => {
|
|
1214
|
+
const url = typeof input === "string" ? input : input && input.url || String(input);
|
|
1215
|
+
let host = null;
|
|
1216
|
+
try {
|
|
1217
|
+
host = new URL(url).hostname;
|
|
1218
|
+
} catch {
|
|
1219
|
+
host = null;
|
|
1220
|
+
}
|
|
1221
|
+
const method = init && init.method || input && input.method || "GET";
|
|
1222
|
+
if (block(url, host, method)) {
|
|
1223
|
+
throw new Error(`Patchstack blocked an outbound request to a disallowed address: ${host ?? url}`);
|
|
1224
|
+
}
|
|
1225
|
+
return originalFetch(input, init);
|
|
1129
1226
|
};
|
|
1227
|
+
guarded.__patchstackGuarded = true;
|
|
1228
|
+
globalThis.fetch = guarded;
|
|
1229
|
+
restores.push(() => {
|
|
1230
|
+
if (globalThis.fetch === guarded) globalThis.fetch = originalFetch;
|
|
1231
|
+
});
|
|
1130
1232
|
}
|
|
1131
|
-
const
|
|
1132
|
-
const url = typeof input === "string" ? input : input && input.url || String(input);
|
|
1133
|
-
let host = null;
|
|
1233
|
+
for (const moduleName of ["node:http", "node:https"]) {
|
|
1134
1234
|
try {
|
|
1135
|
-
|
|
1235
|
+
const mod = await import(moduleName);
|
|
1236
|
+
const restore = patchHttpModule(mod.default ?? mod, block);
|
|
1237
|
+
if (restore) restores.push(restore);
|
|
1136
1238
|
} catch {
|
|
1137
|
-
host = null;
|
|
1138
1239
|
}
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1240
|
+
}
|
|
1241
|
+
const OriginalWS = globalThis.WebSocket;
|
|
1242
|
+
if (typeof OriginalWS === "function" && !OriginalWS.__patchstackGuarded) {
|
|
1243
|
+
const GuardedWS = new Proxy(OriginalWS, {
|
|
1244
|
+
construct(target, args, newTarget) {
|
|
1245
|
+
const url = String(args?.[0] ?? "");
|
|
1246
|
+
let host = null;
|
|
1247
|
+
try {
|
|
1248
|
+
host = new URL(url).hostname;
|
|
1249
|
+
} catch {
|
|
1250
|
+
host = null;
|
|
1251
|
+
}
|
|
1252
|
+
if (block(url, host, "WEBSOCKET")) {
|
|
1253
|
+
throw new Error(`Patchstack blocked an outbound WebSocket to a disallowed address: ${host ?? url}`);
|
|
1254
|
+
}
|
|
1255
|
+
return Reflect.construct(target, args, newTarget);
|
|
1256
|
+
}
|
|
1257
|
+
});
|
|
1258
|
+
OriginalWS.__patchstackGuarded = true;
|
|
1259
|
+
globalThis.WebSocket = GuardedWS;
|
|
1260
|
+
restores.push(() => {
|
|
1261
|
+
if (globalThis.WebSocket === GuardedWS) globalThis.WebSocket = OriginalWS;
|
|
1262
|
+
delete OriginalWS.__patchstackGuarded;
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
return () => {
|
|
1266
|
+
for (const restore of restores) {
|
|
1267
|
+
try {
|
|
1268
|
+
restore();
|
|
1269
|
+
} catch {
|
|
1270
|
+
}
|
|
1143
1271
|
}
|
|
1144
|
-
return original(input, init);
|
|
1145
1272
|
};
|
|
1146
|
-
|
|
1147
|
-
|
|
1273
|
+
}
|
|
1274
|
+
function patchHttpModule(http, block) {
|
|
1275
|
+
if (!http || typeof http.request !== "function" || http.__patchstackGuarded) return null;
|
|
1276
|
+
const originalRequest = http.request;
|
|
1277
|
+
const originalGet = http.get;
|
|
1278
|
+
const wrap = (original) => function(...args) {
|
|
1279
|
+
const target = extractHttpTarget(args);
|
|
1280
|
+
if (target && block(target.url, target.host, target.method)) {
|
|
1281
|
+
throw new Error(`Patchstack blocked an outbound request to a disallowed address: ${target.host ?? target.url}`);
|
|
1282
|
+
}
|
|
1283
|
+
return original.apply(this, args);
|
|
1284
|
+
};
|
|
1285
|
+
http.request = wrap(originalRequest);
|
|
1286
|
+
if (typeof originalGet === "function") http.get = wrap(originalGet);
|
|
1287
|
+
http.__patchstackGuarded = true;
|
|
1148
1288
|
return () => {
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1289
|
+
http.request = originalRequest;
|
|
1290
|
+
if (typeof originalGet === "function") http.get = originalGet;
|
|
1291
|
+
delete http.__patchstackGuarded;
|
|
1152
1292
|
};
|
|
1153
1293
|
}
|
|
1294
|
+
function extractHttpTarget(args) {
|
|
1295
|
+
const first = args[0];
|
|
1296
|
+
try {
|
|
1297
|
+
if (typeof first === "string" || first instanceof URL) {
|
|
1298
|
+
const url = new URL(String(first));
|
|
1299
|
+
const opts = args.find((a) => a && typeof a === "object" && !(a instanceof URL));
|
|
1300
|
+
return { url: url.href, host: url.hostname, method: opts && opts.method || "GET" };
|
|
1301
|
+
}
|
|
1302
|
+
if (first && typeof first === "object") {
|
|
1303
|
+
const host = normalizeHost(first.hostname || first.host);
|
|
1304
|
+
const protocol = first.protocol || "http:";
|
|
1305
|
+
const port = first.port ? `:${first.port}` : "";
|
|
1306
|
+
const path = first.path || "/";
|
|
1307
|
+
return { url: `${protocol}//${host}${port}${path}`, host, method: first.method || "GET" };
|
|
1308
|
+
}
|
|
1309
|
+
} catch {
|
|
1310
|
+
}
|
|
1311
|
+
return null;
|
|
1312
|
+
}
|
|
1313
|
+
function normalizeHost(raw) {
|
|
1314
|
+
const host = String(raw || "").trim();
|
|
1315
|
+
const bracketed = /^\[([^\]]+)\]/.exec(host);
|
|
1316
|
+
if (bracketed) return bracketed[1];
|
|
1317
|
+
if ((host.match(/:/g) || []).length > 1) return host;
|
|
1318
|
+
return host.split(":")[0];
|
|
1319
|
+
}
|
|
1154
1320
|
|
|
1155
1321
|
// src/protect/defaults.js
|
|
1156
1322
|
var DEFAULT_RESPONSE_RULES = [
|
|
@@ -1201,6 +1367,25 @@ var DEFAULT_RESPONSE_RULES = [
|
|
|
1201
1367
|
category: "info-exposure",
|
|
1202
1368
|
action: "redact",
|
|
1203
1369
|
rule_v2: [{ parameter: "response.body", match: { type: "regex", value: "/\\n\\s+at\\s+.+\\(.+:\\d+:\\d+\\)/" } }]
|
|
1370
|
+
},
|
|
1371
|
+
{
|
|
1372
|
+
id: "resp-sql-error",
|
|
1373
|
+
title: "SQL / ORM error disclosure in response body",
|
|
1374
|
+
phase: "response",
|
|
1375
|
+
category: "info-exposure",
|
|
1376
|
+
action: "redact",
|
|
1377
|
+
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" } }]
|
|
1378
|
+
},
|
|
1379
|
+
{
|
|
1380
|
+
id: "resp-exception-trace",
|
|
1381
|
+
title: "Backend exception / stack trace disclosure in response body",
|
|
1382
|
+
phase: "response",
|
|
1383
|
+
category: "info-exposure",
|
|
1384
|
+
action: "redact",
|
|
1385
|
+
// Multi-language exception/traceback signatures a normal API response never carries:
|
|
1386
|
+
// Python traceback, Java "Exception in thread", .NET System.*Exception, JVM stack frames,
|
|
1387
|
+
// Go goroutine dumps. (Node `at fn (file:line:col)` frames are handled by resp-stack-trace.)
|
|
1388
|
+
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+ \\[)/' } }]
|
|
1204
1389
|
}
|
|
1205
1390
|
];
|
|
1206
1391
|
var DEFAULT_EGRESS_RULES = [
|
|
@@ -1213,6 +1398,89 @@ var DEFAULT_EGRESS_RULES = [
|
|
|
1213
1398
|
}
|
|
1214
1399
|
];
|
|
1215
1400
|
|
|
1401
|
+
// src/protect/block-page.js
|
|
1402
|
+
function escapeHtml(s) {
|
|
1403
|
+
return String(s).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
1404
|
+
}
|
|
1405
|
+
function renderBlockPage({ url = "", code = "patchstack" } = {}) {
|
|
1406
|
+
const safeUrl = escapeHtml(url);
|
|
1407
|
+
const safeCode = escapeHtml(code || "patchstack");
|
|
1408
|
+
return `<!DOCTYPE html>
|
|
1409
|
+
<html lang="en">
|
|
1410
|
+
<head>
|
|
1411
|
+
<meta charset="UTF-8">
|
|
1412
|
+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
1413
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1414
|
+
<style type="text/css">
|
|
1415
|
+
.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}}
|
|
1416
|
+
</style>
|
|
1417
|
+
<title>Access Denied</title>
|
|
1418
|
+
</head>
|
|
1419
|
+
<body>
|
|
1420
|
+
<div class="main-container">
|
|
1421
|
+
<div class="container">
|
|
1422
|
+
<div class="content">
|
|
1423
|
+
<span class="access-denied">Access Denied</span>
|
|
1424
|
+
<div class="site-url">
|
|
1425
|
+
<input id="url" class="site-url-text" type="text"
|
|
1426
|
+
value="${safeUrl}" readonly>
|
|
1427
|
+
<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')">
|
|
1428
|
+
</div>
|
|
1429
|
+
<div class="site-url">
|
|
1430
|
+
<input id="response" class="reason-code-text" type="text" value="${safeCode}" readonly>
|
|
1431
|
+
<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')">
|
|
1432
|
+
</div>
|
|
1433
|
+
<div class="descp">
|
|
1434
|
+
<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>
|
|
1435
|
+
</div>
|
|
1436
|
+
</div>
|
|
1437
|
+
</div>
|
|
1438
|
+
<div class="bottom-container">
|
|
1439
|
+
<div class="bottom-text-containers">
|
|
1440
|
+
<img class="arrow-vector" src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iOCIgaGVpZ2h0PSIxMyIgdmlld0JveD0iMCAwIDggMTMiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik03LjQxIDEuNDMzNDRMNiAwLjAyMzQzNzVMMCA2LjAyMzQ0TDYgMTIuMDIzNEw3LjQxIDEwLjYxMzRMMi44MyA2LjAyMzQ0TDcuNDEgMS40MzM0NFoiIGZpbGw9IiNBRkU2MTQiLz4KPC9zdmc+Cg==" alt="Arrow Left">
|
|
1441
|
+
<a href="/" class="return-text">Return to homepage</a>
|
|
1442
|
+
</div>
|
|
1443
|
+
<div class="bottom-second-containers">
|
|
1444
|
+
<span class="secured-text">This website is secured by</span>
|
|
1445
|
+
<a href="https://patchstack.com" class="logoText">
|
|
1446
|
+
<img class="patch-logo" src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTg0IiBoZWlnaHQ9IjIyIiB2aWV3Qm94PSIwIDAgMTg0IDIyIiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTUwLjQwMDEgMTEuNTAwOEM1MC40MDAxIDkuMTAwNzggNDkuMzAwMSA4LjEwMDc4IDQ2LjYwMDEgOC4xMDA3OEM0NC4wMDAxIDguMTAwNzggNDIuNzAwMSA5LjIwMDc4IDQyLjcwMDEgMTEuNTAwOEM0Mi43MDAxIDEzLjgwMDggNDQuMDAwMSAxNC45MDA4IDQ2LjYwMDEgMTQuOTAwOEM0OS4zMDAxIDE0LjkwMDggNTAuNDAwMSAxMy44MDA4IDUwLjQwMDEgMTEuNTAwOFpNNTMuNDAwMSAxMS41MDA4QzUzLjQwMDEgMTUuMzAwOCA1MS4zMDAxIDE3LjYwMDggNDcuNDAwMSAxNy42MDA4QzQ1LjQwMDEgMTcuNjAwOCA0My42MDAxIDE2LjUwMDggNDIuNzAwMSAxNC41MDA4VjIxLjUwMDhIMzkuNjAwMVY1LjYwMDc4SDQyLjcwMDFWOC41MDA3OEM0My42MDAxIDYuNTAwNzggNDUuNDAwMSA1LjMwMDc4IDQ3LjUwMDEgNS4zMDA3OEM1MS40MDAxIDUuMzAwNzggNTMuNDAwMSA3LjcwMDc4IDUzLjQwMDEgMTEuNTAwOFoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNNjYuNDAwMSAxMS40OTg0QzY2LjQwMDEgOS4xOTg0NCA2NS4xMDAxIDguMDk4NDQgNjIuNTAwMSA4LjA5ODQ0QzU5LjgwMDEgOC4wOTg0NCA1OC43MDAxIDkuMDk4NDQgNTguNzAwMSAxMS40OTg0QzU4LjcwMDEgMTMuNzk4NCA1OS44MDAxIDE0Ljg5ODQgNjIuNTAwMSAxNC44OTg0QzY1LjEwMDEgMTQuODk4NCA2Ni40MDAxIDEzLjc5ODQgNjYuNDAwMSAxMS40OTg0Wk02Ni4zMDAxIDUuNTk4NDRINjkuNDAwMVYxNy4zOTg0SDY2LjMwMDFWMTQuNDk4NEM2NS40MDAxIDE2LjQ5ODQgNjMuNzAwMSAxNy42OTg0IDYxLjYwMDEgMTcuNjk4NEM1Ny43MDAxIDE3LjY5ODQgNTUuNjAwMSAxNS4yOTg0IDU1LjYwMDEgMTEuNTk4NEM1NS42MDAxIDcuNzk4NDQgNTcuNjAwMSA1LjM5ODQ0IDYxLjQwMDEgNS4zOTg0NEM2My41MDAxIDUuMzk4NDQgNjUuNDAwMSA2LjU5ODQ0IDY2LjIwMDEgOC41OTg0NFY1LjU5ODQ0SDY2LjMwMDFaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTgxLjM5OTkgOC4zSDc3LjM5OTlWMTQuN0g4MS4wOTk5VjE3LjRINzcuOTk5OUM3NS43OTk5IDE3LjQgNzQuMjk5OSAxNS45IDc0LjI5OTkgMTMuN1Y4LjNINzEuMzk5OVY1LjZINzQuMjk5OVYySDc3LjM5OTlWNS41SDgxLjM5OTlWOC4zWiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik05My42OTk5IDEyLjIwMDhMOTYuNjk5OSAxMi45MDA4Qzk1Ljk5OTkgMTYuMDAwOCA5My42OTk5IDE3LjYwMDggODkuOTk5OSAxNy42MDA4Qzg3Ljc5OTkgMTcuNjAwOCA4Ni4wOTk5IDE3LjAwMDggODQuNzk5OSAxNS45MDA4QzgzLjQ5OTkgMTQuODAwOCA4Mi44OTk5IDEzLjMwMDggODIuODk5OSAxMS41MDA4QzgyLjg5OTkgOS42MDA3OCA4My40OTk5IDguMTAwNzggODQuNzk5OSA3LjAwMDc4Qzg2LjA5OTkgNS45MDA3OCA4Ny43OTk5IDUuMzAwNzggODkuOTk5OSA1LjMwMDc4QzkzLjY5OTkgNS4zMDA3OCA5NS45OTk5IDYuOTAwNzggOTYuNjk5OSAxMC4wMDA4TDkzLjY5OTkgMTAuNzAwOEM5My4yOTk5IDguODAwNzggOTIuMjk5OSA4LjEwMDc4IDg5Ljg5OTkgOC4xMDA3OEM4Ni45OTk5IDguMTAwNzggODUuODk5OSA5LjIwMDc4IDg1Ljg5OTkgMTEuNTAwOEM4NS44OTk5IDEzLjgwMDggODYuOTk5OSAxNC45MDA4IDg5Ljg5OTkgMTQuOTAwOEM5Mi4yOTk5IDE0LjkwMDggOTMuMjk5OSAxNC4yMDA4IDkzLjY5OTkgMTIuMjAwOFoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTExLjggMTAuNlYxNy40SDEwOC43VjExLjJDMTA4LjcgOSAxMDcuOCA4LjEgMTA1LjkgOC4xQzEwMy42IDguMSAxMDIuNCA5LjMgMTAyLjQgMTEuN1YxNy4zSDk5LjI5OThWMC41SDEwMi40VjguM0MxMDMuMSA2LjMgMTA0LjYgNS4zIDEwNyA1LjNDMTEwLjIgNS4zIDExMS44IDcuMSAxMTEuOCAxMC42WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xMTQuMSAxNC4wOTg0TDExNy4xIDEzLjI5ODRDMTE3LjQgMTQuNzk4NCAxMTguMyAxNS4yOTg0IDEyMC4yIDE1LjI5ODRDMTIxLjggMTUuMjk4NCAxMjIuNyAxNC44OTg0IDEyMi43IDE0LjA5ODRDMTIyLjcgMTMuMjk4NCAxMjIuMiAxMi45OTg0IDEyMC42IDEyLjc5ODRMMTE4LjIgMTIuNDk4NEMxMTUuOSAxMi4xOTg0IDExNC41IDEwLjg5ODQgMTE0LjUgOC45OTg0NEMxMTQuNSA2Ljc5ODQ0IDExNi41IDUuMzk4NDQgMTE5LjggNS4zOTg0NEMxMjMuMSA1LjM5ODQ0IDEyNSA2LjY5ODQ0IDEyNS41IDguODk4NDRMMTIyLjUgOS41OTg0NEMxMjIuMyA4LjA5ODQ0IDEyMS41IDcuNjk4NDQgMTE5LjkgNy42OTg0NEMxMTguNCA3LjY5ODQ0IDExNy42IDguMDk4NDQgMTE3LjYgOC44OTg0NEMxMTcuNiA5LjU5ODQ0IDExOC4xIDkuOTk4NDQgMTE5LjMgMTAuMDk4NEwxMjIuMiAxMC40OTg0QzEyNC40IDEwLjc5ODQgMTI1LjggMTEuOTk4NCAxMjUuOCAxMy44OTg0QzEyNS44IDE2LjI5ODQgMTIzLjcgMTcuNzk4NCAxMjAuMSAxNy43OTg0QzExNi43IDE3LjU5ODQgMTE0LjUgMTYuMTk4NCAxMTQuMSAxNC4wOTg0WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xMzYuNyA4LjNIMTMyLjdWMTQuN0gxMzYuNFYxNy40SDEzMy4zQzEzMS4xIDE3LjQgMTI5LjYgMTUuOSAxMjkuNiAxMy43VjguM0gxMjYuN1Y1LjZIMTI5LjZWMkgxMzIuN1Y1LjVIMTM2LjdWOC4zWiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xNDkuMiAxMS40OTg0QzE0OS4yIDkuMTk4NDQgMTQ3LjkgOC4wOTg0NCAxNDUuMyA4LjA5ODQ0QzE0Mi42IDguMDk4NDQgMTQxLjUgOS4wOTg0NCAxNDEuNSAxMS40OTg0QzE0MS41IDEzLjc5ODQgMTQyLjYgMTQuODk4NCAxNDUuMyAxNC44OTg0QzE0Ny45IDE0Ljg5ODQgMTQ5LjIgMTMuNzk4NCAxNDkuMiAxMS40OTg0Wk0xNDkuMSA1LjU5ODQ0SDE1Mi4yVjE3LjM5ODRIMTQ5LjFWMTQuNDk4NEMxNDguMiAxNi40OTg0IDE0Ni41IDE3LjY5ODQgMTQ0LjQgMTcuNjk4NEMxNDAuNSAxNy42OTg0IDEzOC40IDE1LjI5ODQgMTM4LjQgMTEuNTk4NEMxMzguNCA3Ljc5ODQ0IDE0MC40IDUuMzk4NDQgMTQ0LjIgNS4zOTg0NEMxNDYuMyA1LjM5ODQ0IDE0OC4yIDYuNTk4NDQgMTQ5IDguNTk4NDRWNS41OTg0NEgxNDkuMVoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTY1LjYgMTIuMjAwOEwxNjguNiAxMi45MDA4QzE2Ny45IDE2LjAwMDggMTY1LjYgMTcuNjAwOCAxNjEuOSAxNy42MDA4QzE1OS43IDE3LjYwMDggMTU4IDE3LjAwMDggMTU2LjcgMTUuOTAwOEMxNTUuNCAxNC44MDA4IDE1NC44IDEzLjMwMDggMTU0LjggMTEuNTAwOEMxNTQuOCA5LjYwMDc4IDE1NS40IDguMTAwNzggMTU2LjcgNy4wMDA3OEMxNTggNS45MDA3OCAxNTkuNyA1LjMwMDc4IDE2MS45IDUuMzAwNzhDMTY1LjYgNS4zMDA3OCAxNjcuOSA2LjkwMDc4IDE2OC42IDEwLjAwMDhMMTY1LjYgMTAuNzAwOEMxNjUuMiA4LjgwMDc4IDE2NC4yIDguMTAwNzggMTYxLjggOC4xMDA3OEMxNTguOSA4LjEwMDc4IDE1Ny44IDkuMjAwNzggMTU3LjggMTEuNTAwOEMxNTcuOCAxMy44MDA4IDE1OC45IDE0LjkwMDggMTYxLjggMTQuOTAwOEMxNjQuMSAxNC45MDA4IDE2NS4xIDE0LjIwMDggMTY1LjYgMTIuMjAwOFoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTgwLjI1NyAxNy40TDE3Ni42NTcgMTIuM0wxNzQuMTU3IDE0LjZWMTcuNEgxNzEuMTU3VjAuNUgxNzQuMTU3VjExLjFMMTc5Ljg1NyA1LjZIMTgzLjc1N0wxNzguNTU3IDEwLjVMMTgzLjk1NyAxNy40SDE4MC4yNTdaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTAgMjEuNTAwOEg3LjNWMTYuMzAwOEgwVjIxLjUwMDhaIiBmaWxsPSIjQUZFNjE0Ii8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNNy4yOTk4IDIxLjUwMDhIMTQuNTk5OFYxNi4zMDA4SDcuMjk5OFYyMS41MDA4WiIgZmlsbD0iIzZFQkUwMCIvPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTE0LjYwMDEgMTYuM0gyOS4yMDAxVjAuNUgxNC42MDAxVjE2LjNaIiBmaWxsPSIjQUZFNjE0Ii8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMCAxMUgxNC42VjAuNUgwVjExWiIgZmlsbD0iIzZFQkUwMCIvPgo8L3N2Zz4K" alt="Patchstack Logo">
|
|
1447
|
+
</a>
|
|
1448
|
+
</div>
|
|
1449
|
+
</div>
|
|
1450
|
+
|
|
1451
|
+
<div class="mobile-bottom-container">
|
|
1452
|
+
<div class="mobile-bottom-text-containers">
|
|
1453
|
+
<img class="arrow-vector" src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iOCIgaGVpZ2h0PSIxMyIgdmlld0JveD0iMCAwIDggMTMiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik03LjQxIDEuNDMzNDRMNiAwLjAyMzQzNzVMMCA2LjAyMzQ0TDYgMTIuMDIzNEw3LjQxIDEwLjYxMzRMMi44MyA2LjAyMzQ0TDcuNDEgMS40MzM0NFoiIGZpbGw9IiNBRkU2MTQiLz4KPC9zdmc+Cg==" alt="Arrow Left">
|
|
1454
|
+
<a href="/" class="return-text">Return to homepage</a>
|
|
1455
|
+
</div>
|
|
1456
|
+
<div class="mobile-bottom-second-containers">
|
|
1457
|
+
<span class="secured-text">This website is secured by</span>
|
|
1458
|
+
<a href="https://patchstack.com" class="logoText">
|
|
1459
|
+
<img class="patch-logo" src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTg0IiBoZWlnaHQ9IjIyIiB2aWV3Qm94PSIwIDAgMTg0IDIyIiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTUwLjQwMDEgMTEuNTAwOEM1MC40MDAxIDkuMTAwNzggNDkuMzAwMSA4LjEwMDc4IDQ2LjYwMDEgOC4xMDA3OEM0NC4wMDAxIDguMTAwNzggNDIuNzAwMSA5LjIwMDc4IDQyLjcwMDEgMTEuNTAwOEM0Mi43MDAxIDEzLjgwMDggNDQuMDAwMSAxNC45MDA4IDQ2LjYwMDEgMTQuOTAwOEM0OS4zMDAxIDE0LjkwMDggNTAuNDAwMSAxMy44MDA4IDUwLjQwMDEgMTEuNTAwOFpNNTMuNDAwMSAxMS41MDA4QzUzLjQwMDEgMTUuMzAwOCA1MS4zMDAxIDE3LjYwMDggNDcuNDAwMSAxNy42MDA4QzQ1LjQwMDEgMTcuNjAwOCA0My42MDAxIDE2LjUwMDggNDIuNzAwMSAxNC41MDA4VjIxLjUwMDhIMzkuNjAwMVY1LjYwMDc4SDQyLjcwMDFWOC41MDA3OEM0My42MDAxIDYuNTAwNzggNDUuNDAwMSA1LjMwMDc4IDQ3LjUwMDEgNS4zMDA3OEM1MS40MDAxIDUuMzAwNzggNTMuNDAwMSA3LjcwMDc4IDUzLjQwMDEgMTEuNTAwOFoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNNjYuNDAwMSAxMS40OTg0QzY2LjQwMDEgOS4xOTg0NCA2NS4xMDAxIDguMDk4NDQgNjIuNTAwMSA4LjA5ODQ0QzU5LjgwMDEgOC4wOTg0NCA1OC43MDAxIDkuMDk4NDQgNTguNzAwMSAxMS40OTg0QzU4LjcwMDEgMTMuNzk4NCA1OS44MDAxIDE0Ljg5ODQgNjIuNTAwMSAxNC44OTg0QzY1LjEwMDEgMTQuODk4NCA2Ni40MDAxIDEzLjc5ODQgNjYuNDAwMSAxMS40OTg0Wk02Ni4zMDAxIDUuNTk4NDRINjkuNDAwMVYxNy4zOTg0SDY2LjMwMDFWMTQuNDk4NEM2NS40MDAxIDE2LjQ5ODQgNjMuNzAwMSAxNy42OTg0IDYxLjYwMDEgMTcuNjk4NEM1Ny43MDAxIDE3LjY5ODQgNTUuNjAwMSAxNS4yOTg0IDU1LjYwMDEgMTEuNTk4NEM1NS42MDAxIDcuNzk4NDQgNTcuNjAwMSA1LjM5ODQ0IDYxLjQwMDEgNS4zOTg0NEM2My41MDAxIDUuMzk4NDQgNjUuNDAwMSA2LjU5ODQ0IDY2LjIwMDEgOC41OTg0NFY1LjU5ODQ0SDY2LjMwMDFaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTgxLjM5OTkgOC4zSDc3LjM5OTlWMTQuN0g4MS4wOTk5VjE3LjRINzcuOTk5OUM3NS43OTk5IDE3LjQgNzQuMjk5OSAxNS45IDc0LjI5OTkgMTMuN1Y4LjNINzEuMzk5OVY1LjZINzQuMjk5OVYySDc3LjM5OTlWNS41SDgxLjM5OTlWOC4zWiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik05My42OTk5IDEyLjIwMDhMOTYuNjk5OSAxMi45MDA4Qzk1Ljk5OTkgMTYuMDAwOCA5My42OTk5IDE3LjYwMDggODkuOTk5OSAxNy42MDA4Qzg3Ljc5OTkgMTcuNjAwOCA4Ni4wOTk5IDE3LjAwMDggODQuNzk5OSAxNS45MDA4QzgzLjQ5OTkgMTQuODAwOCA4Mi44OTk5IDEzLjMwMDggODIuODk5OSAxMS41MDA4QzgyLjg5OTkgOS42MDA3OCA4My40OTk5IDguMTAwNzggODQuNzk5OSA3LjAwMDc4Qzg2LjA5OTkgNS45MDA3OCA4Ny43OTk5IDUuMzAwNzggODkuOTk5OSA1LjMwMDc4QzkzLjY5OTkgNS4zMDA3OCA5NS45OTk5IDYuOTAwNzggOTYuNjk5OSAxMC4wMDA4TDkzLjY5OTkgMTAuNzAwOEM5My4yOTk5IDguODAwNzggOTIuMjk5OSA4LjEwMDc4IDg5Ljg5OTkgOC4xMDA3OEM4Ni45OTk5IDguMTAwNzggODUuODk5OSA5LjIwMDc4IDg1Ljg5OTkgMTEuNTAwOEM4NS44OTk5IDEzLjgwMDggODYuOTk5OSAxNC45MDA4IDg5Ljg5OTkgMTQuOTAwOEM5Mi4yOTk5IDE0LjkwMDggOTMuMjk5OSAxNC4yMDA4IDkzLjY5OTkgMTIuMjAwOFoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTExLjggMTAuNlYxNy40SDEwOC43VjExLjJDMTA4LjcgOSAxMDcuOCA4LjEgMTA1LjkgOC4xQzEwMy42IDguMSAxMDIuNCA5LjMgMTAyLjQgMTEuN1YxNy4zSDk5LjI5OThWMC41SDEwMi40VjguM0MxMDMuMSA2LjMgMTA0LjYgNS4zIDEwNyA1LjNDMTEwLjIgNS4zIDExMS44IDcuMSAxMTEuOCAxMC42WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xMTQuMSAxNC4wOTg0TDExNy4xIDEzLjI5ODRDMTE3LjQgMTQuNzk4NCAxMTguMyAxNS4yOTg0IDEyMC4yIDE1LjI5ODRDMTIxLjggMTUuMjk4NCAxMjIuNyAxNC44OTg0IDEyMi43IDE0LjA5ODRDMTIyLjcgMTMuMjk4NCAxMjIuMiAxMi45OTg0IDEyMC42IDEyLjc5ODRMMTE4LjIgMTIuNDk4NEMxMTUuOSAxMi4xOTg0IDExNC41IDEwLjg5ODQgMTE0LjUgOC45OTg0NEMxMTQuNSA2Ljc5ODQ0IDExNi41IDUuMzk4NDQgMTE5LjggNS4zOTg0NEMxMjMuMSA1LjM5ODQ0IDEyNSA2LjY5ODQ0IDEyNS41IDguODk4NDRMMTIyLjUgOS41OTg0NEMxMjIuMyA4LjA5ODQ0IDEyMS41IDcuNjk4NDQgMTE5LjkgNy42OTg0NEMxMTguNCA3LjY5ODQ0IDExNy42IDguMDk4NDQgMTE3LjYgOC44OTg0NEMxMTcuNiA5LjU5ODQ0IDExOC4xIDkuOTk4NDQgMTE5LjMgMTAuMDk4NEwxMjIuMiAxMC40OTg0QzEyNC40IDEwLjc5ODQgMTI1LjggMTEuOTk4NCAxMjUuOCAxMy44OTg0QzEyNS44IDE2LjI5ODQgMTIzLjcgMTcuNzk4NCAxMjAuMSAxNy43OTg0QzExNi43IDE3LjU5ODQgMTE0LjUgMTYuMTk4NCAxMTQuMSAxNC4wOTg0WiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xMzYuNyA4LjNIMTMyLjdWMTQuN0gxMzYuNFYxNy40SDEzMy4zQzEzMS4xIDE3LjQgMTI5LjYgMTUuOSAxMjkuNiAxMy43VjguM0gxMjYuN1Y1LjZIMTI5LjZWMkgxMzIuN1Y1LjVIMTM2LjdWOC4zWiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xNDkuMiAxMS40OTg0QzE0OS4yIDkuMTk4NDQgMTQ3LjkgOC4wOTg0NCAxNDUuMyA4LjA5ODQ0QzE0Mi42IDguMDk4NDQgMTQxLjUgOS4wOTg0NCAxNDEuNSAxMS40OTg0QzE0MS41IDEzLjc5ODQgMTQyLjYgMTQuODk4NCAxNDUuMyAxNC44OTg0QzE0Ny45IDE0Ljg5ODQgMTQ5LjIgMTMuNzk4NCAxNDkuMiAxMS40OTg0Wk0xNDkuMSA1LjU5ODQ0SDE1Mi4yVjE3LjM5ODRIMTQ5LjFWMTQuNDk4NEMxNDguMiAxNi40OTg0IDE0Ni41IDE3LjY5ODQgMTQ0LjQgMTcuNjk4NEMxNDAuNSAxNy42OTg0IDEzOC40IDE1LjI5ODQgMTM4LjQgMTEuNTk4NEMxMzguNCA3Ljc5ODQ0IDE0MC40IDUuMzk4NDQgMTQ0LjIgNS4zOTg0NEMxNDYuMyA1LjM5ODQ0IDE0OC4yIDYuNTk4NDQgMTQ5IDguNTk4NDRWNS41OTg0NEgxNDkuMVoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTY1LjYgMTIuMjAwOEwxNjguNiAxMi45MDA4QzE2Ny45IDE2LjAwMDggMTY1LjYgMTcuNjAwOCAxNjEuOSAxNy42MDA4QzE1OS43IDE3LjYwMDggMTU4IDE3LjAwMDggMTU2LjcgMTUuOTAwOEMxNTUuNCAxNC44MDA4IDE1NC44IDEzLjMwMDggMTU0LjggMTEuNTAwOEMxNTQuOCA5LjYwMDc4IDE1NS40IDguMTAwNzggMTU2LjcgNy4wMDA3OEMxNTggNS45MDA3OCAxNTkuNyA1LjMwMDc4IDE2MS45IDUuMzAwNzhDMTY1LjYgNS4zMDA3OCAxNjcuOSA2LjkwMDc4IDE2OC42IDEwLjAwMDhMMTY1LjYgMTAuNzAwOEMxNjUuMiA4LjgwMDc4IDE2NC4yIDguMTAwNzggMTYxLjggOC4xMDA3OEMxNTguOSA4LjEwMDc4IDE1Ny44IDkuMjAwNzggMTU3LjggMTEuNTAwOEMxNTcuOCAxMy44MDA4IDE1OC45IDE0LjkwMDggMTYxLjggMTQuOTAwOEMxNjQuMSAxNC45MDA4IDE2NS4xIDE0LjIwMDggMTY1LjYgMTIuMjAwOFoiIGZpbGw9IndoaXRlIi8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMTgwLjI1NyAxNy40TDE3Ni42NTcgMTIuM0wxNzQuMTU3IDE0LjZWMTcuNEgxNzEuMTU3VjAuNUgxNzQuMTU3VjExLjFMMTc5Ljg1NyA1LjZIMTgzLjc1N0wxNzguNTU3IDEwLjVMMTgzLjk1NyAxNy40SDE4MC4yNTdaIiBmaWxsPSJ3aGl0ZSIvPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTAgMjEuNTAwOEg3LjNWMTYuMzAwOEgwVjIxLjUwMDhaIiBmaWxsPSIjQUZFNjE0Ii8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNNy4yOTk4IDIxLjUwMDhIMTQuNTk5OFYxNi4zMDA4SDcuMjk5OFYyMS41MDA4WiIgZmlsbD0iIzZFQkUwMCIvPgo8cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTE0LjYwMDEgMTYuM0gyOS4yMDAxVjAuNUgxNC42MDAxVjE2LjNaIiBmaWxsPSIjQUZFNjE0Ii8+CjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBkPSJNMCAxMUgxNC42VjAuNUgwVjExWiIgZmlsbD0iIzZFQkUwMCIvPgo8L3N2Zz4K" alt="Patchstack Logo">
|
|
1460
|
+
</a>
|
|
1461
|
+
</div>
|
|
1462
|
+
</div>
|
|
1463
|
+
</div>
|
|
1464
|
+
</body>
|
|
1465
|
+
<script>
|
|
1466
|
+
function copyText(id) {
|
|
1467
|
+
var copyText = document.getElementById(id)
|
|
1468
|
+
copyText.select();
|
|
1469
|
+
copyText.setSelectionRange(0, 99999);
|
|
1470
|
+
navigator.clipboard.writeText(copyText.value);
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
function hover(element) {
|
|
1474
|
+
element.setAttribute('src', 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTkiIGhlaWdodD0iMjMiIHZpZXdCb3g9IjAgMCAxOSAyMyIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE0IDAuMDIzNDM3NUgwVjE2LjAyMzRIMlYyLjAyMzQ0SDE0VjAuMDIzNDM3NVpNMTkgNC4wMjM0NEg0VjIyLjAyMzRIMTlWNC4wMjM0NFpNMTcgMjAuMDIzNEg2VjYuMDIzNDRIMTdWMjAuMDIzNFoiIGZpbGw9IiNBRkU2MTQiLz4KPC9zdmc+Cg==');
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
function unhover(element) {
|
|
1478
|
+
element.setAttribute('src', 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTkiIGhlaWdodD0iMjMiIHZpZXdCb3g9IjAgMCAxOSAyMyIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE0IDAuMDIzNDM3NUgwVjE2LjAyMzRIMlYyLjAyMzQ0SDE0VjAuMDIzNDM3NVpNMTkgNC4wMjM0NEg0VjIyLjAyMzRIMTlWNC4wMjM0NFpNMTcgMjAuMDIzNEg2VjYuMDIzNDRIMTdWMjAuMDIzNFoiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPgo=');
|
|
1479
|
+
}
|
|
1480
|
+
</script>
|
|
1481
|
+
</html>`;
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1216
1484
|
// src/protect/runtime.js
|
|
1217
1485
|
var import_node_fs = require("fs");
|
|
1218
1486
|
var import_node_path = require("path");
|
|
@@ -1338,15 +1606,27 @@ async function createProtection(options = {}) {
|
|
|
1338
1606
|
if (mode !== "block" || !blockRule && !redactions.length) return { verdict: "pass" };
|
|
1339
1607
|
if (blockRule) return { verdict: "block" };
|
|
1340
1608
|
let body = text;
|
|
1341
|
-
|
|
1342
|
-
|
|
1609
|
+
const headers = { ...meta.headers || {} };
|
|
1610
|
+
for (const { rule, redactors } of redactions) {
|
|
1611
|
+
const mask = maskFn(rule.category);
|
|
1612
|
+
body = applyRedactors(body, redactors, mask);
|
|
1613
|
+
for (const name of Object.keys(headers)) {
|
|
1614
|
+
const value = headers[name];
|
|
1615
|
+
if (typeof value === "string") {
|
|
1616
|
+
headers[name] = applyRedactors(value, redactors, mask);
|
|
1617
|
+
} else if (Array.isArray(value)) {
|
|
1618
|
+
headers[name] = value.map((item) => typeof item === "string" ? applyRedactors(item, redactors, mask) : item);
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
return { verdict: "redact", body, headers };
|
|
1343
1623
|
};
|
|
1344
1624
|
const screenResp = async (response) => {
|
|
1345
1625
|
const text = await readTextResponse(response);
|
|
1346
1626
|
if (text == null) return response;
|
|
1347
1627
|
const r = screenText(text, { status: response.status, headers: headerObject(response.headers) });
|
|
1348
1628
|
if (r.verdict === "block") return leakResponse();
|
|
1349
|
-
if (r.verdict === "redact") return rebuildResponse(response, r.body);
|
|
1629
|
+
if (r.verdict === "redact") return rebuildResponse(response, r.body, r.headers);
|
|
1350
1630
|
return response;
|
|
1351
1631
|
};
|
|
1352
1632
|
const wrapNodeResponse = (res) => {
|
|
@@ -1396,7 +1676,7 @@ async function createProtection(options = {}) {
|
|
|
1396
1676
|
}
|
|
1397
1677
|
let r;
|
|
1398
1678
|
try {
|
|
1399
|
-
r = screenText(text, { status: res.statusCode, headers: {} });
|
|
1679
|
+
r = screenText(text, { status: res.statusCode, headers: res.getHeaders ? res.getHeaders() : {} });
|
|
1400
1680
|
} catch (err) {
|
|
1401
1681
|
onError?.(err);
|
|
1402
1682
|
for (const c of chunks) origWrite(c);
|
|
@@ -1415,6 +1695,22 @@ async function createProtection(options = {}) {
|
|
|
1415
1695
|
res.removeHeader && res.removeHeader("content-length");
|
|
1416
1696
|
} catch {
|
|
1417
1697
|
}
|
|
1698
|
+
if (r.headers && res.setHeader) {
|
|
1699
|
+
const current = res.getHeaders ? res.getHeaders() : {};
|
|
1700
|
+
for (const [name, value] of Object.entries(r.headers)) {
|
|
1701
|
+
if (Array.isArray(value)) {
|
|
1702
|
+
try {
|
|
1703
|
+
res.setHeader(name, value);
|
|
1704
|
+
} catch {
|
|
1705
|
+
}
|
|
1706
|
+
} else if (typeof value === "string" && current[name] !== value) {
|
|
1707
|
+
try {
|
|
1708
|
+
res.setHeader(name, value);
|
|
1709
|
+
} catch {
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1418
1714
|
return origEnd(r.body, cb);
|
|
1419
1715
|
}
|
|
1420
1716
|
for (const c of chunks) origWrite(c);
|
|
@@ -1451,7 +1747,7 @@ async function createProtection(options = {}) {
|
|
|
1451
1747
|
onError?.(err);
|
|
1452
1748
|
return null;
|
|
1453
1749
|
}
|
|
1454
|
-
return decide("request", result, () => blockResponse(result), () => null);
|
|
1750
|
+
return decide("request", result, () => blockResponse(result, request), () => null);
|
|
1455
1751
|
};
|
|
1456
1752
|
},
|
|
1457
1753
|
// Wrap a fetch handler: screens the request, then the response (redact/block).
|
|
@@ -1479,7 +1775,13 @@ async function createProtection(options = {}) {
|
|
|
1479
1775
|
decide(
|
|
1480
1776
|
"request",
|
|
1481
1777
|
result,
|
|
1482
|
-
() =>
|
|
1778
|
+
() => {
|
|
1779
|
+
if (isDocumentNavigation((n) => req.headers?.[n])) {
|
|
1780
|
+
res.status(403).type("html").send(renderBlockPage({ url: req.originalUrl || req.url || "/", code: result?.rule?.id }));
|
|
1781
|
+
} else {
|
|
1782
|
+
res.status(403).json(blockBody(result));
|
|
1783
|
+
}
|
|
1784
|
+
},
|
|
1483
1785
|
() => {
|
|
1484
1786
|
if (exprOptions.screenResponses) wrapNodeResponse(res);
|
|
1485
1787
|
next();
|
|
@@ -1523,8 +1825,13 @@ async function createProtection(options = {}) {
|
|
|
1523
1825
|
result,
|
|
1524
1826
|
() => {
|
|
1525
1827
|
res.statusCode = 403;
|
|
1526
|
-
|
|
1527
|
-
|
|
1828
|
+
if (isDocumentNavigation((n) => req.headers?.[n])) {
|
|
1829
|
+
res.setHeader("content-type", "text/html; charset=utf-8");
|
|
1830
|
+
res.end(renderBlockPage({ url: req.url || "/", code: result?.rule?.id }));
|
|
1831
|
+
} else {
|
|
1832
|
+
res.setHeader("content-type", "application/json");
|
|
1833
|
+
res.end(JSON.stringify(blockBody(result)));
|
|
1834
|
+
}
|
|
1528
1835
|
},
|
|
1529
1836
|
() => {
|
|
1530
1837
|
if (req.body === void 0) req.body = shaped.body;
|
|
@@ -1537,7 +1844,7 @@ async function createProtection(options = {}) {
|
|
|
1537
1844
|
}
|
|
1538
1845
|
};
|
|
1539
1846
|
if (options.egress) {
|
|
1540
|
-
protection.uninstallEgress = installEgressGuard({ shouldBlock: egressShouldBlock, onBlock: options.onEgressBlock });
|
|
1847
|
+
protection.uninstallEgress = await installEgressGuard({ shouldBlock: egressShouldBlock, onBlock: options.onEgressBlock });
|
|
1541
1848
|
}
|
|
1542
1849
|
return protection;
|
|
1543
1850
|
}
|
|
@@ -1563,6 +1870,8 @@ function headerObject(headers) {
|
|
|
1563
1870
|
headers?.forEach?.((v, k) => {
|
|
1564
1871
|
out[k.toLowerCase()] = v;
|
|
1565
1872
|
});
|
|
1873
|
+
const setCookies = headers?.getSetCookie?.();
|
|
1874
|
+
if (setCookies && setCookies.length) out["set-cookie"] = setCookies;
|
|
1566
1875
|
return out;
|
|
1567
1876
|
}
|
|
1568
1877
|
function extractRedactors(rule) {
|
|
@@ -1597,9 +1906,27 @@ function applyRedactors(body, redactors, mask) {
|
|
|
1597
1906
|
}
|
|
1598
1907
|
return out;
|
|
1599
1908
|
}
|
|
1600
|
-
function rebuildResponse(response, body) {
|
|
1909
|
+
function rebuildResponse(response, body, redactedHeaders) {
|
|
1601
1910
|
const headers = new Headers(response.headers);
|
|
1602
1911
|
headers.delete("content-length");
|
|
1912
|
+
if (redactedHeaders) {
|
|
1913
|
+
for (const [name, value] of Object.entries(redactedHeaders)) {
|
|
1914
|
+
if (typeof value === "string") {
|
|
1915
|
+
if (headers.get(name) !== value) {
|
|
1916
|
+
try {
|
|
1917
|
+
headers.set(name, value);
|
|
1918
|
+
} catch {
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
} else if (Array.isArray(value)) {
|
|
1922
|
+
try {
|
|
1923
|
+
headers.delete(name);
|
|
1924
|
+
for (const item of value) headers.append(name, String(item));
|
|
1925
|
+
} catch {
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1603
1930
|
return new Response(body, { status: response.status, statusText: response.statusText, headers });
|
|
1604
1931
|
}
|
|
1605
1932
|
function leakResponse() {
|
|
@@ -1609,40 +1936,43 @@ function leakResponse() {
|
|
|
1609
1936
|
});
|
|
1610
1937
|
}
|
|
1611
1938
|
async function resolveRules(options) {
|
|
1939
|
+
const cache = makeCache(options);
|
|
1612
1940
|
if (options.siteUuid) {
|
|
1613
|
-
const
|
|
1941
|
+
const prior = await cache.read();
|
|
1942
|
+
const client = new PulseRuleClient({ siteUuid: options.siteUuid, baseUrl: options.pulseRulesUrl, etag: prior?.etag });
|
|
1614
1943
|
const res = await client.getRules();
|
|
1615
|
-
if (res.success)
|
|
1944
|
+
if (res.success && res.notModified && prior?.bundle) return normalizeBundle(prior.bundle);
|
|
1945
|
+
if (res.success && !res.notModified) {
|
|
1616
1946
|
const bundle = normalizeBundle(res);
|
|
1617
|
-
|
|
1947
|
+
await cache.write({ bundle, etag: res.etag ?? null });
|
|
1618
1948
|
return bundle;
|
|
1619
1949
|
}
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
return normalizeBundle(cached);
|
|
1950
|
+
if (prior?.bundle) {
|
|
1951
|
+
options.onError?.(new Error(`pulse rule fetch failed (${res.error ?? "no usable response"}); using cached bundle`));
|
|
1952
|
+
return normalizeBundle(prior.bundle);
|
|
1624
1953
|
}
|
|
1625
1954
|
if (options.rules) {
|
|
1626
|
-
options.onError?.(new Error(`pulse rule fetch failed (${res.error}); using bundled fallback`));
|
|
1955
|
+
options.onError?.(new Error(`pulse rule fetch failed (${res.error ?? "no usable response"}); using bundled fallback`));
|
|
1627
1956
|
return normalizeBundle(options.rules);
|
|
1628
1957
|
}
|
|
1629
|
-
options.onError?.(new Error(`pulse rule fetch failed (${res.error}); no cache \u2014 running with no rules`));
|
|
1958
|
+
options.onError?.(new Error(`pulse rule fetch failed (${res.error ?? "no usable response"}); no cache \u2014 running with no rules`));
|
|
1630
1959
|
return emptyBundle();
|
|
1631
1960
|
}
|
|
1632
1961
|
if (options.token) {
|
|
1633
|
-
const
|
|
1962
|
+
const prior = await cache.read();
|
|
1963
|
+
const client = new PatchstackRuleClient({ token: options.token, baseUrl: options.baseUrl, etag: prior?.etag });
|
|
1634
1964
|
const res = await client.getRules();
|
|
1635
|
-
if (res.success)
|
|
1965
|
+
if (res.success && res.notModified && prior?.bundle) return normalizeBundle(prior.bundle);
|
|
1966
|
+
if (res.success && !res.notModified) {
|
|
1636
1967
|
const bundle = normalizeBundle(res);
|
|
1637
|
-
|
|
1968
|
+
await cache.write({ bundle, etag: res.etag ?? null });
|
|
1638
1969
|
return bundle;
|
|
1639
1970
|
}
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
return normalizeBundle(cached);
|
|
1971
|
+
if (prior?.bundle) {
|
|
1972
|
+
options.onError?.(new Error(`rule fetch failed (${res.error ?? "no usable response"}); using cached bundle`));
|
|
1973
|
+
return normalizeBundle(prior.bundle);
|
|
1644
1974
|
}
|
|
1645
|
-
options.onError?.(new Error(`rule fetch failed (${res.error}); no cache \u2014 running with no rules`));
|
|
1975
|
+
options.onError?.(new Error(`rule fetch failed (${res.error ?? "no usable response"}); no cache \u2014 running with no rules`));
|
|
1646
1976
|
return emptyBundle();
|
|
1647
1977
|
}
|
|
1648
1978
|
if (options.rules) {
|
|
@@ -1660,33 +1990,76 @@ function normalizeBundle(b) {
|
|
|
1660
1990
|
function emptyBundle() {
|
|
1661
1991
|
return { firewall: [], whitelists: [], whitelist_keys: {} };
|
|
1662
1992
|
}
|
|
1993
|
+
function makeCache(options) {
|
|
1994
|
+
const adapter = options.ruleCache;
|
|
1995
|
+
if (adapter && typeof adapter.read === "function" && typeof adapter.write === "function") {
|
|
1996
|
+
return {
|
|
1997
|
+
read: async () => {
|
|
1998
|
+
try {
|
|
1999
|
+
return toEnvelope(await adapter.read());
|
|
2000
|
+
} catch {
|
|
2001
|
+
return null;
|
|
2002
|
+
}
|
|
2003
|
+
},
|
|
2004
|
+
write: async (env) => {
|
|
2005
|
+
try {
|
|
2006
|
+
await adapter.write(env);
|
|
2007
|
+
} catch {
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
};
|
|
2011
|
+
}
|
|
2012
|
+
const dir = options.cacheDir;
|
|
2013
|
+
return {
|
|
2014
|
+
read: async () => cacheRead(dir),
|
|
2015
|
+
write: async (env) => cacheWrite(dir, env)
|
|
2016
|
+
};
|
|
2017
|
+
}
|
|
1663
2018
|
function cachePath(dir) {
|
|
1664
2019
|
return (0, import_node_path.join)(dir, "patchstack-rules.json");
|
|
1665
2020
|
}
|
|
1666
|
-
function cacheWrite(dir,
|
|
2021
|
+
function cacheWrite(dir, env) {
|
|
1667
2022
|
if (!dir) return;
|
|
1668
2023
|
try {
|
|
1669
2024
|
(0, import_node_fs.mkdirSync)(dir, { recursive: true });
|
|
1670
|
-
(0, import_node_fs.writeFileSync)(cachePath(dir), JSON.stringify(
|
|
2025
|
+
(0, import_node_fs.writeFileSync)(cachePath(dir), JSON.stringify(env));
|
|
1671
2026
|
} catch {
|
|
1672
2027
|
}
|
|
1673
2028
|
}
|
|
1674
2029
|
function cacheRead(dir) {
|
|
1675
2030
|
if (!dir) return null;
|
|
1676
2031
|
try {
|
|
1677
|
-
return JSON.parse((0, import_node_fs.readFileSync)(cachePath(dir), "utf8"));
|
|
2032
|
+
return toEnvelope(JSON.parse((0, import_node_fs.readFileSync)(cachePath(dir), "utf8")));
|
|
1678
2033
|
} catch {
|
|
1679
2034
|
return null;
|
|
1680
2035
|
}
|
|
1681
2036
|
}
|
|
2037
|
+
function toEnvelope(value) {
|
|
2038
|
+
if (!value || typeof value !== "object") return null;
|
|
2039
|
+
if (value.bundle && typeof value.bundle === "object") {
|
|
2040
|
+
return { bundle: value.bundle, etag: value.etag ?? null };
|
|
2041
|
+
}
|
|
2042
|
+
if (Array.isArray(value.firewall) || Array.isArray(value.whitelists)) {
|
|
2043
|
+
return { bundle: value, etag: null };
|
|
2044
|
+
}
|
|
2045
|
+
return null;
|
|
2046
|
+
}
|
|
2047
|
+
var BLOCK_MESSAGE = "This request has been blocked by Patchstack.";
|
|
1682
2048
|
function blockBody(result) {
|
|
1683
|
-
return {
|
|
1684
|
-
error: "Blocked by Patchstack",
|
|
1685
|
-
rule: result.rule?.id,
|
|
1686
|
-
message: result.message
|
|
1687
|
-
};
|
|
2049
|
+
return { error: BLOCK_MESSAGE, message: BLOCK_MESSAGE, rule: result?.rule?.id };
|
|
1688
2050
|
}
|
|
1689
|
-
function
|
|
2051
|
+
function isDocumentNavigation(getHeader) {
|
|
2052
|
+
const dest = getHeader("sec-fetch-dest");
|
|
2053
|
+
if (dest) return dest === "document";
|
|
2054
|
+
return (getHeader("accept") || "").includes("text/html");
|
|
2055
|
+
}
|
|
2056
|
+
function blockResponse(result, request) {
|
|
2057
|
+
if (request && isDocumentNavigation((n) => request.headers.get(n))) {
|
|
2058
|
+
return new Response(renderBlockPage({ url: request.url, code: result?.rule?.id }), {
|
|
2059
|
+
status: 403,
|
|
2060
|
+
headers: { "content-type": "text/html; charset=utf-8" }
|
|
2061
|
+
});
|
|
2062
|
+
}
|
|
1690
2063
|
return new Response(JSON.stringify(blockBody(result)), {
|
|
1691
2064
|
status: 403,
|
|
1692
2065
|
headers: { "content-type": "application/json" }
|