Loading CHANGES.md +66 −0 Original line number Diff line number Diff line Loading @@ -48,6 +48,26 @@ To be released. [#493]: https://github.com/fedify-dev/fedify/pull/493 Version 1.9.2 ------------- Released on December 20, 2025. ### @fedify/fedify - Fixed a ReDoS (Regular Expression Denial of Service) vulnerability in the document loader's HTML parsing. An attacker-controlled server could respond with a malicious HTML payload that blocked the event loop. [[CVE-2025-68475]] ### @fedify/sqlite - Fixed `SyntaxError: Identifier 'Temporal' has already been declared` error that occurred when using `SqliteKvStore` on Node.js or Bun. The error was caused by duplicate `Temporal` imports during the build process. [[#487]] Version 1.9.1 ------------- Loading Loading @@ -365,6 +385,28 @@ Released on October 14, 2025. CommonJS-based Node.js applications. [[#429], [#431]] Version 1.8.15 -------------- Released on December 20, 2025. ### @fedify/fedify - Fixed a ReDoS (Regular Expression Denial of Service) vulnerability in the document loader's HTML parsing. An attacker-controlled server could respond with a malicious HTML payload that blocked the event loop. [[CVE-2025-68475]] ### @fedify/sqlite - Fixed `SyntaxError: Identifier 'Temporal' has already been declared` error that occurred when using `SqliteKvStore` on Node.js or Bun. The error was caused by duplicate `Temporal` imports during the build process. [[#487]] [#487]: https://github.com/fedify-dev/fedify/issues/487 Version 1.8.14 -------------- Loading Loading @@ -800,6 +842,17 @@ the versioning. [iTerm]: https://iterm2.com/ Version 1.7.14 -------------- Released on December 20, 2025. - Fixed a ReDoS (Regular Expression Denial of Service) vulnerability in the document loader's HTML parsing. An attacker-controlled server could respond with a malicious HTML payload that blocked the event loop. [[CVE-2025-68475]] Version 1.7.13 -------------- Loading Loading @@ -985,6 +1038,19 @@ Released on June 25, 2025. [#252]: https://github.com/fedify-dev/fedify/pull/252 Version 1.6.13 -------------- Released on December 20, 2025. - Fixed a ReDoS (Regular Expression Denial of Service) vulnerability in the document loader's HTML parsing. An attacker-controlled server could respond with a malicious HTML payload that blocked the event loop. [[CVE-2025-68475]] [CVE-2025-68475]: https://github.com/fedify-dev/fedify/security/advisories/GHSA-rchf-xwx2-hm93 Version 1.6.12 -------------- Loading docs/manual/kv.md +19 −0 Original line number Diff line number Diff line Loading @@ -127,9 +127,28 @@ const federation = createFederation<void>({ ::: > [!TIP] > If you are using Bun, you may encounter a type error in your IDE because > TypeScript's language server runs on Node.js and resolves the > `@fedify/sqlite` module using Node.js [conditional exports] instead of > Bun's. To fix this, add the following to your *tsconfig.json*: > > ~~~~ json > { > "compilerOptions": { > "moduleResolution": "bundler", > "customConditions": ["bun"] > } > } > ~~~~ > > This tells TypeScript to use Bun's module resolution when resolving > [conditional exports]. [`SqliteKvStore`]: https://jsr.io/@fedify/sqlite/doc/kv/~/SqliteKvStore [`node:sqlite`]: https://nodejs.org/api/sqlite.html [`bun:sqlite`]: https://bun.com/docs/api/sqlite [conditional exports]: https://nodejs.org/api/packages.html#conditional-exports ### `DenoKvStore` (Deno only) Loading packages/fedify/src/runtime/docloader.test.ts +29 −1 Original line number Diff line number Diff line import { assertEquals, assertRejects, assertThrows } from "@std/assert"; import { assert, assertEquals, assertRejects, assertThrows } from "@std/assert"; import fetchMock from "fetch-mock"; import process from "node:process"; import metadata from "../../deno.json" with { type: "json" }; Loading Loading @@ -366,6 +366,34 @@ test("getDocumentLoader()", async (t) => { ); }); // Regression test for ReDoS vulnerability (CVE-2025-68475) // Malicious HTML payload: <a a="b" a="b" ... (unclosed tag) // With the vulnerable regex, this causes catastrophic backtracking const maliciousPayload = "<a" + ' a="b"'.repeat(30) + " "; fetchMock.get("https://example.com/redos", { body: maliciousPayload, headers: { "Content-Type": "text/html; charset=utf-8" }, }); await t.step("ReDoS resistance (CVE-2025-68475)", async () => { const start = performance.now(); // The malicious HTML will fail JSON parsing, but the important thing is // that it should complete quickly (not hang due to ReDoS) await assertRejects( () => fetchDocumentLoader("https://example.com/redos"), SyntaxError, ); const elapsed = performance.now() - start; // Should complete in under 1 second. With the vulnerable regex, // this would take 14+ seconds for 30 repetitions. assert( elapsed < 1000, `Potential ReDoS vulnerability detected: ${elapsed}ms (expected < 1000ms)`, ); }); fetchMock.hardReset(); }); Loading packages/fedify/src/runtime/docloader.ts +46 −28 Original line number Diff line number Diff line Loading @@ -255,21 +255,38 @@ export async function getRemoteDocument( contentType === "application/xhtml+xml" || contentType?.startsWith("application/xhtml+xml;")) ) { const p = /<(a|link)((\s+[a-z][a-z:_-]*=("[^"]*"|'[^']*'|[^\s>]+))+)\s*\/?>/ig; const p2 = /\s+([a-z][a-z:_-]*)=("([^"]*)"|'([^']*)'|([^\s>]+))/ig; // Security: Limit HTML response size to mitigate ReDoS attacks const MAX_HTML_SIZE = 1024 * 1024; // 1MB const html = await response.text(); let m: RegExpExecArray | null; const rawAttribs: string[] = []; while ((m = p.exec(html)) !== null) rawAttribs.push(m[2]); for (const rawAttrs of rawAttribs) { let m2: RegExpExecArray | null; if (html.length > MAX_HTML_SIZE) { logger.warn( "HTML response too large, skipping alternate link discovery: {url}", { url: documentUrl, size: html.length }, ); document = JSON.parse(html); } else { // Safe regex patterns without nested quantifiers to prevent ReDoS // (CVE-2025-68475) // Step 1: Extract <a ...> or <link ...> tags const tagPattern = /<(a|link)\s+([^>]*?)\s*\/?>/gi; // Step 2: Parse attributes const attrPattern = /([a-z][a-z:_-]*)=(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi; let tagMatch: RegExpExecArray | null; while ((tagMatch = tagPattern.exec(html)) !== null) { const tagContent = tagMatch[2]; let attrMatch: RegExpExecArray | null; const attribs: Record<string, string> = {}; while ((m2 = p2.exec(rawAttrs)) !== null) { const key = m2[1].toLowerCase(); const value = m2[3] ?? m2[4] ?? m2[5] ?? ""; // Reset regex state for attribute parsing attrPattern.lastIndex = 0; while ((attrMatch = attrPattern.exec(tagContent)) !== null) { const key = attrMatch[1].toLowerCase(); const value = attrMatch[2] ?? attrMatch[3] ?? attrMatch[4] ?? ""; attribs[key] = value; } if ( attribs.rel === "alternate" && "type" in attribs && ( attribs.type === "application/activity+json" || Loading @@ -286,6 +303,7 @@ export async function getRemoteDocument( } } document = JSON.parse(html); } } else { document = await response.json(); } Loading packages/sqlite/deno.json +0 −2 Original line number Diff line number Diff line Loading @@ -7,8 +7,6 @@ "./kv": "./src/kv.ts" }, "imports": { "@fedify/sqlite": "./src/mod.ts", "@fedify/sqlite/": "./src/", "#sqlite": "./src/sqlite.node.ts" }, "exclude": [ Loading Loading
CHANGES.md +66 −0 Original line number Diff line number Diff line Loading @@ -48,6 +48,26 @@ To be released. [#493]: https://github.com/fedify-dev/fedify/pull/493 Version 1.9.2 ------------- Released on December 20, 2025. ### @fedify/fedify - Fixed a ReDoS (Regular Expression Denial of Service) vulnerability in the document loader's HTML parsing. An attacker-controlled server could respond with a malicious HTML payload that blocked the event loop. [[CVE-2025-68475]] ### @fedify/sqlite - Fixed `SyntaxError: Identifier 'Temporal' has already been declared` error that occurred when using `SqliteKvStore` on Node.js or Bun. The error was caused by duplicate `Temporal` imports during the build process. [[#487]] Version 1.9.1 ------------- Loading Loading @@ -365,6 +385,28 @@ Released on October 14, 2025. CommonJS-based Node.js applications. [[#429], [#431]] Version 1.8.15 -------------- Released on December 20, 2025. ### @fedify/fedify - Fixed a ReDoS (Regular Expression Denial of Service) vulnerability in the document loader's HTML parsing. An attacker-controlled server could respond with a malicious HTML payload that blocked the event loop. [[CVE-2025-68475]] ### @fedify/sqlite - Fixed `SyntaxError: Identifier 'Temporal' has already been declared` error that occurred when using `SqliteKvStore` on Node.js or Bun. The error was caused by duplicate `Temporal` imports during the build process. [[#487]] [#487]: https://github.com/fedify-dev/fedify/issues/487 Version 1.8.14 -------------- Loading Loading @@ -800,6 +842,17 @@ the versioning. [iTerm]: https://iterm2.com/ Version 1.7.14 -------------- Released on December 20, 2025. - Fixed a ReDoS (Regular Expression Denial of Service) vulnerability in the document loader's HTML parsing. An attacker-controlled server could respond with a malicious HTML payload that blocked the event loop. [[CVE-2025-68475]] Version 1.7.13 -------------- Loading Loading @@ -985,6 +1038,19 @@ Released on June 25, 2025. [#252]: https://github.com/fedify-dev/fedify/pull/252 Version 1.6.13 -------------- Released on December 20, 2025. - Fixed a ReDoS (Regular Expression Denial of Service) vulnerability in the document loader's HTML parsing. An attacker-controlled server could respond with a malicious HTML payload that blocked the event loop. [[CVE-2025-68475]] [CVE-2025-68475]: https://github.com/fedify-dev/fedify/security/advisories/GHSA-rchf-xwx2-hm93 Version 1.6.12 -------------- Loading
docs/manual/kv.md +19 −0 Original line number Diff line number Diff line Loading @@ -127,9 +127,28 @@ const federation = createFederation<void>({ ::: > [!TIP] > If you are using Bun, you may encounter a type error in your IDE because > TypeScript's language server runs on Node.js and resolves the > `@fedify/sqlite` module using Node.js [conditional exports] instead of > Bun's. To fix this, add the following to your *tsconfig.json*: > > ~~~~ json > { > "compilerOptions": { > "moduleResolution": "bundler", > "customConditions": ["bun"] > } > } > ~~~~ > > This tells TypeScript to use Bun's module resolution when resolving > [conditional exports]. [`SqliteKvStore`]: https://jsr.io/@fedify/sqlite/doc/kv/~/SqliteKvStore [`node:sqlite`]: https://nodejs.org/api/sqlite.html [`bun:sqlite`]: https://bun.com/docs/api/sqlite [conditional exports]: https://nodejs.org/api/packages.html#conditional-exports ### `DenoKvStore` (Deno only) Loading
packages/fedify/src/runtime/docloader.test.ts +29 −1 Original line number Diff line number Diff line import { assertEquals, assertRejects, assertThrows } from "@std/assert"; import { assert, assertEquals, assertRejects, assertThrows } from "@std/assert"; import fetchMock from "fetch-mock"; import process from "node:process"; import metadata from "../../deno.json" with { type: "json" }; Loading Loading @@ -366,6 +366,34 @@ test("getDocumentLoader()", async (t) => { ); }); // Regression test for ReDoS vulnerability (CVE-2025-68475) // Malicious HTML payload: <a a="b" a="b" ... (unclosed tag) // With the vulnerable regex, this causes catastrophic backtracking const maliciousPayload = "<a" + ' a="b"'.repeat(30) + " "; fetchMock.get("https://example.com/redos", { body: maliciousPayload, headers: { "Content-Type": "text/html; charset=utf-8" }, }); await t.step("ReDoS resistance (CVE-2025-68475)", async () => { const start = performance.now(); // The malicious HTML will fail JSON parsing, but the important thing is // that it should complete quickly (not hang due to ReDoS) await assertRejects( () => fetchDocumentLoader("https://example.com/redos"), SyntaxError, ); const elapsed = performance.now() - start; // Should complete in under 1 second. With the vulnerable regex, // this would take 14+ seconds for 30 repetitions. assert( elapsed < 1000, `Potential ReDoS vulnerability detected: ${elapsed}ms (expected < 1000ms)`, ); }); fetchMock.hardReset(); }); Loading
packages/fedify/src/runtime/docloader.ts +46 −28 Original line number Diff line number Diff line Loading @@ -255,21 +255,38 @@ export async function getRemoteDocument( contentType === "application/xhtml+xml" || contentType?.startsWith("application/xhtml+xml;")) ) { const p = /<(a|link)((\s+[a-z][a-z:_-]*=("[^"]*"|'[^']*'|[^\s>]+))+)\s*\/?>/ig; const p2 = /\s+([a-z][a-z:_-]*)=("([^"]*)"|'([^']*)'|([^\s>]+))/ig; // Security: Limit HTML response size to mitigate ReDoS attacks const MAX_HTML_SIZE = 1024 * 1024; // 1MB const html = await response.text(); let m: RegExpExecArray | null; const rawAttribs: string[] = []; while ((m = p.exec(html)) !== null) rawAttribs.push(m[2]); for (const rawAttrs of rawAttribs) { let m2: RegExpExecArray | null; if (html.length > MAX_HTML_SIZE) { logger.warn( "HTML response too large, skipping alternate link discovery: {url}", { url: documentUrl, size: html.length }, ); document = JSON.parse(html); } else { // Safe regex patterns without nested quantifiers to prevent ReDoS // (CVE-2025-68475) // Step 1: Extract <a ...> or <link ...> tags const tagPattern = /<(a|link)\s+([^>]*?)\s*\/?>/gi; // Step 2: Parse attributes const attrPattern = /([a-z][a-z:_-]*)=(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi; let tagMatch: RegExpExecArray | null; while ((tagMatch = tagPattern.exec(html)) !== null) { const tagContent = tagMatch[2]; let attrMatch: RegExpExecArray | null; const attribs: Record<string, string> = {}; while ((m2 = p2.exec(rawAttrs)) !== null) { const key = m2[1].toLowerCase(); const value = m2[3] ?? m2[4] ?? m2[5] ?? ""; // Reset regex state for attribute parsing attrPattern.lastIndex = 0; while ((attrMatch = attrPattern.exec(tagContent)) !== null) { const key = attrMatch[1].toLowerCase(); const value = attrMatch[2] ?? attrMatch[3] ?? attrMatch[4] ?? ""; attribs[key] = value; } if ( attribs.rel === "alternate" && "type" in attribs && ( attribs.type === "application/activity+json" || Loading @@ -286,6 +303,7 @@ export async function getRemoteDocument( } } document = JSON.parse(html); } } else { document = await response.json(); } Loading
packages/sqlite/deno.json +0 −2 Original line number Diff line number Diff line Loading @@ -7,8 +7,6 @@ "./kv": "./src/kv.ts" }, "imports": { "@fedify/sqlite": "./src/mod.ts", "@fedify/sqlite/": "./src/", "#sqlite": "./src/sqlite.node.ts" }, "exclude": [ Loading