Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions cli/db_restore.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@
re.compile(r"^\s*REVOKE\b", re.IGNORECASE),
re.compile(r"^\s*ALTER\s+DEFAULT\s+PRIVILEGES\b", re.IGNORECASE),
)
PSQL_META_COMMAND_PATTERNS = (
# Newer pg_dump versions emit these psql-only commands for safer restores.
# Older local psql clients reject them, so drop them from staged restores.
re.compile(r"^\s*\\restrict\b", re.IGNORECASE),
re.compile(r"^\s*\\unrestrict\b", re.IGNORECASE),
)


class LocalDbRestoreError(RuntimeError):
Expand Down Expand Up @@ -81,9 +87,13 @@ def _sanitize_sql_dump(source_path: Path, target_path: Path) -> None:
with open(source_path, "r", encoding="utf-8") as infile:
with open(target_path, "w", encoding="utf-8") as outfile:
for line in infile:
if any(
matches_role_sql = any(
pattern.search(line) for pattern in ROLE_DEPENDENT_SQL_PATTERNS
):
)
matches_psql_meta = any(
pattern.search(line) for pattern in PSQL_META_COMMAND_PATTERNS
)
if matches_role_sql or matches_psql_meta:
Comment on lines +93 to +96
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Do not strip pg_dump's \restrict safety guard

PostgreSQL 17.6 added \restrict specifically to block injected psql meta-commands during plain-text restores (see the 17.6 release notes for CVE-2025-8714, and app-psql's note that restricted mode only allows \unrestrict). Because this sanitizer removes those lines and then still feeds the file to psql -f, any dump restored from GCS or another compromised source can once again execute meta-commands such as \! on the operator's machine. For older local clients we should fail fast or require a newer psql, not silently discard the protection.

Useful? React with 👍 / 👎.

continue
outfile.write(line)
except UnicodeError as exc:
Expand Down Expand Up @@ -235,6 +245,7 @@ def restore_local_db_from_sql(
) from exc

return LocalDbRestoreResult(
sql_file=staged_sql_file,
Comment on lines 247 to +248
Comment on lines 247 to +248
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid returning a staged SQL path that is already deleted

result.sql_file now points at staged_sql_file, but _stage_restore_source() creates that file inside a TemporaryDirectory() and removes it before restore_local_db_from_sql() returns. Any caller that tries to inspect or reuse result.sql_file after a successful restore will immediately hit a missing file, so the new field does not actually provide the staged SQL path advertised by LocalDbRestoreResult.

Useful? React with 👍 / 👎.

source=source_description,
host=host,
port=port,
Expand Down
2 changes: 2 additions & 0 deletions tests/test_cli_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,10 +164,12 @@ def fake_associate(source_directory):
def test_restore_local_db_invokes_psql(monkeypatch, tmp_path):
sql_file = tmp_path / "restore.sql"
sql_file.write_text(
"\\restrict abc123\n"
"SET ROLE ocotillo;\n"
"ALTER TABLE public.sample OWNER TO ocotillo;\n"
"GRANT ALL ON TABLE public.sample TO ocotillo;\n"
"select 1;\n"
"\\unrestrict abc123\n"
)
captured: dict[str, object] = {}
call_order: list[str] = []
Expand Down
Loading