@poe-platform/safe-fs 0.1.73 → 0.1.74

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.
@@ -1,7 +1,7 @@
1
1
  import { FsError, isErrnoCode, isFsError } from "../../contracts/errors.js";
2
2
  import { composeAbortSignals } from "../../contracts/abort.js";
3
3
  import { readBytes } from "../../contracts/io.js";
4
- import { davChild, davChildren, parseXml, scalar } from "./xml.js";
4
+ import { davChild, davChildren, parseXml, scalar, XmlResponseLimitError } from "./xml.js";
5
5
  import { admitDirectoryEntries, directoryEntryLimit } from "../directory-admission.js";
6
6
  import { assertCallbackAuthorityAllowed, compareEntries, registerEntryAuthority } from "../mount/comparison.js";
7
7
  import { compareWebDavResources, ownedResponseIdentifier, recordOwnedResourceStat, registerResourceQuery, resourceIdentifier } from "./resource-id.js";
@@ -474,20 +474,26 @@ export class WebDavFileSystem {
474
474
  reader.releaseLock();
475
475
  }
476
476
  }
477
- async xml(response, signal) {
477
+ async xml(response, signal, maxResponses) {
478
478
  const data = await this.bytes(response, this.maxXmlBytes, signal);
479
479
  const encoding = (data[0] === 0xff && data[1] === 0xfe) || (data[0] === 0x3c && data[1] === 0)
480
480
  ? "utf-16le" : (data[0] === 0xfe && data[1] === 0xff) || (data[0] === 0 && data[1] === 0x3c) ? "utf-16be" : "utf-8";
481
- return parseXml(new TextDecoder(encoding, { fatal: true }).decode(data), {
482
- maxNodes: this.maxXmlBytes, maxAttributes: this.maxXmlBytes,
483
- });
481
+ return parseXml(new TextDecoder(encoding, { fatal: true }).decode(data), maxResponses === undefined ? undefined : { maxResponses });
484
482
  }
485
483
  async multistatus(response, signal, method = "PROPFIND", path = "") {
486
484
  const link = response.headers.get("link");
487
485
  if (link && /\brel\s*=\s*(?:"[^"]*\bnext\b[^"]*"|'[^']*\bnext\b[^']*'|next\b)/i.test(link)) {
488
486
  fail("ENOTSUP", method, path, "paginated WebDAV responses are unsupported");
489
487
  }
490
- const root = await this.xml(response, signal);
488
+ let root;
489
+ try {
490
+ root = await this.xml(response, signal, this.maxEntries);
491
+ }
492
+ catch (error) {
493
+ if (error instanceof XmlResponseLimitError)
494
+ fail("EFBIG", method, path, "response exceeds entry limit");
495
+ throw error;
496
+ }
491
497
  if (root.namespace !== "DAV:" || root.localName !== "multistatus")
492
498
  throw new Error("expected DAV:multistatus");
493
499
  const responses = davChildren(root, "response");
@@ -8,6 +8,9 @@ export interface XmlLimits {
8
8
  readonly maxDepth?: number;
9
9
  readonly maxNodes?: number;
10
10
  readonly maxAttributes?: number;
11
+ readonly maxResponses?: number;
12
+ }
13
+ export declare class XmlResponseLimitError extends SyntaxError {
11
14
  }
12
15
  export declare function parseXml(input: string, limits?: XmlLimits): XmlElement;
13
16
  export declare function davChildren(element: XmlElement, localName: string): XmlElement[];
@@ -1,3 +1,5 @@
1
+ export class XmlResponseLimitError extends SyntaxError {
2
+ }
1
3
  const xmlNamespace = "http://www.w3.org/XML/1998/namespace";
2
4
  const xmlnsNamespace = "http://www.w3.org/2000/xmlns/";
3
5
  function invalid(message) {
@@ -66,7 +68,7 @@ export function parseXml(input, limits = {}) {
66
68
  const maxDepth = limits.maxDepth ?? 64;
67
69
  const maxNodes = limits.maxNodes ?? 100_000;
68
70
  const maxAttributes = limits.maxAttributes ?? 10_000;
69
- for (const limit of [maxDepth, maxNodes, maxAttributes]) {
71
+ for (const limit of [maxDepth, maxNodes, maxAttributes, ...(limits.maxResponses === undefined ? [] : [limits.maxResponses])]) {
70
72
  if (!Number.isSafeInteger(limit) || limit < 1)
71
73
  throw new RangeError("XML limits must be positive integers");
72
74
  }
@@ -80,6 +82,7 @@ export function parseXml(input, limits = {}) {
80
82
  let offset = 0;
81
83
  let nodes = 0;
82
84
  let attributeCount = 0;
85
+ let responses = 0;
83
86
  const whitespace = () => {
84
87
  while (offset < source.length && " \t\n\r".includes(source[offset]))
85
88
  offset++;
@@ -211,9 +214,16 @@ export function parseXml(input, limits = {}) {
211
214
  const [prefix, localName] = qualifiedName(name);
212
215
  if (prefix === "xmlns" || (prefix && !namespaces.has(prefix)))
213
216
  invalid("unbound element prefix");
214
- const element = { namespace: namespaces.get(prefix) ?? "", localName, children: [], text: "" };
215
217
  if (++nodes > maxNodes || stack.length + 1 > maxDepth)
216
218
  invalid("XML resource limit exceeded");
219
+ const namespace = namespaces.get(prefix) ?? "";
220
+ if (limits.maxResponses !== undefined && stack.length === 1
221
+ && root?.namespace === "DAV:" && root.localName === "multistatus"
222
+ && namespace === "DAV:" && localName === "response"
223
+ && ++responses > limits.maxResponses) {
224
+ throw new XmlResponseLimitError("WebDAV XML response limit exceeded");
225
+ }
226
+ const element = { namespace, localName, children: [], text: "" };
217
227
  const parent = stack.at(-1);
218
228
  if (parent)
219
229
  parent.element.children.push(element);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poe-platform/safe-fs",
3
- "version": "0.1.73",
3
+ "version": "0.1.74",
4
4
  "description": "Composable filesystem with a portable core and explicit Node adapters",
5
5
  "type": "module",
6
6
  "license": "MIT",