Skip to content

Commit 0e4c2ed

Browse files
committed
Merge branch 'main' into next
2 parents a392e49 + d74bbbd commit 0e4c2ed

5 files changed

Lines changed: 161 additions & 30 deletions

File tree

CHANGES.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,26 @@ To be released.
232232
[#493]: https://github.com/fedify-dev/fedify/pull/493
233233

234234

235+
Version 1.9.2
236+
-------------
237+
238+
Released on December 20, 2025.
239+
240+
### @fedify/fedify
241+
242+
- Fixed a ReDoS (Regular Expression Denial of Service) vulnerability in
243+
the document loader's HTML parsing. An attacker-controlled server could
244+
respond with a malicious HTML payload that blocked the event loop.
245+
[[CVE-2025-68475]]
246+
247+
### @fedify/sqlite
248+
249+
- Fixed `SyntaxError: Identifier 'Temporal' has already been declared` error
250+
that occurred when using `SqliteKvStore` on Node.js or Bun. The error
251+
was caused by duplicate `Temporal` imports during the build process.
252+
[[#487]]
253+
254+
235255
Version 1.9.1
236256
-------------
237257

@@ -549,6 +569,28 @@ Released on October 14, 2025.
549569
CommonJS-based Node.js applications. [[#429], [#431]]
550570

551571

572+
Version 1.8.15
573+
--------------
574+
575+
Released on December 20, 2025.
576+
577+
### @fedify/fedify
578+
579+
- Fixed a ReDoS (Regular Expression Denial of Service) vulnerability in
580+
the document loader's HTML parsing. An attacker-controlled server could
581+
respond with a malicious HTML payload that blocked the event loop.
582+
[[CVE-2025-68475]]
583+
584+
### @fedify/sqlite
585+
586+
- Fixed `SyntaxError: Identifier 'Temporal' has already been declared` error
587+
that occurred when using `SqliteKvStore` on Node.js or Bun. The error
588+
was caused by duplicate `Temporal` imports during the build process.
589+
[[#487]]
590+
591+
[#487]: https://github.com/fedify-dev/fedify/issues/487
592+
593+
552594
Version 1.8.14
553595
--------------
554596

@@ -984,6 +1026,17 @@ the versioning.
9841026
[iTerm]: https://iterm2.com/
9851027

9861028

1029+
Version 1.7.14
1030+
--------------
1031+
1032+
Released on December 20, 2025.
1033+
1034+
- Fixed a ReDoS (Regular Expression Denial of Service) vulnerability in
1035+
the document loader's HTML parsing. An attacker-controlled server could
1036+
respond with a malicious HTML payload that blocked the event loop.
1037+
[[CVE-2025-68475]]
1038+
1039+
9871040
Version 1.7.13
9881041
--------------
9891042

@@ -1169,6 +1222,19 @@ Released on June 25, 2025.
11691222
[#252]: https://github.com/fedify-dev/fedify/pull/252
11701223

11711224

1225+
Version 1.6.13
1226+
--------------
1227+
1228+
Released on December 20, 2025.
1229+
1230+
- Fixed a ReDoS (Regular Expression Denial of Service) vulnerability in
1231+
the document loader's HTML parsing. An attacker-controlled server could
1232+
respond with a malicious HTML payload that blocked the event loop.
1233+
[[CVE-2025-68475]]
1234+
1235+
[CVE-2025-68475]: https://github.com/fedify-dev/fedify/security/advisories/GHSA-rchf-xwx2-hm93
1236+
1237+
11721238
Version 1.6.12
11731239
--------------
11741240

docs/manual/kv.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,9 +127,28 @@ const federation = createFederation<void>({
127127

128128
:::
129129

130+
> [!TIP]
131+
> If you are using Bun, you may encounter a type error in your IDE because
132+
> TypeScript's language server runs on Node.js and resolves the
133+
> `@fedify/sqlite` module using Node.js [conditional exports] instead of
134+
> Bun's. To fix this, add the following to your *tsconfig.json*:
135+
>
136+
> ~~~~ json
137+
> {
138+
> "compilerOptions": {
139+
> "moduleResolution": "bundler",
140+
> "customConditions": ["bun"]
141+
> }
142+
> }
143+
> ~~~~
144+
>
145+
> This tells TypeScript to use Bun's module resolution when resolving
146+
> [conditional exports].
147+
130148
[`SqliteKvStore`]: https://jsr.io/@fedify/sqlite/doc/kv/~/SqliteKvStore
131149
[`node:sqlite`]: https://nodejs.org/api/sqlite.html
132150
[`bun:sqlite`]: https://bun.com/docs/api/sqlite
151+
[conditional exports]: https://nodejs.org/api/packages.html#conditional-exports
133152
134153
### `DenoKvStore` (Deno only)
135154

packages/sqlite/src/kv.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { PlatformDatabase } from "#sqlite";
2+
import { SqliteKvStore } from "@fedify/sqlite/kv";
23
import * as temporal from "@js-temporal/polyfill";
34
import { delay } from "@std/async/delay";
45
import assert from "node:assert/strict";
56
import { test } from "node:test";
6-
import { SqliteKvStore } from "@fedify/sqlite/kv";
77

88
let Temporal: typeof temporal.Temporal;
99
if ("Temporal" in globalThis) {

packages/vocab-runtime/src/docloader.test.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import fetchMock from "fetch-mock";
2-
import { deepStrictEqual, rejects } from "node:assert";
2+
import { deepStrictEqual, ok, rejects } from "node:assert";
33
import { test } from "node:test";
44
import preloadedContexts from "./contexts.ts";
55
import { getDocumentLoader } from "./docloader.ts";
@@ -361,5 +361,33 @@ test("getDocumentLoader()", async (t) => {
361361
);
362362
});
363363

364+
// Regression test for ReDoS vulnerability (CVE-2025-68475)
365+
// Malicious HTML payload: <a a="b" a="b" ... (unclosed tag)
366+
// With the vulnerable regex, this causes catastrophic backtracking
367+
const maliciousPayload = "<a" + ' a="b"'.repeat(30) + " ";
368+
369+
fetchMock.get("https://example.com/redos", {
370+
body: maliciousPayload,
371+
headers: { "Content-Type": "text/html; charset=utf-8" },
372+
});
373+
374+
await t.test("ReDoS resistance (CVE-2025-68475)", async () => {
375+
const start = performance.now();
376+
// The malicious HTML will fail JSON parsing, but the important thing is
377+
// that it should complete quickly (not hang due to ReDoS)
378+
await rejects(
379+
() => fetchDocumentLoader("https://example.com/redos"),
380+
SyntaxError,
381+
);
382+
const elapsed = performance.now() - start;
383+
384+
// Should complete in under 1 second. With the vulnerable regex,
385+
// this would take 14+ seconds for 30 repetitions.
386+
ok(
387+
elapsed < 1000,
388+
`Potential ReDoS vulnerability detected: ${elapsed}ms (expected < 1000ms)`,
389+
);
390+
});
391+
364392
fetchMock.hardReset();
365393
});

packages/vocab-runtime/src/docloader.ts

Lines changed: 46 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -191,37 +191,55 @@ export async function getRemoteDocument(
191191
contentType === "application/xhtml+xml" ||
192192
contentType?.startsWith("application/xhtml+xml;"))
193193
) {
194-
const p =
195-
/<(a|link)((\s+[a-z][a-z:_-]*=("[^"]*"|'[^']*'|[^\s>]+))+)\s*\/?>/ig;
196-
const p2 = /\s+([a-z][a-z:_-]*)=("([^"]*)"|'([^']*)'|([^\s>]+))/ig;
194+
// Security: Limit HTML response size to mitigate ReDoS attacks
195+
const MAX_HTML_SIZE = 1024 * 1024; // 1MB
197196
const html = await response.text();
198-
let m: RegExpExecArray | null;
199-
const rawAttribs: string[] = [];
200-
while ((m = p.exec(html)) !== null) rawAttribs.push(m[2]);
201-
for (const rawAttrs of rawAttribs) {
202-
let m2: RegExpExecArray | null;
203-
const attribs: Record<string, string> = {};
204-
while ((m2 = p2.exec(rawAttrs)) !== null) {
205-
const key = m2[1].toLowerCase();
206-
const value = m2[3] ?? m2[4] ?? m2[5] ?? "";
207-
attribs[key] = value;
208-
}
209-
if (
210-
attribs.rel === "alternate" && "type" in attribs && (
211-
attribs.type === "application/activity+json" ||
212-
attribs.type === "application/ld+json" ||
213-
attribs.type.startsWith("application/ld+json;")
214-
) && "href" in attribs &&
215-
new URL(attribs.href, docUrl).href !== docUrl.href
216-
) {
217-
logger.debug(
218-
"Found alternate document: {alternateUrl} from {url}",
219-
{ alternateUrl: attribs.href, url: documentUrl },
220-
);
221-
return await fetch(new URL(attribs.href, docUrl).href);
197+
if (html.length > MAX_HTML_SIZE) {
198+
logger.warn(
199+
"HTML response too large, skipping alternate link discovery: {url}",
200+
{ url: documentUrl, size: html.length },
201+
);
202+
document = JSON.parse(html);
203+
} else {
204+
// Safe regex patterns without nested quantifiers to prevent ReDoS
205+
// (CVE-2025-68475)
206+
// Step 1: Extract <a ...> or <link ...> tags
207+
const tagPattern = /<(a|link)\s+([^>]*?)\s*\/?>/gi;
208+
// Step 2: Parse attributes
209+
const attrPattern =
210+
/([a-z][a-z:_-]*)=(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi;
211+
212+
let tagMatch: RegExpExecArray | null;
213+
while ((tagMatch = tagPattern.exec(html)) !== null) {
214+
const tagContent = tagMatch[2];
215+
let attrMatch: RegExpExecArray | null;
216+
const attribs: Record<string, string> = {};
217+
218+
// Reset regex state for attribute parsing
219+
attrPattern.lastIndex = 0;
220+
while ((attrMatch = attrPattern.exec(tagContent)) !== null) {
221+
const key = attrMatch[1].toLowerCase();
222+
const value = attrMatch[2] ?? attrMatch[3] ?? attrMatch[4] ?? "";
223+
attribs[key] = value;
224+
}
225+
226+
if (
227+
attribs.rel === "alternate" && "type" in attribs && (
228+
attribs.type === "application/activity+json" ||
229+
attribs.type === "application/ld+json" ||
230+
attribs.type.startsWith("application/ld+json;")
231+
) && "href" in attribs &&
232+
new URL(attribs.href, docUrl).href !== docUrl.href
233+
) {
234+
logger.debug(
235+
"Found alternate document: {alternateUrl} from {url}",
236+
{ alternateUrl: attribs.href, url: documentUrl },
237+
);
238+
return await fetch(new URL(attribs.href, docUrl).href);
239+
}
222240
}
241+
document = JSON.parse(html);
223242
}
224-
document = JSON.parse(html);
225243
} else {
226244
document = await response.json();
227245
}

0 commit comments

Comments
 (0)