CVE-2026-25544: Verified Repro With Script Download
CVE-2026-25544: Payload CMS: Blind SQL Injection in JSON/RichText Queries via Drizzle Adapters
CVE-2026-25544 is verified against @payloadcms/drizzle · npm. Affected versions: < 3.73.0. Fixed in 3.73.0. Vulnerability class: SQLi. This critical reproduction includes runnable sandbox proof, artifacts, and a plain-text agent view under REPRO-2026-00092.
What Is CVE-2026-25544?
CVE-2026-25544 (GHSA-xx6w-jxg9-2wh8) is a critical blind SQL injection vulnerability in Payload CMS's @payloadcms/drizzle package, reachable when querying JSON or richText fields. Pruva reproduced it (reproduction REPRO-2026-00092).
CVE-2026-25544 Severity & CVSS Score
CVE-2026-25544 is rated critical severity, with a CVSS base score of 9.8 out of 10.
Critical — the most severe class — typically remotely exploitable with severe impact. Treat as an emergency.
Affected @payloadcms/drizzle Versions
@payloadcms/drizzle · npm versions < 3.73.0 are affected.
How to Reproduce CVE-2026-25544
pruva-verify REPRO-2026-00092 curl -O https://pruva.dev/api/v1/reproductions/REPRO-2026-00092/artifacts/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh Proof of Reproduction for CVE-2026-25544
Reproduced by Pruva's autonomous agents — 102 tool calls over 16 min. Full root-cause analysis and the complete transcript are below.
How the agent worked
Root Cause and Exploit Chain for CVE-2026-25544
Summary
GHSA-xx6w-jxg9-2wh8 is a critical SQL injection vulnerability in Payload CMS's @payloadcms/drizzle package affecting versions prior to 3.73.0. The vulnerability exists in the parseParams.ts file where user-supplied input for JSON and RichText field queries is directly concatenated into SQL query strings without proper escaping or parameterization. This allows unauthenticated attackers to perform blind SQL injection attacks, potentially extracting sensitive data like emails and password reset tokens, leading to full account takeover.
Impact
- Package:
@payloadcms/drizzle(npm) - Affected Versions: < 3.73.0
- Fixed Version: 3.73.0
- Risk Level: Critical (CVSS 9.8)
- Attack Vector: Network-based, no authentication required
- Affected Adapters: PostgreSQL, SQLite, Vercel PostgreSQL, D1 SQLite
- Safe: MongoDB adapter users are not affected
Consequences:
- Blind SQL injection allowing arbitrary data extraction
- Exposure of sensitive user data (emails, password reset tokens)
- Complete account takeover without password cracking
- Potential for privilege escalation and data manipulation
Root Cause
The vulnerability exists in packages/drizzle/src/queries/parseParams.ts in the JSON/richText field query handling code. When building SQL queries for JSON or RichText field filters (like equals, contains, like), the code directly interpolates user input into SQL strings:
Vulnerable code (v3.72.0, line 192):
formattedValue = `'${operatorKeys[operator].wildcard}${val}${operatorKeys[operator].wildcard}'`
The variable val comes directly from user input via the REST API where clause and is embedded in the SQL without escaping. This allows attackers to inject SQL metacharacters to alter query logic.
The Fix (v3.73.0):
formattedValue = `'${operatorKeys[operator].wildcard}${escapeSQLValue(val)}${operatorKeys[operator].wildcard}'`
The escapeSQLValue() function was added which validates input against a safe regex pattern /^[\w @.\-+:]*$/ and throws an error for any input containing potentially dangerous characters.
Additional vulnerable pattern (line 190):
formattedValue = `(${val.map((v) => `${v}`).join(',')})`
This was also fixed to use escapeSQLValue(v).
Fix commit reference: The vulnerability was fixed in commit 4f5a9c28346aaea78d53240166000d7210c35fc7 (though this was primarily an IN query fix, the escapeSQLValue changes were part of the v3.73.0 security release).
Reproduction Steps
The reproduction script is located at repro/reproduction_steps.sh.
What the Script Does:
- Clones the vulnerable version (v3.72.0) of Payload CMS
- Analyzes the source code in
packages/drizzle/src/queries/parseParams.ts - Runs a simulated SQL injection test that demonstrates:
- How malicious input like
' OR '1'='1is directly embedded in SQL - How the vulnerable code generates exploitable SQL
- How the patched version with
escapeSQLValue()blocks malicious input
- How malicious input like
- Verifies the vulnerability by:
- Confirming
escapeSQLValueis absent from the vulnerable version - Finding the exact vulnerable line:
formattedValue = '\${operatorKeys[operator].wildcard}\${val}\${operatorKeys[operator].wildcard}' - Comparing with the patched version showing
escapeSQLValue(val)
- Confirming
Expected Evidence:
The script produces evidence in logs/:
clone.log- Git clone outputtest-results.log- JavaScript test showing SQL injection possibilityvulnerability-confirmation.txt- Summary of findings
Key outputs confirming the vulnerability:
[OK] escapeSQLValue NOT found - vulnerable version confirmed
[OK] VULNERABLE PATTERN CONFIRMED: formattedValue = '...${val}...'
Evidence
Log File Locations:
logs/clone.log- Repository clone outputlogs/test-results.log- SQL injection simulation test resultslogs/vulnerability-confirmation.txt- Confirmed vulnerability details
Key Excerpts:
From test-results.log - Demonstrating SQL injection:
[Test 1] Basic SQL Injection via equals operator:
Input: ' OR '1'='1
Vulnerable output: '' OR '1'='1'
VULNERABLE: YES - SQL INJECTION POSSIBLE
[Test 2] Data extraction attempt:
Input: ' UNION SELECT email, password FROM users --
Vulnerable output: '' UNION SELECT email, password FROM users --'
VULNERABLE: YES - SQL INJECTION POSSIBLE
From vulnerability-confirmation.txt:
VULNERABILITY CONFIRMED: GHSA-xx6w-jxg9-2wh8
File: packages/drizzle/src/queries/parseParams.ts
Issue: SQL Injection in JSON/RichText field queries
VULNERABLE CODE:
In v3.72.0, user input is directly concatenated into SQL:
formattedValue = '${operatorKeys[operator].wildcard}${val}${operatorKeys[operator].wildcard}'
PATCH (v3.73.0):
formattedValue = '${operatorKeys[operator].wildcard}${escapeSQLValue(val)}${operatorKeys[operator].wildcard}'
Vulnerable code location:
- File:
packages/drizzle/src/queries/parseParams.ts - Line 192 (in v3.72.0):
formattedValue = '\${operatorKeys[operator].wildcard}\${val}\${operatorKeys[operator].wildcard}'
Environment Details:
- Repository: payloadcms/payload
- Vulnerable version: v3.72.0 (tag: fbf48d2e1962a7b779b47b23452fc14491651483)
- Fixed version: v3.73.0 (tag: b3796f587e237f91fea7ed55a4b0d3a58a78a9bd)
- Node.js runtime for simulation tests
Recommendations / Next Steps
Immediate Actions:
- Upgrade to v3.73.0 or later - The fix adds input validation via
escapeSQLValue()
Temporary Mitigation (if upgrade not possible):
- Set
access: { read: () => false }on all JSON and RichText fields as a temporary measure - Monitor access logs for suspicious
whereclause patterns containing SQL keywords
Testing Recommendations:
- After upgrading, verify queries on JSON/RichText fields still work for legitimate use cases
- Test that malicious payloads are now rejected:
' OR '1'='1should be blocked'; DROP TABLE users; --should be blocked- Normal alphanumeric values should work normally
Suggested Fix Approach (for understanding):
The implemented fix uses a whitelist approach:
const SAFE_STRING_REGEX = /^[\w @.\-+:]*$/
export const escapeSQLValue = (value: unknown): boolean | null | number | string => {
if (typeof value !== 'string') {
throw new Error('Invalid value type')
}
if (!SAFE_STRING_REGEX.test(value)) {
throw new APIError(`${value} is not allowed as a JSON query value`, 400)
}
const escaped = value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')
return escaped
}
This approach:
- Validates input against a strict safe character set
- Rejects any input containing SQL metacharacters
- Escapes backslashes and quotes for defense in depth
- Returns HTTP 400 for rejected inputs
Additional Notes
Idempotency Confirmation:
The reproduction script has been run twice consecutively with identical successful results. Both runs:
- Successfully cloned the vulnerable version
- Confirmed absence of
escapeSQLValuein v3.72.0 - Confirmed presence of
escapeSQLValuein v3.73.0 - Demonstrated SQL injection vectors in simulated tests
Edge Cases:
Array values (IN/NOT IN operators): The vulnerability also affects array processing at line 190 where array elements are joined without escaping:
`(${val.map((v) => `${v}`).join(',')})`RichText fields: The vulnerability affects both
jsonandrichTextfield typesSQLite vs PostgreSQL: The vulnerable code path is primarily in the SQLite/JSON handling section, but PostgreSQL is also affected via the
createJSONQuerypath which usessql.raw(constraint)with potentially unsanitized input
Limitations of Reproduction:
The reproduction focuses on static code analysis and simulation rather than a live exploit against a running Payload CMS instance. A full end-to-end exploit would require:
- Setting up a Payload CMS instance with a PostgreSQL/SQLite database
- Creating a collection with a JSON field
- Making authenticated or unauthenticated API requests (depending on collection access configuration)
However, the code analysis definitively demonstrates the vulnerability exists in the source code and shows exactly how user input flows into SQL queries without escaping.
CVE-2026-25544 Reproduction Transcript
The agent's step-by-step process — every tool call, every handoff, the moment the exploit fired.
Full session Replay every step — scrub the timeline or play it back.
Artifacts and Evidence for CVE-2026-25544
Scripts, logs, diffs, and output captured during the reproduction.
No artifacts available
How to Fix CVE-2026-25544
Upgrade @payloadcms/drizzle · npm to 3.73.0 or later.
FAQ: CVE-2026-25544
How does the CVE-2026-25544 SQL injection attack work?
Which Payload CMS configurations are affected by CVE-2026-25544, and where is it fixed?
How severe is CVE-2026-25544?
How can I reproduce CVE-2026-25544?
References for CVE-2026-25544
Authoritative sources for CVE-2026-25544 — official vulnerability databases and the upstream advisory. Pruva's reproduction verifies the issue firsthand; these are the primary records to corroborate it.