CVE-2025-69873 is a ReDoS (Regular Expression Denial of Service) vulnerability discovered in ajv, the most widely used JSON Schema validation library in the Node.js ecosystem. When exploited, this type of vulnerability causes an application to consume 100% of a CPU core for an extended period, potentially rendering the service unresponsive.
The specific attack vector requires the $data option
to be deliberately enabled when creating an ajv instance. This advanced feature allows JSON schema
validation rules to reference values within the data being validated itself -- a powerful but dangerous
capability that opens the door to attacker-controlled regular expressions.
For general consumers of ajv who use the $data feature,
this is a HIGH severity vulnerability. An attacker could craft a malicious JSON payload containing
a pathological regular expression, causing the server to hang and denying service to legitimate users.
For cdxgen specifically: After thorough code review of the entire source tree, the
$data option is never enabled in any ajv instantiation.
The cdxgen project's use of ajv is limited to validating internally generated SBOM (Software Bill of Materials)
files against fixed CycloneDX schemas. The schema definitions are loaded from disk, not from user input.
The risk surface is therefore effectively zero for the current codebase.
| Priority | Action | Rationale |
|---|---|---|
| Recommended | Add a code comment in validator.js explicitly documenting that $data must never be enabled due to this CVE |
Prevents future developers from inadvertently enabling the vulnerable feature |
| Nice to have | Add an automated test that asserts the Ajv instance is created without $data: true |
Regression protection -- catches accidental introduction during refactoring |
| Monitor | Watch the ajv project for a patched release addressing the underlying ReDoS patterns | Even without $data, other latent ReDoS patterns may exist in the library (acknowledged by repo owner) |
| Informational | Close or accept the GitHub issue with a documented mitigation rationale | The issue is correctly labeled code_not_reachable by the maintainer |
Regular Expression Denial of Service (ReDoS) exploits catastrophic backtracking
in regex engines. When a regex with ambiguous quantifiers (e.g., (a+)+)
is applied to an input that almost-but-not-quite matches, the engine explores an exponential number
of paths. For a suitably crafted input string of length n, evaluation time grows as
O(2n), blocking the JavaScript event loop and causing a full server hang.
The ajv-specific attack surface ($data) is described below.
Ajv's $data option enables runtime pattern injection.
When enabled, schema keywords like pattern, minimum,
and maximum can reference another field in the validated data using
JSON Pointer syntax:
json - malicious schema (only dangerous when $data: true)
{
"properties": {
"regex_field": { "type": "string" },
"target_field": {
"type": "string",
"pattern": { "$data": "1/regex_field" }
// ^^^ The pattern is taken FROM the data being validated!
}
}
}
An attacker who controls regex_field can inject
(a+)+$ (a catastrophic backtracking pattern) and trigger 100% CPU
consumption when target_field contains a long non-matching string.
There are three entry points in cdxgen that call validateBom().
Each is analyzed below.
Note: validation is gated behind options.validate in Path 1.
It only runs when the user explicitly passes the --validate flag.
Evinse always calls validateBom() unconditionally after
SBOM generation -- there is no flag gate. The input BOM comes from a user-supplied file
(-i bom.json), making this the wider attack surface of the two paths.
This is the exact Ajv instantiation at lib/helpers/validator.js lines 47-57.
The absence of $data: true is the key mitigation:
lib/helpers/validator.js -- lines 1-78 (validateBom function)
import { readFileSync } from "node:fs";
import { join } from "node:path";
import Ajv from "ajv"; // version 8.18.0 (pinned in package.json overrides)
import addFormats from "ajv-formats";
export const validateBom = (bomJson) => {
if (!bomJson) return true;
const specVersion = bomJson.specVersion;
const schema = JSON.parse(
readFileSync(join(dirName, "data", `bom-${specVersion}.schema.json`), "utf-8")
);
// ... (three more schema files loaded from disk, not from user input)
const ajv = new Ajv({
schemas,
strict: false,
logger: false,
verbose: true,
code: { source: true, lines: true, optimize: true },
// $data: true <-- NOT PRESENT. CVE-2025-69873 code path is BLOCKED HERE.
});
addFormats(ajv);
const validate = ajv.getSchema(`http://cyclonedx.org/schema/bom-${specVersion}.schema.json`);
const isValid = validate(bomJson);
if (!isValid) {
console.log(`Schema validation failed for ${bomJson.metadata.component.name}`);
return false;
}
return (
validateMetadata(bomJson) &&
validatePurls(bomJson) &&
validateRefs(bomJson) &&
validateProps(bomJson)
);
};
| Identifier | Name | Applicability | Evidence |
|---|---|---|---|
| CWE-400 | Uncontrolled Resource Consumption | Primary | ReDoS consumes unbounded CPU by design; no timeout in Node.js regex evaluation |
| CWE-1333 | Inefficient Regular Expression Complexity | Primary | The ajv library uses regex patterns that can catastrophically backtrack on adversarial input |
| CWE-20 | Improper Input Validation | Secondary | Root cause: when $data:true, attacker-supplied data is used as a schema pattern without sanitization |
| CWE-829 | Inclusion of Functionality from Untrusted Control Sphere | Secondary | The $data feature dynamically sources validation logic (regex) from untrusted input |
| OWASP A06:2021 | Vulnerable and Outdated Components | Primary | The vulnerability is in ajv (a third-party dependency); cdxgen depends on it without isolation |
| OWASP A05:2021 | Security Misconfiguration | Secondary | Enabling $data: true without input sanitization represents a dangerous optional feature misconfiguration |
| ASVS V5.3.5 | Verify regex not susceptible to catastrophic backtracking | ASVS Control | The ajv library's internal pattern handling does not guarantee polynomial-time evaluation |
| ASVS V14.2.1 | Verify all components are up to date | ASVS Control | Dependency management should track known CVEs in transitive dependencies |
| Dimension | Value for ajv (library) | Value for cdxgen | Rationale |
|---|---|---|---|
| Attack Vector | Network | N/A (not exploitable) | ajv can be reached remotely if serving HTTP; cdxgen BOM validation is local |
| Attack Complexity | Low | N/A | Crafting a catastrophic regex is well-documented; PoC exists publicly |
| Privileges Required | None | N/A | No authentication required to supply a BOM to evinse's -i flag |
| User Interaction | None | N/A | Fully automated trigger once the file is submitted |
| Scope | Unchanged | N/A | DoS is confined to the Node.js process; no privilege escalation |
| Confidentiality | None | None | ReDoS leaks no data |
| Integrity | None | None | No data modification |
| Availability | High | None | In the library, full CPU hang. In cdxgen, $data is absent so no impact. |
Add an explicit comment documenting the CVE and a test that asserts $data
is never present in the Ajv config. This costs nothing and provides permanent regression protection.
Before (current, lib/helpers/validator.js ~line 47):
const ajv = new Ajv({
schemas,
strict: false,
logger: false,
verbose: true,
code: { source: true, lines: true, optimize: true },
});
After (hardened):
// SECURITY: $data option intentionally NOT enabled.
// Enabling $data: true would expose CVE-2025-69873 (ReDoS via runtime pattern injection).
// See https://github.com/cdxgen/cdxgen/issues/3484
const ajv = new Ajv({
schemas,
strict: false,
logger: false,
verbose: true,
code: { source: true, lines: true, optimize: true },
// DO NOT ADD $data: true -- CVE-2025-69873
});
// Test that Ajv is never instantiated with $data: true (CVE-2025-69873 guard)
import assert from "node:assert";
import { readFileSync } from "node:fs";
const validatorSource = readFileSync("./lib/helpers/validator.js", "utf-8");
// Ensure $data: true is never present in the Ajv options
assert.ok(
!/new\s+Ajv\s*\([^)]*\$data\s*:\s*true/s.test(validatorSource),
"CVE-2025-69873: Ajv must not be instantiated with $data:true"
);
When ajv releases a version that mitigates the underlying ReDoS patterns (not just the
$data gate), update the overrides
section in package.json. Currently pinned at:
// package.json overrides section (current)
"ajv": "8.18.0" // update to patched version when released
"ajv-formats": "3.0.1" // keep in sync
A more robust long-term option for the evinse path is to add a file
size and structure sanity check before invoking validateBom(), to
limit the amount of attacker-controlled data that enters the validator:
// In bin/evinse.js, before calling validateBom(bomJson)
const MAX_BOM_SIZE_BYTES = 10 * 1024 * 1024; // 10 MB
const inputStats = fs.statSync(args.input);
if (inputStats.size > MAX_BOM_SIZE_BYTES) {
console.error("Input BOM file exceeds maximum size limit. Skipping validation.");
process.exit(1);
}
// Then validate normally
if (!validateBom(bomJson)) {
process.exit(1);
}
(a+)+$ or
(a|aa)+$, cause the regex engine to retry exponentially
many path combinations when the input nearly (but not quite) matches. The time grows
as O(2n) where n is the input length.
$data: true)
to be enabled. This is a very common pattern: a library offers a powerful but dangerous
advanced feature. The safe default is to leave it off. Always check how you instantiate
third-party libraries.
Look for these patterns when reviewing Node.js code that uses ajv:
new Ajv({ $data: true }) — this is the direct triggergrep -r "new Ajv(" --include="*.js" .$data option.$data: true instance, trace where the schema comes from. If it can be influenced by user input, it is vulnerable."pattern": {"$data": "1/userControlledField"} and a payload with userControlledField: "(a+)+$" and targetField: "aaaaaaaaaaaaaaab". Measure evaluation time.| Tool | Type | What it finds |
|---|---|---|
| safe-regex / vuln-regex-detector | Static analysis | Detects catastrophic backtracking patterns in regex literals |
| npm audit / Snyk / Socket.dev | SCA (Software Composition Analysis) | Matches package versions against CVE databases including CVE-2025-69873 |
| Semgrep | SAST | Write a custom rule: search for new Ajv({ ... $data: true ... }) |
| OWASP Dependency-Check | SCA | Scans node_modules against NVD CVE database |
| k6 / Artillery | Load testing | Test server response time degradation with crafted payloads |
$data: true. A comment and a regression test cost nothing and prevent the bug from ever being introduced.