← Notes

Vitess Type_RAW Bind Variable SQL Injection

Overview

AdvisoryGHSA-fr6v-4xmf-vvjx
ComponentVTGate gRPC bind variables, VTTablet query generation
AffectedVitess 18.0.7-18.0.8, 19.0.6-19.0.10, 20.0.2-<23.0.4, and 24.0.0-rc1
FixedVitess 23.0.4 and 24.0.0
ClassCWE-89 (SQL Injection)
CVSS v3.16.5 (Moderate) -- CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N
Found byLukas 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.