@arcis/node 1.4.2 → 1.4.4
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/README.md +11 -3
- package/dist/cli/arcis.d.ts +23 -0
- package/dist/cli/arcis.d.ts.map +1 -0
- package/dist/cli/arcis.js +312 -0
- package/dist/cli/arcis.js.map +1 -0
- package/dist/cli/arcis.mjs +309 -0
- package/dist/cli/arcis.mjs.map +1 -0
- package/dist/core/constants.d.ts +2 -2
- package/dist/core/constants.d.ts.map +1 -1
- package/dist/core/index.js +4 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +4 -1
- package/dist/core/index.mjs.map +1 -1
- package/dist/core/types.d.ts +17 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +658 -161
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +655 -162
- package/dist/index.mjs.map +1 -1
- package/dist/logging/index.js.map +1 -1
- package/dist/logging/index.mjs.map +1 -1
- package/dist/middleware/bot-detection.d.ts.map +1 -1
- package/dist/middleware/cookies.d.ts.map +1 -1
- package/dist/middleware/csrf.d.ts +10 -0
- package/dist/middleware/csrf.d.ts.map +1 -1
- package/dist/middleware/hpp.d.ts.map +1 -1
- package/dist/middleware/index.d.ts +2 -0
- package/dist/middleware/index.d.ts.map +1 -1
- package/dist/middleware/index.js +833 -12
- package/dist/middleware/index.js.map +1 -1
- package/dist/middleware/index.mjs +832 -13
- package/dist/middleware/index.mjs.map +1 -1
- package/dist/middleware/main.d.ts.map +1 -1
- package/dist/middleware/rate-limit.d.ts.map +1 -1
- package/dist/middleware/signup-protection.d.ts +65 -0
- package/dist/middleware/signup-protection.d.ts.map +1 -0
- package/dist/middleware/telemetry.d.ts +36 -0
- package/dist/middleware/telemetry.d.ts.map +1 -0
- package/dist/sanitizers/index.d.ts +2 -1
- package/dist/sanitizers/index.d.ts.map +1 -1
- package/dist/sanitizers/index.js +238 -152
- package/dist/sanitizers/index.js.map +1 -1
- package/dist/sanitizers/index.mjs +238 -153
- package/dist/sanitizers/index.mjs.map +1 -1
- package/dist/sanitizers/pii.d.ts.map +1 -1
- package/dist/sanitizers/sanitize.d.ts +13 -0
- package/dist/sanitizers/sanitize.d.ts.map +1 -1
- package/dist/sanitizers/ssti.d.ts.map +1 -1
- package/dist/sanitizers/xxe.d.ts.map +1 -1
- package/dist/stores/index.js.map +1 -1
- package/dist/stores/index.mjs.map +1 -1
- package/dist/telemetry/client.d.ts +63 -0
- package/dist/telemetry/client.d.ts.map +1 -0
- package/dist/telemetry/index.d.ts +3 -0
- package/dist/telemetry/index.d.ts.map +1 -0
- package/dist/telemetry/types.d.ts +71 -0
- package/dist/telemetry/types.d.ts.map +1 -0
- package/dist/validation/index.js +3 -0
- package/dist/validation/index.js.map +1 -1
- package/dist/validation/index.mjs +3 -0
- package/dist/validation/index.mjs.map +1 -1
- package/package.json +10 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../src/middleware/main.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,KAAK,EACV,YAAY,EACZ,aAAa,EACb,oBAAoB,EAIrB,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../src/middleware/main.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,KAAK,EACV,YAAY,EACZ,aAAa,EACb,oBAAoB,EAIrB,MAAM,eAAe,CAAC;AAwCvB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,KAAK,CAAC,OAAO,GAAE,YAAiB,GAAG,oBAAoB,CA4DtE;AAGD,QAAA,MAAM,gBAAgB,EAAY,aAAa,CAAC;AAQhD,OAAO,EAAE,gBAAgB,IAAI,aAAa,EAAE,CAAC;AAC7C,eAAe,gBAAgB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../../src/middleware/rate-limit.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,OAAO,KAAK,EAAE,gBAAgB,EAAE,qBAAqB,EAAkB,MAAM,eAAe,CAAC;AAO7F;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,GAAE,gBAAqB,GAAG,qBAAqB,
|
|
1
|
+
{"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../../src/middleware/rate-limit.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,OAAO,KAAK,EAAE,gBAAgB,EAAE,qBAAqB,EAAkB,MAAM,eAAe,CAAC;AAO7F;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,GAAE,gBAAqB,GAAG,qBAAqB,CAuIvF;AAED;;;GAGG;AACH,eAAO,MAAM,SAAS,0BAAoB,CAAC"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @arcis/node/middleware/signup-protection
|
|
3
|
+
*
|
|
4
|
+
* Composite signup-form protection: one middleware that combines email
|
|
5
|
+
* validation (syntax + disposable), bot detection, and a dedicated
|
|
6
|
+
* per-IP rate limit. Matches the Arcjet `protectSignup` convenience
|
|
7
|
+
* primitive but stays fully local — no cloud lookups.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* app.post('/signup', signupProtection(), handler);
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* app.post('/signup', signupProtection({
|
|
14
|
+
* emailField: 'email',
|
|
15
|
+
* rateLimit: { max: 5, windowMs: 60_000 },
|
|
16
|
+
* blockDisposable: true,
|
|
17
|
+
* }), handler);
|
|
18
|
+
*/
|
|
19
|
+
import type { Request, RequestHandler } from 'express';
|
|
20
|
+
import { type BotCategory } from './bot-detection';
|
|
21
|
+
export type SignupBlockReason = 'missing_email' | 'invalid_email' | 'disposable_email' | 'bot' | 'rate_limited';
|
|
22
|
+
export interface SignupCheckResult {
|
|
23
|
+
allowed: boolean;
|
|
24
|
+
reason: SignupBlockReason | 'ok';
|
|
25
|
+
details?: Record<string, unknown>;
|
|
26
|
+
}
|
|
27
|
+
export interface SignupProtectionOptions {
|
|
28
|
+
/** Request body field holding the email address. Default: 'email' */
|
|
29
|
+
emailField?: string;
|
|
30
|
+
/** Run email validation. Default: true */
|
|
31
|
+
checkEmail?: boolean;
|
|
32
|
+
/** Reject disposable email domains. Default: true */
|
|
33
|
+
blockDisposable?: boolean;
|
|
34
|
+
/** Run bot detection. Default: true */
|
|
35
|
+
checkBot?: boolean;
|
|
36
|
+
/** Bot categories allowed through (e.g. test harnesses). Default: [] — all bots blocked */
|
|
37
|
+
allowedBotCategories?: BotCategory[];
|
|
38
|
+
/** Per-IP rate limit on signup endpoint. Set to `false` to disable. Default: 5 requests / 60s */
|
|
39
|
+
rateLimit?: {
|
|
40
|
+
max?: number;
|
|
41
|
+
windowMs?: number;
|
|
42
|
+
} | false;
|
|
43
|
+
/** Extra email domains to allow (bypasses disposable check) */
|
|
44
|
+
allowedEmailDomains?: string[];
|
|
45
|
+
/** Extra email domains to block */
|
|
46
|
+
blockedEmailDomains?: string[];
|
|
47
|
+
/** Called when a request is blocked — for telemetry/logging */
|
|
48
|
+
onBlocked?: (req: Request, result: SignupCheckResult) => void;
|
|
49
|
+
}
|
|
50
|
+
export interface SignupProtectionMiddleware extends RequestHandler {
|
|
51
|
+
/** Release the rate-limiter cleanup interval */
|
|
52
|
+
close: () => void;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Pure signup check — no rate-limit mutation, no response writes.
|
|
56
|
+
* Useful for framework adapters or custom control flow.
|
|
57
|
+
*/
|
|
58
|
+
export declare function checkSignup(req: Request, options?: SignupProtectionOptions): SignupCheckResult;
|
|
59
|
+
/**
|
|
60
|
+
* Express middleware: applies bot + email + rate-limit checks to a signup
|
|
61
|
+
* endpoint. Responds 400/403/429 with a JSON body on block; otherwise
|
|
62
|
+
* calls `next()`.
|
|
63
|
+
*/
|
|
64
|
+
export declare function signupProtection(options?: SignupProtectionOptions): SignupProtectionMiddleware;
|
|
65
|
+
//# sourceMappingURL=signup-protection.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signup-protection.d.ts","sourceRoot":"","sources":["../../src/middleware/signup-protection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAA0B,cAAc,EAAE,MAAM,SAAS,CAAC;AAE/E,OAAO,EAAa,KAAK,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAG9D,MAAM,MAAM,iBAAiB,GACzB,eAAe,GACf,eAAe,GACf,kBAAkB,GAClB,KAAK,GACL,cAAc,CAAC;AAEnB,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,iBAAiB,GAAG,IAAI,CAAC;IACjC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;AAED,MAAM,WAAW,uBAAuB;IACtC,qEAAqE;IACrE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,0CAA0C;IAC1C,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,qDAAqD;IACrD,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,uCAAuC;IACvC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,2FAA2F;IAC3F,oBAAoB,CAAC,EAAE,WAAW,EAAE,CAAC;IACrC,iGAAiG;IACjG,SAAS,CAAC,EAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,KAAK,CAAC;IACxD,+DAA+D;IAC/D,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC/B,mCAAmC;IACnC,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC/B,+DAA+D;IAC/D,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,iBAAiB,KAAK,IAAI,CAAC;CAC/D;AAED,MAAM,WAAW,0BAA2B,SAAQ,cAAc;IAChE,gDAAgD;IAChD,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB;AAED;;;GAGG;AACH,wBAAgB,WAAW,CACzB,GAAG,EAAE,OAAO,EACZ,OAAO,GAAE,uBAA4B,GACpC,iBAAiB,CAwCnB;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,GAAE,uBAA4B,GACpC,0BAA0B,CAiD5B"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module @arcis/node/middleware/telemetry
|
|
3
|
+
* Bridges Arcis middleware decisions to a TelemetryClient.
|
|
4
|
+
* Pattern 3 (two-layer): the client owns transport; this layer owns Express plumbing.
|
|
5
|
+
*/
|
|
6
|
+
import type { RequestHandler } from 'express';
|
|
7
|
+
import type { TelemetryClient } from '../telemetry/client';
|
|
8
|
+
import type { TelemetryDecision, TelemetrySeverity } from '../telemetry/types';
|
|
9
|
+
/** Marker that inner middleware writes to and the emitter reads from. */
|
|
10
|
+
export interface ArcisTelemetryMarker {
|
|
11
|
+
vector?: string;
|
|
12
|
+
rule?: string;
|
|
13
|
+
severity?: TelemetrySeverity;
|
|
14
|
+
matchedPattern?: string;
|
|
15
|
+
reason?: string;
|
|
16
|
+
/** Pre-decided decision. If absent, the emitter infers from response status. */
|
|
17
|
+
decision?: TelemetryDecision;
|
|
18
|
+
}
|
|
19
|
+
declare module 'express-serve-static-core' {
|
|
20
|
+
interface Request {
|
|
21
|
+
/** Per-request marker populated by Arcis middlewares for telemetry attribution. */
|
|
22
|
+
__arcis?: ArcisTelemetryMarker;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Express middleware that records a telemetry event for every request.
|
|
27
|
+
* Captures latency from entry, hooks `res.on('finish')`, and infers the
|
|
28
|
+
* final decision from response status + any `req.__arcis` marker.
|
|
29
|
+
*/
|
|
30
|
+
export declare function createTelemetryEmitter(client: TelemetryClient): RequestHandler;
|
|
31
|
+
/**
|
|
32
|
+
* Wraps the sanitizer middleware so SecurityThreatError → req.__arcis marker.
|
|
33
|
+
* The emitter on `finish` will then have vector/rule/severity attribution.
|
|
34
|
+
*/
|
|
35
|
+
export declare function tapSanitizerThreats(handler: RequestHandler): RequestHandler;
|
|
36
|
+
//# sourceMappingURL=telemetry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"telemetry.d.ts","sourceRoot":"","sources":["../../src/middleware/telemetry.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAW,cAAc,EAAE,MAAM,SAAS,CAAC;AACvD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,KAAK,EAAkB,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAG/F,yEAAyE;AACzE,MAAM,WAAW,oBAAoB;IACnC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gFAAgF;IAChF,QAAQ,CAAC,EAAE,iBAAiB,CAAC;CAC9B;AAED,OAAO,QAAQ,2BAA2B,CAAC;IACzC,UAAU,OAAO;QACf,mFAAmF;QACnF,OAAO,CAAC,EAAE,oBAAoB,CAAC;KAChC;CACF;AAcD;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,eAAe,GAAG,cAAc,CAe9E;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,cAAc,GAAG,cAAc,CAiB3E"}
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* @module @arcis/node/sanitizers
|
|
3
3
|
* All sanitization functions for Arcis
|
|
4
4
|
*/
|
|
5
|
-
export { sanitizeString, sanitizeObject, createSanitizer } from './sanitize';
|
|
5
|
+
export { sanitizeString, sanitizeObject, createSanitizer, scanThreats } from './sanitize';
|
|
6
|
+
export type { ThreatHit } from './sanitize';
|
|
6
7
|
export { sanitizeXss, detectXss } from './xss';
|
|
7
8
|
export { sanitizeSql, detectSql } from './sql';
|
|
8
9
|
export { sanitizePath, detectPathTraversal } from './path';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/sanitizers/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/sanitizers/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC1F,YAAY,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAG5C,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAC/C,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAC/C,OAAO,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,QAAQ,CAAC;AAC3D,OAAO,EAAE,eAAe,EAAE,sBAAsB,EAAE,MAAM,WAAW,CAAC;AAGpE,OAAO,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC;AAG3F,OAAO,EAAE,mBAAmB,EAAE,wBAAwB,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAGnG,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AAGlD,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAG/C,OAAO,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAC;AAGtE,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,qBAAqB,EAAE,MAAM,WAAW,CAAC;AAGxF,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,OAAO,CAAC;AAGtF,OAAO,EAAE,aAAa,EAAE,kBAAkB,EAAE,WAAW,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAGtG,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,QAAQ,CAAC;AAGjF,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC"}
|
package/dist/sanitizers/index.js
CHANGED
|
@@ -35,13 +35,19 @@ var XSS_PATTERNS = [
|
|
|
35
35
|
/** base href hijacking — redirects all relative URLs to attacker domain */
|
|
36
36
|
/<base[\s>]/gi,
|
|
37
37
|
/** link tag injection — stylesheet or preload CSRF attacks */
|
|
38
|
-
/<link[\s>]/gi
|
|
38
|
+
/<link[\s>]/gi,
|
|
39
|
+
/** style tag — CSS expression() / behavior: / IE-era attacks. Mirrors
|
|
40
|
+
* Python's xss-style-tag from packages/core/patterns.json. */
|
|
41
|
+
/<style[\s>]/gi
|
|
39
42
|
];
|
|
40
43
|
var XSS_REMOVE_PATTERNS = [
|
|
41
44
|
/** Full script blocks (content + tags) */
|
|
42
45
|
/<script[^>]*>[\s\S]*?<\/script>/gi,
|
|
43
46
|
/** Standalone/unclosed script tags */
|
|
44
47
|
/<script[^>]*>/gi,
|
|
48
|
+
/** style — CSS expression() and behavior: attacks (IE-era but still relevant) */
|
|
49
|
+
/<style[^>]*>[\s\S]*?<\/style>/gi,
|
|
50
|
+
/<style[^>]*/gi,
|
|
45
51
|
/** iframe — full block and partial/unclosed */
|
|
46
52
|
/<iframe[^>]*>[\s\S]*?<\/iframe>/gi,
|
|
47
53
|
/<iframe[^>]*/gi,
|
|
@@ -425,6 +431,168 @@ function detectCommandInjection(input) {
|
|
|
425
431
|
return false;
|
|
426
432
|
}
|
|
427
433
|
|
|
434
|
+
// src/sanitizers/ssti.ts
|
|
435
|
+
var SSTI_DETECT_PATTERNS = [
|
|
436
|
+
/** Jinja2 / Twig / Nunjucks: {{ ... }} */
|
|
437
|
+
/\{\{.*?\}\}/g,
|
|
438
|
+
/** Freemarker / Thymeleaf / Spring EL: ${ ... } */
|
|
439
|
+
/\$\{.*?\}/g,
|
|
440
|
+
/** ERB / EJS: <%= ... %> or <% ... %> */
|
|
441
|
+
/<%[=\-]?.*?%>/gs,
|
|
442
|
+
/** Pug / Jade / Slim: #{ ... } */
|
|
443
|
+
/#\{.*?\}/g,
|
|
444
|
+
/** Python dunder sandbox escape */
|
|
445
|
+
/__(?:class|mro|subclasses|globals|builtins|import)__/gi,
|
|
446
|
+
/** Jinja2 config leak: {{config.X}} or {{config['X']}} */
|
|
447
|
+
/\{\{\s*config[.\[]/gi,
|
|
448
|
+
/** Jinja2 built-in objects */
|
|
449
|
+
/\{\{\s*(?:self|request|lipsum|cycler|joiner|namespace|range)\b/gi
|
|
450
|
+
];
|
|
451
|
+
var SSTI_REMOVE_PATTERNS = [
|
|
452
|
+
/** Jinja2 / Twig: {{ ... }} — always strip (not valid in any JS context) */
|
|
453
|
+
/\{\{.*?\}\}/g,
|
|
454
|
+
/**
|
|
455
|
+
* Freemarker / Spring EL: ${...} — strip when expression contains operators,
|
|
456
|
+
* method calls, or Python dunder patterns (sandbox escape).
|
|
457
|
+
* Bare ${name} and ${user.name} are left intact (JS template literal syntax).
|
|
458
|
+
*/
|
|
459
|
+
/\$\{[^}]*__\w+__[^}]*\}/g,
|
|
460
|
+
/\$\{[^}]*[?!()*+\-/][^}]*\}/g,
|
|
461
|
+
/** ERB / EJS: <%= ... %> */
|
|
462
|
+
/<%[=\-]?.*?%>/gs,
|
|
463
|
+
/**
|
|
464
|
+
* Pug / Jade: #{...} — same narrowing as ${ above, plus dunder detection.
|
|
465
|
+
* #{name} output expressions are left intact.
|
|
466
|
+
*/
|
|
467
|
+
/#\{[^}]*__\w+__[^}]*\}/g,
|
|
468
|
+
/#\{[^}]*[?!()*+\-/][^}]*\}/g,
|
|
469
|
+
/** Python dunder sandbox escape — always strip */
|
|
470
|
+
/__(?:class|mro|subclasses|globals|builtins|import)__/gi
|
|
471
|
+
];
|
|
472
|
+
function sanitizeSsti(input, collectThreats = false) {
|
|
473
|
+
if (typeof input !== "string") {
|
|
474
|
+
return collectThreats ? { value: String(input), wasSanitized: false, threats: [] } : String(input);
|
|
475
|
+
}
|
|
476
|
+
const threats = [];
|
|
477
|
+
let value = input;
|
|
478
|
+
let wasSanitized = false;
|
|
479
|
+
for (const pattern of SSTI_REMOVE_PATTERNS) {
|
|
480
|
+
pattern.lastIndex = 0;
|
|
481
|
+
if (pattern.test(value)) {
|
|
482
|
+
pattern.lastIndex = 0;
|
|
483
|
+
if (collectThreats) {
|
|
484
|
+
const matches = value.match(pattern);
|
|
485
|
+
if (matches) {
|
|
486
|
+
for (const match of matches) {
|
|
487
|
+
threats.push({
|
|
488
|
+
type: "ssti",
|
|
489
|
+
pattern: pattern.source,
|
|
490
|
+
original: match
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
value = value.replace(pattern, "");
|
|
496
|
+
wasSanitized = true;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
if (collectThreats) {
|
|
500
|
+
return { value, wasSanitized, threats };
|
|
501
|
+
}
|
|
502
|
+
return value;
|
|
503
|
+
}
|
|
504
|
+
function detectSsti(input) {
|
|
505
|
+
if (typeof input !== "string") return false;
|
|
506
|
+
for (const pattern of SSTI_DETECT_PATTERNS) {
|
|
507
|
+
pattern.lastIndex = 0;
|
|
508
|
+
if (pattern.test(input)) {
|
|
509
|
+
return true;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
return false;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// src/sanitizers/xxe.ts
|
|
516
|
+
var MAX_XXE_INPUT_BYTES = 1e6;
|
|
517
|
+
var MAX_ENTITY_REFERENCES = 64;
|
|
518
|
+
var XXE_DETECT_PATTERNS = [
|
|
519
|
+
/** DOCTYPE declaration */
|
|
520
|
+
/<!DOCTYPE\b/gi,
|
|
521
|
+
/** ENTITY declaration */
|
|
522
|
+
/<!ENTITY\b/gi,
|
|
523
|
+
/** SYSTEM keyword with URI */
|
|
524
|
+
/\bSYSTEM\s+["']/gi,
|
|
525
|
+
/** PUBLIC keyword with URI */
|
|
526
|
+
/\bPUBLIC\s+["']/gi,
|
|
527
|
+
/** Parameter entity reference (%entity;) */
|
|
528
|
+
/%\s*\w+\s*;/g,
|
|
529
|
+
/** CDATA section (often used to smuggle payloads) */
|
|
530
|
+
/<!\[CDATA\[/gi
|
|
531
|
+
];
|
|
532
|
+
var XXE_REMOVE_PATTERNS = [
|
|
533
|
+
/** Full DOCTYPE block with optional internal subset: <!DOCTYPE ... [...]> */
|
|
534
|
+
/<!DOCTYPE\s[^[>]*(?:\[[^\]]*\]\s*)?>|<!DOCTYPE\s[^>]*>/gi,
|
|
535
|
+
/** Full ENTITY declaration: <!ENTITY ... > */
|
|
536
|
+
/<!ENTITY[^>]*>/gi,
|
|
537
|
+
/** CDATA sections: <![CDATA[ ... ]]> */
|
|
538
|
+
/<!\[CDATA\[[\s\S]*?\]\]>/gi
|
|
539
|
+
];
|
|
540
|
+
function sanitizeXxe(input, collectThreats = false) {
|
|
541
|
+
if (typeof input !== "string") {
|
|
542
|
+
return collectThreats ? { value: String(input), wasSanitized: false, threats: [] } : String(input);
|
|
543
|
+
}
|
|
544
|
+
const threats = [];
|
|
545
|
+
let value = input;
|
|
546
|
+
let wasSanitized = false;
|
|
547
|
+
if (value.length > MAX_XXE_INPUT_BYTES) {
|
|
548
|
+
if (collectThreats) {
|
|
549
|
+
threats.push({ type: "xxe", pattern: "oversize_input", original: `length=${value.length}` });
|
|
550
|
+
}
|
|
551
|
+
return collectThreats ? { value: "", wasSanitized: true, threats } : "";
|
|
552
|
+
}
|
|
553
|
+
const entityRefs = value.match(/&\w+;/g);
|
|
554
|
+
if (entityRefs && entityRefs.length > MAX_ENTITY_REFERENCES) {
|
|
555
|
+
if (collectThreats) {
|
|
556
|
+
threats.push({ type: "xxe", pattern: "entity_expansion", original: `count=${entityRefs.length}` });
|
|
557
|
+
}
|
|
558
|
+
return collectThreats ? { value: "", wasSanitized: true, threats } : "";
|
|
559
|
+
}
|
|
560
|
+
for (const pattern of XXE_REMOVE_PATTERNS) {
|
|
561
|
+
pattern.lastIndex = 0;
|
|
562
|
+
if (pattern.test(value)) {
|
|
563
|
+
pattern.lastIndex = 0;
|
|
564
|
+
if (collectThreats) {
|
|
565
|
+
const matches = value.match(pattern);
|
|
566
|
+
if (matches) {
|
|
567
|
+
for (const match of matches) {
|
|
568
|
+
threats.push({
|
|
569
|
+
type: "xxe",
|
|
570
|
+
pattern: pattern.source,
|
|
571
|
+
original: match
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
value = value.replace(pattern, "");
|
|
577
|
+
wasSanitized = true;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
if (collectThreats) {
|
|
581
|
+
return { value, wasSanitized, threats };
|
|
582
|
+
}
|
|
583
|
+
return value;
|
|
584
|
+
}
|
|
585
|
+
function detectXxe(input) {
|
|
586
|
+
if (typeof input !== "string") return false;
|
|
587
|
+
for (const pattern of XXE_DETECT_PATTERNS) {
|
|
588
|
+
pattern.lastIndex = 0;
|
|
589
|
+
if (pattern.test(input)) {
|
|
590
|
+
return true;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
return false;
|
|
594
|
+
}
|
|
595
|
+
|
|
428
596
|
// src/sanitizers/sanitize.ts
|
|
429
597
|
function sanitizeString(value, options = {}) {
|
|
430
598
|
if (typeof value !== "string") return value;
|
|
@@ -494,9 +662,73 @@ function sanitizeObjectDepth(obj, options, depth) {
|
|
|
494
662
|
}
|
|
495
663
|
return result;
|
|
496
664
|
}
|
|
665
|
+
function scanThreats(data, depth = 0) {
|
|
666
|
+
if (depth > INPUT.MAX_RECURSION_DEPTH) return null;
|
|
667
|
+
if (data && typeof data === "object" && !Array.isArray(data)) {
|
|
668
|
+
for (const key of Object.keys(data)) {
|
|
669
|
+
const lower = key.toLowerCase();
|
|
670
|
+
if (DANGEROUS_PROTO_KEYS.has(lower)) {
|
|
671
|
+
return { vector: "prototype", rule: "prototype/match", matchedPattern: key };
|
|
672
|
+
}
|
|
673
|
+
if (NOSQL_DANGEROUS_KEYS.has(key)) {
|
|
674
|
+
return { vector: "nosql", rule: "nosql/match", matchedPattern: key };
|
|
675
|
+
}
|
|
676
|
+
const inner = scanThreats(data[key], depth + 1);
|
|
677
|
+
if (inner) return inner;
|
|
678
|
+
}
|
|
679
|
+
return null;
|
|
680
|
+
}
|
|
681
|
+
if (Array.isArray(data)) {
|
|
682
|
+
for (const item of data) {
|
|
683
|
+
const inner = scanThreats(item, depth + 1);
|
|
684
|
+
if (inner) return inner;
|
|
685
|
+
}
|
|
686
|
+
return null;
|
|
687
|
+
}
|
|
688
|
+
if (typeof data !== "string") return null;
|
|
689
|
+
const sample = data.slice(0, 80);
|
|
690
|
+
if (detectXss(data)) {
|
|
691
|
+
return { vector: "xss", rule: "xss/match", matchedPattern: sample };
|
|
692
|
+
}
|
|
693
|
+
if (detectSsti(data)) {
|
|
694
|
+
return { vector: "ssti", rule: "ssti/match", matchedPattern: sample };
|
|
695
|
+
}
|
|
696
|
+
if (detectXxe(data)) {
|
|
697
|
+
return { vector: "xxe", rule: "xxe/match", matchedPattern: sample };
|
|
698
|
+
}
|
|
699
|
+
if (detectSql(data)) {
|
|
700
|
+
return { vector: "sql", rule: "sql/match", matchedPattern: sample };
|
|
701
|
+
}
|
|
702
|
+
if (detectPathTraversal(data)) {
|
|
703
|
+
return { vector: "path", rule: "path/match", matchedPattern: sample };
|
|
704
|
+
}
|
|
705
|
+
if (detectCommandInjection(data)) {
|
|
706
|
+
return { vector: "command", rule: "command/match", matchedPattern: sample };
|
|
707
|
+
}
|
|
708
|
+
return null;
|
|
709
|
+
}
|
|
497
710
|
function createSanitizer(options = {}) {
|
|
498
|
-
return (req,
|
|
711
|
+
return (req, res, next) => {
|
|
499
712
|
try {
|
|
713
|
+
if (options.block) {
|
|
714
|
+
const hit = scanThreats(req.body) || scanThreats(req.query) || scanThreats(req.params) || scanThreats(req.path);
|
|
715
|
+
if (hit) {
|
|
716
|
+
req.__arcis = {
|
|
717
|
+
vector: hit.vector,
|
|
718
|
+
rule: hit.rule,
|
|
719
|
+
severity: "high",
|
|
720
|
+
matchedPattern: hit.matchedPattern,
|
|
721
|
+
reason: `${hit.vector} pattern detected in request`,
|
|
722
|
+
decision: "deny"
|
|
723
|
+
};
|
|
724
|
+
res.status(403).json({
|
|
725
|
+
error: "Request blocked for security reasons",
|
|
726
|
+
code: "SECURITY_THREAT",
|
|
727
|
+
vector: hit.vector
|
|
728
|
+
});
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
500
732
|
if (req.body && typeof req.body === "object") {
|
|
501
733
|
req.body = sanitizeObject(req.body, options);
|
|
502
734
|
}
|
|
@@ -569,158 +801,11 @@ function getDangerousProtoKeys() {
|
|
|
569
801
|
return Array.from(DANGEROUS_PROTO_KEYS);
|
|
570
802
|
}
|
|
571
803
|
|
|
572
|
-
// src/sanitizers/ssti.ts
|
|
573
|
-
var SSTI_DETECT_PATTERNS = [
|
|
574
|
-
/** Jinja2 / Twig / Nunjucks: {{ ... }} */
|
|
575
|
-
/\{\{.*?\}\}/g,
|
|
576
|
-
/** Freemarker / Thymeleaf / Spring EL: ${ ... } */
|
|
577
|
-
/\$\{.*?\}/g,
|
|
578
|
-
/** ERB / EJS: <%= ... %> or <% ... %> */
|
|
579
|
-
/<%[=\-]?.*?%>/gs,
|
|
580
|
-
/** Pug / Jade / Slim: #{ ... } */
|
|
581
|
-
/#\{.*?\}/g,
|
|
582
|
-
/** Python dunder sandbox escape */
|
|
583
|
-
/__(?:class|mro|subclasses|globals|builtins|import)__/gi,
|
|
584
|
-
/** Jinja2 config leak: {{config.X}} or {{config['X']}} */
|
|
585
|
-
/\{\{\s*config[.\[]/gi,
|
|
586
|
-
/** Jinja2 built-in objects */
|
|
587
|
-
/\{\{\s*(?:self|request|lipsum|cycler|joiner|namespace|range)\b/gi
|
|
588
|
-
];
|
|
589
|
-
var SSTI_REMOVE_PATTERNS = [
|
|
590
|
-
/** Jinja2 / Twig: {{ ... }} — always strip (not valid in any JS context) */
|
|
591
|
-
/\{\{.*?\}\}/g,
|
|
592
|
-
/**
|
|
593
|
-
* Freemarker / Spring EL: ${...} — only strip when the expression contains
|
|
594
|
-
* operators (?!*+-/), method calls (), or known-dangerous prefixes.
|
|
595
|
-
* Bare ${name} and ${user.name} are left intact (JS template literal syntax).
|
|
596
|
-
*/
|
|
597
|
-
/\$\{[^}]*[?!()*+\-/][^}]*\}/g,
|
|
598
|
-
/** ERB / EJS: <%= ... %> */
|
|
599
|
-
/<%[=\-]?.*?%>/gs,
|
|
600
|
-
/**
|
|
601
|
-
* Pug / Jade: #{...} — same narrowing as ${ above.
|
|
602
|
-
* #{name} output expressions are left intact.
|
|
603
|
-
*/
|
|
604
|
-
/#\{[^}]*[?!()*+\-/][^}]*\}/g,
|
|
605
|
-
/** Python dunder sandbox escape — always strip */
|
|
606
|
-
/__(?:class|mro|subclasses|globals|builtins|import)__/gi
|
|
607
|
-
];
|
|
608
|
-
function sanitizeSsti(input, collectThreats = false) {
|
|
609
|
-
if (typeof input !== "string") {
|
|
610
|
-
return collectThreats ? { value: String(input), wasSanitized: false, threats: [] } : String(input);
|
|
611
|
-
}
|
|
612
|
-
const threats = [];
|
|
613
|
-
let value = input;
|
|
614
|
-
let wasSanitized = false;
|
|
615
|
-
for (const pattern of SSTI_REMOVE_PATTERNS) {
|
|
616
|
-
pattern.lastIndex = 0;
|
|
617
|
-
if (pattern.test(value)) {
|
|
618
|
-
pattern.lastIndex = 0;
|
|
619
|
-
if (collectThreats) {
|
|
620
|
-
const matches = value.match(pattern);
|
|
621
|
-
if (matches) {
|
|
622
|
-
for (const match of matches) {
|
|
623
|
-
threats.push({
|
|
624
|
-
type: "ssti",
|
|
625
|
-
pattern: pattern.source,
|
|
626
|
-
original: match
|
|
627
|
-
});
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
value = value.replace(pattern, "");
|
|
632
|
-
wasSanitized = true;
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
if (collectThreats) {
|
|
636
|
-
return { value, wasSanitized, threats };
|
|
637
|
-
}
|
|
638
|
-
return value;
|
|
639
|
-
}
|
|
640
|
-
function detectSsti(input) {
|
|
641
|
-
if (typeof input !== "string") return false;
|
|
642
|
-
for (const pattern of SSTI_DETECT_PATTERNS) {
|
|
643
|
-
pattern.lastIndex = 0;
|
|
644
|
-
if (pattern.test(input)) {
|
|
645
|
-
return true;
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
return false;
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
// src/sanitizers/xxe.ts
|
|
652
|
-
var XXE_DETECT_PATTERNS = [
|
|
653
|
-
/** DOCTYPE declaration */
|
|
654
|
-
/<!DOCTYPE\b/gi,
|
|
655
|
-
/** ENTITY declaration */
|
|
656
|
-
/<!ENTITY\b/gi,
|
|
657
|
-
/** SYSTEM keyword with URI */
|
|
658
|
-
/\bSYSTEM\s+["']/gi,
|
|
659
|
-
/** PUBLIC keyword with URI */
|
|
660
|
-
/\bPUBLIC\s+["']/gi,
|
|
661
|
-
/** Parameter entity reference (%entity;) */
|
|
662
|
-
/%\s*\w+\s*;/g,
|
|
663
|
-
/** CDATA section (often used to smuggle payloads) */
|
|
664
|
-
/<!\[CDATA\[/gi
|
|
665
|
-
];
|
|
666
|
-
var XXE_REMOVE_PATTERNS = [
|
|
667
|
-
/** Full DOCTYPE block with optional internal subset: <!DOCTYPE ... [...]> */
|
|
668
|
-
/<!DOCTYPE\s[^[>]*(?:\[[^\]]*\]\s*)?>|<!DOCTYPE\s[^>]*>/gi,
|
|
669
|
-
/** Full ENTITY declaration: <!ENTITY ... > */
|
|
670
|
-
/<!ENTITY[^>]*>/gi,
|
|
671
|
-
/** CDATA sections: <![CDATA[ ... ]]> */
|
|
672
|
-
/<!\[CDATA\[[\s\S]*?\]\]>/gi
|
|
673
|
-
];
|
|
674
|
-
function sanitizeXxe(input, collectThreats = false) {
|
|
675
|
-
if (typeof input !== "string") {
|
|
676
|
-
return collectThreats ? { value: String(input), wasSanitized: false, threats: [] } : String(input);
|
|
677
|
-
}
|
|
678
|
-
const threats = [];
|
|
679
|
-
let value = input;
|
|
680
|
-
let wasSanitized = false;
|
|
681
|
-
for (const pattern of XXE_REMOVE_PATTERNS) {
|
|
682
|
-
pattern.lastIndex = 0;
|
|
683
|
-
if (pattern.test(value)) {
|
|
684
|
-
pattern.lastIndex = 0;
|
|
685
|
-
if (collectThreats) {
|
|
686
|
-
const matches = value.match(pattern);
|
|
687
|
-
if (matches) {
|
|
688
|
-
for (const match of matches) {
|
|
689
|
-
threats.push({
|
|
690
|
-
type: "xxe",
|
|
691
|
-
pattern: pattern.source,
|
|
692
|
-
original: match
|
|
693
|
-
});
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
value = value.replace(pattern, "");
|
|
698
|
-
wasSanitized = true;
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
if (collectThreats) {
|
|
702
|
-
return { value, wasSanitized, threats };
|
|
703
|
-
}
|
|
704
|
-
return value;
|
|
705
|
-
}
|
|
706
|
-
function detectXxe(input) {
|
|
707
|
-
if (typeof input !== "string") return false;
|
|
708
|
-
for (const pattern of XXE_DETECT_PATTERNS) {
|
|
709
|
-
pattern.lastIndex = 0;
|
|
710
|
-
if (pattern.test(input)) {
|
|
711
|
-
return true;
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
return false;
|
|
715
|
-
}
|
|
716
|
-
|
|
717
804
|
// src/sanitizers/jsonp.ts
|
|
718
|
-
var SAFE_CALLBACK_PATTERN = /^[a-zA-Z_$][a-zA-Z0-9_$.
|
|
805
|
+
var SAFE_CALLBACK_PATTERN = /^[a-zA-Z_$][a-zA-Z0-9_$.]*$/;
|
|
719
806
|
var DANGEROUS_CALLBACK_PATTERNS = [
|
|
720
|
-
|
|
807
|
+
/\.\./
|
|
721
808
|
// prototype chain traversal
|
|
722
|
-
/\[\s*\]/
|
|
723
|
-
// empty bracket access
|
|
724
809
|
];
|
|
725
810
|
function sanitizeJsonpCallback(callback, maxLength = 128) {
|
|
726
811
|
if (typeof callback !== "string" || callback.length === 0) {
|
|
@@ -805,7 +890,7 @@ function detectHeaderInjection(input) {
|
|
|
805
890
|
|
|
806
891
|
// src/sanitizers/pii.ts
|
|
807
892
|
var EMAIL_RE = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z]{2,})+/g;
|
|
808
|
-
var PHONE_RE = /(?:\+?1[-.\s]?)?\(?[2-9]\d{2}\)?[-.\s]?\d{3}[-.\s]?\d{4}/g;
|
|
893
|
+
var PHONE_RE = /(?<!\d)(?:\+?1[-.\s]?)?\(?[2-9]\d{2}\)?[-.\s]?\d{3}[-.\s]?\d{4}(?!\d)/g;
|
|
809
894
|
var CREDIT_CARD_RE = /\b(?:\d[ -]*?){13,19}\b/g;
|
|
810
895
|
var SSN_RE = /\b\d{3}[-\s]\d{2}[-\s]\d{4}\b/g;
|
|
811
896
|
var IPV4_RE = /\b(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\b/g;
|
|
@@ -1064,5 +1149,6 @@ exports.sanitizeXss = sanitizeXss;
|
|
|
1064
1149
|
exports.sanitizeXxe = sanitizeXxe;
|
|
1065
1150
|
exports.scanObjectPii = scanObjectPii;
|
|
1066
1151
|
exports.scanPii = scanPii;
|
|
1152
|
+
exports.scanThreats = scanThreats;
|
|
1067
1153
|
//# sourceMappingURL=index.js.map
|
|
1068
1154
|
//# sourceMappingURL=index.js.map
|