@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
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "DEMO sample rules scaffolded by `patchstack-connect protect --demo` — a broad, many-class set (SQLi/XSS/XXE/SSRF/PII/…) for demonstrations, NOT the production corpus. In production, rules come from the Patchstack API per site (via a baked site UUID / token). Kept in sync with examples/protect/demo-rules.json (minus its _demo test vectors).",
|
|
3
|
+
"firewall": [
|
|
4
|
+
{
|
|
5
|
+
"id": "demo-prototype-pollution",
|
|
6
|
+
"title": "Prototype pollution in request body",
|
|
7
|
+
"category": "prototype-pollution",
|
|
8
|
+
"rule_v2": [
|
|
9
|
+
{
|
|
10
|
+
"parameter": "raw",
|
|
11
|
+
"mutations": [
|
|
12
|
+
"urldecode"
|
|
13
|
+
],
|
|
14
|
+
"match": {
|
|
15
|
+
"type": "contains",
|
|
16
|
+
"value": "__proto__"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"parameter": "rules",
|
|
21
|
+
"rules": [
|
|
22
|
+
{
|
|
23
|
+
"parameter": "raw",
|
|
24
|
+
"mutations": [
|
|
25
|
+
"urldecode"
|
|
26
|
+
],
|
|
27
|
+
"match": {
|
|
28
|
+
"type": "contains",
|
|
29
|
+
"value": "constructor"
|
|
30
|
+
},
|
|
31
|
+
"inclusive": true
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"parameter": "raw",
|
|
35
|
+
"mutations": [
|
|
36
|
+
"urldecode"
|
|
37
|
+
],
|
|
38
|
+
"match": {
|
|
39
|
+
"type": "contains",
|
|
40
|
+
"value": "prototype"
|
|
41
|
+
},
|
|
42
|
+
"inclusive": true
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"id": "demo-path-traversal",
|
|
50
|
+
"title": "Path traversal in a file/path parameter",
|
|
51
|
+
"category": "lfi",
|
|
52
|
+
"rule_v2": [
|
|
53
|
+
{
|
|
54
|
+
"parameter": [
|
|
55
|
+
"get.file",
|
|
56
|
+
"post.file",
|
|
57
|
+
"raw.file",
|
|
58
|
+
"get.path",
|
|
59
|
+
"post.path"
|
|
60
|
+
],
|
|
61
|
+
"mutations": [
|
|
62
|
+
"urldecode"
|
|
63
|
+
],
|
|
64
|
+
"match": {
|
|
65
|
+
"type": "contains",
|
|
66
|
+
"value": ".."
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
]
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"id": "demo-sqli",
|
|
73
|
+
"title": "SQL injection in a query parameter",
|
|
74
|
+
"category": "sqli",
|
|
75
|
+
"rule_v2": [
|
|
76
|
+
{
|
|
77
|
+
"parameter": [
|
|
78
|
+
"get.q",
|
|
79
|
+
"post.q",
|
|
80
|
+
"get.id",
|
|
81
|
+
"post.id",
|
|
82
|
+
"raw"
|
|
83
|
+
],
|
|
84
|
+
"mutations": [
|
|
85
|
+
"urldecode"
|
|
86
|
+
],
|
|
87
|
+
"match": {
|
|
88
|
+
"type": "regex",
|
|
89
|
+
"value": "/union\\s+select|;\\s*drop\\s+table|\\bor\\s+1\\s*=\\s*1\\b/i"
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
]
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"id": "demo-xss",
|
|
96
|
+
"title": "Reflected/stored XSS in a text field",
|
|
97
|
+
"category": "xss",
|
|
98
|
+
"rule_v2": [
|
|
99
|
+
{
|
|
100
|
+
"parameter": [
|
|
101
|
+
"get.q",
|
|
102
|
+
"post.comment",
|
|
103
|
+
"post.title",
|
|
104
|
+
"raw"
|
|
105
|
+
],
|
|
106
|
+
"mutations": [
|
|
107
|
+
"urldecode",
|
|
108
|
+
"htmlentitydecode"
|
|
109
|
+
],
|
|
110
|
+
"match": {
|
|
111
|
+
"type": "regex",
|
|
112
|
+
"value": "/<script\\b|onerror\\s*=|javascript:/i"
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
]
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"id": "demo-command-injection",
|
|
119
|
+
"title": "OS command injection in a host/cmd parameter",
|
|
120
|
+
"category": "command-injection",
|
|
121
|
+
"rule_v2": [
|
|
122
|
+
{
|
|
123
|
+
"parameter": [
|
|
124
|
+
"get.host",
|
|
125
|
+
"post.host",
|
|
126
|
+
"get.cmd",
|
|
127
|
+
"post.cmd"
|
|
128
|
+
],
|
|
129
|
+
"mutations": [
|
|
130
|
+
"urldecode"
|
|
131
|
+
],
|
|
132
|
+
"match": {
|
|
133
|
+
"type": "regex",
|
|
134
|
+
"value": "/[;&|`$()<>]/"
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
]
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
"id": "demo-nosql-injection",
|
|
141
|
+
"title": "NoSQL operator injection in request body",
|
|
142
|
+
"category": "nosqli",
|
|
143
|
+
"rule_v2": [
|
|
144
|
+
{
|
|
145
|
+
"parameter": "raw",
|
|
146
|
+
"match": {
|
|
147
|
+
"type": "regex",
|
|
148
|
+
"value": "/\"\\$(?:ne|gt|lt|gte|lte|where|regex|in|nin)\"\\s*:/i"
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
]
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
"id": "demo-xxe",
|
|
155
|
+
"title": "XML external entity (XXE) in request body",
|
|
156
|
+
"category": "xxe",
|
|
157
|
+
"rule_v2": [
|
|
158
|
+
{
|
|
159
|
+
"parameter": "raw",
|
|
160
|
+
"match": {
|
|
161
|
+
"type": "regex",
|
|
162
|
+
"value": "/<!ENTITY|<!DOCTYPE[^>]*SYSTEM/i"
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
]
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
"id": "demo-ssrf-request",
|
|
169
|
+
"title": "SSRF via a url parameter (request-side)",
|
|
170
|
+
"category": "ssrf",
|
|
171
|
+
"rule_v2": [
|
|
172
|
+
{
|
|
173
|
+
"parameter": [
|
|
174
|
+
"get.url",
|
|
175
|
+
"post.url",
|
|
176
|
+
"raw.url"
|
|
177
|
+
],
|
|
178
|
+
"mutations": [
|
|
179
|
+
"urldecode"
|
|
180
|
+
],
|
|
181
|
+
"match": {
|
|
182
|
+
"type": "regex",
|
|
183
|
+
"value": "/localhost|127\\.0\\.0\\.1|169\\.254\\.169\\.254|::1|metadata\\.google/i"
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
]
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
"id": "demo-resp-pii-email",
|
|
190
|
+
"title": "Email address leaking in response body",
|
|
191
|
+
"phase": "response",
|
|
192
|
+
"category": "pii-exposure",
|
|
193
|
+
"action": "redact",
|
|
194
|
+
"rule_v2": [
|
|
195
|
+
{
|
|
196
|
+
"parameter": "response.body",
|
|
197
|
+
"match": {
|
|
198
|
+
"type": "regex",
|
|
199
|
+
"value": "/[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}/"
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
]
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
"id": "demo-resp-credit-card",
|
|
206
|
+
"title": "Credit-card number leaking in response body",
|
|
207
|
+
"phase": "response",
|
|
208
|
+
"category": "pii-exposure",
|
|
209
|
+
"action": "redact",
|
|
210
|
+
"rule_v2": [
|
|
211
|
+
{
|
|
212
|
+
"parameter": "response.body",
|
|
213
|
+
"match": {
|
|
214
|
+
"type": "regex",
|
|
215
|
+
"value": "/(?:\\d[ -]?){13,16}/"
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
]
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
"id": "demo-egress-blocklist-host",
|
|
222
|
+
"title": "Outbound request to a known exfiltration host",
|
|
223
|
+
"phase": "egress",
|
|
224
|
+
"category": "exfiltration",
|
|
225
|
+
"rule_v2": [
|
|
226
|
+
{
|
|
227
|
+
"parameter": "egress.host",
|
|
228
|
+
"match": {
|
|
229
|
+
"type": "equals",
|
|
230
|
+
"value": "evil.example.com"
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
]
|
|
234
|
+
}
|
|
235
|
+
],
|
|
236
|
+
"whitelists": [],
|
|
237
|
+
"whitelist_keys": {}
|
|
238
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Patchstack runtime guard for CommonJS Express apps. Managed by `patchstack-connect protect`.
|
|
2
|
+
const { createProtection } = require("@patchstack/connect/protect");
|
|
3
|
+
const fallbackRules = require("./rules.json");
|
|
4
|
+
|
|
5
|
+
const PS_SITE_UUID = "__PATCHSTACK_SITE_UUID__";
|
|
6
|
+
let protection;
|
|
7
|
+
|
|
8
|
+
async function getProtection() {
|
|
9
|
+
if (!protection) {
|
|
10
|
+
const mode = process.env.PATCHSTACK_MODE === "dry-run" ? "dry-run" : "block";
|
|
11
|
+
const token = process.env.PATCHSTACK_WAF_TOKEN;
|
|
12
|
+
const siteUuid = PS_SITE_UUID.startsWith("__") ? process.env.PATCHSTACK_SITE_UUID : PS_SITE_UUID;
|
|
13
|
+
const common = { mode, egress: true };
|
|
14
|
+
protection = await createProtection(
|
|
15
|
+
siteUuid
|
|
16
|
+
? { ...common, siteUuid, rules: fallbackRules, cacheDir: ".patchstack" }
|
|
17
|
+
: token
|
|
18
|
+
? { ...common, token, cacheDir: ".patchstack" }
|
|
19
|
+
: { ...common, rules: fallbackRules },
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
return protection;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function patchstackMiddleware(req, res, next) {
|
|
26
|
+
getProtection()
|
|
27
|
+
.then((active) => active.express()(req, res, next))
|
|
28
|
+
.catch(() => next());
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module.exports = { patchstackMiddleware };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Patchstack runtime guard for ESM Express apps. Managed by `patchstack-connect protect`.
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { createProtection } from "@patchstack/connect/protect";
|
|
4
|
+
|
|
5
|
+
const fallbackRules = JSON.parse(readFileSync(new URL("./rules.json", import.meta.url), "utf8"));
|
|
6
|
+
const PS_SITE_UUID = "__PATCHSTACK_SITE_UUID__";
|
|
7
|
+
let protection;
|
|
8
|
+
|
|
9
|
+
async function getProtection() {
|
|
10
|
+
if (!protection) {
|
|
11
|
+
const mode = process.env.PATCHSTACK_MODE === "dry-run" ? "dry-run" : "block";
|
|
12
|
+
const token = process.env.PATCHSTACK_WAF_TOKEN;
|
|
13
|
+
const siteUuid = PS_SITE_UUID.startsWith("__") ? process.env.PATCHSTACK_SITE_UUID : PS_SITE_UUID;
|
|
14
|
+
const common = { mode, egress: true };
|
|
15
|
+
protection = await createProtection(
|
|
16
|
+
siteUuid
|
|
17
|
+
? { ...common, siteUuid, rules: fallbackRules, cacheDir: ".patchstack" }
|
|
18
|
+
: token
|
|
19
|
+
? { ...common, token, cacheDir: ".patchstack" }
|
|
20
|
+
: { ...common, rules: fallbackRules },
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
return protection;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function patchstackMiddleware(req, res, next) {
|
|
27
|
+
getProtection()
|
|
28
|
+
.then((active) => active.express()(req, res, next))
|
|
29
|
+
.catch(() => next());
|
|
30
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Patchstack runtime guard for Express. Managed by `patchstack-connect protect`.
|
|
2
|
+
// Register after body parsing and before routes: app.use(patchstackMiddleware).
|
|
3
|
+
import { createProtection } from "@patchstack/connect/protect";
|
|
4
|
+
import fallbackRules from "./rules.json";
|
|
5
|
+
|
|
6
|
+
const PS_SITE_UUID = "__PATCHSTACK_SITE_UUID__";
|
|
7
|
+
let _protection: Awaited<ReturnType<typeof createProtection>> | undefined;
|
|
8
|
+
|
|
9
|
+
async function getProtection() {
|
|
10
|
+
if (!_protection) {
|
|
11
|
+
const mode = process.env.PATCHSTACK_MODE === "dry-run" ? "dry-run" : "block";
|
|
12
|
+
const token = process.env.PATCHSTACK_WAF_TOKEN;
|
|
13
|
+
const siteUuid = PS_SITE_UUID.startsWith("__") ? process.env.PATCHSTACK_SITE_UUID : PS_SITE_UUID;
|
|
14
|
+
const common = { mode, egress: true } as const;
|
|
15
|
+
_protection = await createProtection(
|
|
16
|
+
siteUuid
|
|
17
|
+
? { ...common, siteUuid, rules: fallbackRules as never, cacheDir: ".patchstack" }
|
|
18
|
+
: token
|
|
19
|
+
? { ...common, token, cacheDir: ".patchstack" }
|
|
20
|
+
: { ...common, rules: fallbackRules as never },
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
return _protection;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function patchstackMiddleware(req: unknown, res: unknown, next: (err?: unknown) => void) {
|
|
27
|
+
getProtection()
|
|
28
|
+
.then((protection) =>
|
|
29
|
+
(protection.express() as (a: unknown, b: unknown, c: (e?: unknown) => void) => void)(req, res, next),
|
|
30
|
+
)
|
|
31
|
+
.catch(() => next());
|
|
32
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Patchstack runtime guard for Fastify — plugin. Managed by `patchstack-connect protect`.
|
|
2
|
+
// Register it once (`app.register(patchstackFastify)`); it adds a preHandler hook that runs the
|
|
3
|
+
// request-phase WAF (+ egress SSRF) on every request. Fastify's request/reply aren't Web-Fetch
|
|
4
|
+
// shaped, so we reconstruct a Request from the parsed fastify request and run the fetch guard.
|
|
5
|
+
import { createProtection } from "@patchstack/connect/protect";
|
|
6
|
+
import fallbackRules from "./rules.json";
|
|
7
|
+
|
|
8
|
+
const PS_SITE_UUID = "__PATCHSTACK_SITE_UUID__";
|
|
9
|
+
|
|
10
|
+
let _protection: Awaited<ReturnType<typeof createProtection>> | undefined;
|
|
11
|
+
async function getProtection() {
|
|
12
|
+
if (!_protection) {
|
|
13
|
+
const mode = process.env.PATCHSTACK_MODE === "dry-run" ? "dry-run" : "block";
|
|
14
|
+
const token = process.env.PATCHSTACK_WAF_TOKEN;
|
|
15
|
+
const siteUuid = PS_SITE_UUID.startsWith("__") ? process.env.PATCHSTACK_SITE_UUID : PS_SITE_UUID;
|
|
16
|
+
const common = { mode, egress: true } as const;
|
|
17
|
+
_protection = await createProtection(
|
|
18
|
+
siteUuid
|
|
19
|
+
? { ...common, siteUuid, rules: fallbackRules as never, cacheDir: ".patchstack" }
|
|
20
|
+
: token
|
|
21
|
+
? { ...common, token, cacheDir: ".patchstack" }
|
|
22
|
+
: { ...common, rules: fallbackRules as never },
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
return _protection;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// #region patchstack-fastify (managed by patchstack-connect protect — do not edit)
|
|
29
|
+
export async function patchstackFastify(fastify: any) {
|
|
30
|
+
const protection = await getProtection();
|
|
31
|
+
const guard = protection.fetchGuard();
|
|
32
|
+
fastify.addHook("preHandler", async (request: any, reply: any) => {
|
|
33
|
+
const method = (request.method ?? "GET").toUpperCase();
|
|
34
|
+
const host = request.headers?.host ?? "localhost";
|
|
35
|
+
const url = `http://${host}${request.url ?? "/"}`;
|
|
36
|
+
const hasBody = method !== "GET" && method !== "HEAD" && request.body != null;
|
|
37
|
+
const body = hasBody ? (typeof request.body === "string" ? request.body : JSON.stringify(request.body)) : undefined;
|
|
38
|
+
const blocked = await guard(new Request(url, { method, headers: request.headers as HeadersInit, body }));
|
|
39
|
+
if (blocked) {
|
|
40
|
+
const contentType = blocked.headers.get("content-type");
|
|
41
|
+
reply.code(blocked.status);
|
|
42
|
+
if (contentType) reply.header("content-type", contentType);
|
|
43
|
+
reply.send(await blocked.text());
|
|
44
|
+
return reply; // stop the request here
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
// #endregion patchstack-fastify
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Patchstack runtime protection — GENERIC guard (framework-agnostic).
|
|
2
|
+
//
|
|
3
|
+
// Scaffolded by `patchstack-connect protect` when no built-in adapter matched your stack. Wire
|
|
4
|
+
// whichever helper below fits your server into your request path (see the plan the CLI printed),
|
|
5
|
+
// then run `patchstack-connect protect --check` to confirm it's hooked up. The engine ships inside
|
|
6
|
+
// @patchstack/connect — nothing else to install.
|
|
7
|
+
import { createProtection } from "@patchstack/connect/protect";
|
|
8
|
+
import fallbackRules from "./rules.json";
|
|
9
|
+
|
|
10
|
+
// Baked by `patchstack-connect protect` from .patchstackrc.json when available.
|
|
11
|
+
const PS_SITE_UUID = "__PATCHSTACK_SITE_UUID__";
|
|
12
|
+
|
|
13
|
+
let _protection: Awaited<ReturnType<typeof createProtection>> | undefined;
|
|
14
|
+
|
|
15
|
+
/** One memoized protection policy. Rules come from the Patchstack API per-site (cached); the
|
|
16
|
+
* bundled rules.json is the fallback until a site UUID / token is configured. */
|
|
17
|
+
export async function getProtection() {
|
|
18
|
+
if (!_protection) {
|
|
19
|
+
const mode = process.env.PATCHSTACK_MODE === "dry-run" ? "dry-run" : "block";
|
|
20
|
+
const token = process.env.PATCHSTACK_WAF_TOKEN;
|
|
21
|
+
const siteUuid = PS_SITE_UUID.startsWith("__") ? process.env.PATCHSTACK_SITE_UUID : PS_SITE_UUID;
|
|
22
|
+
const common = { mode, egress: true } as const;
|
|
23
|
+
_protection = await createProtection(
|
|
24
|
+
siteUuid
|
|
25
|
+
? { ...common, siteUuid, rules: fallbackRules as never, cacheDir: ".patchstack" }
|
|
26
|
+
: token
|
|
27
|
+
? { ...common, token, cacheDir: ".patchstack" }
|
|
28
|
+
: { ...common, rules: fallbackRules as never },
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
return _protection;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// --- Web Fetch (Cloudflare Workers, Bun, Deno, Hono, Next edge, TanStack server.ts) ---------
|
|
35
|
+
// Wrap your fetch handler: export default { fetch: protectFetch(originalFetch) }
|
|
36
|
+
export function protectFetch<H extends (request: Request, ...rest: unknown[]) => unknown>(handler: H): H {
|
|
37
|
+
return (async (request: Request, ...rest: unknown[]) => {
|
|
38
|
+
const protection = await getProtection();
|
|
39
|
+
const blocked = await protection.fetchGuard()(request);
|
|
40
|
+
if (blocked) return blocked;
|
|
41
|
+
return protection.screenResponse(await handler(request, ...rest) as Response);
|
|
42
|
+
}) as H;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// --- Node / Express -------------------------------------------------------------------------
|
|
46
|
+
// Add before your routes: app.use(patchstackMiddleware)
|
|
47
|
+
export function patchstackMiddleware(req: unknown, res: unknown, next: (err?: unknown) => void) {
|
|
48
|
+
getProtection()
|
|
49
|
+
.then((protection) => (protection.node() as (a: unknown, b: unknown, c: (e?: unknown) => void) => void)(req, res, next))
|
|
50
|
+
.catch(() => next()); // fail open
|
|
51
|
+
}
|
|
@@ -89,3 +89,17 @@ export async function screenResponse<T>(response: T): Promise<T> {
|
|
|
89
89
|
return response; // fail open
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
|
+
|
|
93
|
+
// Optional route-level WAF: evaluate the request-phase rules on EVERY incoming request (not just
|
|
94
|
+
// the Supabase tunnel / server functions) and return a 403 on a match. Off unless the app sets
|
|
95
|
+
// PATCHSTACK_ROUTE_WAF=1 — enabling it runs the request rules on all traffic, which is a classic
|
|
96
|
+
// WAF false-positive surface, so it is opt-in. `request.clone()` so the app can still read the body.
|
|
97
|
+
let _reqGuard: ((request: Request) => Promise<Response | null>) | undefined;
|
|
98
|
+
export async function guardRequest(request: Request): Promise<Response | null> {
|
|
99
|
+
try {
|
|
100
|
+
if (!_reqGuard) _reqGuard = (await getProtection()).fetchGuard();
|
|
101
|
+
return await _reqGuard(request.clone());
|
|
102
|
+
} catch {
|
|
103
|
+
return null; // fail open
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Patchstack runtime guard for Next.js — edge middleware. Managed by `patchstack-connect protect`.
|
|
2
|
+
// Runs the request-phase WAF (+ egress SSRF) on every request before it reaches your route.
|
|
3
|
+
// Note: response-body redaction isn't available in Next middleware (it runs before the response);
|
|
4
|
+
// wire the connect runtime in your route handlers if you also need response screening there.
|
|
5
|
+
import { createProtection } from "@patchstack/connect/protect";
|
|
6
|
+
import fallbackRules from "./patchstack.rules.json";
|
|
7
|
+
|
|
8
|
+
const PS_SITE_UUID = "__PATCHSTACK_SITE_UUID__";
|
|
9
|
+
|
|
10
|
+
let _protection: Awaited<ReturnType<typeof createProtection>> | undefined;
|
|
11
|
+
async function getProtection() {
|
|
12
|
+
if (!_protection) {
|
|
13
|
+
const mode = process.env.PATCHSTACK_MODE === "dry-run" ? "dry-run" : "block";
|
|
14
|
+
const token = process.env.PATCHSTACK_WAF_TOKEN;
|
|
15
|
+
const siteUuid = PS_SITE_UUID.startsWith("__") ? process.env.PATCHSTACK_SITE_UUID : PS_SITE_UUID;
|
|
16
|
+
const common = { mode, egress: true } as const;
|
|
17
|
+
_protection = await createProtection(
|
|
18
|
+
siteUuid
|
|
19
|
+
? { ...common, siteUuid, rules: fallbackRules as never, cacheDir: ".patchstack" }
|
|
20
|
+
: token
|
|
21
|
+
? { ...common, token, cacheDir: ".patchstack" }
|
|
22
|
+
: { ...common, rules: fallbackRules as never },
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
return _protection;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// #region patchstack-next (managed by patchstack-connect protect — do not edit)
|
|
29
|
+
export async function middleware(request: Request) {
|
|
30
|
+
const protection = await getProtection();
|
|
31
|
+
const blocked = await protection.fetchGuard()(request);
|
|
32
|
+
if (blocked) return blocked; // 403 — blocked before it reaches your route
|
|
33
|
+
// otherwise fall through (Next continues to the route)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const config = { matcher: "/:path*" };
|
|
37
|
+
// #endregion patchstack-next
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Patchstack runtime guard for SvelteKit — server hook. Managed by `patchstack-connect protect`.
|
|
2
|
+
// Runs the request-phase WAF (+ egress SSRF) and screens the response body/headers for every request.
|
|
3
|
+
// If you already have a `handle`, compose them with `sequence()` from "@sveltejs/kit/hooks".
|
|
4
|
+
import type { Handle } from "@sveltejs/kit";
|
|
5
|
+
import { createProtection } from "@patchstack/connect/protect";
|
|
6
|
+
import fallbackRules from "./patchstack.rules.json";
|
|
7
|
+
|
|
8
|
+
const PS_SITE_UUID = "__PATCHSTACK_SITE_UUID__";
|
|
9
|
+
|
|
10
|
+
let _protection: Awaited<ReturnType<typeof createProtection>> | undefined;
|
|
11
|
+
async function getProtection() {
|
|
12
|
+
if (!_protection) {
|
|
13
|
+
const mode = process.env.PATCHSTACK_MODE === "dry-run" ? "dry-run" : "block";
|
|
14
|
+
const token = process.env.PATCHSTACK_WAF_TOKEN;
|
|
15
|
+
const siteUuid = PS_SITE_UUID.startsWith("__") ? process.env.PATCHSTACK_SITE_UUID : PS_SITE_UUID;
|
|
16
|
+
const common = { mode, egress: true } as const;
|
|
17
|
+
_protection = await createProtection(
|
|
18
|
+
siteUuid
|
|
19
|
+
? { ...common, siteUuid, rules: fallbackRules as never, cacheDir: ".patchstack" }
|
|
20
|
+
: token
|
|
21
|
+
? { ...common, token, cacheDir: ".patchstack" }
|
|
22
|
+
: { ...common, rules: fallbackRules as never },
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
return _protection;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// #region patchstack-sveltekit (managed by patchstack-connect protect — do not edit)
|
|
29
|
+
export const handle: Handle = async ({ event, resolve }) => {
|
|
30
|
+
const protection = await getProtection();
|
|
31
|
+
const blocked = await protection.fetchGuard()(event.request);
|
|
32
|
+
if (blocked) return blocked; // 403 — blocked before it reaches your route
|
|
33
|
+
return protection.screenResponse(await resolve(event));
|
|
34
|
+
};
|
|
35
|
+
// #endregion patchstack-sveltekit
|