Teach psql to skip in-line COPY ... FROM STDIN data after a failure.

If the COPY command fails before sending PGRES_COPY_IN, psql did
not realize that it ought to consume any in-line data following
the command.  Failing to do so leads to trying to execute that
data as SQL commands, which in the best case is wrong and in the
worst case is a SQL-injection hazard.

To fix:

1. Extend psqlscan.l to recognize COPY ... FROM STDIN.  This can
be done with a pretty simple extension to the logic that already
recognizes nested BEGIN blocks within CREATE FUNCTION et al.
But unlike that case, we need to consider and count multiple COPY
commands within a single query string (separated by "\;").  The
fallout from that is that psql_scan_reset must now always be called
before starting a new query string.  (The comment for it that claimed
we didn't need that because "the scan state must be INITIAL" was
really obsolete already, since it has long reset more state besides
start_state.)

2. Teach handleCopyIn() to read and discard data when passed
NULL for "conn".

3. Add logic to SendQuery() to call handleCopyIn() that way
if the query string contained COPY ... FROM STDIN command(s)
that remain unaccounted-for at the end.

Now that we have this counting logic, we can also detect
if the backend sends an unexpected PGRES_COPY_IN message.
That should never happen, but perhaps a malicious server
could try to extract data that way.

A side-effect of doing this is that we have to adjust a number of test
scripts that thought they needn't write "\." after a COPY FROM STDIN
that they expect to fail.  On the whole this is an improvement, since
there's now a uniform rule "write \. after COPY FROM STDIN, whether
you expect it to work or not".  But it is an annoying amount of test
churn.

A loose end in this patch is that if it has to skip data, it assumes
that that data is text not binary.  It seems unduly difficult to
detect whether the COPY command requested binary (we could handle the
old-style COPY BINARY ... syntax, but not the new style with format
options).  In practice, copying in-line binary data is unsupported
anyway, because there's no way to write an end marker: the textual
terminator sequence "\n\\.\n" could appear in binary data and there's
no provision for escaping it, so neither psql nor the server look for
it when in binary mode.

Reported-by: Alexander Lakhin <[email protected]>
Author: Tom Lane <[email protected]>
Reviewed-by: Noah Misch <[email protected]>
Backpatch-through: 14
Security: CVE-2026-6464

Branch
------
REL_17_STABLE

Details
-------
https://git.postgresql.org/pg/commitdiff/46fa1f6f373865fae7553043790cd17944893ac3
Author: Tom Lane <[email protected]>

Modified Files
--------------
src/bin/psql/common.c                | 109 ++++++++++++++--
src/bin/psql/common.h                |   2 +-
src/bin/psql/copy.c                  |  48 ++++---
src/bin/psql/mainloop.c              |  15 ++-
src/bin/psql/psqlscanslash.l         |  18 +--
src/bin/psql/startup.c               |   2 +-
src/fe_utils/psqlscan.l              | 243 ++++++++++++++++++++++++++---------
src/include/fe_utils/psqlscan.h      |   2 +
src/include/fe_utils/psqlscan_int.h  |  15 ++-
src/test/regress/expected/copy.out   |   4 -
src/test/regress/expected/psql.out   |   5 +
src/test/regress/sql/alter_table.sql |   2 +
src/test/regress/sql/copy.sql        |   1 +
src/test/regress/sql/copy2.sql       |  44 +++++++
src/test/regress/sql/copyselect.sql  |   1 +
src/test/regress/sql/generated.sql   |   4 +
src/test/regress/sql/privileges.sql  |   3 +
src/test/regress/sql/psql.sql        |   9 ++
src/test/regress/sql/rowsecurity.sql |   4 +
19 files changed, 413 insertions(+), 118 deletions(-)

Reply via email to