The database holds everything worth stealing, and the SQL injection lesson covered only the most famous way in. There are quieter routes: an application account with administrator rights, a connection string in a public repository, a port open to the internet, a backup on an unencrypted disk. This lesson covers the layers around the query.
In this lesson you will learn to give the application the minimum privileges it needs, protect the connection and credentials, keep queries and models from becoming injection paths, and secure data at rest and in backups.
The application should not connect as the database superuser. Create a dedicated user whose grants match what the code actually does, and consider separate users for read-heavy and write paths so a compromised reporting endpoint cannot modify data.
-- PostgreSQL: an application role with data access only, no schema changes
CREATE ROLE shop_app LOGIN PASSWORD 'from-secrets-manager';
GRANT CONNECT ON DATABASE shop TO shop_app;
GRANT USAGE ON SCHEMA public TO shop_app;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO shop_app;
-- Migrations run under a different, more privileged role from CI, never from the appFor multi-tenant systems, PostgreSQL row-level security can enforce the tenant filter in the database itself, so a forgotten WHERE tenant_id = ? in application code is not fatal. MongoDB offers the same idea through per-database users and roles such as readWrite scoped to one database.
Parameterized queries stop SQL injection; the equivalent discipline for document databases and ORMs is to keep user input out of the structure of the query:
// Vulnerable: an object in req.query becomes a query operator
await User.find({ email: req.query.email }); // ?email[$ne]= matches everyone// Safe: cast to the expected type, with sanitizeFilter as a backstop
await User.find({ email: String(req.query.email) });The sample schema adds three model-level defenses: strict: "throw" rejects unknown fields, immutable: true on role prevents privilege changes through ordinary updates, and select: false keeps the password hash out of every query unless explicitly requested. With SQL ORMs, the same rules apply: never pass raw user objects to update, and treat any "raw query" helper as a place where parameterization is your responsibility.
Encryption at rest (managed disks, or the database engine's own feature) protects against a stolen disk or snapshot, and is usually one setting on a cloud provider. It does not protect against a compromised application account, which is why the least-privilege section matters more than most teams expect.
Backups are copies of your most sensitive data and deserve the same controls: encrypted, stored in a separate account, with access limited to the backup process and a small restore group. Test a restore regularly; an untested backup is a hope, not a control. Finally, enable the database's audit log for privileged operations (schema changes, grants, bulk exports) and alert on events that should never happen, such as the application role attempting DROP TABLE.
Why should database migrations run under a different role than the application?
sanitizeFilter, and use strict, immutable, select: false model settings.Next lesson: Secrets Management: Environment Variables, Vaults and Rotation — keeping the keys to everything out of code and logs.