Vitess Type_RAW Bind Variable SQL Injection
Overview
| Advisory | GHSA-fr6v-4xmf-vvjx |
| Component | VTGate gRPC bind variables, VTTablet query generation |
| Affected | Vitess 18.0.7-18.0.8, 19.0.6-19.0.10, 20.0.2-<23.0.4, and 24.0.0-rc1 |
| Fixed | Vitess 23.0.4 and 24.0.0 |
| Class | CWE-89 (SQL Injection) |
| CVSS v3.1 | 6.5 (Moderate) -- CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N |
| Found by | Lukas Johannes Moeller (@JohannesLks) |
TL;DR
Vitess accepted Type_RAW bind variables from a VTGate gRPC client. That type should never have been a safe client input. Because its numeric value included the quoted-type flag, validation let it through. Later, when VTTablet generated the final SQL string, EncodeValue wrote the raw bytes directly into the query.
The bug does not give access to VTGate by itself. The interesting boundary is after access: Vitess planned and checked one statement, then executed another statement after raw substitution.
Root Cause
The vulnerable split is small:
// go/vt/sqlparser/parsed_query.go
case querypb.Type_RAW:
v, _ := sqltypes.BindVariableToValue(value)
buf.Write(v.Raw()) // no escaping, no quoting
ValidateBindVariable delegates to NewValue. NewValue accepted Type_RAW because IsQuoted(Type_RAW) returned true. The reason was just flag math: Type_RAW had value 2084, and that value still had flagIsQuoted set.
2084 = 0x824
flagIsQuoted = 0x20
0x824 & 0x20 = 0x20
So the validation layer saw a quoted type. The query generation layer saw raw SQL bytes.
Where it crossed the boundary
VTGate's evalengine rejects Type_RAW in expression context, for example SELECT :v1. The working path is narrower: bind variables inside predicates are forwarded to VTTablet without going through that expression evaluation path.
That means Vitess plans and routes a query containing placeholders. VTTablet later expands the placeholder with raw bytes. Any Vitess-side statement checks that happened before expansion were looking at the placeholder form, not the final SQL text sent to MySQL.
Proof excerpt
baseline:
SELECT * FROM poc_sqli WHERE id = :v1
v1 = INT64(1)
rows returned: 1
raw bind variable:
SELECT * FROM poc_sqli WHERE id = :v1
v1 = RAW("1 OR 1=1")
rows returned: 3
union check:
v1 = RAW("0 UNION SELECT 99, @@version")
database version returned through the injected branch
Impact
A client needs access to the VTGate gRPC endpoint first. This is not an unauthenticated internet SQL injection.
With that access, the bug lets the client smuggle SQL through a bind variable after Vitess has planned and checked the placeholder query. In affected deployments, that can bypass Vitess-level controls such as table ACLs or query rules and run the injected SQL on MySQL with the tablet database user's privileges.
Fix
The fix corrects the type flag bits for RAW and ROW_TUPLE. RAW is no longer treated as a quoted client value, so validation rejects client-supplied raw bind variables. Older clients that still send numeric 2084 are treated as ordinary quoted values and escaped.
Patch: vitessio/vitess#19920. Upgrade to 23.0.4 / 24.0.0 or later. For older EOL lines, restrict VTGate gRPC access to trusted clients.