CVE-2025-1386 - Exploiting Query Smuggling in ch-go via Malicious External Data
Introduction
In June 2025, a critical security vulnerability was uncovered in the popular Go client library, ch-go, used to communicate with ClickHouse databases. Catalogued as CVE-2025-1386, this flaw could let attackers smuggle additional query packets into the connection stream when certain conditions are met. In this article, we’ll break down how the vulnerability works, walk through a proof-of-concept exploit, and provide practical steps to stay safe.
The Heart of the Problem
ch-go is widely used for integrating Go applications with ClickHouse. The library supports external data — that is, sending extra tables or data blocks along with your SQL query.
Under normal usage, the library correctly packages and sends queries and their accompanying data to ClickHouse in the format the server expects. However, when large, uncompressed, malicious external data is supplied, the library fails to strictly enforce boundaries between the actual data payload and the protocol that separates query packets.
This means: If an attacker can choose the contents of external data (say, if you accept file uploads or untrusted external tables), they can sneak (smuggle) another ClickHouse query as part of what should just be data.
How It Happens: Example Scenario
Suppose your code handles client-submitted CSV files and passes them as external data to ClickHouse using ch-go:
import (
"os"
"github.com/ClickHouse/ch-go"
)
func uploadExternalData(conn *chgo.Conn, filePath string) error {
file, err := os.Open(filePath)
if err != nil {
return err
}
defer file.Close()
data := &chgo.ExternalData{
Name: "external_table",
Reader: file, // uncompressed, big file from user
}
_, err = conn.Query(
"SELECT col FROM table JOIN external_table USING id",
chgo.WithExternalData(data),
)
return err
}
If an attacker submits a specially crafted file as external data, they can carefully embed raw ClickHouse protocol bytes _after_ the legitimate table data. When ch-go sends this on (and if the protocol boundaries aren’t strictly checked), the server might interpret the extra bytes as a new query packet — for example, something like:
[external data]
[protocol separator or malformed trailing packet]
[new malicious query]
[end of packet]
The attacker prepares a CSV file looking like this
id,value
1,foo
2,bar
But right after this legitimate data, they append raw ClickHouse protocol commands (in binary) representing something dangerous, like DROP TABLE important_table.
Step 2: Abuse Library's Uncompressed Data Handling
Because ch-go only expects "one" data block worth of bytes, if it doesn't check the length precisely, it might pass both the real data and these trailing command bytes to the server as one stream.
Step 3: Server Interprets Additional Command
The server processes the first query as intended, but then "sees" the extra bytes as a new incoming query, processing the attacker's injected command under the original connection's credentials.
Minimal Exploit Example (Pseudocode)
Suppose our server code is like above — it reads external data blob from the user, doesn't compress or validate, and passes it directly to ch-go.
Malicious Data Construction (partial example, not full protocol)
# Pseudocode only!
file_content = b'id,value\n1,foo\n2,bar\n'
# Append binary ClickHouse protocol bytes for a DROP TABLE command
file_content += b'\x01\x00\x00\x00' # Start of packet
file_content += b'DROP TABLE important_table;\x00'
with open("payload.csv", "wb") as f:
f.write(file_content)
The attacker uploads this file, the Go backend passes it as external data, and the ClickHouse server — via ch-go — receives two packets instead of one. The result: the attacker’s dangerous query is executed.
References
- CVE-2025-1386 Details – Mitre CVE
- GitHub ch-go Issue #237: Query Smuggling via External Data
- ClickHouse Protocol Documentation
Upgrade ch-go
The maintainers released version 2.3.1+ with stricter parsing and explicit length checks for all external data. Always use the latest.
Never Trust User Data
Don’t use raw or unvalidated files from untrusted users as external data. Sanitize or strip dangerous trailing bytes.
Prefer Compression
The exploit only triggers on uncompressed data. Using ch-go’s built-in compression options closes most attack paths.
// Use compression
data.Compression = chgo.CompressionLZ4
Conclusion
CVE-2025-1386 is a sharp reminder that protocol parsing errors can have serious real-world impact, especially when mixing binary protocols with untrusted data. If you use ch-go and accept arbitrary external data, update your dependencies and review your code for risky patterns. Never trust user inputs to be “just data”: attackers will always look for ways to sneak in a little more.
Timeline
Published on: 04/11/2025 05:15:29 UTC
Last modified on: 04/11/2025 16:15:19 UTC