react-rsc-vuln-scanner
Version:
CLI tool to scan projects for React Server Components vulnerabilities (CVE-2025-55182, CVE-2025-55184, CVE-2025-55183)
504 lines (434 loc) • 18 kB
JavaScript
const fs = require('node:fs');
const path = require('node:path');
// React Server Components Security Vulnerabilities
// CVE-2025-55182 - RCE (CRITICAL, CVSS 10.0) - https://react.dev/blog/2025/12/03/critical-security-vulnerability-in-react-server-components
// CVE-2025-55184 - Denial of Service (HIGH, CVSS 7.5) - https://react.dev/blog/2025/12/11/denial-of-service-and-source-code-exposure-in-react-server-components
// CVE-2025-55183 - Source Code Exposure (MEDIUM, CVSS 5.3) - https://react.dev/blog/2025/12/11/denial-of-service-and-source-code-exposure-in-react-server-components
// All CVEs tracked with their affected versions
const CVE_DATABASE = {
'CVE-2025-55182': {
name: 'Remote Code Execution (React2Shell)',
severity: 'CRITICAL',
cvss: 10.0,
vulnerable: ['19.0.0', '19.1.0', '19.1.1', '19.2.0'],
fixed: ['19.0.1', '19.1.2', '19.2.1', '19.0.2', '19.1.3', '19.2.2'],
url: 'https://react.dev/blog/2025/12/03/critical-security-vulnerability-in-react-server-components',
},
'CVE-2025-55184': {
name: 'Denial of Service',
severity: 'HIGH',
cvss: 7.5,
vulnerable: ['19.0.0', '19.0.1', '19.1.0', '19.1.1', '19.1.2', '19.2.0', '19.2.1'],
fixed: ['19.0.2', '19.1.3', '19.2.2'],
url: 'https://react.dev/blog/2025/12/11/denial-of-service-and-source-code-exposure-in-react-server-components',
},
'CVE-2025-55183': {
name: 'Source Code Exposure',
severity: 'MEDIUM',
cvss: 5.3,
vulnerable: ['19.0.0', '19.0.1', '19.1.0', '19.1.1', '19.1.2', '19.2.0', '19.2.1'],
fixed: ['19.0.2', '19.1.3', '19.2.2'],
url: 'https://react.dev/blog/2025/12/11/denial-of-service-and-source-code-exposure-in-react-server-components',
},
};
// Packages affected by all CVEs
const VULNERABLE_PACKAGES = {
'react-server-dom-webpack': {
allVulnerable: ['19.0.0', '19.0.1', '19.1.0', '19.1.1', '19.1.2', '19.2.0', '19.2.1'],
fixed: ['19.0.2', '19.1.3', '19.2.2'],
},
'react-server-dom-parcel': {
allVulnerable: ['19.0.0', '19.0.1', '19.1.0', '19.1.1', '19.1.2', '19.2.0', '19.2.1'],
fixed: ['19.0.2', '19.1.3', '19.2.2'],
},
'react-server-dom-turbopack': {
allVulnerable: ['19.0.0', '19.0.1', '19.1.0', '19.1.1', '19.1.2', '19.2.0', '19.2.1'],
fixed: ['19.0.2', '19.1.3', '19.2.2'],
},
};
// Frameworks that may include vulnerable packages
// Next.js fixed versions from: https://vercel.com/kb/bulletin/security-bulletin-cve-2025-55184-and-cve-2025-55183
const AFFECTED_FRAMEWORKS = {
'next': {
description: 'Next.js - check if using App Router (Pages Router not affected)',
fixedVersions: {
'13.x': '14.2.35 (upgrade required)',
'14.x': '14.2.35',
'15.0.x': '15.0.7',
'15.1.x': '15.1.11',
'15.2.x': '15.2.8',
'15.3.x': '15.3.8',
'15.4.x': '15.4.10',
'15.5.x': '15.5.9',
'16.0.x': '16.0.10',
},
},
'react-router': { description: 'React Router - check if using unstable RSC APIs' },
'waku': { description: 'Waku framework' },
'@parcel/rsc': { description: 'Parcel RSC plugin' },
'@vitejs/plugin-rsc': { description: 'Vite RSC plugin' },
'rwsdk': { description: 'Redwood SDK' },
};
const COLORS = {
reset: '\x1b[0m',
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
magenta: '\x1b[35m',
cyan: '\x1b[36m',
white: '\x1b[37m',
bold: '\x1b[1m',
dim: '\x1b[2m',
};
function colorize(text, color) {
return `${COLORS[color]}${text}${COLORS.reset}`;
}
function parseVersion(version) {
if (!version) return null;
// Remove common prefixes like ^, ~, >=, etc.
const cleaned = version.replace(/^[\^~>=<]+/, '').trim();
// Handle versions like "19.0.0-rc.1" by taking just the main version
const match = cleaned.match(/^(\d+)\.(\d+)\.(\d+)/);
if (match) {
return {
major: parseInt(match[1], 10),
minor: parseInt(match[2], 10),
patch: parseInt(match[3], 10),
raw: cleaned,
};
}
return null;
}
function getAffectedCVEs(version) {
const parsed = parseVersion(version);
if (!parsed) return { vulnerable: false, cves: [], reason: 'Could not parse version' };
const affectedCVEs = [];
for (const [cveId, cveInfo] of Object.entries(CVE_DATABASE)) {
for (const vulnVersion of cveInfo.vulnerable) {
const vulnParsed = parseVersion(vulnVersion);
if (!vulnParsed) continue;
if (
parsed.major === vulnParsed.major &&
parsed.minor === vulnParsed.minor &&
parsed.patch === vulnParsed.patch
) {
affectedCVEs.push({
id: cveId,
...cveInfo,
});
break;
}
}
}
// Check if it's a potentially vulnerable range (19.x before latest fixes)
if (affectedCVEs.length === 0 && parsed.major === 19) {
// All versions 19.0.x before 19.0.2, 19.1.x before 19.1.3, 19.2.x before 19.2.2 are vulnerable
if (parsed.minor === 0 && parsed.patch < 2) {
return {
vulnerable: true,
cves: Object.entries(CVE_DATABASE).map(([id, info]) => ({ id, ...info })),
reason: '19.0.x before 19.0.2'
};
}
if (parsed.minor === 1 && parsed.patch < 3) {
return {
vulnerable: true,
cves: Object.entries(CVE_DATABASE).map(([id, info]) => ({ id, ...info })),
reason: '19.1.x before 19.1.3'
};
}
if (parsed.minor === 2 && parsed.patch < 2) {
return {
vulnerable: true,
cves: Object.entries(CVE_DATABASE).map(([id, info]) => ({ id, ...info })),
reason: '19.2.x before 19.2.2'
};
}
}
return {
vulnerable: affectedCVEs.length > 0,
cves: affectedCVEs
};
}
function checkDependency(pkgName, version, findings) {
// Check for vulnerable packages
if (VULNERABLE_PACKAGES[pkgName]) {
const pkgInfo = VULNERABLE_PACKAGES[pkgName];
const vulnCheck = getAffectedCVEs(version);
findings.vulnerablePackages.push({
name: pkgName,
version,
...vulnCheck,
fixedVersions: pkgInfo.fixed,
});
}
// Check for affected frameworks
if (AFFECTED_FRAMEWORKS[pkgName]) {
const frameworkInfo = AFFECTED_FRAMEWORKS[pkgName];
findings.affectedFrameworks.push({
name: pkgName,
version: version,
description: frameworkInfo.description,
fixedVersions: frameworkInfo.fixedVersions,
});
}
}
function findManifestFiles(directory, results = [], scannedProjects = []) {
const SKIP_DIRS = ['node_modules', '.git', '.next', 'dist', 'build', '.cache', 'coverage'];
const MANIFESTS = ['package.json', 'bun.lock'];
let entries;
try {
entries = fs.readdirSync(directory, { withFileTypes: true });
} catch (err) {
return { results, scannedProjects };
}
for (const entry of entries) {
const fullPath = path.join(directory, entry.name);
if (entry.isDirectory()) {
if (!SKIP_DIRS.includes(entry.name)) {
findManifestFiles(fullPath, results, scannedProjects);
}
} else if (MANIFESTS.includes(entry.name)) {
results.push(fullPath);
// Track unique project directories
const dir = path.dirname(fullPath);
if (!scannedProjects.includes(dir)) {
scannedProjects.push(dir);
}
}
}
return { results, scannedProjects };
}
function analyzePackageJson(packageJsonPath) {
let packageData;
try {
const content = fs.readFileSync(packageJsonPath, 'utf8');
packageData = JSON.parse(content);
} catch {
return null;
}
const projectName = packageData.name || path.basename(path.dirname(packageJsonPath));
const findings = {
fileType: 'package.json',
projectName,
projectPath: path.dirname(packageJsonPath),
filePath: packageJsonPath,
vulnerablePackages: [],
affectedFrameworks: [],
hasLockFile: false,
};
// Check for lock files
const projectDir = path.dirname(packageJsonPath);
findings.hasLockFile =
fs.existsSync(path.join(projectDir, 'package-lock.json')) ||
fs.existsSync(path.join(projectDir, 'yarn.lock')) ||
fs.existsSync(path.join(projectDir, 'pnpm-lock.yaml')) ||
fs.existsSync(path.join(projectDir, 'bun.lock'));
const allDeps = {
...packageData.dependencies,
...packageData.devDependencies,
...packageData.peerDependencies,
};
for (const [pkgName, version] of Object.entries(allDeps)) {
checkDependency(pkgName, version, findings);
}
return findings;
}
function fromBunLockfileToJson(content) {
return content
.replaceAll("\r", "\n")
.replaceAll("\n\n", "\n")
.replaceAll(" ", "")
.replaceAll(",\n}", "}")
.replaceAll(",\n]", "]")
.replaceAll(",}", "}")
.replaceAll(",]", "]");
}
function analyzeBunLock(lockPath) {
const content = fromBunLockfileToJson(fs.readFileSync(lockPath, 'utf8'));
let lockData;
try {
lockData = JSON.parse(content);
} catch {
return null;
}
const findings = {
fileType: 'bun.lock',
projectName: lockData.name || path.basename(path.dirname(lockPath)),
projectPath: path.dirname(lockPath),
filePath: lockPath,
vulnerablePackages: [],
affectedFrameworks: [],
};
if (lockData.packages) {
for (const entry of Object.values(lockData.packages)) {
if (!Array.isArray(entry) || entry.length === 0) continue;
// Index 0 contains the full specifier e.g. "@babel/parser@7.28.5"
const fullSpec = entry[0];
if (typeof fullSpec !== 'string') continue;
// Extract name and version
// Find the *last* '@' to split name and version (handles scoped packages @foo/bar@1.0.0)
const lastAtIndex = fullSpec.lastIndexOf('@');
if (lastAtIndex === -1) continue;
const pkgName = fullSpec.substring(0, lastAtIndex);
const version = fullSpec.substring(lastAtIndex + 1);
checkDependency(pkgName, version, findings);
}
}
return findings;
}
function printBanner() {
console.log('\n' + colorize('═'.repeat(70), 'cyan'));
console.log(colorize(' React Server Components Vulnerability Scanner', 'bold'));
console.log(colorize('═'.repeat(70), 'cyan'));
console.log(colorize(' Checking for 3 CVEs:', 'white'));
console.log(colorize(' • CVE-2025-55182 - RCE (CRITICAL, CVSS 10.0)', 'red'));
console.log(colorize(' • CVE-2025-55184 - Denial of Service (HIGH, CVSS 7.5)', 'yellow'));
console.log(colorize(' • CVE-2025-55183 - Source Code Exposure (MEDIUM, CVSS 5.3)', 'yellow'));
console.log(colorize('─'.repeat(70), 'dim'));
console.log(colorize(' References:', 'dim'));
console.log(colorize(' https://react.dev/blog/2025/12/03/critical-security-vulnerability-in-react-server-components', 'dim'));
console.log(colorize(' https://react.dev/blog/2025/12/11/denial-of-service-and-source-code-exposure-in-react-server-components', 'dim'));
console.log();
}
function printResults(scanDirectory, scannedProjects, vulnerableProjects, frameworkProjects) {
console.log(colorize('\n📂 Scan Directory:', 'bold'), scanDirectory);
console.log(colorize('📊 Total Projects Scanned:', 'bold'), scannedProjects.length);
console.log();
// List all scanned projects
console.log(colorize('─'.repeat(70), 'dim'));
console.log(colorize('📋 All Scanned Projects:', 'cyan'));
console.log(colorize('─'.repeat(70), 'dim'));
for (const project of scannedProjects) {
const relativePath = path.relative(scanDirectory, project) || '.';
console.log(` ${colorize('•', 'dim')} ${relativePath}`);
}
if (vulnerableProjects.length === 0) {
console.log(colorize('\n\n ✅ No directly vulnerable packages found!', 'green'));
} else {
console.log(colorize('\n'+'─'.repeat(70), 'dim'));
console.log(colorize('🚨 VULNERABLE PACKAGES FOUND:', 'red'));
console.log(colorize('─'.repeat(70), 'dim'));
for (const project of vulnerableProjects) {
console.log();
console.log(colorize(` 📁 ${project.projectName}`, 'bold'));
console.log(colorize(` Path: ${project.projectPath}`, 'dim'));
console.log(colorize(` Source: ${project.fileType}`, 'blue'));
for (const pkg of project.vulnerablePackages) {
const status = pkg.vulnerable ? colorize('VULNERABLE', 'red') : colorize('OK', 'green');
console.log(` ${colorize('•', 'red')} ${pkg.name}@${pkg.version} [${status}]`);
if (pkg.vulnerable && pkg.cves && pkg.cves.length > 0) {
for (const cve of pkg.cves) {
const severityColor = cve.severity === 'CRITICAL' ? 'red' : cve.severity === 'HIGH' ? 'yellow' : 'white';
console.log(colorize(` ⚠️ ${cve.id}: ${cve.name} (${cve.severity}, CVSS ${cve.cvss})`, severityColor));
}
console.log(colorize(` 🔧 Upgrade to: ${pkg.fixedVersions.join(' or ')}`, 'green'));
}
}
}
}
if (frameworkProjects.length === 0) {
console.log(colorize('\n\n ✅ No affected frameworks found!', 'green'));
} else {
console.log(colorize('\n'+'─'.repeat(70), 'dim'));
console.log(colorize('⚠️ PROJECTS WITH POTENTIALLY AFFECTED FRAMEWORKS:', 'yellow'));
console.log(colorize('─'.repeat(70), 'dim'));
for (const project of frameworkProjects) {
console.log();
console.log(colorize(` 📁 ${project.projectName}`, 'bold'));
console.log(colorize(` Path: ${project.projectPath}`, 'dim'));
console.log(colorize(` Source: ${project.fileType}`, 'blue'));
for (const framework of project.affectedFrameworks) {
console.log(` ${colorize('•', 'yellow')} ${framework.name}@${framework.version}`);
console.log(colorize(` ${framework.description}`, 'dim'));
if (framework.fixedVersions) {
console.log(colorize(` Check fixed versions for your release line`, 'yellow'));
}
}
}
}
// Summary
console.log(colorize('\n'+'═'.repeat(70), 'cyan'));
console.log(colorize('📈 SUMMARY', 'bold'));
console.log(colorize('═'.repeat(70), 'cyan'));
console.log(` Total projects scanned: ${scannedProjects.length}`);
console.log(` Projects with vulnerable packages: ${colorize(vulnerableProjects.length.toString(), vulnerableProjects.length > 0 ? 'red' : 'green')}`);
console.log(` Projects with affected frameworks: ${colorize(frameworkProjects.length.toString(), frameworkProjects.length > 0 ? 'yellow' : 'green')}`);
if (vulnerableProjects.length > 0 || frameworkProjects.length > 0) {
console.log(colorize('\n⚠️ ACTION REQUIRED:', 'red'));
console.log(' 1. Update vulnerable packages immediately to 19.0.2, 19.1.3, or 19.2.2');
console.log(' 2. Check node_modules for transitive dependencies');
console.log(' 3. Run `npm ls react-server-dom-webpack` to check nested deps');
console.log(' 4. References:');
console.log(' - https://react.dev/blog/2025/12/03/critical-security-vulnerability-in-react-server-components');
console.log(' - https://react.dev/blog/2025/12/11/denial-of-service-and-source-code-exposure-in-react-server-components');
} else {
console.log(colorize('\n✅ No immediate action required based on package.json and bun.lock (package-lock.json and other coming soon) analysis.', 'green'));
console.log(colorize(' Note: Also check node_modules for transitive dependencies.', 'dim'));
}
console.log();
}
function main() {
const args = process.argv.slice(2);
if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
console.log(`
${colorize('Usage:', 'bold')} node scan-react-rsc-vuln.js <absolute-path-to-scan>
${colorize('Description:', 'bold')}
Scans a directory recursively for Node.js projects that may be affected
by React Server Components security vulnerabilities:
• CVE-2025-55182 - Remote Code Execution (CRITICAL, CVSS 10.0)
• CVE-2025-55184 - Denial of Service (HIGH, CVSS 7.5)
• CVE-2025-55183 - Source Code Exposure (MEDIUM, CVSS 5.3)
${colorize('Example:', 'bold')}
node scan-react-rsc-vuln.js /Users/username/projects
${colorize('Checks for:', 'bold')}
- react-server-dom-webpack (vulnerable: 19.0.0-19.2.1, fixed: 19.0.2, 19.1.3, 19.2.2)
- react-server-dom-parcel (vulnerable: 19.0.0-19.2.1, fixed: 19.0.2, 19.1.3, 19.2.2)
- react-server-dom-turbopack (vulnerable: 19.0.0-19.2.1, fixed: 19.0.2, 19.1.3, 19.2.2)
- Affected frameworks: next, react-router, waku, @parcel/rsc, @vitejs/plugin-rsc, rwsdk
`);
process.exit(0);
}
const scanDirectory = args[0];
// Validate path
if (!path.isAbsolute(scanDirectory)) {
console.error(colorize('Error: Please provide an absolute path.', 'red'));
console.error(` Received: ${scanDirectory}`);
process.exit(1);
}
if (!fs.existsSync(scanDirectory)) {
console.error(colorize('Error: Directory does not exist.', 'red'));
console.error(` Path: ${scanDirectory}`);
process.exit(1);
}
if (!fs.statSync(scanDirectory).isDirectory()) {
console.error(colorize('Error: Path is not a directory.', 'red'));
console.error(` Path: ${scanDirectory}`);
process.exit(1);
}
printBanner();
console.log(colorize('🔍 Scanning for package.json and bun.lock files...', 'cyan'));
const { results: manifestFiles, scannedProjects } = findManifestFiles(scanDirectory);
const vulnerableProjects = [];
const frameworkProjects = [];
for (const manifestPath of manifestFiles) {
const fileName = path.basename(manifestPath);
let analysis = null;
if (fileName === 'package.json') {
analysis = analyzePackageJson(manifestPath);
} else if (fileName === 'bun.lock') {
analysis = analyzeBunLock(manifestPath);
}
if (!analysis) continue;
if (analysis.vulnerablePackages.length > 0) {
vulnerableProjects.push(analysis);
}
if (analysis.affectedFrameworks.length > 0) {
frameworkProjects.push(analysis);
}
}
printResults(scanDirectory, scannedProjects, vulnerableProjects, frameworkProjects);
}
main();