Add safe subprocess helpers and the full --check-safety audit - #3
Merged
Conversation
Credentials reach psql and pg_dump through the environment only. A single run_command() is the one child-process call site in the file, so the credential handling can be reviewed at a glance. safe_env() inherits the operator's environment, so their PGSSLMODE and PGSSLROOTCERT keep working, strips every DBPROFILER_* variable so no child sees the raw URL or the token key, overlays the parsed connection settings, and pins LC_ALL. redact_error() scrubs connection URLs, password assignments, and libpq env values from child stderr before it is surfaced. PGPASSWORD is replaced however short; other values only at length >= 4, because blind-replacing a two-character PGUSER shreds unrelated words and destroys the diagnostic. PGPORT is never replaced -- a bare port number matches too much text. --check-safety now audits the source, not just the SQL. It parses this file with ast and requires exactly one subprocess call site, an explicit env= on it, no shell=, and no credentialed connection URL in any string literal. Three deviations from the plan: The allowlist has no pg_catalog wildcard. That schema is not a safety boundary: it holds role password hashes (pg_authid, pg_shadow), user data (pg_largeobject), raw statistic values without the permission filtering the pg_stats view applies (pg_statistic), and two relations whose option fields carry passwords (pg_subscription, pg_user_mapping). Relations are enumerated one by one, with a DENIED_RELATIONS map recording a reason per trap, and a schema qualifier other than pg_catalog is itself a violation. The psql arguments drop -A and -F,. --csv is its own output format and -A placed after it overrides it, which would have made the parse order-dependent. Both commands gain -w. Without it a missing password blocks the child on a terminal read, and the run fails by timeout instead of immediately. ON_ERROR_STOP turns a SQL error into a nonzero exit rather than a silently empty result. pg_inherits is deliberately absent: task 5 reads it, and a relation is allowlisted in the same change as its collector. 92 tests pass. Co-Authored-By: roachdev-claude <roachdev-claude-bot@cockroachlabs.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
EOF2
Task 3 of the MVP plan. Credentials now flow to
psqlandpg_dumpthrough theenvironment only, and
--check-safetygrew from a token scan into a real audit ofthis file's own SQL and child-process usage.
Subprocess helpers
A single
run_command()is the onlysubprocess.runin the file. That is the point:one call site is what makes the credential handling reviewable at a glance, and
--check-safetynow enforces it mechanically.safe_env()starts from our own environment, so an operator'sPGSSLMODEandPGSSLROOTCERTkeep working, drops everyDBPROFILER_*variable so no child eversees the raw URL or the token key, overlays the parsed connection settings, and pins
LC_ALL=Cso error text does not depend on the operator's locale.On top of it:
run_psql,run_psql_scalar,run_pg_dump, andprobe_server_version— which finally gives the version gate from task 2 a caller.Redaction
redact_error()scrubs connection URLs,password=/PGPASSWORD=assignments, andlibpq env values (longest first) out of child stderr before it is surfaced. Assume
every error path is printed.
PGPASSWORDis replaced however short. Other libpq values are replaced only atlength >= 4, because blind-replacing a two-character
PGUSERshreds unrelated wordsand destroys the diagnostic.
PGPORTis never replaced — a bare port number matchesfar too much text. There is a test for each of those three behaviours.
--check-safetynow audits the source, not just the SQLIt parses this file with
astrather than grepping it, and requires exactly onesubprocess call site, an explicit
env=on it, noshell=, and no credentialedconnection URL in any string literal. A bare
postgres://...placeholder in adocstring stays legal; one with an
@does not.Deviations from the plan
The allowlist has no
pg_catalogwildcard. The plan called for one. It shouldnot have:
pg_catalogis not a safety boundary. It holds role password hashes(
pg_authid,pg_shadow), actual user data (pg_largeobject), raw statistic valueswithout the per-permission filtering the
pg_statsview applies (pg_statistic),and two relations whose option fields carry passwords (
pg_subscription,pg_user_mapping). Relations are now enumerated one by one, with aDENIED_RELATIONSmap recording a reason per trap, and a schema qualifier other thanpg_catalogis itself a violation — soevil.pg_classis rejected even thoughpg_classis allowed. Set-returning functions get their own small allowlist, soFROM unnest(...)passes andFROM pg_read_file(...)does not.Dropped
-Aand-F,from the psql arguments.--csvis its own output format,and
-Aplaced after it overrides it. Keeping both would have made the parseorder-dependent.
Added
-wto both commands, andON_ERROR_STOP=1. Without-wa missingpassword blocks the child on a terminal read, and the run fails by timeout instead of
immediately.
ON_ERROR_STOPturns a SQL error into a nonzero exit rather than asilently empty result set.
pg_inheritsis deliberately still absent. Task 5 reads it, and the house rule isthat a relation is allowlisted in the same change as its collector.
Verification
python3 dbprofiler.py --check-safetyexits 0.ruff checkclean.S608added to the test per-file ignores: the safety testsplant deliberately unsafe SQL to prove the guard rejects it.
produces all three expected violations.