Skip to content

Commit 151de82

Browse files
authored
Strip psql meta-commands on load (#736)
Strip psql meta-commands from schema files to fix `dbmate load` failures on newer PostgreSQL versions. PostgreSQL 15.14+/16.10+/17.6+ `pg_dump` outputs `\restrict` and `\unrestrict` commands as a security measure (CVE-2025-8714). These are `psql` client meta-commands, not SQL statements, causing `sqlDB.Exec()` to fail with a syntax error when `dbmate load` attempts to execute them directly against the database server. This PR adds a filter to remove these lines before execution. Fixes #735
1 parent b54ca34 commit 151de82

3 files changed

Lines changed: 99 additions & 0 deletions

File tree

pkg/dbmate/db.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,13 @@ func (db *DB) LoadSchema() error {
247247
return err
248248
}
249249

250+
// Strip psql meta-commands (e.g., \restrict, \unrestrict) that cannot be
251+
// executed directly against the database server.
252+
bytes, err = dbutil.StripPsqlMetaCommands(bytes)
253+
if err != nil {
254+
return err
255+
}
256+
250257
result, err := sqlDB.Exec(string(bytes))
251258
if err != nil {
252259
return err

pkg/dbutil/dbutil.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,48 @@ func TrimLeadingSQLComments(data []byte) ([]byte, error) {
9797
return out.Bytes(), nil
9898
}
9999

100+
// StripPsqlMetaCommands removes psql meta-commands (backslash commands) from SQL.
101+
// PostgreSQL 15.14+/16.10+/17.6+ pg_dump adds \restrict and \unrestrict commands
102+
// to schema dumps as a security measure (CVE-2025-8714). These are psql client
103+
// commands that cannot be executed directly against the PostgreSQL server.
104+
//
105+
// Note: This is a naive line-based implementation that does not parse SQL properly.
106+
// It will incorrectly strip lines inside multi-line string literals if they start
107+
// with a backslash, e.g.: INSERT INTO t VALUES ('line1\n\line2');
108+
// In practice this is not an issue because:
109+
// - This function is only used to process pg_dump schema output
110+
// - pg_dump generates DDL statements which rarely contain multi-line string literals
111+
// - pg_dump uses proper escaping (E'...' or $$...$$) for complex strings
112+
func StripPsqlMetaCommands(data []byte) ([]byte, error) {
113+
out := bytes.NewBuffer(make([]byte, 0, len(data)))
114+
115+
scanner := bufio.NewScanner(bytes.NewReader(data))
116+
for scanner.Scan() {
117+
line := scanner.Bytes()
118+
119+
// Skip lines that are psql meta-commands (\restrict, \unrestrict, etc.)
120+
// This naive approach works for pg_dump output but could theoretically
121+
// break on hand-crafted SQL with multi-line strings containing backslashes.
122+
trimmed := bytes.TrimLeftFunc(line, unicode.IsSpace)
123+
if len(trimmed) > 0 && trimmed[0] == '\\' {
124+
continue
125+
}
126+
127+
// copy line to output buffer
128+
if _, err := out.Write(line); err != nil {
129+
return nil, err
130+
}
131+
if _, err := out.WriteString("\n"); err != nil {
132+
return nil, err
133+
}
134+
}
135+
if err := scanner.Err(); err != nil {
136+
return nil, err
137+
}
138+
139+
return out.Bytes(), nil
140+
}
141+
100142
// QueryColumn runs a SQL statement and returns a slice of strings
101143
// it is assumed that the statement returns only one column
102144
// e.g. schema_migrations table

pkg/dbutil/dbutil_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,56 @@ func TestTrimLeadingSQLComments(t *testing.T) {
3636
require.Equal(t, "real stuff\n-- end\n", string(out))
3737
}
3838

39+
func TestStripPsqlMetaCommands(t *testing.T) {
40+
t.Run("strips restrict and unrestrict", func(t *testing.T) {
41+
in := "\\restrict dbmate\n" +
42+
"-- comment\n" +
43+
"SET statement_timeout = 0;\n" +
44+
"CREATE TABLE users (id int);\n" +
45+
"\\unrestrict dbmate\n"
46+
out, err := dbutil.StripPsqlMetaCommands([]byte(in))
47+
require.NoError(t, err)
48+
expected := "-- comment\n" +
49+
"SET statement_timeout = 0;\n" +
50+
"CREATE TABLE users (id int);\n"
51+
require.Equal(t, expected, string(out))
52+
})
53+
54+
t.Run("strips indented backslash commands", func(t *testing.T) {
55+
in := " \\restrict dbmate\n" +
56+
"SELECT 1;\n" +
57+
"\t\\unrestrict dbmate\n"
58+
out, err := dbutil.StripPsqlMetaCommands([]byte(in))
59+
require.NoError(t, err)
60+
require.Equal(t, "SELECT 1;\n", string(out))
61+
})
62+
63+
t.Run("preserves non-backslash content", func(t *testing.T) {
64+
in := "-- This is a comment\n" +
65+
"CREATE TABLE test (name varchar(100));\n" +
66+
"INSERT INTO test VALUES ('hello\\world');\n"
67+
out, err := dbutil.StripPsqlMetaCommands([]byte(in))
68+
require.NoError(t, err)
69+
require.Equal(t, in, string(out))
70+
})
71+
72+
t.Run("handles empty input", func(t *testing.T) {
73+
out, err := dbutil.StripPsqlMetaCommands([]byte(""))
74+
require.NoError(t, err)
75+
require.Equal(t, "", string(out))
76+
})
77+
78+
t.Run("strips all backslash commands", func(t *testing.T) {
79+
// Any line starting with backslash is a psql meta-command
80+
in := "\\connect dbname\n" +
81+
"SELECT 1;\n" +
82+
"\\quit\n"
83+
out, err := dbutil.StripPsqlMetaCommands([]byte(in))
84+
require.NoError(t, err)
85+
require.Equal(t, "SELECT 1;\n", string(out))
86+
})
87+
}
88+
3989
// connect to in-memory sqlite database for testing
4090
const sqliteMemoryDB = "file:dbutil.sqlite3?mode=memory&cache=shared"
4191

0 commit comments

Comments
 (0)