@c9up/transit 0.1.2 → 0.1.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 (47) hide show
  1. package/dist/TransitProvider.d.ts +2 -0
  2. package/dist/TransitProvider.d.ts.map +1 -1
  3. package/dist/TransitProvider.js +16 -2
  4. package/dist/TransitProvider.js.map +1 -1
  5. package/dist/ber.d.ts +9 -0
  6. package/dist/ber.d.ts.map +1 -1
  7. package/dist/ber.js +16 -1
  8. package/dist/ber.js.map +1 -1
  9. package/dist/drivers/SamlDriver.d.ts.map +1 -1
  10. package/dist/drivers/SamlDriver.js +6 -1
  11. package/dist/drivers/SamlDriver.js.map +1 -1
  12. package/dist/ldap.d.ts.map +1 -1
  13. package/dist/ldap.js +13 -1
  14. package/dist/ldap.js.map +1 -1
  15. package/dist/quasar.d.ts +7 -8
  16. package/dist/quasar.d.ts.map +1 -1
  17. package/dist/quasar.js +15 -35
  18. package/dist/quasar.js.map +1 -1
  19. package/dist/services/main.d.ts +10 -1
  20. package/dist/services/main.d.ts.map +1 -1
  21. package/dist/services/main.js +12 -1
  22. package/dist/services/main.js.map +1 -1
  23. package/dist/vendor/nodeEnv.d.ts +5 -0
  24. package/dist/vendor/nodeEnv.d.ts.map +1 -0
  25. package/dist/{nodeEnv.js → vendor/nodeEnv.js} +13 -8
  26. package/dist/vendor/nodeEnv.js.map +1 -0
  27. package/dist/vendor/quasarConnection.d.ts +35 -0
  28. package/dist/vendor/quasarConnection.d.ts.map +1 -0
  29. package/dist/vendor/quasarConnection.js +62 -0
  30. package/dist/vendor/quasarConnection.js.map +1 -0
  31. package/dist/xml.d.ts +20 -1
  32. package/dist/xml.d.ts.map +1 -1
  33. package/dist/xml.js +43 -5
  34. package/dist/xml.js.map +1 -1
  35. package/package.json +9 -9
  36. package/src/TransitProvider.ts +19 -4
  37. package/src/ber.ts +19 -1
  38. package/src/drivers/SamlDriver.ts +6 -1
  39. package/src/ldap.ts +17 -0
  40. package/src/quasar.ts +15 -52
  41. package/src/services/main.ts +13 -1
  42. package/src/{nodeEnv.ts → vendor/nodeEnv.ts} +14 -8
  43. package/src/vendor/quasarConnection.ts +129 -0
  44. package/src/xml.ts +74 -5
  45. package/dist/nodeEnv.d.ts +0 -17
  46. package/dist/nodeEnv.d.ts.map +0 -1
  47. package/dist/nodeEnv.js.map +0 -1
package/src/xml.ts CHANGED
@@ -74,13 +74,48 @@ class XmlError extends Error {
74
74
  }
75
75
  }
76
76
 
77
+ /**
78
+ * What a document is allowed to cost.
79
+ *
80
+ * A SAML response is parsed BEFORE its signature is checked — it has to be,
81
+ * since the signature is inside it — so every one of these bounds applies to
82
+ * bytes an unauthenticated caller chose. Without them a single POST could
83
+ * exhaust the heap, or nest deeply enough to overflow the stack in `#element`,
84
+ * which recurses once per level.
85
+ */
86
+ export interface XmlLimits {
87
+ /** Characters in the document. Default 1 MiB — a large SAML response is tens of KiB. */
88
+ maxLength?: number;
89
+ /** Nesting levels. Default 100; the recursion is one frame per level. */
90
+ maxDepth?: number;
91
+ /** Elements, text runs, comments and instructions, together. Default 50 000. */
92
+ maxNodes?: number;
93
+ /** Attributes on one element. Default 256. */
94
+ maxAttributes?: number;
95
+ }
96
+
97
+ const DEFAULT_LIMITS: Required<XmlLimits> = {
98
+ maxLength: 1024 * 1024,
99
+ maxDepth: 100,
100
+ maxNodes: 50_000,
101
+ maxAttributes: 256,
102
+ };
103
+
77
104
  /** Parse a document and return its root element. */
78
- export function parseXml(source: string): XmlElement {
105
+ export function parseXml(source: string, limits?: XmlLimits): XmlElement {
106
+ const resolved = { ...DEFAULT_LIMITS, ...limits };
107
+ // Checked before the copy below, not after: normalising duplicates the whole
108
+ // string, so a 300 MB body cost 600 MB before anything looked at its size.
109
+ if (source.length > resolved.maxLength) {
110
+ throw new XmlError(
111
+ `the document is ${source.length} characters, over the ${resolved.maxLength} allowed`,
112
+ );
113
+ }
79
114
  // Line endings are normalised before parsing, as the canonicalization
80
115
  // specification requires — otherwise the same document signed on one
81
116
  // platform digests differently on another.
82
117
  const input = source.replace(/\r\n?/g, "\n");
83
- const parser = new Parser(input);
118
+ const parser = new Parser(input, resolved);
84
119
  return parser.document();
85
120
  }
86
121
 
@@ -151,7 +186,22 @@ export function textOf(element: XmlElement): string {
151
186
 
152
187
  class Parser {
153
188
  #at = 0;
154
- constructor(private readonly source: string) {}
189
+ /** Nodes built so far, across the whole document. */
190
+ #nodes = 0;
191
+ constructor(
192
+ private readonly source: string,
193
+ private readonly limits: Required<XmlLimits>,
194
+ ) {}
195
+
196
+ /** Count one node, and refuse a document that keeps producing them. */
197
+ #node(): void {
198
+ this.#nodes += 1;
199
+ if (this.#nodes > this.limits.maxNodes) {
200
+ throw new XmlError(
201
+ `the document has more than ${this.limits.maxNodes} nodes`,
202
+ );
203
+ }
204
+ }
155
205
 
156
206
  document(): XmlElement {
157
207
  let root: XmlElement | undefined;
@@ -184,7 +234,17 @@ class Parser {
184
234
  return root;
185
235
  }
186
236
 
187
- #element(parent: XmlElement | undefined): XmlElement {
237
+ #element(parent: XmlElement | undefined, depth = 0): XmlElement {
238
+ // Checked on the way IN: `#element` recurses once per level, so a
239
+ // document nested deeply enough overflowed the stack before any of this
240
+ // ran — and the stack it overflows is the one holding the signature check
241
+ // that has not happened yet.
242
+ if (depth > this.limits.maxDepth) {
243
+ throw new XmlError(
244
+ `the document nests deeper than ${this.limits.maxDepth}`,
245
+ );
246
+ }
247
+ this.#node();
188
248
  this.#expect("<");
189
249
  const qname = this.#name();
190
250
  const element: XmlElement = {
@@ -210,6 +270,11 @@ class Parser {
210
270
  this.#expect("=");
211
271
  this.#skipSpace();
212
272
  raw.push({ qname: name, value: this.#attributeValue() });
273
+ if (raw.length > this.limits.maxAttributes) {
274
+ throw new XmlError(
275
+ `element <${qname}> carries more than ${this.limits.maxAttributes} attributes`,
276
+ );
277
+ }
213
278
  }
214
279
 
215
280
  for (const { qname: name, value } of raw) {
@@ -289,6 +354,7 @@ class Parser {
289
354
  if (this.#peek("<!--")) {
290
355
  const end = this.source.indexOf("-->", this.#at);
291
356
  if (end === -1) throw new XmlError("a comment is never closed");
357
+ this.#node();
292
358
  element.children.push({
293
359
  type: "comment",
294
360
  value: this.source.slice(this.#at + 4, end),
@@ -299,6 +365,7 @@ class Parser {
299
365
  if (this.#peek("<![CDATA[")) {
300
366
  const end = this.source.indexOf("]]>", this.#at);
301
367
  if (end === -1) throw new XmlError("a CDATA section is never closed");
368
+ this.#node();
302
369
  element.children.push({
303
370
  type: "text",
304
371
  value: this.source.slice(this.#at + 9, end),
@@ -315,6 +382,7 @@ class Parser {
315
382
  throw new XmlError("a processing instruction is never closed");
316
383
  const body = this.source.slice(this.#at + 2, end);
317
384
  const space = body.search(/\s/);
385
+ this.#node();
318
386
  element.children.push({
319
387
  type: "instruction",
320
388
  target: space === -1 ? body : body.slice(0, space),
@@ -324,11 +392,12 @@ class Parser {
324
392
  continue;
325
393
  }
326
394
  if (this.#peek("<")) {
327
- element.children.push(this.#element(element));
395
+ element.children.push(this.#element(element, depth + 1));
328
396
  continue;
329
397
  }
330
398
  const next = this.source.indexOf("<", this.#at);
331
399
  const end = next === -1 ? this.source.length : next;
400
+ this.#node();
332
401
  element.children.push({
333
402
  type: "text",
334
403
  value: resolveEntities(this.source.slice(this.#at, end)),
package/dist/nodeEnv.d.ts DELETED
@@ -1,17 +0,0 @@
1
- /**
2
- * Reading `NODE_ENV`, with the aliases people actually set.
3
- *
4
- * `NODE_ENV=prod` is ordinary in a Dockerfile or a platform dashboard. Read
5
- * verbatim it answers "not production" — and here that means the in-process
6
- * SAML replay store is accepted on a production deployment, where it bounds
7
- * replay within one replica and not beyond it.
8
- *
9
- * Duplicated rather than imported: transit depends on no other package in this
10
- * workspace, and a security decision that only holds when an optional peer is
11
- * installed is not a decision.
12
- */
13
- /** The canonical name for whatever `NODE_ENV` holds. */
14
- export declare function normalizeNodeEnv(value: string | undefined): string;
15
- /** Whether this process is running in production, under any spelling. */
16
- export declare function inProduction(): boolean;
17
- //# sourceMappingURL=nodeEnv.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"nodeEnv.d.ts","sourceRoot":"","sources":["../src/nodeEnv.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAMH,wDAAwD;AACxD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAOlE;AAED,yEAAyE;AACzE,wBAAgB,YAAY,IAAI,OAAO,CAEtC"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"nodeEnv.js","sourceRoot":"","sources":["../src/nodeEnv.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,MAAM,QAAQ,GAAG,CAAC,KAAK,EAAE,SAAS,EAAE,aAAa,CAAC,CAAC;AACnD,MAAM,SAAS,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;AACzC,MAAM,SAAS,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;AAEtC,wDAAwD;AACxD,MAAM,UAAU,gBAAgB,CAAC,KAAyB;IACzD,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAC;IAC1D,MAAM,GAAG,GAAG,KAAK,CAAC,WAAW,EAAE,CAAC;IAChC,IAAI,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,OAAO,aAAa,CAAC;IACjD,IAAI,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,OAAO,YAAY,CAAC;IACjD,IAAI,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,OAAO,MAAM,CAAC;IAC3C,OAAO,GAAG,CAAC;AACZ,CAAC;AAED,yEAAyE;AACzE,MAAM,UAAU,YAAY;IAC3B,OAAO,gBAAgB,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,KAAK,YAAY,CAAC;AAChE,CAAC"}