@fedify/vocab-runtime 2.3.7 → 2.3.8
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.
- package/deno.json +1 -1
- package/dist/mod.cjs +166 -101
- package/dist/mod.js +166 -101
- package/dist/tests/body-B82RILk5.mjs +77 -0
- package/dist/tests/body-CKFs5kO9.cjs +95 -0
- package/dist/tests/body.test.cjs +90 -0
- package/dist/tests/body.test.d.cts +1 -0
- package/dist/tests/body.test.d.mts +1 -0
- package/dist/tests/body.test.mjs +90 -0
- package/dist/tests/decimal.test.cjs +6 -6
- package/dist/tests/decimal.test.mjs +6 -6
- package/dist/tests/{docloader-DI-3ZVtd.mjs → docloader-BetS1A0I.mjs} +26 -34
- package/dist/tests/{docloader-hPeotBri.cjs → docloader-DfLdNMgM.cjs} +31 -33
- package/dist/tests/docloader.test.cjs +83 -4
- package/dist/tests/docloader.test.mjs +83 -4
- package/dist/tests/internal/multicodec.test.cjs +2 -2
- package/dist/tests/internal/multicodec.test.mjs +2 -2
- package/dist/tests/{key-pMmqUKuo.cjs → key-CWDxavIR.cjs} +2 -2
- package/dist/tests/{key-CrrK9mYh.mjs → key-Qj7u1qmH.mjs} +2 -2
- package/dist/tests/key.test.cjs +3 -3
- package/dist/tests/key.test.mjs +3 -3
- package/dist/tests/langstr.test.cjs +2 -2
- package/dist/tests/langstr.test.mjs +2 -2
- package/dist/tests/link.test.cjs +2 -2
- package/dist/tests/link.test.mjs +2 -2
- package/dist/tests/multibase/multibase.test.cjs +2 -2
- package/dist/tests/multibase/multibase.test.mjs +2 -2
- package/dist/tests/{request-BMbeYb8u.mjs → request-Borhn2tu.mjs} +1 -1
- package/dist/tests/{request-Cj1FlU8G.cjs → request-FmFI0z5J.cjs} +1 -1
- package/dist/tests/request.test.cjs +2 -2
- package/dist/tests/request.test.mjs +2 -2
- package/dist/tests/temporal.test.cjs +1 -1
- package/dist/tests/temporal.test.mjs +1 -1
- package/dist/tests/url.test.cjs +2 -2
- package/dist/tests/url.test.mjs +2 -2
- package/package.json +1 -1
- package/src/body.test.ts +125 -0
- package/src/body.ts +95 -0
- package/src/docloader.test.ts +124 -1
- package/src/docloader.ts +41 -43
- /package/dist/tests/{langstr-CbAxaeEZ.cjs → langstr-BOQHfJDr.cjs} +0 -0
- /package/dist/tests/{langstr-Di5AvKpB.mjs → langstr-DDLc4833.mjs} +0 -0
- /package/dist/tests/{link-NUUWCdnK.mjs → link-CYszIegt.mjs} +0 -0
- /package/dist/tests/{link-FguCydMA.cjs → link-qDTmjPw_.cjs} +0 -0
- /package/dist/tests/{multibase-Bz_UUDtL.cjs → multibase-BL45eko3.cjs} +0 -0
- /package/dist/tests/{multibase-B4bvakyA.mjs → multibase-Da9VJCw8.mjs} +0 -0
- /package/dist/tests/{multicodec-CyFp54fI.mjs → multicodec-BBPZjRfg.mjs} +0 -0
- /package/dist/tests/{multicodec-CxGVGa91.cjs → multicodec-ByS9XLsw.cjs} +0 -0
- /package/dist/tests/{url-o0e-aXQY.mjs → url-BBfduh6o.mjs} +0 -0
- /package/dist/tests/{url-BF_Nb_Cu.cjs → url-BbEC9H_R.cjs} +0 -0
package/src/docloader.test.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import fetchMock from "fetch-mock";
|
|
2
2
|
import { deepStrictEqual, ok, rejects } from "node:assert";
|
|
3
3
|
import { test } from "node:test";
|
|
4
|
+
import { createServer } from "node:http";
|
|
5
|
+
import { gzipSync } from "node:zlib";
|
|
4
6
|
import preloadedContexts from "./contexts.ts";
|
|
5
7
|
import cidV1Context from "./contexts/cid-v1.json" with { type: "json" };
|
|
6
|
-
import { getDocumentLoader } from "./docloader.ts";
|
|
8
|
+
import { getDocumentLoader, getRemoteDocument } from "./docloader.ts";
|
|
7
9
|
import { FetchError } from "./request.ts";
|
|
8
10
|
import { UrlError } from "./url.ts";
|
|
9
11
|
|
|
@@ -518,3 +520,124 @@ test("getDocumentLoader()", async (t) => {
|
|
|
518
520
|
|
|
519
521
|
fetchMock.hardReset();
|
|
520
522
|
});
|
|
523
|
+
|
|
524
|
+
test("getDocumentLoader() bounds JSON, HTML, and alternate documents", async () => {
|
|
525
|
+
const url = "https://example.com/bounded";
|
|
526
|
+
fetchMock.mockGlobal();
|
|
527
|
+
let oversized = true;
|
|
528
|
+
try {
|
|
529
|
+
fetchMock.get(url, () =>
|
|
530
|
+
new Response('{"name":"hello"}', {
|
|
531
|
+
headers: {
|
|
532
|
+
"Content-Type": "application/activity+json",
|
|
533
|
+
...(oversized
|
|
534
|
+
? { "Content-Length": String(16 * 1024 * 1024 + 1) }
|
|
535
|
+
: {}),
|
|
536
|
+
},
|
|
537
|
+
}));
|
|
538
|
+
const loader = getDocumentLoader({
|
|
539
|
+
allowPrivateAddress: true,
|
|
540
|
+
});
|
|
541
|
+
await rejects(loader(url), FetchError);
|
|
542
|
+
oversized = false;
|
|
543
|
+
deepStrictEqual((await loader(url)).document, { name: "hello" });
|
|
544
|
+
oversized = true;
|
|
545
|
+
fetchMock.get(
|
|
546
|
+
`${url}/html`,
|
|
547
|
+
() =>
|
|
548
|
+
new Response(" ".repeat(1024 * 1024 + 1), {
|
|
549
|
+
headers: { "Content-Type": "text/html" },
|
|
550
|
+
}),
|
|
551
|
+
);
|
|
552
|
+
await rejects(
|
|
553
|
+
getDocumentLoader({ allowPrivateAddress: true })(`${url}/html`),
|
|
554
|
+
FetchError,
|
|
555
|
+
);
|
|
556
|
+
fetchMock.get(`${url}/alternate`, () =>
|
|
557
|
+
new Response("", {
|
|
558
|
+
headers: {
|
|
559
|
+
"Content-Type": "text/html",
|
|
560
|
+
Link: `<${url}>; rel="alternate"; type="application/activity+json"`,
|
|
561
|
+
},
|
|
562
|
+
}));
|
|
563
|
+
await rejects(loader(`${url}/alternate`), FetchError);
|
|
564
|
+
fetchMock.get(`${url}/redirect`, {
|
|
565
|
+
status: 302,
|
|
566
|
+
headers: { Location: url },
|
|
567
|
+
});
|
|
568
|
+
await rejects(loader(`${url}/redirect`), FetchError);
|
|
569
|
+
} finally {
|
|
570
|
+
fetchMock.hardReset();
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
test("getDocumentLoader() rejects oversized gzip responses from fetch", async () => {
|
|
575
|
+
const document = { name: "a".repeat(16 * 1024 * 1024) };
|
|
576
|
+
const compressed = gzipSync(JSON.stringify(document));
|
|
577
|
+
ok(compressed.byteLength < 64 * 1024);
|
|
578
|
+
const server = createServer((request, response) => {
|
|
579
|
+
const body = request.url === "/small"
|
|
580
|
+
? gzipSync('{"name":"hello"}')
|
|
581
|
+
: compressed;
|
|
582
|
+
response.writeHead(200, {
|
|
583
|
+
"Content-Type": "application/activity+json",
|
|
584
|
+
"Content-Encoding": "gzip",
|
|
585
|
+
"Content-Length": body.byteLength,
|
|
586
|
+
});
|
|
587
|
+
response.end(body);
|
|
588
|
+
});
|
|
589
|
+
await new Promise<void>((resolve, reject) => {
|
|
590
|
+
server.once("error", reject);
|
|
591
|
+
server.listen(0, "127.0.0.1", resolve);
|
|
592
|
+
});
|
|
593
|
+
try {
|
|
594
|
+
const address = server.address();
|
|
595
|
+
ok(address != null && typeof address !== "string");
|
|
596
|
+
const url = `http://127.0.0.1:${address.port}/document`;
|
|
597
|
+
await rejects(
|
|
598
|
+
getDocumentLoader({
|
|
599
|
+
allowPrivateAddress: true,
|
|
600
|
+
})(url),
|
|
601
|
+
FetchError,
|
|
602
|
+
);
|
|
603
|
+
deepStrictEqual(
|
|
604
|
+
(await getDocumentLoader({
|
|
605
|
+
allowPrivateAddress: true,
|
|
606
|
+
})(new URL("/small", url).href)).document,
|
|
607
|
+
{ name: "hello" },
|
|
608
|
+
);
|
|
609
|
+
} finally {
|
|
610
|
+
await new Promise<void>((resolve, reject) => {
|
|
611
|
+
server.close((error) => error == null ? resolve() : reject(error));
|
|
612
|
+
server.closeAllConnections();
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
test("getRemoteDocument() bounds JSON by default", async () => {
|
|
618
|
+
let canceled = false;
|
|
619
|
+
let pulls = 0;
|
|
620
|
+
const response = new Response(
|
|
621
|
+
new ReadableStream({
|
|
622
|
+
pull(controller) {
|
|
623
|
+
pulls++;
|
|
624
|
+
controller.enqueue(new Uint8Array(64 * 1024).fill(32));
|
|
625
|
+
},
|
|
626
|
+
cancel() {
|
|
627
|
+
canceled = true;
|
|
628
|
+
},
|
|
629
|
+
}, { highWaterMark: 0 }),
|
|
630
|
+
);
|
|
631
|
+
await rejects(
|
|
632
|
+
getRemoteDocument(
|
|
633
|
+
"https://example.com/document",
|
|
634
|
+
response,
|
|
635
|
+
() => {
|
|
636
|
+
throw new Error("Unexpected alternate document");
|
|
637
|
+
},
|
|
638
|
+
),
|
|
639
|
+
FetchError,
|
|
640
|
+
);
|
|
641
|
+
deepStrictEqual(canceled, true);
|
|
642
|
+
deepStrictEqual(pulls, 257);
|
|
643
|
+
});
|
package/src/docloader.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { getLogger } from "@logtape/logtape";
|
|
2
2
|
import { SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
|
|
3
3
|
import metadata from "../deno.json" with { type: "json" };
|
|
4
|
+
import { MAX_BODY_SIZE, readBoundedText } from "./body.ts";
|
|
4
5
|
import preloadedContexts from "./contexts.ts";
|
|
5
6
|
import { HttpHeaderLink } from "./link.ts";
|
|
6
7
|
import {
|
|
@@ -202,55 +203,52 @@ export async function getRemoteDocument(
|
|
|
202
203
|
) {
|
|
203
204
|
// Security: Limit HTML response size to mitigate ReDoS attacks
|
|
204
205
|
const MAX_HTML_SIZE = 1024 * 1024; // 1MB
|
|
205
|
-
const html = await
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
const tagPattern = /<(a|link)\s+([^>]*?)\s*\/?>/gi;
|
|
217
|
-
// Step 2: Parse attributes
|
|
218
|
-
const attrPattern =
|
|
219
|
-
/([a-z][a-z:_-]*)=(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi;
|
|
206
|
+
const html = await readBoundedText(
|
|
207
|
+
response,
|
|
208
|
+
MAX_HTML_SIZE,
|
|
209
|
+
documentUrl,
|
|
210
|
+
);
|
|
211
|
+
// Safe regex patterns without nested quantifiers to prevent ReDoS
|
|
212
|
+
// (CVE-2025-68475)
|
|
213
|
+
// Step 1: Extract <a ...> or <link ...> tags
|
|
214
|
+
const tagPattern = /<(a|link)\s+([^>]*?)\s*\/?>/gi;
|
|
215
|
+
// Step 2: Parse attributes
|
|
216
|
+
const attrPattern = /([a-z][a-z:_-]*)=(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi;
|
|
220
217
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
218
|
+
let tagMatch: RegExpExecArray | null;
|
|
219
|
+
while ((tagMatch = tagPattern.exec(html)) !== null) {
|
|
220
|
+
const tagContent = tagMatch[2];
|
|
221
|
+
let attrMatch: RegExpExecArray | null;
|
|
222
|
+
const attribs: Record<string, string> = {};
|
|
226
223
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
224
|
+
// Reset regex state for attribute parsing
|
|
225
|
+
attrPattern.lastIndex = 0;
|
|
226
|
+
while ((attrMatch = attrPattern.exec(tagContent)) !== null) {
|
|
227
|
+
const key = attrMatch[1].toLowerCase();
|
|
228
|
+
const value = attrMatch[2] ?? attrMatch[3] ?? attrMatch[4] ?? "";
|
|
229
|
+
attribs[key] = value;
|
|
230
|
+
}
|
|
234
231
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
}
|
|
232
|
+
if (
|
|
233
|
+
attribs.rel === "alternate" && "type" in attribs && (
|
|
234
|
+
attribs.type === "application/activity+json" ||
|
|
235
|
+
attribs.type === "application/ld+json" ||
|
|
236
|
+
attribs.type.startsWith("application/ld+json;")
|
|
237
|
+
) && "href" in attribs &&
|
|
238
|
+
new URL(attribs.href, docUrl).href !== docUrl.href
|
|
239
|
+
) {
|
|
240
|
+
logger.debug(
|
|
241
|
+
"Found alternate document: {alternateUrl} from {url}",
|
|
242
|
+
{ alternateUrl: attribs.href, url: documentUrl },
|
|
243
|
+
);
|
|
244
|
+
return await fetch(new URL(attribs.href, docUrl).href);
|
|
249
245
|
}
|
|
250
|
-
document = JSON.parse(html);
|
|
251
246
|
}
|
|
247
|
+
document = JSON.parse(html);
|
|
252
248
|
} else {
|
|
253
|
-
document =
|
|
249
|
+
document = JSON.parse(
|
|
250
|
+
await readBoundedText(response, MAX_BODY_SIZE, documentUrl),
|
|
251
|
+
);
|
|
254
252
|
}
|
|
255
253
|
logger.debug(
|
|
256
254
|
"Fetched document: {status} {url} {headers}",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|