Skip to content

Commit bcf3c56

Browse files
ggnmstrJelteF
andauthored
fix: do not allow relative path in DuckDB COPY statements (#827)
Vanilla PostgreSQL does not allow COPY statements with relative path as it may lead to overriding database files and result in database corruption. This puts the same restriction on pg_duckdb. It's important to continue to allow COPY to URIs though. --------- Co-authored-by: Jelte Fennema-Nio <jelte@motherduck.com>
1 parent 4573ff0 commit bcf3c56

File tree

4 files changed

+45
-4
lines changed

4 files changed

+45
-4
lines changed

src/utility/copy.cpp

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,27 @@ StringOneOfInternal(const char *str, const char *compare_to[], int length_of_com
235235
return false;
236236
}
237237

238+
bool
239+
MatchesURIScheme(const char *str) {
240+
if (str == NULL) {
241+
return false;
242+
}
243+
244+
const char *p = str;
245+
246+
// First character must be a letter
247+
if (!isalpha(*p))
248+
return false;
249+
p++;
250+
251+
// Continue with alphanumeric
252+
while (*p && (isalnum(*p)))
253+
p++;
254+
255+
// Must be followed by ://
256+
return (strncmp(p, "://", 3) == 0);
257+
}
258+
238259
#define StringOneOf(str, compare_to) StringOneOfInternal(str, compare_to, lengthof(compare_to))
239260

240261
static bool
@@ -262,6 +283,11 @@ IsAllowedStatement(CopyStmt *stmt, bool throw_error = false) {
262283
return false;
263284
}
264285

286+
if (!stmt->is_from && !is_absolute_path(stmt->filename) && !MatchesURIScheme(stmt->filename)) {
287+
ereport(elevel, (errcode(ERRCODE_INVALID_NAME), errmsg("relative path not allowed for COPY to file")));
288+
return false;
289+
}
290+
265291
return true;
266292
}
267293

test/pycheck/copy_test.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,13 @@ def test_copy_to_local(cur: Cursor, tmp_path: Path):
3636
f"COPY test_table TO '{csv_path}' WITH (FORMAT CSV, UNKNOWN_OPTION true)"
3737
)
3838

39+
# copying to relative paths is not allowed though, in accordance with
40+
# Postgres behaviour to avoid overwriting datababase file accidentally.
41+
with pytest.raises(
42+
psycopg.errors.InvalidName, match="relative path not allowed for COPY to file"
43+
):
44+
cur.sql("COPY test_table TO 'test_copy.csv' WITH (FORMAT CSV)")
45+
3946
# Disabling duckdb.force_execution makes the query fail with a different
4047
# error.
4148
cur.sql("SET duckdb.force_execution = false")
@@ -56,6 +63,14 @@ def test_copy_to_local(cur: Cursor, tmp_path: Path):
5663
(2, "Bob"),
5764
]
5865

66+
# Again relative paths are not allowed for COPY TO, this becomes an
67+
# internal error though, due to our failure to propagate error codes
68+
# correctly.
69+
with pytest.raises(
70+
psycopg.errors.InternalError, match="relative path not allowed for COPY to file"
71+
):
72+
cur.sql("COPY test_table TO 'test_copy.parquet' WITH (FORMAT PARQUET)")
73+
5974
# We can copy the result of a DuckDB query using Postgres its COPY logic
6075
cur.sql(
6176
f"COPY (select * from duckdb.query($$ select 123 as xyz $$)) TO '{csv_path}'"
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
SET duckdb.force_execution = TRUE;
2-
COPY (SELECT 1 INTO frak UNION SELECT 2) TO 'blob';
2+
COPY (SELECT 1 INTO frak UNION SELECT 2) TO '/blob';
33
ERROR: COPY (SELECT INTO) is not supported
4-
COPY (SELECT 1 INTO frak UNION SELECT 2) TO 'blob.parquet';
4+
COPY (SELECT 1 INTO frak UNION SELECT 2) TO '/blob.parquet';
55
ERROR: (PGDuckDB/DuckdbUtilityHook_Cpp) Executor Error: (PGDuckDB/MakeDuckdbCopyQuery) DuckDB COPY only supports SELECT statements

test/regression/sql/issue_789.sql

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
SET duckdb.force_execution = TRUE;
22

3-
COPY (SELECT 1 INTO frak UNION SELECT 2) TO 'blob';
4-
COPY (SELECT 1 INTO frak UNION SELECT 2) TO 'blob.parquet';
3+
COPY (SELECT 1 INTO frak UNION SELECT 2) TO '/blob';
4+
COPY (SELECT 1 INTO frak UNION SELECT 2) TO '/blob.parquet';

0 commit comments

Comments
 (0)