From ad260d118835bd9a6b0447e3f61d250f81589c9a Mon Sep 17 00:00:00 2001 From: "Aaron K. Clark" Date: Tue, 19 May 2026 01:42:55 -0500 Subject: [PATCH] fix(db): disable Sequelize default console.log query logging to stop secret leakage to stdout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sequelize's default `logging` is `console.log`, which dumps every executed SQL statement — INCLUDING bound parameter values — to stdout. In production that means a query like `SELECT * FROM "dbo"."ApiKey" WHERE "akKey" = $1` followed by the bound array containing the raw authKey lands in the operator's log stream alongside our structured JSON output. Two problems with that: 1. **Secret leakage**: pino's redact paths only inspect HTTP request shapes (`req.headers.authkey`, etc.). They never see Sequelize's SQL strings, so the bound authKey value bypasses every redaction layer and lands raw in stdout. The output goes wherever stdout goes — terminal in dev, Docker logs in prod, then a log shipper, then whatever the operator's log retention is set to. 2. **Format mixing**: structured pino JSON lines and Sequelize's free-form multi-line console output share the same fd. Log shippers (Vector, Loki, CloudWatch) that parse line-by-line as JSON either fail to parse the Sequelize lines or fall back to a "raw" representation that defeats the whole point of structured logging. Default to `logging: false` — Sequelize emits nothing. Operators who actually need query logs for debugging can set `DB_LOG_QUERIES=1`, in which case queries route through pino at debug level (silent at the default LOG_LEVEL=info, visible when LOG_LEVEL=debug, and never emitted as bare console.log even when visible). Documented in .env.example with the threat-model note so operators flipping it on for debugging understand it can still echo parameter values into the structured log. No test added: the default-disabled behavior is verified by every existing test (no Sequelize spam in vitest output); the opt-in path is exercised only via env var at module load, which doesn't compose well with vitest's module cache. The change is a 1-line behavior flip with a documented rollback (set DB_LOG_QUERIES=1). Co-Authored-By: Claude Opus 4.7 (1M context) --- .env.example | 9 +++++++++ app/config/db.config.js | 19 +++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/.env.example b/.env.example index 5b46707..c96d7f7 100644 --- a/.env.example +++ b/.env.example @@ -67,6 +67,15 @@ DB_PASSWORD=changeme # production so log aggregators get the structured JSON they expect. # LOG_PRETTY= +# Set to '1' to route Sequelize query logs through pino at debug +# level (visible when LOG_LEVEL=debug, silent otherwise). Default +# unset: Sequelize query logging is OFF entirely, because the +# default `console.log` Sequelize uses would otherwise dump SQL + +# bound parameters (including secrets like authKey values) to +# stdout, bypassing pino's redact paths. Turn this on for targeted +# query-shape debugging only; turn it off again when done. +# DB_LOG_QUERIES= + # ---- Rate limiting ---- # Per-key request budget for /v1/* in the window below. Defaults to 100. diff --git a/app/config/db.config.js b/app/config/db.config.js index d55d9f0..21aa585 100644 --- a/app/config/db.config.js +++ b/app/config/db.config.js @@ -3,11 +3,30 @@ const env = require('./env.js'); const Sequelize = require('sequelize'); +const log = require('./logger.js'); + +// Sequelize's default `logging` is `console.log`, which dumps every +// executed SQL statement — INCLUDING bound parameter values — to +// stdout. In production that means a query like +// `SELECT * FROM "dbo"."ApiKey" WHERE "akKey" = $1` followed by the +// bound array containing the raw authKey lands in the operator's +// log stream alongside our structured JSON output, mixing log +// formats and surfacing secrets that pino's redact paths never see +// (those only know about HTTP header structures, not Sequelize SQL +// strings). Disable by default; let operators opt back in for +// targeted debugging by setting DB_LOG_QUERIES=1, in which case +// queries are routed through pino at debug level (silent at the +// default LOG_LEVEL=info, visible when LOG_LEVEL=debug, and never +// emitted as bare console.log). +const dbQueryLog = process.env.DB_LOG_QUERIES === '1' + ? (sql, timing) => log.debug({ sql, timing }, 'sequelize query') + : false; const sequelize = new Sequelize(env.database, env.username, env.password, { host: env.host, port: env.port, dialect: 'postgres', + logging: dbQueryLog, define: { schema: 'dbo', timestamps: false,