Skip to content

Commit 386a99f

Browse files
Expose entity decoding limits
1 parent ae464fc commit 386a99f

7 files changed

Lines changed: 158 additions & 29 deletions

File tree

README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,7 @@ The available configuration options are as follows:
222222
| `authType` | `null` | The authentication type to use. If not provided, defaults to trying to detect based upon whether `username` and `password` were provided. |
223223
| `attributeNamePrefix` | `@` | Prefix used to identify attributes on the property object |
224224
| `contactHref` | _[This URL](https://github.com/perry-mitchell/webdav-client/blob/master/LOCK_CONTACT.md)_ | Contact URL used for LOCKs. |
225+
| `entityDecoder` | _None_ | Entity decoder configuration for XML parsing. See [entity decoder](#entity-decoder). |
225226
| `headers` | `{}` | Additional headers provided to all requests. Headers provided here are overridden by method-specific headers, including `Authorization`. |
226227
| `httpAgent` | _None_ | HTTP agent instance. Available only in Node. See [http.Agent](https://nodejs.org/api/http.html#http_class_http_agent). |
227228
| `httpsAgent` | _None_ | HTTPS agent instance. Available only in Node. See [https.Agent](https://nodejs.org/api/https.html#https_class_https_agent). |
@@ -230,6 +231,29 @@ The available configuration options are as follows:
230231
| `username` | _None_ | Username for authentication. |
231232
| `withCredentials` | _None_ | Credentials inclusion setting for the request, |
232233

234+
### Entity decoder
235+
236+
The `entityDecoder` option controls how XML entity references are decoded during XML parsing. When not set, entity expansion limits are unlimited (no restrictions).
237+
238+
```typescript
239+
const client = createClient("https://some-server.org", {
240+
username: "user",
241+
password: "pass",
242+
entityDecoder: {
243+
limit: {
244+
maxTotalExpansions: 1000,
245+
maxExpandedLength: 50000
246+
}
247+
}
248+
});
249+
```
250+
251+
| Property | Default | Description |
252+
|-----------------|----------|-------------------------------------------------------|
253+
| `limit` | _None_ | Security limits for entity expansion. |
254+
| `limit.maxTotalExpansions` | `0` | Maximum number of entity references expanded per document. `0` means unlimited. |
255+
| `limit.maxExpandedLength` | `0` | Maximum number of characters added by entity expansion per document. `0` means unlimited. |
256+
233257
### Client methods
234258

235259
The `WebDAVClient` interface type contains all the methods and signatures for the WebDAV client instance.

package-lock.json

Lines changed: 23 additions & 23 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@
7171
"base-64": "^1.0.0",
7272
"byte-length": "^1.0.2",
7373
"entities": "^6.0.1",
74-
"fast-xml-parser": "^5.6.0",
74+
"fast-xml-parser": "^5.7.2",
7575
"hot-patcher": "^2.0.1",
7676
"layerr": "^3.0.0",
7777
"md5": "^2.3.0",

source/factory.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ export function createClient(remoteURL: string, options: WebDAVClientOptions = {
5252
authType: authTypeRaw = null,
5353
remoteBasePath,
5454
contactHref = DEFAULT_CONTACT_HREF,
55+
entityDecoder,
5556
ha1,
5657
headers = {},
5758
httpAgent,
@@ -77,6 +78,7 @@ export function createClient(remoteURL: string, options: WebDAVClientOptions = {
7778
parsing: {
7879
attributeNamePrefix: options.attributeNamePrefix ?? "@",
7980
attributeParsers: [],
81+
entityDecoder,
8082
tagParsers: [displaynameTagParser]
8183
},
8284
remotePath: extractURLPath(remoteURL),

source/tools/dav.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import path from "path-posix";
22
import { XMLParser } from "fast-xml-parser";
3+
// @ts-expect-error Types declare default export but runtime provides named export
4+
import { EntityDecoder } from "@nodable/entities";
35
import nestedProp from "nested-property";
46
import { encodePath, normalisePath } from "./path.js";
57
import type {
@@ -33,9 +35,10 @@ function toJPathString(
3335
function getParser({
3436
attributeNamePrefix,
3537
attributeParsers,
38+
entityDecoder: entityDecoderOptions,
3639
tagParsers
3740
}: WebDAVParsingContext): XMLParser {
38-
return new XMLParser({
41+
const parserOptions: Record<string, unknown> = {
3942
allowBooleanAttributes: true,
4043
attributeNamePrefix,
4144
textNodeName: "text",
@@ -55,7 +58,7 @@ function getParser({
5558
return value;
5659
}
5760
} catch (error) {
58-
// skipping this invalid parser
61+
// skipping this invalid processor
5962
}
6063
}
6164
return attrValue;
@@ -69,12 +72,21 @@ function getParser({
6972
return value;
7073
}
7174
} catch (error) {
72-
// skipping this invalid parser
75+
// skipping this invalid processor
7376
}
7477
}
7578
return tagValue;
7679
}
77-
});
80+
};
81+
if (entityDecoderOptions) {
82+
parserOptions.entityDecoder = new EntityDecoder({
83+
limit: {
84+
maxTotalExpansions: entityDecoderOptions.limit?.maxTotalExpansions ?? 0,
85+
maxExpandedLength: entityDecoderOptions.limit?.maxExpandedLength ?? 0
86+
}
87+
});
88+
}
89+
return new XMLParser(parserOptions);
7890
}
7991

8092
/**

source/types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,7 @@ export interface WebDAVClientOptions {
360360
authType?: AuthType;
361361
remoteBasePath?: string;
362362
contactHref?: string;
363+
entityDecoder?: WebDAVEntityDecoderOptions;
363364
ha1?: string;
364365
headers?: Headers;
365366
httpAgent?: any;
@@ -397,8 +398,16 @@ export type WebDAVAttributeParser = (
397398
*/
398399
export type WebDAVTagParser = (jPath: string, tagValue: string) => string | unknown | undefined;
399400

401+
export interface WebDAVEntityDecoderOptions {
402+
limit?: {
403+
maxTotalExpansions?: number;
404+
maxExpandedLength?: number;
405+
};
406+
}
407+
400408
export interface WebDAVParsingContext {
401409
attributeNamePrefix?: string;
402410
attributeParsers: WebDAVAttributeParser[];
411+
entityDecoder?: WebDAVEntityDecoderOptions;
403412
tagParsers: WebDAVTagParser[];
404413
}

test/node/tools/dav.spec.ts

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, expect, it } from "vitest";
22
import { readFile } from "fs/promises";
3-
import { parseXML } from "../../../source/index.js";
3+
import { parseXML, type WebDAVEntityDecoderOptions } from "../../../source/index.js";
44

55
describe("parseXML", function () {
66
it("keeps numeric-looking displaynames", async function () {
@@ -149,4 +149,86 @@ describe("parseXML", function () {
149149
}
150150
]);
151151
});
152+
153+
describe("entityDecoder", function () {
154+
it("parses XML with entities when entityDecoder is not set", async function () {
155+
const xml = `<?xml version="1.0"?>
156+
<d:multistatus xmlns:d="DAV:">
157+
<d:response>
158+
<d:href>/file.txt</d:href>
159+
<d:propstat>
160+
<d:prop>
161+
<displayname>A &amp; B &lt; C</displayname>
162+
</d:prop>
163+
<d:status>HTTP/1.1 200 OK</d:status>
164+
</d:propstat>
165+
</d:response>
166+
</d:multistatus>`;
167+
168+
const parsed = await parseXML(xml);
169+
expect(parsed.multistatus.response).to.have.length(1);
170+
expect(parsed.multistatus.response[0].propstat.prop.displayname).to.equal("A & B < C");
171+
});
172+
173+
it("parses XML with entities when entityDecoder limit is set", async function () {
174+
const decoderOptions: WebDAVEntityDecoderOptions = {
175+
limit: {
176+
maxTotalExpansions: 0,
177+
maxExpandedLength: 0
178+
}
179+
};
180+
181+
const xml = `<?xml version="1.0"?>
182+
<d:multistatus xmlns:d="DAV:">
183+
<d:response>
184+
<d:href>/file.txt</d:href>
185+
<d:propstat>
186+
<d:prop>
187+
<displayname>A &amp; B &lt; C</displayname>
188+
</d:prop>
189+
<d:status>HTTP/1.1 200 OK</d:status>
190+
</d:propstat>
191+
</d:response>
192+
</d:multistatus>`;
193+
194+
const parsed = await parseXML(xml, {
195+
attributeNamePrefix: "@",
196+
attributeParsers: [],
197+
entityDecoder: decoderOptions,
198+
tagParsers: []
199+
});
200+
expect(parsed.multistatus.response).to.have.length(1);
201+
expect(parsed.multistatus.response[0].propstat.prop.displayname).to.equal("A & B < C");
202+
});
203+
204+
it("applies maxTotalExpansions limit when set", async function () {
205+
const decoderOptions: WebDAVEntityDecoderOptions = {
206+
limit: {
207+
maxTotalExpansions: 1
208+
}
209+
};
210+
211+
const xml = `<?xml version="1.0"?>
212+
<d:multistatus xmlns:d="DAV:">
213+
<d:response>
214+
<d:href>/file.txt</d:href>
215+
<d:propstat>
216+
<d:prop>
217+
<displayname>A &amp; B</displayname>
218+
</d:prop>
219+
<d:status>HTTP/1.1 200 OK</d:status>
220+
</d:propstat>
221+
</d:response>
222+
</d:multistatus>`;
223+
224+
const parsed = await parseXML(xml, {
225+
attributeNamePrefix: "@",
226+
attributeParsers: [],
227+
entityDecoder: decoderOptions,
228+
tagParsers: []
229+
});
230+
expect(parsed.multistatus.response).to.have.length(1);
231+
expect(parsed.multistatus.response[0].propstat.prop.displayname).to.equal("A & B");
232+
});
233+
});
152234
});

0 commit comments

Comments
 (0)