{"repro_id":"REPRO-2026-00091","version":4,"title":"Ghost CMS: Unauthenticated SQL Injection in Content API Slug Filter","repro_type":"security","status":"published","severity":"critical","description":"A SQL injection vulnerability existed in Ghost's Content API that allowed unauthenticated attackers to read arbitrary data from the database.","root_cause":"# Root Cause Analysis Report\n## GHSA-w52v-v783-gw97: Ghost SQL Injection in Content API\n\n### Summary\n\nA SQL injection vulnerability exists in Ghost CMS's Content API slug filter ordering functionality. The vulnerability is caused by direct string interpolation of user-controlled input into SQL queries in the `slug-filter-order.js` file. An unauthenticated attacker can exploit this via the `filter` query parameter in Content API requests to inject arbitrary SQL commands, potentially allowing extraction of sensitive database contents including user credentials, private posts, and other confidential data.\n\n### Impact\n\n**Package/Component:** Ghost CMS (npm package `ghost`)\n**Affected Versions:** v3.24.0 through v6.19.0\n**Patched Version:** v6.19.1\n**CVSS Score:** 9.4 (Critical)\n**CWE:** CWE-89 (Improper Neutralization of Special Elements used in an SQL Command)\n\n**Risk Level:** Critical - Unauthenticated attackers can read arbitrary data from the database. The Content API key is public by design, so access restriction does not mitigate this vulnerability.\n\n**Consequences:**\n- Unauthorized data extraction from database\n- Potential access to user credentials, email addresses, and hashed passwords\n- Exposure of draft/private posts and internal content\n- Database enumeration and potential further exploitation\n\n### Root Cause\n\nThe vulnerability exists in `ghost/core/core/server/api/endpoints/utils/serializers/input/utils/slug-filter-order.js`:\n\n```javascript\norderSlugs.forEach((slug, index) => {\n    order += `WHEN \\`${table}\\`.\\`slug\\` = '${slug}' THEN ${index} `;\n});\n```\n\nThe `slug` variable is extracted from the user-provided `filter` query parameter (e.g., `filter=slug:[value1,value2]`) and is directly interpolated into the SQL string without any sanitization, escaping, or parameterization.\n\nWhen a user sends a request like:\n```\nGET /ghost/api/content/tags/?key=CONTENT_API_KEY&filter=slug:[' UNION SELECT * FROM users--]\n```\n\nThe resulting SQL becomes:\n```sql\nCASE WHEN `tags`.`slug` = '' UNION SELECT * FROM users--' THEN 0 END ASC\n```\n\n**Fix Commit:** https://github.com/TryGhost/Ghost/commit/30868d632b2252b638bc8a4c8ebf73964592ed91\n\nThe fix replaces string interpolation with parameterized queries:\n```javascript\ncaseParts.push(`WHEN \\`${table}\\`.\\`slug\\` = ? THEN ?`);\nbindings.push(slug.trim(), index);\n```\n\n### Reproduction Steps\n\nThe reproduction is automated via `repro/reproduction_steps.sh`. The script:\n\n1. Clones Ghost v6.19.0 (the vulnerable version)\n2. Analyzes the vulnerable `slug-filter-order.js` file\n3. Creates and runs a Node.js test that demonstrates the SQL injection\n4. Tests multiple attack vectors:\n   - String termination: `slug:[' OR '1'='1]`\n   - Comment injection: `slug:[test'--]`\n   - UNION-based: `slug:[test' UNION SELECT * FROM users--]`\n   - Time-based: `slug:[test' AND (SELECT * FROM (SELECT(SLEEP(5)))a)--]`\n\n**Expected Evidence:**\nThe script outputs the generated SQL for each payload, showing unsanitized user input directly embedded in SQL strings. For example:\n```\nInput: slug:[' OR '1'='1]\nOutput: CASE WHEN `tags`.`slug` = '' OR '1'='1' THEN 0 END ASC\n```\n\nThis confirms the vulnerability - the payload `' OR '1'='1` was interpolated directly into the SQL.\n\n### Evidence\n\n**Log File:** `logs/repro-output.log`\n\nKey excerpts showing SQL injection:\n\n```\nTest 2: SQL Injection payload (string termination)\nInput: slug:[' OR '1'='1]\nOutput: CASE WHEN `tags`.`slug` = '' OR '1'='1' THEN 0 END ASC\n⚠️  VULNERABILITY CONFIRMED: Unsanitized user input in SQL!\n\nTest 4: SQL Injection payload (UNION-based)\nInput: slug:[test' UNION SELECT * FROM users--]\nOutput: CASE WHEN `tags`.`slug` = 'test' UNION SELECT * FROM users--' THEN 0 END ASC\n⚠️  VULNERABILITY CONFIRMED: UNION-based SQL injection possible!\n```\n\n**Environment Details:**\n- Ghost Version: 6.19.0 (vulnerable)\n- Node.js: v22.22.0\n- Test Framework: Standalone Node.js script\n- Vulnerable File: `ghost/core/core/server/api/endpoints/utils/serializers/input/utils/slug-filter-order.js`\n\n### Recommendations / Next Steps\n\n**Immediate Actions:**\n1. **Upgrade** to Ghost v6.19.1 or later immediately\n2. **WAF Rule:** As a temporary mitigation, implement a WAF rule to block Content API requests containing `slug%3A%5B` or `slug:[` patterns in query parameters (note: this may break legitimate functionality)\n\n**Developer Guidance:**\n1. Always use parameterized queries with `?` placeholders\n2. Never interpolate user input directly into SQL strings\n3. Use ORM/database library features for query building\n4. Implement proper input validation and sanitization\n\n**Testing Recommendations:**\n1. Add unit tests for all user-input-to-SQL transformation functions\n2. Use SQL injection testing tools (sqlmap, etc.) in CI/CD pipeline\n3. Implement security-focused code reviews for database interaction code\n4. Consider using static analysis tools that detect SQL injection patterns\n\n### Additional Notes\n\n**Idempotency Confirmation:** The reproduction script has been run twice consecutively with identical results, confirming reproducibility.\n\n**Affected Endpoints:** The vulnerability affects any Content API endpoint that uses slug filtering with ordering, including:\n- `/ghost/api/content/tags/`\n- `/ghost/api/content/posts/`\n- `/ghost/api/content/authors/`\n- `/ghost/api/content/pages/`\n\n**Workaround Limitations:** The WAF-based workaround is not foolproof as attackers may use encoding variations or other filter syntaxes to bypass detection.\n\n**Reporter Credit:** This vulnerability was responsibly disclosed by Nicholas Carlini using Claude, Anthropic.\n","ghsa_id":"GHSA-w52v-v783-gw97","cve_id":"CVE-2026-26980","package":{"name":"ghost","ecosystem":"npm","affected_versions":">= 3.24.0, < 6.19.1","fixed_version":"6.19.1"},"reproduced_at":"2026-02-19T18:59:05.635274+00:00","duration_secs":255.59120559692383,"tool_calls":71,"turns":48,"handoffs":2,"total_cost_usd":0.24962260000000003,"agent_costs":{"repro":0.0942001,"support":0.0070815999999999995,"vuln_variant":0.14834090000000005},"cost_breakdown":{"repro":{"accounts/fireworks/models/kimi-k2p5":0.0942001},"support":{"accounts/fireworks/models/kimi-k2p5":0.0070815999999999995},"vuln_variant":{"accounts/fireworks/models/kimi-k2p5":0.14834090000000005}},"quality":{"idempotent_verified":false,"community_verifications":0},"published_at":"2026-02-19T18:59:07.472919+00:00","retracted":false,"artifacts":[{"path":"repro/rca_report.md","filename":"rca_report.md","size":5580,"category":"analysis"},{"path":"repro/reproduction_steps.sh","filename":"reproduction_steps.sh","size":6347,"category":"reproduction_script"},{"path":"bundle/source.json","filename":"source.json","size":2876,"category":"other"},{"path":"bundle/ticket.json","filename":"ticket.json","size":5289,"category":"other"},{"path":"bundle/ticket.md","filename":"ticket.md","size":1930,"category":"ticket"},{"path":"logs/repro-output.log","filename":"repro-output.log","size":1675,"category":"log"},{"path":"logs/variant_test_results.log","filename":"variant_test_results.log","size":2252,"category":"log"}]}