@arcis/node 1.5.1 → 1.6.0

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 (66) hide show
  1. package/README.md +48 -7
  2. package/dist/astro/index.js.map +1 -1
  3. package/dist/astro/index.mjs.map +1 -1
  4. package/dist/bun/index.js.map +1 -1
  5. package/dist/bun/index.mjs.map +1 -1
  6. package/dist/core/constants.d.ts +2 -2
  7. package/dist/core/constants.d.ts.map +1 -1
  8. package/dist/core/index.js +19 -1
  9. package/dist/core/index.js.map +1 -1
  10. package/dist/core/index.mjs +19 -1
  11. package/dist/core/index.mjs.map +1 -1
  12. package/dist/fastify/index.js.map +1 -1
  13. package/dist/fastify/index.mjs.map +1 -1
  14. package/dist/hono/index.js.map +1 -1
  15. package/dist/hono/index.mjs.map +1 -1
  16. package/dist/index.d.ts +3 -1
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +407 -8
  19. package/dist/index.js.map +1 -1
  20. package/dist/index.mjs +407 -9
  21. package/dist/index.mjs.map +1 -1
  22. package/dist/koa/index.js.map +1 -1
  23. package/dist/koa/index.mjs.map +1 -1
  24. package/dist/logging/index.js.map +1 -1
  25. package/dist/logging/index.mjs.map +1 -1
  26. package/dist/middleware/correlation.d.ts +87 -0
  27. package/dist/middleware/correlation.d.ts.map +1 -0
  28. package/dist/middleware/graphql.d.ts.map +1 -1
  29. package/dist/middleware/index.d.ts +3 -1
  30. package/dist/middleware/index.d.ts.map +1 -1
  31. package/dist/middleware/index.js +366 -8
  32. package/dist/middleware/index.js.map +1 -1
  33. package/dist/middleware/index.mjs +366 -9
  34. package/dist/middleware/index.mjs.map +1 -1
  35. package/dist/middleware/protect.d.ts +32 -0
  36. package/dist/middleware/protect.d.ts.map +1 -1
  37. package/dist/nestjs/index.js +55 -2
  38. package/dist/nestjs/index.js.map +1 -1
  39. package/dist/nestjs/index.mjs +55 -2
  40. package/dist/nestjs/index.mjs.map +1 -1
  41. package/dist/nextjs/index.js.map +1 -1
  42. package/dist/nextjs/index.mjs.map +1 -1
  43. package/dist/nuxt/index.js.map +1 -1
  44. package/dist/nuxt/index.mjs.map +1 -1
  45. package/dist/sanitizers/deserialization.d.ts +30 -0
  46. package/dist/sanitizers/deserialization.d.ts.map +1 -0
  47. package/dist/sanitizers/graphql.d.ts +20 -3
  48. package/dist/sanitizers/graphql.d.ts.map +1 -1
  49. package/dist/sanitizers/index.d.ts +2 -0
  50. package/dist/sanitizers/index.d.ts.map +1 -1
  51. package/dist/sanitizers/index.js +150 -7
  52. package/dist/sanitizers/index.js.map +1 -1
  53. package/dist/sanitizers/index.mjs +149 -8
  54. package/dist/sanitizers/index.mjs.map +1 -1
  55. package/dist/sanitizers/prompt-injection.d.ts.map +1 -1
  56. package/dist/sanitizers/sanitize.d.ts +0 -20
  57. package/dist/sanitizers/sanitize.d.ts.map +1 -1
  58. package/dist/stores/index.js.map +1 -1
  59. package/dist/stores/index.mjs.map +1 -1
  60. package/dist/sveltekit/index.js.map +1 -1
  61. package/dist/sveltekit/index.mjs.map +1 -1
  62. package/dist/validation/index.js +55 -2
  63. package/dist/validation/index.js.map +1 -1
  64. package/dist/validation/index.mjs +55 -2
  65. package/dist/validation/index.mjs.map +1 -1
  66. package/package.json +2 -2
@@ -0,0 +1,30 @@
1
+ /**
2
+ * @module @arcis/node/sanitizers/deserialization
3
+ *
4
+ * V33 — Modern deserialization marker detection (improvements.md §1.2).
5
+ *
6
+ * Detect input that LOOKS like a serialized-object payload for
7
+ * runtimes where deserialization equals code execution: Python
8
+ * pickle, Java FastJSON, PHP unserialize, Ruby Marshal, .NET
9
+ * BinaryFormatter.
10
+ *
11
+ * Detection-only — the right response to a hit is "refuse the
12
+ * request" not "strip the bytes and pass through" (a forgiving
13
+ * parser might still deserialize the remainder to something
14
+ * dangerous). Caller decides.
15
+ *
16
+ * Mirrors `arcis-python/arcis/sanitizers/deserialization.py`. Both
17
+ * SDKs must accept the same base corpus per Pattern 7.
18
+ */
19
+ export type DeserializeRuntime = 'python_pickle' | 'java_fastjson' | 'php_unserialize' | 'ruby_marshal' | 'dotnet_binary_formatter';
20
+ /**
21
+ * Detect a serialized-object marker for any known runtime.
22
+ *
23
+ * Returns the runtime tag if a marker matches, or null if the input
24
+ * looks safe. Precedence: head-byte markers (pickle / Ruby / .NET)
25
+ * before embedded markers (FastJSON / PHP).
26
+ */
27
+ export declare function detectDeserialization(payload: string): DeserializeRuntime | null;
28
+ /** Convenience boolean wrapper around `detectDeserialization`. */
29
+ export declare function isSerializedPayload(payload: string): boolean;
30
+ //# sourceMappingURL=deserialization.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deserialization.d.ts","sourceRoot":"","sources":["../../src/sanitizers/deserialization.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,MAAM,MAAM,kBAAkB,GAC1B,eAAe,GACf,eAAe,GACf,iBAAiB,GACjB,cAAc,GACd,yBAAyB,CAAC;AAiB9B;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,MAAM,GACd,kBAAkB,GAAG,IAAI,CAU3B;AAED,kEAAkE;AAClE,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAE5D"}
@@ -44,17 +44,34 @@ export interface GraphqlGuardOptions {
44
44
  * Studio. Production should leave this on.
45
45
  */
46
46
  blockIntrospection?: boolean;
47
+ /**
48
+ * Maximum number of field aliases per query (`label: field`).
49
+ * Default: 50. Alias-bomb attacks repeat the same expensive field
50
+ * under many labels to multiply backend cost. Real queries rarely
51
+ * use more than 20 aliases. improvements.md §1.2 V34.
52
+ */
53
+ maxAliases?: number;
54
+ /**
55
+ * Reject queries whose fragment definitions form a cycle (direct
56
+ * self-reference `fragment A on T { ...A }` or indirect
57
+ * `A → B → A`). Such cycles either infinite-loop a naive resolver
58
+ * or get rejected by `graphql-core` with a 500. Default: true.
59
+ * improvements.md §1.2 V34.
60
+ */
61
+ blockFragmentCycles?: boolean;
47
62
  }
48
- export type GraphqlViolation = 'depth' | 'length' | 'introspection';
63
+ export type GraphqlViolation = 'depth' | 'length' | 'introspection' | 'aliases' | 'fragment_cycle';
49
64
  export interface GraphqlGuardResult {
50
65
  /** True if the query violated any configured limit. */
51
66
  blocked: boolean;
52
- /** Which limit fired first (depth → introspection → length precedence). */
67
+ /** Which limit fired first. Precedence: depth → introspection → aliases → fragment_cycle → length. */
53
68
  reason?: GraphqlViolation;
54
- /** Observed nesting depth. Always returned, even on clean queries. */
69
+ /** Observed nesting depth. Always returned. */
55
70
  depth: number;
56
71
  /** Observed length. Always returned. */
57
72
  length: number;
73
+ /** Observed alias count (improvements.md §1.2 V34). Always returned. */
74
+ aliases: number;
58
75
  }
59
76
  /**
60
77
  * Inspect a GraphQL query against the configured limits. Returns a
@@ -1 +1 @@
1
- {"version":3,"file":"graphql.d.ts","sourceRoot":"","sources":["../../src/sanitizers/graphql.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,MAAM,WAAW,mBAAmB;IAClC,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iEAAiE;IACjE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B;AAED,MAAM,MAAM,gBAAgB,GAAG,OAAO,GAAG,QAAQ,GAAG,eAAe,CAAC;AAEpE,MAAM,WAAW,kBAAkB;IACjC,uDAAuD;IACvD,OAAO,EAAE,OAAO,CAAC;IACjB,2EAA2E;IAC3E,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAC1B,sEAAsE;IACtE,KAAK,EAAE,MAAM,CAAC;IACd,wCAAwC;IACxC,MAAM,EAAE,MAAM,CAAC;CAChB;AA2CD;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,mBAAwB,GAChC,kBAAkB,CAwBpB;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,MAAM,EACb,OAAO,CAAC,EAAE,mBAAmB,GAC5B,OAAO,CAGT"}
1
+ {"version":3,"file":"graphql.d.ts","sourceRoot":"","sources":["../../src/sanitizers/graphql.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,MAAM,WAAW,mBAAmB;IAClC,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iEAAiE;IACjE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;;;OAMG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC/B;AAED,MAAM,MAAM,gBAAgB,GACxB,OAAO,GACP,QAAQ,GACR,eAAe,GACf,SAAS,GACT,gBAAgB,CAAC;AAErB,MAAM,WAAW,kBAAkB;IACjC,uDAAuD;IACvD,OAAO,EAAE,OAAO,CAAC;IACjB,sGAAsG;IACtG,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAC1B,+CAA+C;IAC/C,KAAK,EAAE,MAAM,CAAC;IACd,wCAAwC;IACxC,MAAM,EAAE,MAAM,CAAC;IACf,wEAAwE;IACxE,OAAO,EAAE,MAAM,CAAC;CACjB;AA8HD;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,MAAM,EACb,OAAO,GAAE,mBAAwB,GAChC,kBAAkB,CAiCpB;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,MAAM,EACb,OAAO,CAAC,EAAE,mBAAmB,GAC5B,OAAO,CAGT"}
@@ -20,5 +20,7 @@ export { sanitizeLdapFilter, sanitizeLdapDn, detectLdapInjection } from './ldap'
20
20
  export { sanitizeXpath, detectXpathInjection } from './xpath';
21
21
  export { inspectGraphqlQuery, detectGraphqlAbuse, } from './graphql';
22
22
  export type { GraphqlGuardOptions, GraphqlGuardResult, GraphqlViolation, } from './graphql';
23
+ export { detectDeserialization, isSerializedPayload } from './deserialization';
24
+ export type { DeserializeRuntime } from './deserialization';
23
25
  export { encodeHtmlEntities, isPlainObject } from './utils';
24
26
  //# 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,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,EACL,mBAAmB,EACnB,eAAe,EACf,qBAAqB,EACrB,mBAAmB,EACnB,0BAA0B,GAC3B,MAAM,WAAW,CAAC;AAGnB,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,aAAa,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAC;AAG9D,OAAO,EACL,mBAAmB,EACnB,kBAAkB,GACnB,MAAM,WAAW,CAAC;AACnB,YAAY,EACV,mBAAmB,EACnB,kBAAkB,EAClB,gBAAgB,GACjB,MAAM,WAAW,CAAC;AAGnB,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,EACL,mBAAmB,EACnB,eAAe,EACf,qBAAqB,EACrB,mBAAmB,EACnB,0BAA0B,GAC3B,MAAM,WAAW,CAAC;AAGnB,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,aAAa,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAC;AAG9D,OAAO,EACL,mBAAmB,EACnB,kBAAkB,GACnB,MAAM,WAAW,CAAC;AACnB,YAAY,EACV,mBAAmB,EACnB,kBAAkB,EAClB,gBAAgB,GACjB,MAAM,WAAW,CAAC;AAGnB,OAAO,EAAE,qBAAqB,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAC/E,YAAY,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAG5D,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC"}
@@ -102,7 +102,16 @@ var SQL_PATTERNS = [
102
102
  /** Time-based blind: PostgreSQL pg_sleep() */
103
103
  /\bpg_sleep\s*\(/gi,
104
104
  /** Time-based blind: MSSQL WAITFOR DELAY */
105
- /\bWAITFOR\s+DELAY\b/gi
105
+ /\bWAITFOR\s+DELAY\b/gi,
106
+ /**
107
+ * Oracle DBMS_* stdlib packages used for time-based blind SQLi
108
+ * (DBMS_LOCK.SLEEP, DBMS_PIPE.RECEIVE_MESSAGE) and other Oracle
109
+ * abuse paths. No legitimate user input contains these. Mirrors
110
+ * `sqli-oracle-dbms-packages` in packages/core/patterns.json —
111
+ * improvements.md §1.1.e Q3. Must stay in sync until Node
112
+ * migrates to patterns.json-at-runtime (planned v1.7).
113
+ */
114
+ /\bDBMS_(?:LOCK|PIPE|UTILITY|XSLPROCESSOR|JAVA|OUTPUT|SCHEDULER)\b/gi
106
115
  ];
107
116
  var PATH_PATTERNS = [
108
117
  /** Unix path traversal */
@@ -140,6 +149,15 @@ var COMMAND_PATTERNS = [
140
149
  /[;&|`]/g,
141
150
  /** Command substitution: $( ... ) — matched as a pair to reduce false positives */
142
151
  /\$\(/g,
152
+ /**
153
+ * POSIX shell IFS-substitution: ${IFS} or ${IFS%??}.
154
+ * Attackers use this to inject spaces past metacharacter filters
155
+ * in payloads like `;cat${IFS}/etc/passwd`. Mirrors
156
+ * `cmdi-ifs-bypass` in packages/core/patterns.json — improvements.md
157
+ * §1.1.e Q5. Must stay in sync until Node migrates to
158
+ * patterns.json-at-runtime (planned v1.7).
159
+ */
160
+ /\$\{IFS(?:%[^}]*)?\}/g,
143
161
  /** URL-encoded control characters (%00-%0F): null, tab, vtab, formfeed, LF, CR */
144
162
  /%0[0-9a-f]/gi
145
163
  ];
@@ -677,6 +695,40 @@ var sanitizeEmailHeader = sanitizeHeaderValue;
677
695
  var detectEmailHeaderInjection = detectHeaderInjection;
678
696
 
679
697
  // src/sanitizers/sanitize.ts
698
+ function multiDecode(value, maxPasses = 4) {
699
+ for (let i = 0; i < maxPasses; i++) {
700
+ const prev = value;
701
+ try {
702
+ value = decodeURIComponent(value);
703
+ } catch {
704
+ }
705
+ value = htmlEntityDecode(value);
706
+ if (value === prev) break;
707
+ }
708
+ return value;
709
+ }
710
+ function htmlEntityDecode(s) {
711
+ s = s.replace(/&#(\d+);/g, (_m, n) => {
712
+ const code = parseInt(n, 10);
713
+ return Number.isFinite(code) && code >= 0 && code <= 1114111 ? String.fromCodePoint(code) : _m;
714
+ });
715
+ s = s.replace(/&#x([0-9a-fA-F]+);/g, (_m, h) => {
716
+ const code = parseInt(h, 16);
717
+ return Number.isFinite(code) && code >= 0 && code <= 1114111 ? String.fromCodePoint(code) : _m;
718
+ });
719
+ const named = {
720
+ "&lt;": "<",
721
+ "&gt;": ">",
722
+ "&amp;": "&",
723
+ "&quot;": '"',
724
+ "&apos;": "'",
725
+ "&nbsp;": " "
726
+ };
727
+ for (const [entity, ch] of Object.entries(named)) {
728
+ s = s.split(entity).join(ch);
729
+ }
730
+ return s;
731
+ }
680
732
  function sanitizeString(value, options = {}) {
681
733
  if (typeof value !== "string") return value;
682
734
  const maxSize = options.maxSize ?? INPUT.DEFAULT_MAX_SIZE;
@@ -684,7 +736,8 @@ function sanitizeString(value, options = {}) {
684
736
  throw new InputTooLargeError(maxSize, value.length);
685
737
  }
686
738
  const reject = options.mode === "reject";
687
- let result = value;
739
+ let result = value.normalize("NFKC");
740
+ result = multiDecode(result);
688
741
  if (options.sql !== false) {
689
742
  if (reject) {
690
743
  if (detectSql(result)) {
@@ -1137,7 +1190,9 @@ function encodeForCss(value) {
1137
1190
  var DEFAULTS = {
1138
1191
  maxDepth: 10,
1139
1192
  maxLength: 1e4,
1140
- blockIntrospection: true
1193
+ blockIntrospection: true,
1194
+ maxAliases: 50,
1195
+ blockFragmentCycles: true
1141
1196
  };
1142
1197
  var INTROSPECTION_PATTERN = /\b__(schema|type|typeKind|directive)\b/;
1143
1198
  function computeDepth(query) {
@@ -1154,30 +1209,117 @@ function computeDepth(query) {
1154
1209
  }
1155
1210
  return max;
1156
1211
  }
1212
+ var ALIAS_PATTERN = /\b([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*([a-zA-Z_][a-zA-Z0-9_]*)\b/g;
1213
+ var FRAGMENT_DEF_PATTERN = /\bfragment\s+([a-zA-Z_][a-zA-Z0-9_]*)\s+on\s+[a-zA-Z_][a-zA-Z0-9_]*\s*\{/g;
1214
+ var FRAGMENT_SPREAD_PATTERN = /\.\.\.\s*([a-zA-Z_][a-zA-Z0-9_]*)\b/g;
1215
+ function countAliases(query) {
1216
+ let n = 0;
1217
+ ALIAS_PATTERN.lastIndex = 0;
1218
+ while (ALIAS_PATTERN.exec(query) !== null) n++;
1219
+ return n;
1220
+ }
1221
+ function hasFragmentCycle(query) {
1222
+ const deps = /* @__PURE__ */ new Map();
1223
+ FRAGMENT_DEF_PATTERN.lastIndex = 0;
1224
+ let match;
1225
+ while ((match = FRAGMENT_DEF_PATTERN.exec(query)) !== null) {
1226
+ const name = match[1];
1227
+ const bodyStart = match.index + match[0].length;
1228
+ let depth = 1;
1229
+ let i = bodyStart;
1230
+ while (i < query.length && depth > 0) {
1231
+ const ch = query[i];
1232
+ if (ch === "{") depth++;
1233
+ else if (ch === "}") depth--;
1234
+ i++;
1235
+ }
1236
+ const bodyEnd = depth === 0 ? i - 1 : i;
1237
+ const body = query.slice(bodyStart, bodyEnd);
1238
+ const spreads = /* @__PURE__ */ new Set();
1239
+ FRAGMENT_SPREAD_PATTERN.lastIndex = 0;
1240
+ let sm;
1241
+ while ((sm = FRAGMENT_SPREAD_PATTERN.exec(body)) !== null) {
1242
+ spreads.add(sm[1]);
1243
+ }
1244
+ deps.set(name, spreads);
1245
+ }
1246
+ if (deps.size === 0) return false;
1247
+ const WHITE = 0;
1248
+ const GRAY = 1;
1249
+ const BLACK = 2;
1250
+ const color = /* @__PURE__ */ new Map();
1251
+ for (const name of deps.keys()) color.set(name, WHITE);
1252
+ function visit(name) {
1253
+ if (color.get(name) === GRAY) return true;
1254
+ if (color.get(name) === BLACK) return false;
1255
+ if (!deps.has(name)) return false;
1256
+ color.set(name, GRAY);
1257
+ for (const child of deps.get(name)) {
1258
+ if (visit(child)) return true;
1259
+ }
1260
+ color.set(name, BLACK);
1261
+ return false;
1262
+ }
1263
+ for (const name of deps.keys()) {
1264
+ if (visit(name)) return true;
1265
+ }
1266
+ return false;
1267
+ }
1157
1268
  function inspectGraphqlQuery(query, options = {}) {
1158
1269
  const maxDepth = options.maxDepth ?? DEFAULTS.maxDepth;
1159
1270
  const maxLength = options.maxLength ?? DEFAULTS.maxLength;
1160
1271
  const blockIntrospection = options.blockIntrospection ?? DEFAULTS.blockIntrospection;
1272
+ const maxAliases = options.maxAliases ?? DEFAULTS.maxAliases;
1273
+ const blockFragmentCycles = options.blockFragmentCycles ?? DEFAULTS.blockFragmentCycles;
1161
1274
  const length = query.length;
1162
1275
  const depth = computeDepth(query);
1276
+ const aliases = countAliases(query);
1163
1277
  if (depth > maxDepth) {
1164
- return { blocked: true, reason: "depth", depth, length };
1278
+ return { blocked: true, reason: "depth", depth, length, aliases };
1165
1279
  }
1166
1280
  if (blockIntrospection && INTROSPECTION_PATTERN.test(query)) {
1167
- return { blocked: true, reason: "introspection", depth, length };
1281
+ return { blocked: true, reason: "introspection", depth, length, aliases };
1282
+ }
1283
+ if (aliases > maxAliases) {
1284
+ return { blocked: true, reason: "aliases", depth, length, aliases };
1285
+ }
1286
+ if (blockFragmentCycles && hasFragmentCycle(query)) {
1287
+ return { blocked: true, reason: "fragment_cycle", depth, length, aliases };
1168
1288
  }
1169
1289
  if (length > maxLength) {
1170
- return { blocked: true, reason: "length", depth, length };
1290
+ return { blocked: true, reason: "length", depth, length, aliases };
1171
1291
  }
1172
- return { blocked: false, depth, length };
1292
+ return { blocked: false, depth, length, aliases };
1173
1293
  }
1174
1294
  function detectGraphqlAbuse(query, options) {
1175
1295
  if (typeof query !== "string" || query.length === 0) return false;
1176
1296
  return inspectGraphqlQuery(query, options).blocked;
1177
1297
  }
1178
1298
 
1299
+ // src/sanitizers/deserialization.ts
1300
+ var PICKLE_HEAD = /^\x80[\x02-\x05]/;
1301
+ var RUBY_MARSHAL_HEAD = /^\x04\x08/;
1302
+ var DOTNET_BINFMT_HEAD = /^\x00\x01\x00\x00\x00/;
1303
+ var FASTJSON_AUTOTYPE = /"@type"\s*:\s*"[a-zA-Z_$][\w$.]*"/;
1304
+ var PHP_UNSERIALIZE = /O:\d+:"[a-zA-Z_\\][\w\\]*":\d+:\{/;
1305
+ function detectDeserialization(payload) {
1306
+ if (typeof payload !== "string" || payload.length === 0) {
1307
+ return null;
1308
+ }
1309
+ if (PICKLE_HEAD.test(payload)) return "python_pickle";
1310
+ if (RUBY_MARSHAL_HEAD.test(payload)) return "ruby_marshal";
1311
+ if (DOTNET_BINFMT_HEAD.test(payload)) return "dotnet_binary_formatter";
1312
+ if (FASTJSON_AUTOTYPE.test(payload)) return "java_fastjson";
1313
+ if (PHP_UNSERIALIZE.test(payload)) return "php_unserialize";
1314
+ return null;
1315
+ }
1316
+ function isSerializedPayload(payload) {
1317
+ return detectDeserialization(payload) !== null;
1318
+ }
1319
+
1179
1320
  exports.createSanitizer = createSanitizer;
1180
1321
  exports.detectCommandInjection = detectCommandInjection;
1322
+ exports.detectDeserialization = detectDeserialization;
1181
1323
  exports.detectEmailHeaderInjection = detectEmailHeaderInjection;
1182
1324
  exports.detectGraphqlAbuse = detectGraphqlAbuse;
1183
1325
  exports.detectHeaderInjection = detectHeaderInjection;
@@ -1204,6 +1346,7 @@ exports.inspectGraphqlQuery = inspectGraphqlQuery;
1204
1346
  exports.isDangerousNoSqlKey = isDangerousNoSqlKey;
1205
1347
  exports.isDangerousProtoKey = isDangerousProtoKey;
1206
1348
  exports.isPlainObject = isPlainObject;
1349
+ exports.isSerializedPayload = isSerializedPayload;
1207
1350
  exports.redactObjectPii = redactObjectPii;
1208
1351
  exports.redactPii = redactPii;
1209
1352
  exports.sanitizeCommand = sanitizeCommand;