SEC.CFG.STATEMENT_LOGGING — Statements and their values are written to the server log
- Category: privacy
- Severity: medium
- Level: 0
- Confidence: deterministic
- Downtime class: none — the finding is about a server parameter, not about a statement
- Stability: stable
- Suites: audit
- Applies to: PostgreSQL 18
What the value means
log_statement is an enum with four members, and two of them reach data.
| Value | What is logged | Does it carry row values? |
|---|---|---|
none | nothing | no |
ddl | schema statements | no — column names, never rows |
mod | statements that change data | yes — every row a migration or job writes |
all | every statement | yes — including every WHERE email = … |
ddl is where the line falls, and the reason is data rather than verbosity: a schema statement
names columns, so it says nothing about a person. ddl on a production server is an ordinary audit
trail, and reporting it would train people to ignore this rule.
Why the log is the problem rather than the setting
Nothing here is a defect. The parameter is doing exactly what it says, and the server is functioning correctly — which is why this is reported rather than prevented.
What changes is where the data lives. A log file rarely enjoys the database's own protection:
- it is read by operators who were never granted the tables it quotes,
- it is shipped to an aggregator that keeps it under a different retention policy,
- and it sits in backups that outlive the row an erasure request removed.
None of those paths involves a privilege on the database. The grant model you designed does not reach the file.
Why the environment decides
This is the only rule in the pack whose verdict depends on something outside the database, and it is the reason the rule exists in this shape rather than as a plain server-baseline check.
log_statement = all on a laptop is how people debug. The same value on a production instance is
personal data leaving the database's protection. The value cannot tell those apart, so a rule
that judged it alone would be muted within a week by the people it is meant to help.
So the rule establishes the environment first, and answers three-valued:
| What the run can establish | Answer |
|---|---|
| this is production | the value decides — all or mod is a finding |
| this is not production | pass, with its sentence — checked, and it exposes nothing here |
| nothing places it | undetermined, with the reason named |
That third row is the point. app.env unset, or carrying a name this check does not recognize, and
the connection name saying nothing either, is not a reason to report a pass — a pass would be a
statement about a server nobody placed.
Why medium and not higher
The data is inside your own infrastructure. This is exposure to a wider circle than the database grants, not exposure to the public.
Medium also sits below the CI gate's high default, so an honest finding does not block a pipeline
that never opted into privacy gating.
What it looks like
# postgresql.conf: every statement and every parameter value, written to the server log.
log_statement = all
What to do about it
# Schema statements carry column names, never row values — an audit trail without the data.
log_statement = ddl
log_statement takes effect on a configuration reload; no restart is needed. Its context is
superuser, so a session may also set it for itself — which is why this rule judges what a fresh
session resets to rather than what the audit's own connection is running with.
What this rule cannot see
Who can read the log, where it is shipped, how long it is kept, and whether anything redacts it on the way. Those are questions about the host rather than about the server's catalog, and this finding does not claim them.
It also cannot tell a server deliberately logging everything during an incident from a switch left on afterwards. That is the honest limit of a value read at one moment.