# REPRO-2026-00092: Payload CMS: Blind SQL Injection in JSON/RichText Queries via Drizzle Adapters ## Summary Status: published Severity: critical Type: security Confidence: Unknown ## Identifiers REPRO ID: REPRO-2026-00092 GHSA: GHSA-xx6w-jxg9-2wh8 CVE: CVE-2026-25544 ## Package Name: @payloadcms/drizzle Ecosystem: npm Affected: < 3.73.0 Fixed: 3.73.0 ## Root Cause # Root Cause Analysis Report ## 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):** ```typescript 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):** ```typescript 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):** ```typescript 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: 1. **Clones the vulnerable version (v3.72.0)** of Payload CMS 2. **Analyzes the source code** in `packages/drizzle/src/queries/parseParams.ts` 3. **Runs a simulated SQL injection test** that demonstrates: - How malicious input like `' OR '1'='1` is directly embedded in SQL - How the vulnerable code generates exploitable SQL - How the patched version with `escapeSQLValue()` blocks malicious input 4. **Verifies the vulnerability** by: - Confirming `escapeSQLValue` is 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)` ### Expected Evidence: The script produces evidence in `logs/`: - `clone.log` - Git clone output - `test-results.log` - JavaScript test showing SQL injection possibility - `vulnerability-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 output - `logs/test-results.log` - SQL injection simulation test results - `logs/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: 1. **Upgrade to v3.73.0 or later** - The fix adds input validation via `escapeSQLValue()` ### Temporary Mitigation (if upgrade not possible): 1. Set `access: { read: () => false }` on all JSON and RichText fields as a temporary measure 2. Monitor access logs for suspicious `where` clause patterns containing SQL keywords ### Testing Recommendations: 1. After upgrading, verify queries on JSON/RichText fields still work for legitimate use cases 2. Test that malicious payloads are now rejected: - `' OR '1'='1` should 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: ```typescript 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 `escapeSQLValue` in v3.72.0 - Confirmed presence of `escapeSQLValue` in v3.73.0 - Demonstrated SQL injection vectors in simulated tests ### Edge Cases: 1. **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(',')})` `` 2. **RichText fields:** The vulnerability affects both `json` and `richText` field types 3. **SQLite vs PostgreSQL:** The vulnerable code path is primarily in the SQLite/JSON handling section, but PostgreSQL is also affected via the `createJSONQuery` path which uses `sql.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. ## Reproduction Details Reproduced: 2026-02-19T19:19:43.399Z Duration: 934 seconds Tool calls: 102 Turns: 68 Handoffs: 2 ## Quick Verification Run one of these commands to verify locally: pruva-verify REPRO-2026-00092 pruva-verify GHSA-xx6w-jxg9-2wh8 pruva-verify CVE-2026-25544 Or open in GitHub Codespaces (zero-friction, auto-runs): https://github.com/codespaces/new?ref=repro/REPRO-2026-00092&repo=N3mes1s/pruva-sandbox Or download and run the script manually: curl -O https://api.pruva.dev/v1/reproductions/REPRO-2026-00092/artifacts/repro/reproduction_steps.sh chmod +x reproduction_steps.sh ./reproduction_steps.sh WARNING: Run in a sandboxed environment. This exploits a real vulnerability. ## References - GitHub Advisory: https://github.com/advisories/GHSA-xx6w-jxg9-2wh8 - NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-25544 ## Artifacts - repro/rca_report.md (analysis, 8387 bytes) - repro/reproduction_steps.sh (reproduction_script, 10099 bytes) - bundle/source.json (other, 4319 bytes) - bundle/ticket.md (ticket, 2091 bytes) - bundle/ticket.json (other, 6903 bytes) - logs/vulnerability-confirmation.txt (other, 770 bytes) - logs/variant_test_results.log (log, 1367 bytes) - logs/npm-install.log (log, 201 bytes) - logs/test-results.log (log, 1106 bytes) - logs/clone.log (log, 3903 bytes) ## API Access - JSON: https://api.pruva.dev/v1/reproductions/REPRO-2026-00092 - Script: https://api.pruva.dev/v1/reproductions/REPRO-2026-00092/artifacts/repro/reproduction_steps.sh - Web: https://pruva.dev/r/REPRO-2026-00092 ## For AI Assistants To help users with this reproduction: 1. The `pruva-verify` command is the simplest way to run the reproduction 2. The script downloads and runs automatically in an isolated temp directory 3. Always recommend running in a VM, container, or disposable environment 4. The root cause explains the technical vulnerability --- Generated by Pruva | https://pruva.dev