By clicking “Accept”, you agree to the storing of cookies on your device to enhance site navigation, analyze site usage, and assist in our marketing efforts. View our Privacy Policy for more information.
18px_cookie
e-remove
Blog
Glossary
Customer Story
Video
eBook / Report
Solution Brief

When Regex Isn’t Enough: How We Discovered CVE-2025-13780 in pgAdmin

CVE-2025-13780 is a critical vulnerability in pgAdmin 4 where whitespace characters bypass regex filters, a common failure mode in input validation.

CVE-2025-13780 is a critical vulnerability in pgAdmin 4 where whitespace characters bypass regex filters, a common failure mode in input validation.

CVE-2025-13780 is a critical vulnerability in pgAdmin 4 where whitespace characters bypass regex filters, a common failure mode in input validation.

Written by
Meenakshi S L
Meenakshi S L
Published on
December 12, 2025

CVE-2025-13780 is a critical vulnerability in pgAdmin 4 where whitespace characters bypass regex filters, a common failure mode in input validation.

CVE-2025-13780 is a critical vulnerability in pgAdmin 4 where whitespace characters bypass regex filters, a common failure mode in input validation.

pgAdmin 4's plain-text restore feature relied on a regex-based check to block dangerous psql meta-commands from uploaded database dumps. Our research into CVE-2025-13780 demonstrates how strategically placed whitespace characters bypass this filter entirely, allowing an attacker to execute shell commands on the pgAdmin host.

This post walks through how we got there:

  • How the original “regex firewall” around plain SQL restore worked and why it failed.
  • Concrete payloads that bypassed the filter and yielded remote code execution (RCE).
  • What pgAdmin changed in 9.11 (issue #9368) by running restore with \restrict.
  • Why regex-based patches are almost always the wrong abstraction.

For reference, CVE-2025-13780 is tracked at the NVD entry for CVE-2025-13780. PoC can be obtained here.

Background

When you use pgAdmin 4 to restore a PLAIN format dump, it does not talk directly to PostgreSQL over the protocol. Instead, pgAdmin launches the psql client on the pgAdmin host and feeds it the uploaded SQL file.

That has an important consequence: psql is not just a “dumb” SQL runner. It has its own mini-language of meta-commands, all of which are prefixed by a backslash:

  • \! /bin/sh -c "..." – run arbitrary shell commands.
  • \copy ... PROGRAM '...' – spawn processes via PROGRAM.
  • \include, \ir, \set, and many others.

From a security point of view, if an attacker controls a file that pgAdmin hands to psql, then they control a script for a fully capable command interpreter, not just SQL.

When we started looking at pgAdmin’s plain restore path, it was clear that any gap in the meta-command detection would immediately turn into code execution on the pgAdmin host. 

Historically, pgAdmin tried to mitigate this risk with a Python helper that scanned the uploaded file for meta-commands and refused to restore if any were present. Our research showed that this helper was not strict enough, leaving room for meta-command injection.

The original guardrail

The original check in web/pgadmin/tools/restore/__init__.py looked broadly like this (simplified):

META_CMD_RE = re.compile(br'(^|\n)[ \t]*\\')
def has_meta_commands(path):
    data = open(path, 'rb').read()
    return bool(META_CMD_RE.search(data))

Conceptually, the approach is:

  1. Only treat backslashes at the start of a line as meta-commands.
  2. Allow leading spaces/tabs after a newline.
  3. Anything else is “safe” SQL.

If the regex matched, pgAdmin would reject the upload before it ever reached the background restore job.

There are three big problems with this:

  1. psql’s idea of “whitespace and line boundaries” is much richer than \n plus [ \t].
  2. The check ran on raw bytes, not on a normalized view of the text.
  3. The regex expresses only a subset of psql’s grammar, so any mismatch between the two becomes an attack surface.

Exploiting the gaps

In our test setup (macOS 14, PostgreSQL 16.11, pgAdmin 4 v9.10 in server mode), we crafted a series of dumps that were valid from psql’s perspective but invisible to the regex.

CVE-2025-13780 PoC

One of the simplest payloads we ended up with was crlf_attack.sql:

CREATE TABLE IF NOT EXISTS crlf_attack_log(
    ts   TIMESTAMP DEFAULT now(),
    note TEXT
);
-- The next logical line is: LF, CR, then backslash-bang
\n\r\! /bin/sh -c "echo crlf_attack > /tmp/pgadmin_crlf"
INSERT INTO crlf_attack_log(note)
VALUES ('crlf attack attempted');

Key observations:

  • The regex only cares about \n followed by spaces/tabs and then \.
  • We inject \r between the newline and the backslash, so the bytes do not match \n[ \t]*\\.
  • psql, however, happily treats this as a new logical line starting with \!.

From the pgAdmin web UI we:

  1. Uploaded the SQL as a user-owned file (crlf_attack.sql).
  2. Triggered a plain restore via the /restore/job/ endpoint.

Within seconds, /tmp/pgadmin_crlf appeared on the pgAdmin host.
psql had executed the attacker-controlled \! command, despite the “meta-command filter” being enabled and passing.

Other whitespace variants

The same pattern worked when we swapped in other characters that psql treats as whitespace or line breaks, but the regex does not:

  • Vertical Tab (\x0b) – a classic C whitespace character.
  • Form Feed (\x0c) – used as a page break but also “empty space” for many parsers.
  • Bare \r – old Mac-style line endings.

By mixing and matching these, you end up with many permutations where:

  • psql sees “start of logical line, then optional whitespace, then \!”.
  • The regex sees “some random byte soup not matching (^|\n)[ \t]\\”.

This is a fundamental misalignment between what’s being filtered and what the actual interpreter will execute.

Why regex-based patches keep failing

This is not an isolated story. Regex-shaped “parsers” often fail in the same ways:

  1. They under-approximate the target grammar. You write a pattern for the obvious cases, but real-world parsers accept more constructs (extra whitespace, Unicode normals, alternative encodings, nested quoting).
  1. They operate on the wrong abstraction. A regex sees bytes or code units; the runtime sees decoded characters, normalized newlines, and semantic tokens.
  1. They are bolted on at the edges. Instead of constraining the dangerous behavior where it actually executes (inside psql), you try to pre-sanitize at an earlier layer, leading to a TOCTOU vulnerability (time-of-check to time-of-use)

Attackers exploit the difference between these two worlds. In CVE-2025-13780, the difference was “what counts as the start of a line, and what counts as whitespace before a meta-command.” But the pattern is general:

  • Any time you try to parse SQL, HTML, shell, or a bespoke DSL with a handful of regexes, you’re betting that you fully understand and can continuously track the real parser’s behavior.
  • That bet rarely pays off long-term, especially across versions, locales, Unicode evolutions, and character encodings.

What pgAdmin changed in 9.11

Starting with pgAdmin 4 v9.11, the project introduces an additional mitigation for plain SQL restores via issue #9368: run the plain restore with the \restrict option.

The pgAdmin behavior change is:

  • For PLAIN-format restores, the backend now launches psql with a \restrict directive applied to the session/script.
  • Even if an attacker smuggles \! or similarly dangerous commands past any pre-filter, psql itself will refuse to execute them in restricted mode.

This is a fundamentally different kind of fix from the earlier regex approach:

  • The enforcement moves from “a Python regex scanning bytes before execution” to “a mode bit inside the actual interpreter that decides what is allowed to run.”
  • It no longer depends on pgAdmin precisely recreating psql’s whitespace/encoding rules.
  • Future grammar changes in psql land on the \restrict implementation, not in scattered regexes in downstream tools.

In other words, pgAdmin is now delegating security policy to the component that actually understands and executes the language.

Actions pgAdmin users should take

For teams running pgAdmin 4 today, our recommendations are:

  • Upgrade to a fixed version
  • Ensure psql supports \restrict
  • Treat plain-text restores as high risk
  • Harden where pgAdmin runs
  • Monitor and log restores

Key takeaways

The main takeaway from CVE-2025-13780 is not that regex is bad in the abstract. It’s that regex-based patches are almost always the wrong tool for input validation. Better patterns include:

  • Constrain execution at the source. Use options like \restrict, sandbox modes, or dedicated “safe” execution paths rather than trying to detect every dangerous construct ahead of time.
  • Normalize early, parse once. If you must inspect content (for auditing or UX), operate on the same normalized representation that the interpreter will use, and fail closed on ambiguity.
  • Assume your pre-filters are bypassable.
    Treat regex guards as helpful heuristics, not as the primary security boundary.

For pgAdmin, shifting from “regex-based meta-command detection” to “psql running in \restrict mode” represents exactly this kind of architectural improvement. It takes the burden of perfectly tracking psql’s evolving grammar off pgAdmin and puts security policy where it belongs: inside the component that actually executes the script.

Malicious Package Detection

Detect and block malware

Find out More

The Challenge

The Solution

The Impact

Book a Demo

Book a Demo

Book a Demo

Welcome to the resistance
Oops! Something went wrong while submitting the form.

Book a Demo

Book a Demo

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.

Book a Demo