@roboteby/parry 1.1.0-rc.1
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/CHANGELOG.md +19 -0
- package/LICENSE +21 -0
- package/README.md +284 -0
- package/config/defaults.js +65 -0
- package/constants/patterns.js +77 -0
- package/package.json +89 -0
- package/src/admin/admin-router.js +106 -0
- package/src/admin/auth/admin-auth.js +176 -0
- package/src/admin/auth/index.js +13 -0
- package/src/admin/auth/strategies/alb-auth.js +49 -0
- package/src/admin/auth/strategies/cloudflare-access.js +34 -0
- package/src/admin/auth/strategies/combined.js +50 -0
- package/src/admin/auth/strategies/ip-allowlist.js +13 -0
- package/src/admin/auth/strategies/none.js +20 -0
- package/src/admin/auth/strategies/token.js +25 -0
- package/src/admin/auth/strategies/trusted-proxy.js +52 -0
- package/src/admin/auth/utils/constant-time.js +18 -0
- package/src/admin/auth/utils/external-identity.js +156 -0
- package/src/admin/auth/utils/header-utils.js +39 -0
- package/src/admin/auth/utils/result.js +39 -0
- package/src/admin/ban-normalizer.js +98 -0
- package/src/admin/index.js +12 -0
- package/src/admin/response.js +41 -0
- package/src/brute-force/brute-force-guard.js +268 -0
- package/src/brute-force/index.js +32 -0
- package/src/brute-force/key-builder.js +164 -0
- package/src/brute-force/result.js +35 -0
- package/src/core/engine.js +264 -0
- package/src/core/index.js +7 -0
- package/src/core/logger.js +3 -0
- package/src/core/rate-limit-result.js +13 -0
- package/src/core/rateLimiter.js +3 -0
- package/src/core/scoring.js +18 -0
- package/src/core/threat-event.js +69 -0
- package/src/detectors/hpp.js +30 -0
- package/src/detectors/index.js +19 -0
- package/src/detectors/nosql.js +53 -0
- package/src/detectors/path-traversal.js +72 -0
- package/src/detectors/prototype-pollution.js +69 -0
- package/src/detectors/request-shape.js +76 -0
- package/src/detectors/sql.js +18 -0
- package/src/detectors/xss.js +18 -0
- package/src/events/event-bus.js +51 -0
- package/src/events/index.js +19 -0
- package/src/events/memory-event-store.js +64 -0
- package/src/events/sanitize-event.js +54 -0
- package/src/events/threat-event.js +174 -0
- package/src/express/ip-resolver.js +109 -0
- package/src/express/middleware.js +379 -0
- package/src/express/request-targets.js +35 -0
- package/src/express/response.js +14 -0
- package/src/index.js +41 -0
- package/src/logger/console-reporter.js +75 -0
- package/src/middleware/index.js +7 -0
- package/src/middleware/parry_ddos.js +3 -0
- package/src/observability/index.js +6 -0
- package/src/observability/metrics.js +61 -0
- package/src/observability/snapshot.js +48 -0
- package/src/policies/index.js +15 -0
- package/src/policies/matcher.js +48 -0
- package/src/policies/normalize-policy.js +94 -0
- package/src/policies/presets.js +34 -0
- package/src/rate-limit/keys.js +7 -0
- package/src/rate-limit/limiter.js +124 -0
- package/src/stores/README.md +51 -0
- package/src/stores/index.js +6 -0
- package/src/stores/memory-store.js +278 -0
- package/src/stores/redis-store.js +349 -0
- package/src/utils/decode.js +56 -0
- package/src/utils/flatten.js +27 -0
- package/src/utils/normalize.js +21 -0
- package/types/index.d.ts +555 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
This project follows [Semantic Versioning](https://semver.org/) for public API and
|
|
6
|
+
runtime behavior. The format is based on [Keep a Changelog](https://keepachangelog.com/),
|
|
7
|
+
without adding a changelog generation dependency.
|
|
8
|
+
|
|
9
|
+
## [Unreleased]
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Release workflow preparation.
|
|
14
|
+
- npm package hardening.
|
|
15
|
+
- Package tarball validation scripts.
|
|
16
|
+
|
|
17
|
+
### Security
|
|
18
|
+
|
|
19
|
+
- Documented npm publishing and supply-chain hardening with Trusted Publishing/OIDC.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 RobotEby
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
# parry-express-security-middleware
|
|
2
|
+
|
|
3
|
+
Application-layer security middleware for Express.js.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Parry is a CommonJS security middleware for Express applications. It helps block common application-layer abuse before requests reach route handlers, while keeping the public API small and compatible with standard Express middleware usage.
|
|
8
|
+
|
|
9
|
+
Parry detects and blocks patterns associated with SQL injection, XSS, NoSQL injection, HTTP parameter pollution, prototype pollution, path traversal, rate abuse, and brute-force authentication attempts. It also emits structured Threat Events and metrics that can be inspected through an optional read-only Admin API.
|
|
10
|
+
|
|
11
|
+
Parry is not a complete volumetric DDoS protection product. Network floods, L3/L4 abuse, TLS exhaustion, CDN filtering, and edge rate controls should be handled by CloudFront, AWS WAF, Shield, a CDN, an ALB/load balancer, or equivalent infrastructure controls.
|
|
12
|
+
|
|
13
|
+
## Why Parry
|
|
14
|
+
|
|
15
|
+
Parry centralizes application-layer security policy in one Express middleware. That gives backend teams a consistent place to tune detector behavior, route-based limits, brute-force protection, event logging, and distributed rate limiting.
|
|
16
|
+
|
|
17
|
+
It is designed for development and production-like deployments:
|
|
18
|
+
|
|
19
|
+
- use `MemoryStore` for local development and single-process services;
|
|
20
|
+
- use `RedisStore` for multiple instances, containers, ECS, Kubernetes, PM2 cluster, or load-balanced services;
|
|
21
|
+
- expose the Admin API only behind authentication, private networking, VPN, or a trusted reverse proxy.
|
|
22
|
+
|
|
23
|
+
## Features
|
|
24
|
+
|
|
25
|
+
- SQL injection detection
|
|
26
|
+
- XSS detection
|
|
27
|
+
- NoSQL injection detection
|
|
28
|
+
- HTTP parameter pollution checks
|
|
29
|
+
- Prototype pollution checks
|
|
30
|
+
- Path traversal checks
|
|
31
|
+
- Request shape guard
|
|
32
|
+
- Global and route-based rate limiting
|
|
33
|
+
- BruteForceGuard for authentication routes
|
|
34
|
+
- `MemoryStore` and optional `RedisStore`
|
|
35
|
+
- Structured Threat Events
|
|
36
|
+
- Metrics and observability helpers
|
|
37
|
+
- Optional read-only Admin API
|
|
38
|
+
- Route-based policies and presets
|
|
39
|
+
- Docker demo API
|
|
40
|
+
- AWS reference infrastructure
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npm install @roboteby/parry
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Parry expects Express to be installed by your application.
|
|
49
|
+
|
|
50
|
+
## Quick Start
|
|
51
|
+
|
|
52
|
+
```js
|
|
53
|
+
const express = require('express');
|
|
54
|
+
const { createParry } = require('@roboteby/parry');
|
|
55
|
+
|
|
56
|
+
const app = express();
|
|
57
|
+
|
|
58
|
+
const parry = createParry({
|
|
59
|
+
preset: 'recommended',
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
app.use(express.json());
|
|
63
|
+
app.use(parry.middleware());
|
|
64
|
+
|
|
65
|
+
app.get('/health', (_req, res) => {
|
|
66
|
+
res.json({ ok: true });
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
app.listen(3000);
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Parse JSON and URL-encoded bodies before Parry when you want Parry to inspect request bodies. Keep Parry before routes that should be protected.
|
|
73
|
+
|
|
74
|
+
The legacy `Parry_DDoS(options)` export remains available for existing CommonJS integrations:
|
|
75
|
+
|
|
76
|
+
```js
|
|
77
|
+
const { Parry_DDoS } = require('@roboteby/parry');
|
|
78
|
+
|
|
79
|
+
app.use(express.json());
|
|
80
|
+
app.use(Parry_DDoS({ preset: 'recommended' }));
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Presets
|
|
84
|
+
|
|
85
|
+
Parry supports conservative presets for common application-layer protection:
|
|
86
|
+
|
|
87
|
+
- `off`: no route-policy preset is added.
|
|
88
|
+
- `recommended`: enables practical defaults for common auth routes and low-noise application-layer checks.
|
|
89
|
+
- `strict`: uses more restrictive brute-force and route rate-limit defaults for sensitive environments.
|
|
90
|
+
|
|
91
|
+
Every option can still be configured explicitly. Prefer starting with `recommended`, reviewing logs and events, then tightening route policies where needed.
|
|
92
|
+
|
|
93
|
+
## Stores
|
|
94
|
+
|
|
95
|
+
`MemoryStore` is the default store. It is suitable for tests, demos, local development, and single-process deployments.
|
|
96
|
+
|
|
97
|
+
For distributed deployments, use `RedisStore` with a Redis client created by your application:
|
|
98
|
+
|
|
99
|
+
```js
|
|
100
|
+
const { createClient } = require('redis');
|
|
101
|
+
const { createParry, RedisStore } = require('@roboteby/parry');
|
|
102
|
+
|
|
103
|
+
const redis = createClient({ url: process.env.REDIS_URL });
|
|
104
|
+
await redis.connect();
|
|
105
|
+
|
|
106
|
+
const parry = createParry({
|
|
107
|
+
store: new RedisStore({ client: redis, prefix: 'parry' }),
|
|
108
|
+
rateLimit: {
|
|
109
|
+
enabled: true,
|
|
110
|
+
max: 100,
|
|
111
|
+
windowMs: 60_000,
|
|
112
|
+
headers: true,
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
If your service runs behind multiple instances, containers, or load balancers, use a shared store. `MemoryStore` protects only the current Node.js process.
|
|
118
|
+
|
|
119
|
+
## Brute-Force Protection
|
|
120
|
+
|
|
121
|
+
Route policies can protect sensitive authentication endpoints without making the global rate limit too aggressive:
|
|
122
|
+
|
|
123
|
+
```js
|
|
124
|
+
const parry = createParry({
|
|
125
|
+
policies: [
|
|
126
|
+
{
|
|
127
|
+
name: 'auth-login',
|
|
128
|
+
match: { method: 'POST', path: '/login' },
|
|
129
|
+
rateLimit: {
|
|
130
|
+
enabled: true,
|
|
131
|
+
max: 20,
|
|
132
|
+
windowMs: 60_000,
|
|
133
|
+
key: 'ip',
|
|
134
|
+
},
|
|
135
|
+
bruteForce: {
|
|
136
|
+
enabled: true,
|
|
137
|
+
maxAttempts: 5,
|
|
138
|
+
windowMs: 15 * 60_000,
|
|
139
|
+
blockDurationMs: 10 * 60_000,
|
|
140
|
+
keys: ['ip', 'body.email', 'ip+body.email'],
|
|
141
|
+
failureStatusCodes: [400, 401, 403],
|
|
142
|
+
resetOnSuccess: true,
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
});
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Routes can also report authentication outcomes manually:
|
|
150
|
+
|
|
151
|
+
```js
|
|
152
|
+
app.post('/login', async (req, res) => {
|
|
153
|
+
const user = await authService.validate(req.body.email, req.body.password);
|
|
154
|
+
|
|
155
|
+
if (!user) {
|
|
156
|
+
req.parry?.recordAuthFailure('invalid_credentials');
|
|
157
|
+
return res.status(200).json({ success: false });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
req.parry?.recordAuthSuccess();
|
|
161
|
+
return res.json({ success: true });
|
|
162
|
+
});
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Threat Events and Admin API
|
|
166
|
+
|
|
167
|
+
Parry emits structured Threat Events for blocked requests, rate limits, brute-force blocks, store errors, and hook errors. Events are sanitized before reaching logs, hooks, metrics, or the Admin API.
|
|
168
|
+
|
|
169
|
+
The optional Admin API is read-only and is never mounted automatically:
|
|
170
|
+
|
|
171
|
+
```js
|
|
172
|
+
const { createParry, createParryAdminRouter } = require('@roboteby/parry');
|
|
173
|
+
|
|
174
|
+
const parry = createParry({
|
|
175
|
+
admin: {
|
|
176
|
+
enabled: true,
|
|
177
|
+
auth: {
|
|
178
|
+
mode: 'token',
|
|
179
|
+
token: process.env.PARRY_ADMIN_TOKEN,
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
app.use(parry.middleware());
|
|
185
|
+
app.use('/_parry', createParryAdminRouter(parry));
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Main endpoints:
|
|
189
|
+
|
|
190
|
+
- `GET /_parry/health`
|
|
191
|
+
- `GET /_parry/metrics`
|
|
192
|
+
- `GET /_parry/events`
|
|
193
|
+
- `GET /_parry/events/:id`
|
|
194
|
+
- `GET /_parry/bans`
|
|
195
|
+
- `GET /_parry/policies`
|
|
196
|
+
|
|
197
|
+
Protect the Admin API with token auth for local demos, or with VPN, private networking, Cloudflare Access, AWS ALB/Cognito auth, trusted proxy auth, or IP allowlists in production.
|
|
198
|
+
|
|
199
|
+
## Parry Security Console
|
|
200
|
+
|
|
201
|
+
The separate `parry-security-console` repository provides a read-only dashboard for the Parry Admin API. It displays health, metrics, Threat Events, bans/blocks, and route policies. It does not contain middleware logic, does not execute payloads, and is not a scanner.
|
|
202
|
+
|
|
203
|
+
For local development, the console can use Vite proxying with `VITE_PARRY_API_URL=/api/parry`.
|
|
204
|
+
|
|
205
|
+
## Docker Demo
|
|
206
|
+
|
|
207
|
+
The repository includes a demo Express API with Redis:
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
docker compose up --build
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Useful local checks:
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
curl http://localhost:3000/health
|
|
217
|
+
|
|
218
|
+
curl http://localhost:3000/_parry/health \
|
|
219
|
+
-H "x-parry-admin-token: change-me"
|
|
220
|
+
|
|
221
|
+
curl -X POST http://localhost:3000/echo \
|
|
222
|
+
-H "Content-Type: application/json" \
|
|
223
|
+
-d '{"message":"hello"}'
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
The `change-me` token is for local demos only.
|
|
227
|
+
|
|
228
|
+
## Security Model
|
|
229
|
+
|
|
230
|
+
Parry operates inside the Express application. It helps identify and block suspicious application-layer requests, but it does not replace edge and infrastructure controls.
|
|
231
|
+
|
|
232
|
+
Production deployments should account for:
|
|
233
|
+
|
|
234
|
+
- CloudFront, AWS WAF, Shield, CDN, ALB, or equivalent edge protection for volumetric DDoS and network-layer abuse.
|
|
235
|
+
- RedisStore or another shared store for distributed rate limits and brute-force counters.
|
|
236
|
+
- Admin API authentication and network restrictions.
|
|
237
|
+
- Trusted proxy configuration before accepting `x-forwarded-for`, Cloudflare Access, ALB/Cognito, or reverse-proxy identity headers.
|
|
238
|
+
- Generic authentication responses that do not reveal whether a username or email exists.
|
|
239
|
+
|
|
240
|
+
Browser-visible Admin API tokens are appropriate only for local development and demos. Production consoles should be protected by VPN, private networking, Cloudflare Access, ALB/Cognito, reverse proxy auth, or a backend/admin gateway.
|
|
241
|
+
|
|
242
|
+
## Documentation
|
|
243
|
+
|
|
244
|
+
Additional documentation is available in the repository:
|
|
245
|
+
|
|
246
|
+
- [Admin API](https://github.com/RobotEby/parry-express-security-middleware/blob/main/docs/admin-api.md)
|
|
247
|
+
- [Admin API authentication](https://github.com/RobotEby/parry-express-security-middleware/blob/main/docs/admin-api-auth.md)
|
|
248
|
+
- [AWS Admin API authentication](https://github.com/RobotEby/parry-express-security-middleware/blob/main/docs/aws-admin-auth.md)
|
|
249
|
+
- [Docker demo](https://github.com/RobotEby/parry-express-security-middleware/blob/main/docs/docker-demo.md)
|
|
250
|
+
- [AWS infrastructure notes](https://github.com/RobotEby/parry-express-security-middleware/blob/main/docs/aws-infra.md)
|
|
251
|
+
- [CI/CD](https://github.com/RobotEby/parry-express-security-middleware/blob/main/docs/ci-cd.md)
|
|
252
|
+
- [Release process](https://github.com/RobotEby/parry-express-security-middleware/blob/main/docs/release.md)
|
|
253
|
+
- [Payload regression testing](https://github.com/RobotEby/parry-express-security-middleware/blob/main/docs/testing-payloads.md)
|
|
254
|
+
- [Architecture](https://github.com/RobotEby/parry-express-security-middleware/blob/main/docs/architecture.md)
|
|
255
|
+
|
|
256
|
+
The npm package keeps `docs/`, infrastructure, Docker demo files, and tests out of the published runtime package.
|
|
257
|
+
|
|
258
|
+
## Development
|
|
259
|
+
|
|
260
|
+
```bash
|
|
261
|
+
npm ci
|
|
262
|
+
npm test
|
|
263
|
+
npm run test:fixtures
|
|
264
|
+
npm run test:payload-regression
|
|
265
|
+
npm run package:check
|
|
266
|
+
npm pack --dry-run
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
`npm test` uses local mocks and fake stores. It does not require Redis, AWS, Cloudflare, or external services.
|
|
270
|
+
|
|
271
|
+
## Roadmap
|
|
272
|
+
|
|
273
|
+
Planned areas for future work:
|
|
274
|
+
|
|
275
|
+
- Redis-backed event persistence
|
|
276
|
+
- OpenTelemetry and Prometheus export
|
|
277
|
+
- Express 4 compatibility matrix
|
|
278
|
+
- Optional hashing/redaction for store keys
|
|
279
|
+
- Additional detector tuning with benign counterexamples
|
|
280
|
+
- Admin API hardening and deployment guides
|
|
281
|
+
|
|
282
|
+
## License
|
|
283
|
+
|
|
284
|
+
MIT
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const DEFAULTS = {
|
|
4
|
+
sql: true,
|
|
5
|
+
xss: true,
|
|
6
|
+
nosql: true,
|
|
7
|
+
|
|
8
|
+
hpp: {
|
|
9
|
+
enabled: false,
|
|
10
|
+
allowDuplicateParamsFor: [],
|
|
11
|
+
},
|
|
12
|
+
prototypePollution: {
|
|
13
|
+
enabled: true,
|
|
14
|
+
},
|
|
15
|
+
pathTraversal: {
|
|
16
|
+
enabled: true,
|
|
17
|
+
},
|
|
18
|
+
requestShape: {
|
|
19
|
+
enabled: true,
|
|
20
|
+
maxDepth: 8,
|
|
21
|
+
maxKeys: 500,
|
|
22
|
+
maxArrayLength: 100,
|
|
23
|
+
maxStringLength: 10_000,
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
rateLimit: true,
|
|
27
|
+
maxRequests: 100,
|
|
28
|
+
windowMs: 60_000,
|
|
29
|
+
store: null,
|
|
30
|
+
storeFailureMode: 'fail-open',
|
|
31
|
+
policies: [],
|
|
32
|
+
preset: 'off',
|
|
33
|
+
bruteForce: {
|
|
34
|
+
enabled: false,
|
|
35
|
+
},
|
|
36
|
+
events: {
|
|
37
|
+
maxEvents: 500,
|
|
38
|
+
},
|
|
39
|
+
admin: {
|
|
40
|
+
enabled: false,
|
|
41
|
+
path: '/_parry',
|
|
42
|
+
allowMutations: false,
|
|
43
|
+
allowInsecureAdminApi: false,
|
|
44
|
+
auth: null,
|
|
45
|
+
},
|
|
46
|
+
requestId: {
|
|
47
|
+
enabled: true,
|
|
48
|
+
header: 'x-request-id',
|
|
49
|
+
responseHeader: false,
|
|
50
|
+
},
|
|
51
|
+
trustProxyHeaders: false,
|
|
52
|
+
trustedProxies: [],
|
|
53
|
+
debug: false,
|
|
54
|
+
|
|
55
|
+
suspiciousThreshold: 5,
|
|
56
|
+
banDurationMs: 300_000,
|
|
57
|
+
|
|
58
|
+
logThreats: true,
|
|
59
|
+
|
|
60
|
+
onThreat: null,
|
|
61
|
+
|
|
62
|
+
maxObjectDepth: 5,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
module.exports = { DEFAULTS };
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const SQL_PATTERNS = [
|
|
4
|
+
/\b(union)\b.{0,30}\b(select)\b/i,
|
|
5
|
+
/\b(or|and)\b\s+[\w'"]{1,20}\s*=\s*[\w'"]{1,20}/i,
|
|
6
|
+
/(['"`]\s*(--|#|\/\*)|(--|#|\/\*)\s*$)/,
|
|
7
|
+
/;\s*(drop|alter|truncate|delete|insert|update|create|exec|execute)\b/i,
|
|
8
|
+
/\b(sleep|benchmark|pg_sleep|waitfor\s+delay)\s*\(/i,
|
|
9
|
+
/\b(select|insert|update|delete|drop|alter)\b.{0,80}\b(from|into|table|where)\b/i,
|
|
10
|
+
/'?\s*\bor\b\s+'?1'?\s*=\s*'?1/i,
|
|
11
|
+
/'?\s*\band\b\s+'?1'?\s*=\s*'?1/i,
|
|
12
|
+
/information_schema|sys\.tables|sysobjects|pg_catalog/i,
|
|
13
|
+
/\bchar\s*\(\s*\d+/i,
|
|
14
|
+
/0x[0-9a-f]{2,}/i,
|
|
15
|
+
/\b(load_file|into\s+outfile|into\s+dumpfile)\b/i,
|
|
16
|
+
/\b(exec\s*\(|xp_cmdshell|sp_executesql)\b/i,
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const XSS_PATTERNS = [
|
|
20
|
+
/<\s*script[\s\S]*?>[\s\S]*?<\s*\/\s*script\s*>/i,
|
|
21
|
+
/<\s*script\b[^>]*>/i,
|
|
22
|
+
/\bon\w+\s*=\s*["']?[^"'>]*/i,
|
|
23
|
+
/javascript\s*:/i,
|
|
24
|
+
/vbscript\s*:/i,
|
|
25
|
+
/data\s*:\s*[^,]*script/i,
|
|
26
|
+
/<\s*(img|iframe|object|embed|svg|video|audio|source|track|input)\b[^>]*\s(src|data|href)\s*=\s*["']?\s*javascript/i,
|
|
27
|
+
/<\s*svg\b[^>]*>[\s\S]*?(script|onload|onerror)/i,
|
|
28
|
+
/expression\s*\(/i,
|
|
29
|
+
/url\s*\(\s*["']?\s*javascript/i,
|
|
30
|
+
/\{\{[\s\S]{0,200}\}\}/,
|
|
31
|
+
/\$\{[\s\S]{0,200}\}/,
|
|
32
|
+
/<\s*(base|link|meta|style)\b[^>]*(http-equiv|href|content)\s*=\s*["']?[^"'>]*script/i,
|
|
33
|
+
/\0|%00/,
|
|
34
|
+
/autofocus.{0,30}onfocus/i,
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
const NOSQL_DANGEROUS_OPERATORS = new Set(['$where', '$expr', '$function', '$accumulator']);
|
|
38
|
+
|
|
39
|
+
const NOSQL_SUSPICIOUS_OPERATORS = new Set([
|
|
40
|
+
'$gt',
|
|
41
|
+
'$gte',
|
|
42
|
+
'$lt',
|
|
43
|
+
'$lte',
|
|
44
|
+
'$ne',
|
|
45
|
+
'$in',
|
|
46
|
+
'$nin',
|
|
47
|
+
'$or',
|
|
48
|
+
'$and',
|
|
49
|
+
'$not',
|
|
50
|
+
'$nor',
|
|
51
|
+
'$exists',
|
|
52
|
+
'$type',
|
|
53
|
+
'$regex',
|
|
54
|
+
'$options',
|
|
55
|
+
'$elemMatch',
|
|
56
|
+
'$size',
|
|
57
|
+
'$slice',
|
|
58
|
+
'$meta',
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
const NOSQL_STRING_PATTERNS = [
|
|
62
|
+
/"\$\w+"?\s*:/,
|
|
63
|
+
/\$where\s*[:=]\s*["'`]?(function|this\.|sleep|db\.)/i,
|
|
64
|
+
/\b(mapReduce|runCommand|eval)\s*\(/i,
|
|
65
|
+
/db\.(getCollection|find|update|insert|remove|drop)\s*\(/i,
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
const SENSITIVE_HEADERS = ['user-agent', 'referer', 'x-forwarded-for', 'cookie'];
|
|
69
|
+
|
|
70
|
+
module.exports = {
|
|
71
|
+
SQL_PATTERNS,
|
|
72
|
+
XSS_PATTERNS,
|
|
73
|
+
NOSQL_DANGEROUS_OPERATORS,
|
|
74
|
+
NOSQL_SUSPICIOUS_OPERATORS,
|
|
75
|
+
NOSQL_STRING_PATTERNS,
|
|
76
|
+
SENSITIVE_HEADERS,
|
|
77
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@roboteby/parry",
|
|
3
|
+
"version": "1.1.0-rc.1",
|
|
4
|
+
"description": "Application-layer security middleware for Express.js with injection detection, abuse mitigation, brute-force protection and distributed rate limiting.",
|
|
5
|
+
"main": "./src/index.js",
|
|
6
|
+
"types": "./types/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test": "node tests/index.js",
|
|
9
|
+
"test:unit": "node -e \"(async()=>{ await require('./tests/unit/detectors.test'); await require('./tests/unit/rateLimiter.test'); await require('./tests/unit/memoryStore.test'); await require('./tests/unit/redisStore.test'); await require('./tests/unit/policyMatcher.test'); await require('./tests/unit/keyBuilder.test'); await require('./tests/unit/bruteForceGuard.test'); await require('./tests/unit/engine.test'); await require('./tests/unit/applicationGuards.test'); await require('./tests/unit/observability.test'); await require('./tests/unit/adminAuth.test'); })().catch((err)=>{ console.error(err); process.exit(1); })\"",
|
|
10
|
+
"test:integ": "node tests/integration/middleware.test.js",
|
|
11
|
+
"test:fixtures": "node scripts/payloads/validate-fixtures.js",
|
|
12
|
+
"test:payload-regression": "node tests/regression/index.js",
|
|
13
|
+
"test:payload-report": "node scripts/payloads/generate-payload-report.js",
|
|
14
|
+
"package:check": "node scripts/package/check-package.js",
|
|
15
|
+
"package:check-tag": "node scripts/package/check-version-tag.js",
|
|
16
|
+
"package:dry-run": "npm pack --dry-run",
|
|
17
|
+
"example": "node examples/express-basic.js",
|
|
18
|
+
"test:http": "node scripts/run-tests.js",
|
|
19
|
+
"start:test": "node scripts/test-server.js",
|
|
20
|
+
"docker:build:demo": "docker build -f docker/demo-api/Dockerfile -t parry-demo-api .",
|
|
21
|
+
"format": "prettier --write \"src/**/*.js\"",
|
|
22
|
+
"format:check": "prettier --check \"src/**/*.js\""
|
|
23
|
+
},
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"require": "./src/index.js",
|
|
27
|
+
"types": "./types/index.d.ts"
|
|
28
|
+
},
|
|
29
|
+
"./core": "./src/core/index.js",
|
|
30
|
+
"./detectors": "./src/detectors/index.js",
|
|
31
|
+
"./stores": "./src/stores/index.js",
|
|
32
|
+
"./policies": "./src/policies/index.js",
|
|
33
|
+
"./brute-force": "./src/brute-force/index.js",
|
|
34
|
+
"./events": "./src/events/index.js",
|
|
35
|
+
"./observability": "./src/observability/index.js",
|
|
36
|
+
"./admin": "./src/admin/index.js",
|
|
37
|
+
"./package.json": "./package.json"
|
|
38
|
+
},
|
|
39
|
+
"files": [
|
|
40
|
+
"src",
|
|
41
|
+
"config",
|
|
42
|
+
"constants",
|
|
43
|
+
"types",
|
|
44
|
+
"README.md",
|
|
45
|
+
"LICENSE",
|
|
46
|
+
"CHANGELOG.md",
|
|
47
|
+
"package.json"
|
|
48
|
+
],
|
|
49
|
+
"keywords": [
|
|
50
|
+
"express",
|
|
51
|
+
"middleware",
|
|
52
|
+
"security",
|
|
53
|
+
"application-security",
|
|
54
|
+
"appsec",
|
|
55
|
+
"rate-limit",
|
|
56
|
+
"brute-force",
|
|
57
|
+
"xss",
|
|
58
|
+
"sql-injection",
|
|
59
|
+
"nosql-injection",
|
|
60
|
+
"redis",
|
|
61
|
+
"nodejs"
|
|
62
|
+
],
|
|
63
|
+
"publishConfig": {
|
|
64
|
+
"access": "public"
|
|
65
|
+
},
|
|
66
|
+
"license": "MIT",
|
|
67
|
+
"repository": {
|
|
68
|
+
"type": "git",
|
|
69
|
+
"url": "git+ssh://git@github.com/RobotEby/parry-express-security-middleware.git"
|
|
70
|
+
},
|
|
71
|
+
"bugs": {
|
|
72
|
+
"url": "https://github.com/RobotEby/parry-express-security-middleware/issues"
|
|
73
|
+
},
|
|
74
|
+
"homepage": "https://github.com/RobotEby/parry-express-security-middleware#readme",
|
|
75
|
+
"peerDependencies": {
|
|
76
|
+
"express": "^5.2.1"
|
|
77
|
+
},
|
|
78
|
+
"devDependencies": {
|
|
79
|
+
"eslint-config-prettier": "^10.1.8",
|
|
80
|
+
"express": "^5.2.1",
|
|
81
|
+
"prettier": "^3.8.1"
|
|
82
|
+
},
|
|
83
|
+
"engines": {
|
|
84
|
+
"node": ">=18"
|
|
85
|
+
},
|
|
86
|
+
"dependencies": {
|
|
87
|
+
"ipaddr.js": "^2.4.0"
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const express = require('express');
|
|
4
|
+
const pkg = require('../../package.json');
|
|
5
|
+
const { requireAdminAuth } = require('./auth');
|
|
6
|
+
const { ok, notFound } = require('./response');
|
|
7
|
+
const { listAdminBanEntries } = require('./ban-normalizer');
|
|
8
|
+
const { describeStore, sanitizePolicies } = require('../observability');
|
|
9
|
+
|
|
10
|
+
function createParryAdminRouter(parry, options = {}) {
|
|
11
|
+
const context = resolveParryContext(parry);
|
|
12
|
+
if (!context) {
|
|
13
|
+
throw new Error('createParryAdminRouter requires a Parry instance or middleware.');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const router = express.Router();
|
|
17
|
+
router.use(requireAdminAuth(options, context));
|
|
18
|
+
|
|
19
|
+
router.get('/health', (_req, res) =>
|
|
20
|
+
ok(res, {
|
|
21
|
+
ok: true,
|
|
22
|
+
name: 'parry',
|
|
23
|
+
version: pkg.version,
|
|
24
|
+
uptimeMs: context.metrics.snapshot().uptimeMs,
|
|
25
|
+
store: describeStore(context.store),
|
|
26
|
+
})
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
router.get(
|
|
30
|
+
'/metrics',
|
|
31
|
+
asyncRoute(async (_req, res) => {
|
|
32
|
+
const activeBans = (await listAdminBanEntries(context.store)).length;
|
|
33
|
+
return ok(res, context.metrics.snapshot({ activeBans }));
|
|
34
|
+
})
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
router.get('/events', (req, res) => {
|
|
38
|
+
const result = context.eventBus.getRecentEvents({
|
|
39
|
+
limit: req.query.limit,
|
|
40
|
+
offset: req.query.offset,
|
|
41
|
+
type: req.query.type,
|
|
42
|
+
severity: req.query.severity,
|
|
43
|
+
action: req.query.action,
|
|
44
|
+
detector: req.query.detector,
|
|
45
|
+
ip: req.query.ip,
|
|
46
|
+
path: req.query.path,
|
|
47
|
+
policyName: req.query.policyName,
|
|
48
|
+
});
|
|
49
|
+
return ok(res, result);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
router.get('/events/:id', (req, res) => {
|
|
53
|
+
const event = context.eventBus.getEventById(req.params.id);
|
|
54
|
+
if (!event) return notFound(res);
|
|
55
|
+
return ok(res, event);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
router.get(
|
|
59
|
+
'/bans',
|
|
60
|
+
asyncRoute(async (req, res) => {
|
|
61
|
+
const data = await listAdminBanEntries(context.store, req.query);
|
|
62
|
+
return ok(res, paginateList(data, req.query));
|
|
63
|
+
})
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
router.get('/policies', (req, res) =>
|
|
67
|
+
ok(res, paginateList(sanitizePolicies(context.policies || []), req.query))
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
return router;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function resolveParryContext(parry) {
|
|
74
|
+
if (!parry) return null;
|
|
75
|
+
if (typeof parry.getContext === 'function') return parry.getContext();
|
|
76
|
+
if (parry.__parryContext) return parry.__parryContext;
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function paginateList(data, query = {}) {
|
|
81
|
+
const limit = clampNumber(query.limit, 50, 1, 500);
|
|
82
|
+
const offset = clampNumber(query.offset, 0, 0, data.length);
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
data: data.slice(offset, offset + limit),
|
|
86
|
+
pagination: {
|
|
87
|
+
limit,
|
|
88
|
+
offset,
|
|
89
|
+
total: data.length,
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function clampNumber(value, fallback, min, max) {
|
|
95
|
+
const parsed = Number(value);
|
|
96
|
+
if (!Number.isFinite(parsed)) return fallback;
|
|
97
|
+
return Math.min(max, Math.max(min, Math.floor(parsed)));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function asyncRoute(handler) {
|
|
101
|
+
return function routeHandler(req, res, next) {
|
|
102
|
+
Promise.resolve(handler(req, res, next)).catch(next);
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
module.exports = { createParryAdminRouter, resolveParryContext };
|