Loading .github/workflows/build.yaml +18 −10 Original line number Diff line number Diff line Loading @@ -57,7 +57,7 @@ jobs: ref: ${{ github.event.pull_request.head.sha }} - uses: denoland/setup-deno@v2 with: deno-version: 2.5.3 # Keep in sync with mise.toml deno-version: 2.5.6 # Keep in sync with mise.toml - run: deno task test --coverage=.cov --junit-path=.test-report.xml env: RUST_BACKTRACE: ${{ runner.debug }} Loading Loading @@ -128,7 +128,7 @@ jobs: ref: ${{ github.event.pull_request.head.sha }} - uses: denoland/setup-deno@v2 with: deno-version: 2.5.3 # Keep in sync with mise.toml deno-version: 2.5.6 # Keep in sync with mise.toml - uses: pnpm/action-setup@v4 with: version: 10 Loading Loading @@ -185,7 +185,7 @@ jobs: ref: ${{ github.event.pull_request.head.sha }} - uses: denoland/setup-deno@v2 with: deno-version: 2.5.3 # Keep in sync with mise.toml deno-version: 2.5.6 # Keep in sync with mise.toml - uses: pnpm/action-setup@v4 with: version: 10 Loading @@ -211,7 +211,7 @@ jobs: ref: ${{ github.event.pull_request.head.sha }} - uses: denoland/setup-deno@v2 with: deno-version: 2.5.3 # Keep in sync with mise.toml deno-version: 2.5.6 # Keep in sync with mise.toml - uses: pnpm/action-setup@v4 with: version: 10 Loading @@ -234,7 +234,7 @@ jobs: ref: ${{ github.event.pull_request.head.sha }} - uses: denoland/setup-deno@v2 with: deno-version: 2.5.3 # Keep in sync with mise.toml deno-version: 2.5.6 # Keep in sync with mise.toml - run: deno task hooks:pre-commit release-test: Loading @@ -252,7 +252,7 @@ jobs: ref: ${{ github.event.pull_request.head.sha }} - uses: denoland/setup-deno@v2 with: deno-version: 2.5.3 # Keep in sync with mise.toml deno-version: 2.5.6 # Keep in sync with mise.toml - uses: pnpm/action-setup@v4 with: version: 10 Loading Loading @@ -287,7 +287,7 @@ jobs: ref: ${{ github.event.pull_request.head.sha }} - uses: denoland/setup-deno@v2 with: deno-version: 2.5.3 # Keep in sync with mise.toml deno-version: 2.5.6 # Keep in sync with mise.toml - uses: pnpm/action-setup@v4 with: version: latest Loading Loading @@ -341,7 +341,10 @@ jobs: if [[ "$GITHUB_REF_TYPE" != tag ]]; then rm fedify-cli-*.tgz fi - run: deno task pack - run: | set -ex mkdir -p "$RUNNER_TEMP" TMPDIR="$RUNNER_TEMP" deno task pack working-directory: ${{ github.workspace }}/packages/cli/ - id: extract-changelog uses: dahlia/submark@5a5ff0a58382fb812616a5801402f5aef00f90ce Loading Loading @@ -385,7 +388,12 @@ jobs: set -ex for pkg in fedify-*.tgz; do if [[ "$GITHUB_REF_TYPE" = "tag" ]]; then npm publish --logs-dir=. --provenance --access public "$pkg" \ npm publish \ --logs-dir=. \ --provenance \ --access public \ --tag latest \ "$pkg" \ || grep "Cannot publish over previously published version" *.log rm *.log elif [[ "$GITHUB_EVENT_NAME" = "pull_request_target" ]]; then Loading Loading @@ -488,7 +496,7 @@ jobs: ref: ${{ github.event.pull_request.head.sha }} - uses: denoland/setup-deno@v2 with: deno-version: 2.5.3 # Keep in sync with mise.toml deno-version: 2.5.6 # Keep in sync with mise.toml - run: deno task codegen working-directory: ${{ github.workspace }}/packages/fedify/ - uses: denoland/deployctl@v1 Loading CHANGES.md +60 −0 Original line number Diff line number Diff line Loading @@ -8,6 +8,20 @@ Version 1.9.2 To be released. ### @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 @@ -326,6 +340,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 @@ -761,6 +797,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 @@ -946,6 +993,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 mise.toml +1 −1 Original line number Diff line number Diff line Loading @@ -2,6 +2,6 @@ # Bun version should be kept in sync with GitHub Actions workflows bun = "1.2.22" # Deno version should be kept in sync with GitHub Actions workflows deno = "2.5.3" deno = "2.5.6" node = "22" "npm:pnpm" = "latest" 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 @@ -365,6 +365,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 @@ -254,21 +254,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 @@ -285,6 +302,7 @@ export async function getRemoteDocument( } } document = JSON.parse(html); } } else { document = await response.json(); } Loading Loading
.github/workflows/build.yaml +18 −10 Original line number Diff line number Diff line Loading @@ -57,7 +57,7 @@ jobs: ref: ${{ github.event.pull_request.head.sha }} - uses: denoland/setup-deno@v2 with: deno-version: 2.5.3 # Keep in sync with mise.toml deno-version: 2.5.6 # Keep in sync with mise.toml - run: deno task test --coverage=.cov --junit-path=.test-report.xml env: RUST_BACKTRACE: ${{ runner.debug }} Loading Loading @@ -128,7 +128,7 @@ jobs: ref: ${{ github.event.pull_request.head.sha }} - uses: denoland/setup-deno@v2 with: deno-version: 2.5.3 # Keep in sync with mise.toml deno-version: 2.5.6 # Keep in sync with mise.toml - uses: pnpm/action-setup@v4 with: version: 10 Loading Loading @@ -185,7 +185,7 @@ jobs: ref: ${{ github.event.pull_request.head.sha }} - uses: denoland/setup-deno@v2 with: deno-version: 2.5.3 # Keep in sync with mise.toml deno-version: 2.5.6 # Keep in sync with mise.toml - uses: pnpm/action-setup@v4 with: version: 10 Loading @@ -211,7 +211,7 @@ jobs: ref: ${{ github.event.pull_request.head.sha }} - uses: denoland/setup-deno@v2 with: deno-version: 2.5.3 # Keep in sync with mise.toml deno-version: 2.5.6 # Keep in sync with mise.toml - uses: pnpm/action-setup@v4 with: version: 10 Loading @@ -234,7 +234,7 @@ jobs: ref: ${{ github.event.pull_request.head.sha }} - uses: denoland/setup-deno@v2 with: deno-version: 2.5.3 # Keep in sync with mise.toml deno-version: 2.5.6 # Keep in sync with mise.toml - run: deno task hooks:pre-commit release-test: Loading @@ -252,7 +252,7 @@ jobs: ref: ${{ github.event.pull_request.head.sha }} - uses: denoland/setup-deno@v2 with: deno-version: 2.5.3 # Keep in sync with mise.toml deno-version: 2.5.6 # Keep in sync with mise.toml - uses: pnpm/action-setup@v4 with: version: 10 Loading Loading @@ -287,7 +287,7 @@ jobs: ref: ${{ github.event.pull_request.head.sha }} - uses: denoland/setup-deno@v2 with: deno-version: 2.5.3 # Keep in sync with mise.toml deno-version: 2.5.6 # Keep in sync with mise.toml - uses: pnpm/action-setup@v4 with: version: latest Loading Loading @@ -341,7 +341,10 @@ jobs: if [[ "$GITHUB_REF_TYPE" != tag ]]; then rm fedify-cli-*.tgz fi - run: deno task pack - run: | set -ex mkdir -p "$RUNNER_TEMP" TMPDIR="$RUNNER_TEMP" deno task pack working-directory: ${{ github.workspace }}/packages/cli/ - id: extract-changelog uses: dahlia/submark@5a5ff0a58382fb812616a5801402f5aef00f90ce Loading Loading @@ -385,7 +388,12 @@ jobs: set -ex for pkg in fedify-*.tgz; do if [[ "$GITHUB_REF_TYPE" = "tag" ]]; then npm publish --logs-dir=. --provenance --access public "$pkg" \ npm publish \ --logs-dir=. \ --provenance \ --access public \ --tag latest \ "$pkg" \ || grep "Cannot publish over previously published version" *.log rm *.log elif [[ "$GITHUB_EVENT_NAME" = "pull_request_target" ]]; then Loading Loading @@ -488,7 +496,7 @@ jobs: ref: ${{ github.event.pull_request.head.sha }} - uses: denoland/setup-deno@v2 with: deno-version: 2.5.3 # Keep in sync with mise.toml deno-version: 2.5.6 # Keep in sync with mise.toml - run: deno task codegen working-directory: ${{ github.workspace }}/packages/fedify/ - uses: denoland/deployctl@v1 Loading
CHANGES.md +60 −0 Original line number Diff line number Diff line Loading @@ -8,6 +8,20 @@ Version 1.9.2 To be released. ### @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 @@ -326,6 +340,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 @@ -761,6 +797,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 @@ -946,6 +993,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
mise.toml +1 −1 Original line number Diff line number Diff line Loading @@ -2,6 +2,6 @@ # Bun version should be kept in sync with GitHub Actions workflows bun = "1.2.22" # Deno version should be kept in sync with GitHub Actions workflows deno = "2.5.3" deno = "2.5.6" node = "22" "npm:pnpm" = "latest"
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 @@ -365,6 +365,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 @@ -254,21 +254,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 @@ -285,6 +302,7 @@ export async function getRemoteDocument( } } document = JSON.parse(html); } } else { document = await response.json(); } Loading