@arcis/node 1.4.0 → 1.4.3

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 (65) hide show
  1. package/README.md +1 -1
  2. package/dist/core/constants.d.ts +2 -2
  3. package/dist/core/constants.d.ts.map +1 -1
  4. package/dist/core/index.js +11 -3
  5. package/dist/core/index.js.map +1 -1
  6. package/dist/core/index.mjs +11 -3
  7. package/dist/core/index.mjs.map +1 -1
  8. package/dist/core/types.d.ts +6 -0
  9. package/dist/core/types.d.ts.map +1 -1
  10. package/dist/index.d.ts +4 -0
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +527 -63
  13. package/dist/index.js.map +1 -1
  14. package/dist/index.mjs +525 -65
  15. package/dist/index.mjs.map +1 -1
  16. package/dist/logging/index.js.map +1 -1
  17. package/dist/logging/index.mjs.map +1 -1
  18. package/dist/middleware/bot-detection.d.ts.map +1 -1
  19. package/dist/middleware/cookies.d.ts.map +1 -1
  20. package/dist/middleware/csrf.d.ts +10 -0
  21. package/dist/middleware/csrf.d.ts.map +1 -1
  22. package/dist/middleware/hpp.d.ts.map +1 -1
  23. package/dist/middleware/index.d.ts +2 -0
  24. package/dist/middleware/index.d.ts.map +1 -1
  25. package/dist/middleware/index.js +671 -39
  26. package/dist/middleware/index.js.map +1 -1
  27. package/dist/middleware/index.mjs +671 -41
  28. package/dist/middleware/index.mjs.map +1 -1
  29. package/dist/middleware/main.d.ts.map +1 -1
  30. package/dist/middleware/rate-limit.d.ts.map +1 -1
  31. package/dist/middleware/signup-protection.d.ts +65 -0
  32. package/dist/middleware/signup-protection.d.ts.map +1 -0
  33. package/dist/middleware/telemetry.d.ts +36 -0
  34. package/dist/middleware/telemetry.d.ts.map +1 -0
  35. package/dist/sanitizers/encode.d.ts.map +1 -1
  36. package/dist/sanitizers/index.d.ts +1 -0
  37. package/dist/sanitizers/index.d.ts.map +1 -1
  38. package/dist/sanitizers/index.js +113 -37
  39. package/dist/sanitizers/index.js.map +1 -1
  40. package/dist/sanitizers/index.mjs +111 -38
  41. package/dist/sanitizers/index.mjs.map +1 -1
  42. package/dist/sanitizers/ldap.d.ts +42 -0
  43. package/dist/sanitizers/ldap.d.ts.map +1 -0
  44. package/dist/sanitizers/path.d.ts.map +1 -1
  45. package/dist/sanitizers/pii.d.ts.map +1 -1
  46. package/dist/sanitizers/sanitize.d.ts.map +1 -1
  47. package/dist/sanitizers/ssti.d.ts.map +1 -1
  48. package/dist/sanitizers/xxe.d.ts.map +1 -1
  49. package/dist/stores/index.js +21 -1
  50. package/dist/stores/index.js.map +1 -1
  51. package/dist/stores/index.mjs +21 -1
  52. package/dist/stores/index.mjs.map +1 -1
  53. package/dist/stores/memory.d.ts +4 -10
  54. package/dist/stores/memory.d.ts.map +1 -1
  55. package/dist/telemetry/client.d.ts +60 -0
  56. package/dist/telemetry/client.d.ts.map +1 -0
  57. package/dist/telemetry/index.d.ts +3 -0
  58. package/dist/telemetry/index.d.ts.map +1 -0
  59. package/dist/telemetry/types.d.ts +59 -0
  60. package/dist/telemetry/types.d.ts.map +1 -0
  61. package/dist/validation/index.js +41 -21
  62. package/dist/validation/index.js.map +1 -1
  63. package/dist/validation/index.mjs +41 -21
  64. package/dist/validation/index.mjs.map +1 -1
  65. package/package.json +8 -2
@@ -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,CAyDtE;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,CAiHvF;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"}
@@ -1 +1 @@
1
- {"version":3,"file":"encode.d.ts","sourceRoot":"","sources":["../../src/sanitizers/encode.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAaH;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAGnD;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAiBxD;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAmBjD;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAOlD;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAkBlD"}
1
+ {"version":3,"file":"encode.d.ts","sourceRoot":"","sources":["../../src/sanitizers/encode.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAaH;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAGnD;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAiBxD;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAwBjD;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAOlD;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAkBlD"}
@@ -15,5 +15,6 @@ export { sanitizeJsonpCallback, detectJsonpInjection } from './jsonp';
15
15
  export { sanitizeHeaderValue, sanitizeHeaders, detectHeaderInjection } from './headers';
16
16
  export { scanPii, detectPii, redactPii, scanObjectPii, redactObjectPii } from './pii';
17
17
  export { encodeForHtml, encodeForAttribute, encodeForJs, encodeForUrl, encodeForCss } from './encode';
18
+ export { sanitizeLdapFilter, sanitizeLdapDn, detectLdapInjection } from './ldap';
18
19
  export { encodeHtmlEntities, isPlainObject } from './utils';
19
20
  //# sourceMappingURL=index.d.ts.map
@@ -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,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,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"}
@@ -27,13 +27,24 @@ var XSS_PATTERNS = [
27
27
  /** URL-encoded script tags */
28
28
  /%3Cscript/gi,
29
29
  /** SVG with onload */
30
- /<svg[^>]*onload/gi
30
+ /<svg[^>]*onload/gi,
31
+ /** form tags — phishing/credential harvesting via action= redirection */
32
+ /<form[\s>]/gi,
33
+ /** meta tags — http-equiv refresh redirects or CSP bypass */
34
+ /<meta[\s>]/gi,
35
+ /** base href hijacking — redirects all relative URLs to attacker domain */
36
+ /<base[\s>]/gi,
37
+ /** link tag injection — stylesheet or preload CSRF attacks */
38
+ /<link[\s>]/gi
31
39
  ];
32
40
  var XSS_REMOVE_PATTERNS = [
33
41
  /** Full script blocks (content + tags) */
34
42
  /<script[^>]*>[\s\S]*?<\/script>/gi,
35
43
  /** Standalone/unclosed script tags */
36
44
  /<script[^>]*>/gi,
45
+ /** style — CSS expression() and behavior: attacks (IE-era but still relevant) */
46
+ /<style[^>]*>[\s\S]*?<\/style>/gi,
47
+ /<style[^>]*/gi,
37
48
  /** iframe — full block and partial/unclosed */
38
49
  /<iframe[^>]*>[\s\S]*?<\/iframe>/gi,
39
50
  /<iframe[^>]*/gi,
@@ -54,7 +65,15 @@ var XSS_REMOVE_PATTERNS = [
54
65
  /javascript\s*:/gi,
55
66
  /vbscript\s*:/gi,
56
67
  /** data: URIs with HTML/script content */
57
- /data\s*:\s*text\/html[^>\s]*/gi
68
+ /data\s*:\s*text\/html[^>\s]*/gi,
69
+ /** form tag injection — phishing via action= redirection */
70
+ /<form[\s>][^>]*/gi,
71
+ /** meta tag injection — http-equiv refresh or CSP bypass */
72
+ /<meta[\s>][^>]*/gi,
73
+ /** base href hijacking */
74
+ /<base[\s>][^>]*/gi,
75
+ /** link tag injection — stylesheet or preload attacks */
76
+ /<link[\s>][^>]*/gi
58
77
  ];
59
78
  var SQL_PATTERNS = [
60
79
  /** SQL keywords */
@@ -118,8 +137,8 @@ var COMMAND_PATTERNS = [
118
137
  /[;&|`]/g,
119
138
  /** Command substitution: $( ... ) — matched as a pair to reduce false positives */
120
139
  /\$\(/g,
121
- /** URL-encoded newline/carriage-return injection (%0a, %0d) */
122
- /%0[ad]/gi
140
+ /** URL-encoded control characters (%00-%0F): null, tab, vtab, formfeed, LF, CR */
141
+ /%0[0-9a-f]/gi
123
142
  ];
124
143
  var DANGEROUS_PROTO_KEYS = /* @__PURE__ */ new Set([
125
144
  "__proto__",
@@ -323,26 +342,31 @@ function sanitizePath(input, collectThreats = false) {
323
342
  const threats = [];
324
343
  let value = input;
325
344
  let wasSanitized = false;
326
- for (const pattern of PATH_PATTERNS) {
327
- pattern.lastIndex = 0;
328
- if (pattern.test(value)) {
345
+ value = value.normalize("NFKC");
346
+ let prev;
347
+ do {
348
+ prev = value;
349
+ for (const pattern of PATH_PATTERNS) {
329
350
  pattern.lastIndex = 0;
330
- if (collectThreats) {
331
- const matches = value.match(pattern);
332
- if (matches) {
333
- for (const match of matches) {
334
- threats.push({
335
- type: "path_traversal",
336
- pattern: pattern.source,
337
- original: match
338
- });
351
+ if (pattern.test(value)) {
352
+ pattern.lastIndex = 0;
353
+ if (collectThreats) {
354
+ const matches = value.match(pattern);
355
+ if (matches) {
356
+ for (const match of matches) {
357
+ threats.push({
358
+ type: "path_traversal",
359
+ pattern: pattern.source,
360
+ original: match
361
+ });
362
+ }
339
363
  }
340
364
  }
365
+ value = value.replace(pattern, "");
366
+ wasSanitized = true;
341
367
  }
342
- value = value.replace(pattern, "");
343
- wasSanitized = true;
344
368
  }
345
- }
369
+ } while (value !== prev);
346
370
  if (collectThreats) {
347
371
  return { value, wasSanitized, threats };
348
372
  }
@@ -350,9 +374,10 @@ function sanitizePath(input, collectThreats = false) {
350
374
  }
351
375
  function detectPathTraversal(input) {
352
376
  if (typeof input !== "string") return false;
377
+ const normalized = input.normalize("NFKC");
353
378
  for (const pattern of PATH_PATTERNS) {
354
379
  pattern.lastIndex = 0;
355
- if (pattern.test(input)) {
380
+ if (pattern.test(normalized)) {
356
381
  return true;
357
382
  }
358
383
  }
@@ -410,7 +435,7 @@ function sanitizeString(value, options = {}) {
410
435
  if (value.length > maxSize) {
411
436
  throw new InputTooLargeError(maxSize, value.length);
412
437
  }
413
- const reject = options.mode !== "sanitize";
438
+ const reject = options.mode === "reject";
414
439
  let result = value;
415
440
  if (options.sql !== false) {
416
441
  if (reject) {
@@ -565,10 +590,24 @@ var SSTI_DETECT_PATTERNS = [
565
590
  /\{\{\s*(?:self|request|lipsum|cycler|joiner|namespace|range)\b/gi
566
591
  ];
567
592
  var SSTI_REMOVE_PATTERNS = [
593
+ /** Jinja2 / Twig: {{ ... }} — always strip (not valid in any JS context) */
568
594
  /\{\{.*?\}\}/g,
569
- /\$\{.*?\}/g,
595
+ /**
596
+ * Freemarker / Spring EL: ${...} — strip when expression contains operators,
597
+ * method calls, or Python dunder patterns (sandbox escape).
598
+ * Bare ${name} and ${user.name} are left intact (JS template literal syntax).
599
+ */
600
+ /\$\{[^}]*__\w+__[^}]*\}/g,
601
+ /\$\{[^}]*[?!()*+\-/][^}]*\}/g,
602
+ /** ERB / EJS: <%= ... %> */
570
603
  /<%[=\-]?.*?%>/gs,
571
- /#\{.*?\}/g,
604
+ /**
605
+ * Pug / Jade: #{...} — same narrowing as ${ above, plus dunder detection.
606
+ * #{name} output expressions are left intact.
607
+ */
608
+ /#\{[^}]*__\w+__[^}]*\}/g,
609
+ /#\{[^}]*[?!()*+\-/][^}]*\}/g,
610
+ /** Python dunder sandbox escape — always strip */
572
611
  /__(?:class|mro|subclasses|globals|builtins|import)__/gi
573
612
  ];
574
613
  function sanitizeSsti(input, collectThreats = false) {
@@ -615,6 +654,8 @@ function detectSsti(input) {
615
654
  }
616
655
 
617
656
  // src/sanitizers/xxe.ts
657
+ var MAX_XXE_INPUT_BYTES = 1e6;
658
+ var MAX_ENTITY_REFERENCES = 64;
618
659
  var XXE_DETECT_PATTERNS = [
619
660
  /** DOCTYPE declaration */
620
661
  /<!DOCTYPE\b/gi,
@@ -644,6 +685,19 @@ function sanitizeXxe(input, collectThreats = false) {
644
685
  const threats = [];
645
686
  let value = input;
646
687
  let wasSanitized = false;
688
+ if (value.length > MAX_XXE_INPUT_BYTES) {
689
+ if (collectThreats) {
690
+ threats.push({ type: "xxe", pattern: "oversize_input", original: `length=${value.length}` });
691
+ }
692
+ return collectThreats ? { value: "", wasSanitized: true, threats } : "";
693
+ }
694
+ const entityRefs = value.match(/&\w+;/g);
695
+ if (entityRefs && entityRefs.length > MAX_ENTITY_REFERENCES) {
696
+ if (collectThreats) {
697
+ threats.push({ type: "xxe", pattern: "entity_expansion", original: `count=${entityRefs.length}` });
698
+ }
699
+ return collectThreats ? { value: "", wasSanitized: true, threats } : "";
700
+ }
647
701
  for (const pattern of XXE_REMOVE_PATTERNS) {
648
702
  pattern.lastIndex = 0;
649
703
  if (pattern.test(value)) {
@@ -681,12 +735,10 @@ function detectXxe(input) {
681
735
  }
682
736
 
683
737
  // src/sanitizers/jsonp.ts
684
- var SAFE_CALLBACK_PATTERN = /^[a-zA-Z_$][a-zA-Z0-9_$.[\]]*$/;
738
+ var SAFE_CALLBACK_PATTERN = /^[a-zA-Z_$][a-zA-Z0-9_$.]*$/;
685
739
  var DANGEROUS_CALLBACK_PATTERNS = [
686
- /\.\./,
740
+ /\.\./
687
741
  // prototype chain traversal
688
- /\[\s*\]/
689
- // empty bracket access
690
742
  ];
691
743
  function sanitizeJsonpCallback(callback, maxLength = 128) {
692
744
  if (typeof callback !== "string" || callback.length === 0) {
@@ -771,7 +823,7 @@ function detectHeaderInjection(input) {
771
823
 
772
824
  // src/sanitizers/pii.ts
773
825
  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;
774
- var PHONE_RE = /(?:\+?1[-.\s]?)?\(?[2-9]\d{2}\)?[-.\s]?\d{3}[-.\s]?\d{4}/g;
826
+ var PHONE_RE = /(?<!\d)(?:\+?1[-.\s]?)?\(?[2-9]\d{2}\)?[-.\s]?\d{3}[-.\s]?\d{4}(?!\d)/g;
775
827
  var CREDIT_CARD_RE = /\b(?:\d[ -]*?){13,19}\b/g;
776
828
  var SSN_RE = /\b\d{3}[-\s]\d{2}[-\s]\d{4}\b/g;
777
829
  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;
@@ -932,16 +984,18 @@ function encodeForAttribute(value) {
932
984
  function encodeForJs(value) {
933
985
  if (!value) return "";
934
986
  let result = "";
935
- for (let i = 0; i < value.length; i++) {
936
- const ch = value.charCodeAt(i);
937
- if (ch >= 48 && ch <= 57 || // 0-9
938
- ch >= 65 && ch <= 90 || // A-Z
939
- ch >= 97 && ch <= 122) {
940
- result += value[i];
941
- } else if (ch < 256) {
942
- result += `\\x${ch.toString(16).toUpperCase().padStart(2, "0")}`;
987
+ for (const char of value) {
988
+ const cp = char.codePointAt(0);
989
+ if (cp >= 48 && cp <= 57 || // 0-9
990
+ cp >= 65 && cp <= 90 || // A-Z
991
+ cp >= 97 && cp <= 122) {
992
+ result += char;
993
+ } else if (cp < 256) {
994
+ result += `\\x${cp.toString(16).toUpperCase().padStart(2, "0")}`;
995
+ } else if (cp <= 65535) {
996
+ result += `\\u${cp.toString(16).toUpperCase().padStart(4, "0")}`;
943
997
  } else {
944
- result += `\\u${ch.toString(16).toUpperCase().padStart(4, "0")}`;
998
+ result += `\\u{${cp.toString(16).toUpperCase()}}`;
945
999
  }
946
1000
  }
947
1001
  return result;
@@ -968,10 +1022,30 @@ function encodeForCss(value) {
968
1022
  return result;
969
1023
  }
970
1024
 
1025
+ // src/sanitizers/ldap.ts
1026
+ var LDAP_FILTER_CHARS = /[*()\\\x00]/g;
1027
+ var LDAP_DN_CHARS = /[,+<>;"=\/\\\x00*()\x00]/g;
1028
+ var LDAP_DETECT_PATTERN = /[*()\\\x00]/;
1029
+ var LDAP_INJECTION_PATTERN = /\)\s*\(|\*\s*\)\s*\(/;
1030
+ var escapeChar = (char) => "\\" + char.charCodeAt(0).toString(16).padStart(2, "0");
1031
+ function sanitizeLdapFilter(input) {
1032
+ if (typeof input !== "string") return String(input);
1033
+ return input.replace(LDAP_FILTER_CHARS, escapeChar);
1034
+ }
1035
+ function sanitizeLdapDn(input) {
1036
+ if (typeof input !== "string") return String(input);
1037
+ return input.replace(LDAP_DN_CHARS, escapeChar);
1038
+ }
1039
+ function detectLdapInjection(input) {
1040
+ if (typeof input !== "string") return false;
1041
+ return LDAP_DETECT_PATTERN.test(input) || LDAP_INJECTION_PATTERN.test(input);
1042
+ }
1043
+
971
1044
  exports.createSanitizer = createSanitizer;
972
1045
  exports.detectCommandInjection = detectCommandInjection;
973
1046
  exports.detectHeaderInjection = detectHeaderInjection;
974
1047
  exports.detectJsonpInjection = detectJsonpInjection;
1048
+ exports.detectLdapInjection = detectLdapInjection;
975
1049
  exports.detectNoSqlInjection = detectNoSqlInjection;
976
1050
  exports.detectPathTraversal = detectPathTraversal;
977
1051
  exports.detectPii = detectPii;
@@ -997,6 +1071,8 @@ exports.sanitizeCommand = sanitizeCommand;
997
1071
  exports.sanitizeHeaderValue = sanitizeHeaderValue;
998
1072
  exports.sanitizeHeaders = sanitizeHeaders;
999
1073
  exports.sanitizeJsonpCallback = sanitizeJsonpCallback;
1074
+ exports.sanitizeLdapDn = sanitizeLdapDn;
1075
+ exports.sanitizeLdapFilter = sanitizeLdapFilter;
1000
1076
  exports.sanitizeObject = sanitizeObject;
1001
1077
  exports.sanitizePath = sanitizePath;
1002
1078
  exports.sanitizeSql = sanitizeSql;