@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.
Files changed (64) hide show
  1. package/README.md +11 -3
  2. package/dist/cli/arcis.d.ts +23 -0
  3. package/dist/cli/arcis.d.ts.map +1 -0
  4. package/dist/cli/arcis.js +312 -0
  5. package/dist/cli/arcis.js.map +1 -0
  6. package/dist/cli/arcis.mjs +309 -0
  7. package/dist/cli/arcis.mjs.map +1 -0
  8. package/dist/core/constants.d.ts +2 -2
  9. package/dist/core/constants.d.ts.map +1 -1
  10. package/dist/core/index.js +4 -1
  11. package/dist/core/index.js.map +1 -1
  12. package/dist/core/index.mjs +4 -1
  13. package/dist/core/index.mjs.map +1 -1
  14. package/dist/core/types.d.ts +17 -0
  15. package/dist/core/types.d.ts.map +1 -1
  16. package/dist/index.d.ts +4 -0
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +658 -161
  19. package/dist/index.js.map +1 -1
  20. package/dist/index.mjs +655 -162
  21. package/dist/index.mjs.map +1 -1
  22. package/dist/logging/index.js.map +1 -1
  23. package/dist/logging/index.mjs.map +1 -1
  24. package/dist/middleware/bot-detection.d.ts.map +1 -1
  25. package/dist/middleware/cookies.d.ts.map +1 -1
  26. package/dist/middleware/csrf.d.ts +10 -0
  27. package/dist/middleware/csrf.d.ts.map +1 -1
  28. package/dist/middleware/hpp.d.ts.map +1 -1
  29. package/dist/middleware/index.d.ts +2 -0
  30. package/dist/middleware/index.d.ts.map +1 -1
  31. package/dist/middleware/index.js +833 -12
  32. package/dist/middleware/index.js.map +1 -1
  33. package/dist/middleware/index.mjs +832 -13
  34. package/dist/middleware/index.mjs.map +1 -1
  35. package/dist/middleware/main.d.ts.map +1 -1
  36. package/dist/middleware/rate-limit.d.ts.map +1 -1
  37. package/dist/middleware/signup-protection.d.ts +65 -0
  38. package/dist/middleware/signup-protection.d.ts.map +1 -0
  39. package/dist/middleware/telemetry.d.ts +36 -0
  40. package/dist/middleware/telemetry.d.ts.map +1 -0
  41. package/dist/sanitizers/index.d.ts +2 -1
  42. package/dist/sanitizers/index.d.ts.map +1 -1
  43. package/dist/sanitizers/index.js +238 -152
  44. package/dist/sanitizers/index.js.map +1 -1
  45. package/dist/sanitizers/index.mjs +238 -153
  46. package/dist/sanitizers/index.mjs.map +1 -1
  47. package/dist/sanitizers/pii.d.ts.map +1 -1
  48. package/dist/sanitizers/sanitize.d.ts +13 -0
  49. package/dist/sanitizers/sanitize.d.ts.map +1 -1
  50. package/dist/sanitizers/ssti.d.ts.map +1 -1
  51. package/dist/sanitizers/xxe.d.ts.map +1 -1
  52. package/dist/stores/index.js.map +1 -1
  53. package/dist/stores/index.mjs.map +1 -1
  54. package/dist/telemetry/client.d.ts +63 -0
  55. package/dist/telemetry/client.d.ts.map +1 -0
  56. package/dist/telemetry/index.d.ts +3 -0
  57. package/dist/telemetry/index.d.ts.map +1 -0
  58. package/dist/telemetry/types.d.ts +71 -0
  59. package/dist/telemetry/types.d.ts.map +1 -0
  60. package/dist/validation/index.js +3 -0
  61. package/dist/validation/index.js.map +1 -1
  62. package/dist/validation/index.mjs +3 -0
  63. package/dist/validation/index.mjs.map +1 -1
  64. 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;AAQvB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,KAAK,CAAC,OAAO,GAAE,YAAiB,GAAG,oBAAoB,CAuCtE;AAGD,QAAA,MAAM,gBAAgB,EAAY,aAAa,CAAC;AAQhD,OAAO,EAAE,gBAAgB,IAAI,aAAa,EAAE,CAAC;AAC7C,eAAe,gBAAgB,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,CAoIvF;AAED;;;GAGG;AACH,eAAO,MAAM,SAAS,0BAAoB,CAAC"}
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;AAG7E,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"}
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"}
@@ -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, _res, next) => {
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