--- name: database-security description: "Prevent SQL injection, ORM misuse, credential leaks; enforce least-privilege DB users and safe migrations — Applies to: when generating SQL or raw query strings; when generating ORM model code or queries; when generating database migration files; when wiring connection strings or pooling" --- # Database Security Prevent SQL injection, ORM misuse, credential leaks; enforce least-privilege DB users and safe migrations ## ALWAYS - Use parameterized queries / prepared statements for any SQL that touches user-controlled values. Pass values as parameters, never via string concatenation or formatting (`%s`, `+`, template literals). - Use the ORM's safe query API. In SQLAlchemy: `session.execute(text(":id"), {"id": user_id})`. In Django: ORM methods, `Model.objects.filter(...)`. In Sequelize / Prisma / SQLAlchemy core: `.where({ ... })` builders. In Go: `db.QueryContext(ctx, "select ... where id = $1", id)`. - Validate that identifier columns / table names — which can't be parameterized — come from a hard-coded allowlist, not from user input. - Use a dedicated database user per application with the minimum grants needed. Web apps that only read shouldn't have `INSERT`/`UPDATE`/`DELETE`. Migration jobs run as a separate `DDL`-capable user. - Enable Row-Level Security (Postgres `CREATE POLICY` / Supabase RLS / Azure SQL RLS) for multi-tenant tables and set the tenant context per session. - Pull DB credentials from a secret manager or env var injected at start — never from a committed `database.yml` / `.env`. Rotate on schedule. - Use TLS to the database (`sslmode=require` for Postgres, `requireSSL=true` for MySQL, encrypted connection for MSSQL). Pin the CA where the driver supports it. - Connection pooling has a max size that fits within the DB's `max_connections`, with healthy back-pressure on the application. ## NEVER - Concatenate user input into SQL: `"SELECT * FROM users WHERE name='" + name + "'"`. Even if you "escape" it yourself — drivers escape correctly only when binding through the parameter API. - Use ORM raw query methods (`.raw()`, `.objects.raw()`, `.query(text(...))`) with f-string interpolation of user input. - Run application workloads as the database superuser / `root` / `postgres` / `sa`. Create a service user. - Disable TLS to the database (`sslmode=disable`, `useSSL=false`). - Store secrets, PII, or large blobs in JSON columns without encryption-at-rest and a key rotation plan. - Run destructive migrations (DROP TABLE, DROP COLUMN, ALTER COLUMN type changes on populated tables) inline with deploys without an expand–contract plan and a backup verified to be restorable. - Bind an internet-exposed database listener with no allowlist; databases stay in a private network and are reached via a bastion / VPN / private link. - Log entire SQL statements with bound values at INFO level — bound values are almost always sensitive. ## KNOWN FALSE POSITIVES - Reporting tools that run analyst-authored ad-hoc SQL legitimately interpolate identifiers; they should run against a read-only replica with a separate user whose grants prevent damage. - Some ORMs (Django, SQLAlchemy 1.x) use `%s` placeholders as *parameter markers*, not Python format-string placeholders — that's safe. - Health-check queries (`SELECT 1`) are intentionally trivial.