Unverified Commit 75a324ff authored by Hong Minhee's avatar Hong Minhee
Browse files

Merge tag '1.8.15' into 1.9-maintenance

Fedify 1.8.15
parents 3c25de01 7955e17a
Loading
Loading
Loading
Loading
+18 −10
Original line number Diff line number Diff line
@@ -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 }}
@@ -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
@@ -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
@@ -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
@@ -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:
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
+60 −0
Original line number Diff line number Diff line
@@ -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
-------------
@@ -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
--------------

@@ -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
--------------

@@ -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
--------------

+1 −1
Original line number Diff line number Diff line
@@ -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"
+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" };
@@ -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();
});

+46 −28
Original line number Diff line number Diff line
@@ -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" ||
@@ -285,6 +302,7 @@ export async function getRemoteDocument(
        }
      }
      document = JSON.parse(html);
    }
  } else {
    document = await response.json();
  }
Loading