mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2026-10-27 15:10:05 -05:00
fix(security): reject ambiguous backslash-escape in import/export query guard
_is_query_parens_balanced() always treated \' inside a single-quoted string as an escaped quote, matching psql only when standard_conforming_strings=off. Under the actual default (scs=on, every supported PostgreSQL version), psql treats \ as a literal character, so 'a\' closes the string right there. A crafted query export payload like SELECT 'a\') TO PROGRAM 'cmd' x' passed the balance check while the real ) it hid closed the wrapping \copy (...) context in psql, exposing a live TO PROGRAM clause for RCE. Since the correct interpretation depends on a server setting we can't reliably know, reject any backslash inside a single-quoted string outright instead of guessing. Reported by Arpit Jain (arpitjain099).
This commit is contained in:
1 parent
ef76102bcd
commit
1496fabe28
2 files changed
+37
-20
No files matched your search
@@ -281,12 +281,18 @@ def _is_query_parens_balanced(query):
|
||||
skip content inside those constructs either, or an attacker can
|
||||
hide an unbalanced ) inside them to bypass validation.
|
||||
|
||||
Backslash escapes (\\x) are always handled inside single-quoted
|
||||
strings. psql enables this for E'...' strings unconditionally
|
||||
and for all strings when standard_conforming_strings=off. By
|
||||
always handling backslashes, we match psql in all modes. This
|
||||
may reject some valid queries (e.g. '\\') with scs=on) but
|
||||
will never accept a dangerous one.
|
||||
Backslash handling inside single-quoted strings is ambiguous:
|
||||
psql treats \\ as a plain character when
|
||||
standard_conforming_strings=on (the default since PG 9.1, and
|
||||
the default on every PostgreSQL version pgAdmin 4 supports), but
|
||||
as an escape character when scs=off or inside E'...' strings.
|
||||
Silently picking one interpretation can let a crafted query be
|
||||
accepted here while psql parses it differently (e.g. a \\'
|
||||
that we treat as "still inside the string" while psql, under
|
||||
the default scs=on, treats it as the string's closing quote,
|
||||
exposing a real ')' immediately after). Rather than guess the
|
||||
server's scs setting, we reject any query containing a
|
||||
backslash inside a single-quoted string outright.
|
||||
"""
|
||||
depth = 0
|
||||
i = 0
|
||||
@@ -296,14 +302,15 @@ def _is_query_parens_balanced(query):
|
||||
ch = query[i]
|
||||
|
||||
# Single-quoted string literal: skip to closing quote.
|
||||
# Handles both '' and \' escape sequences to match psql's
|
||||
# strtokx which enables backslash escaping for E-strings
|
||||
# (always) and all strings when scs=off.
|
||||
# Handles the '' escape sequence to match psql's strtokx.
|
||||
# A bare backslash is rejected outright (see docstring):
|
||||
# its meaning depends on standard_conforming_strings, which
|
||||
# we cannot reliably know here, so we refuse to guess.
|
||||
if ch == "'":
|
||||
i += 1
|
||||
while i < length:
|
||||
if query[i] == '\\':
|
||||
i += 2 # backslash escape: skip next char
|
||||
return False # ambiguous escape: reject
|
||||
elif query[i] == "'":
|
||||
if i + 1 < length and query[i + 1] == "'":
|
||||
i += 2 # '' escape
|
||||
|
||||
@@ -86,25 +86,35 @@ class IEQueryParensBalancedTest(BaseTestGenerator):
|
||||
dict(query="SELECT ')", expected=False)),
|
||||
('Rejected: unterminated double-quoted identifier',
|
||||
dict(query='SELECT "col)', expected=False)),
|
||||
# The parser handles backslash escapes inside single-quoted
|
||||
# strings unconditionally (matching E-string and scs=off
|
||||
# semantics), so the close paren is correctly identified as
|
||||
# being inside the string literal. Original test labelled this
|
||||
# as a "false positive" expecting over-rejection; in fact the
|
||||
# parser is precise here. Keep the case to document handling.
|
||||
('Balanced: E-string with escaped quote and paren inside literal',
|
||||
dict(query="SELECT E'\\')'", expected=True)),
|
||||
# Whether a backslash inside a single-quoted string is an
|
||||
# escape character depends on standard_conforming_strings,
|
||||
# which the parser cannot know. Rather than guess (and risk
|
||||
# disagreeing with psql's actual parse), any backslash inside
|
||||
# a single-quoted string is rejected outright.
|
||||
('Rejected: backslash inside single-quoted string is ambiguous',
|
||||
dict(query="SELECT E'\\')'", expected=False)),
|
||||
('Unbalanced: E-string RCE bypass',
|
||||
dict(query="E'\\'' ) TO PROGRAM 'cmd' --'",
|
||||
expected=False)),
|
||||
('Unbalanced: scs=off backslash escape bypass',
|
||||
dict(query="'\\'' ) TO PROGRAM 'cmd' --'",
|
||||
expected=False)),
|
||||
('Balanced: backslash-backslash then close quote',
|
||||
dict(query="SELECT '\\\\' FROM t", expected=True)),
|
||||
('Rejected: backslash-backslash inside string is ambiguous',
|
||||
dict(query="SELECT '\\\\' FROM t", expected=False)),
|
||||
('Unbalanced: injection split across lines',
|
||||
dict(query="SELECT 1)\nTO PROGRAM 'id' --(",
|
||||
expected=False)),
|
||||
# CVE regression: real psql (standard_conforming_strings=on,
|
||||
# the default) treats \\ as a literal character, so 'a\\'
|
||||
# closes the string literal at that second quote -- it does
|
||||
# NOT escape it. The unescaped ')' immediately after is then
|
||||
# live and closes the wrapping \\copy (...) context, exposing
|
||||
# a TO PROGRAM clause for RCE. The old parser treated \\'
|
||||
# as "still inside the string" unconditionally and missed
|
||||
# this. Reported by Arpit Jain (arpitjain099), 2026-07-27.
|
||||
('Unbalanced: scs=on backslash-quote RCE bypass (CVE)',
|
||||
dict(query="SELECT 'a\\') TO PROGRAM 'echo pwned' x'",
|
||||
expected=False)),
|
||||
]
|
||||
|
||||
def runTest(self):
|
||||
|
||||
Reference in new issue
Block a user