CVE-2026-27876: Grafana SQL Expressions Arbitrary File Write to RCE
Published: 0001-01-01
- Severity
- Info
- Affected product
- Disclosed
- 0001-01-01
- Patch status
- Unverified
Archive entry
intelseclab/poc-archiveOn this page
CVSS 9.1 Critical | Arbitrary File Write | Remote Code Execution
Demo
Summary
Grafana’s SQL Expressions feature (sqlExpressions toggle) uses an in-process SQL engine (dolthub/go-mysql-server) with a flawed AST allowlist. The SetOp node (UNION ALL) passes validation and its walkSubtree() does not traverse the Into child — allowing INTO OUTFILE to write arbitrary files to the server filesystem as the Grafana process user.
Any authenticated user (Viewer role or higher) can chain this to full RCE via cron-based reverse shell.
Affected Versions
| Range | Affected | Fixed |
|---|---|---|
| 11.6.x | 11.6.0 – 11.6.13 | 11.6.14 |
| 12.0.x – 12.1.x | 12.0.0 – 12.1.9 | 12.1.10 |
| 12.2.x | 12.2.0 – 12.2.7 | 12.2.8 |
| 12.3.x | 12.3.0 – 12.3.5 | 12.3.6 |
| 12.4.x | 12.4.0 – 12.4.1 | 12.4.2 |
Requires sqlExpressions feature toggle to be enabled.
Root Cause
Two compounding flaws in pkg/expr/sql/:
parser_allow.go:allowedNode()uses a named returnb = true. The*sqlparser.SetOpcase returns true (allowed), andSetOp.walkSubtree()does NOT traverse theIntochild node. This meansUNION ALL ... INTO OUTFILEbypasses the allowlist entirely.db.go: The SQL engine context is created withoutWithDisableFileWrites(true), soINTO OUTFILEwrites to disk.
Bypass Syntax
| |
A single (SELECT ...) INTO OUTFILE produces a ParenSelect node which IS blocked. Two or more SELECT parts joined with UNION ALL produce a SetOp node which bypasses the check.
Exploit Chain
1. Authenticate (Viewer role sufficient)
2. POST /api/ds/query with __expr__ datasource, type "sql"
3. UNION ALL INTO OUTFILE writes reverse shell script to /tmp/
4. Second write places cron entry in /etc/crontabs/root
5. Cron fires within 60 seconds -> reverse shell as rootUsage
Lab Setup
| |
Grafana runs on http://localhost:3333 with credentials admin:admin.
Run Exploit
Terminal 1 — Listener:
| |
Terminal 2 — Exploit:
| |
Reverse shell lands within 60 seconds.
Other Modes
| |
Cleanup
| |
To re-run the exploit after a previous run, clean up first — INTO OUTFILE cannot overwrite existing files.
Fix
Commit 0e5d9e01ef31f072fd41626cd744699374e70127 (PR #121514):
parser_allow.go:case *sqlparser.SetOp: return v.GetInto() == nilparser_allow.go:case *sqlparser.Into: return v == nildb.go:mysql.WithDisableFileWrites(true)
Files
.
├── README.md
├── analysis.md # Full root cause analysis
├── lab/
│ ├── docker-compose.yml # Grafana 12.4.0 lab (confirmed)
│ ├── Dockerfile # Custom image with cron support
│ ├── entrypoint.sh # Starts crond + Grafana
│ └── setup.sh # Auto-setup script
└── poc/
├── exploit.py # Full PoC with RCE
└── cvss-justification.mdReferences
Disclaimer
For authorized security research and controlled lab testing only. Do not use against systems without explicit permission.