-
Notifications
You must be signed in to change notification settings - Fork 4
Database Security Deep Dive
Database security is a critical component of overall application security, as databases often store an organization's most valuable and sensitive information. This guide covers comprehensive strategies for securing databases in modern web applications.
Security should be implemented at multiple layers:
- Network level - Firewalls, VPNs, network segregation
- Database instance level - Authentication, access controls, encryption
- Application level - Input validation, parameterized queries, ORM use
- Data level - Encryption, masking, tokenization
Each component and user should have the minimum access necessary:
- Database users with limited permissions
- Connection pools with restricted service accounts
- Separation between application and administrative access
- Time-limited elevated privileges
-- Disable remote root login
UPDATE mysql.user SET Host='localhost' WHERE User='root';
FLUSH PRIVILEGES;
-- Remove anonymous users
DELETE FROM mysql.user WHERE User='';
FLUSH PRIVILEGES;
-- Remove test database
DROP DATABASE IF EXISTS test;
DELETE FROM mysql.db WHERE Db='test' OR Db='test\\_%';
FLUSH PRIVILEGES;
-- Enable binary logging for audit
SET GLOBAL log_bin = ON;
-- Set connection restrictions in pg_hba.conf
# TYPE DATABASE USER ADDRESS METHOD
local all postgres peer
host all app_user 127.0.0.1/32 scram-sha-256
host all app_user ::1/128 scram-sha-256
-- Configure password encryption
ALTER SYSTEM SET password_encryption = 'scram-sha-256';
-- Set connection limits
ALTER ROLE app_user CONNECTION LIMIT 100;
- Host database on private subnet - Not accessible from public internet
- Use VPC/VPN for secure connections
- IP whitelisting for administrative access
- Enable TLS/SSL for all connections
Example of enabling SSL in PostgreSQL:
-- In postgresql.conf
ssl = on
ssl_cert_file = 'server.crt'
ssl_key_file = 'server.key'
ssl_ca_file = 'root.crt'
-- Force SSL connections
ALTER USER app_user WITH PASSWORD 'strong_password' VALID UNTIL '2025-12-31';
ALTER ROLE app_user SET ssl = true;
-- PostgreSQL example
-- Create application user with limited permissions
CREATE ROLE app_user WITH LOGIN PASSWORD 'strong_password';
-- Grant specific schema permissions
GRANT USAGE ON SCHEMA public TO app_user;
-- Grant table-level permissions
GRANT SELECT, INSERT, UPDATE ON users TO app_user;
GRANT SELECT, UPDATE ON products TO app_user;
-- Grant sequence usage for auto-increment columns
GRANT USAGE ON ALL SEQUENCES IN SCHEMA public TO app_user;
-- Explicitly revoke destructive permissions
REVOKE DELETE ON ALL TABLES IN SCHEMA public FROM app_user;
-- Create roles
CREATE ROLE 'app_read_role', 'app_write_role', 'app_admin_role';
-- Set permissions for roles
GRANT SELECT ON myapp.* TO 'app_read_role';
GRANT SELECT, INSERT, UPDATE ON myapp.* TO 'app_write_role';
GRANT ALL PRIVILEGES ON myapp.* TO 'app_admin_role';
-- Create users
CREATE USER 'read_user'@'localhost' IDENTIFIED BY 'password1';
CREATE USER 'write_user'@'localhost' IDENTIFIED BY 'password2';
CREATE USER 'admin_user'@'localhost' IDENTIFIED BY 'password3';
-- Assign roles to users
GRANT 'app_read_role' TO 'read_user'@'localhost';
GRANT 'app_write_role' TO 'write_user'@'localhost';
GRANT 'app_admin_role' TO 'admin_user'@'localhost';
-- Activate default roles
SET DEFAULT ROLE 'app_read_role' TO 'read_user'@'localhost';
SET DEFAULT ROLE 'app_write_role' TO 'write_user'@'localhost';
SET DEFAULT ROLE 'app_admin_role' TO 'admin_user'@'localhost';
Many enterprise databases support TDE:
- SQL Server: Enable through SQL Server Configuration Manager
- Oracle: Configure using ADMINISTER KEY MANAGEMENT
- PostgreSQL: Use encryption extensions or filesystem encryption
- MySQL: Use keyring plugin for data-at-rest encryption
For targeted protection of sensitive columns:
-- Create table with encrypted columns (PostgreSQL with pgcrypto)
CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE TABLE customers (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
-- Encrypted columns
ssn TEXT NOT NULL,
credit_card TEXT NOT NULL
);
-- Insert with encryption
INSERT INTO customers (name, ssn, credit_card)
VALUES (
'John Doe',
pgp_sym_encrypt('123-45-6789', 'encryption_key'),
pgp_sym_encrypt('4111-1111-1111-1111', 'encryption_key')
);
-- Query with decryption
SELECT
name,
pgp_sym_decrypt(ssn::bytea, 'encryption_key') as ssn,
pgp_sym_decrypt(credit_card::bytea, 'encryption_key') as credit_card
FROM customers;
PostgreSQL example:
-- Enable RLS on table
ALTER TABLE customer_data ENABLE ROW LEVEL SECURITY;
-- Create policy for multi-tenant application
CREATE POLICY tenant_isolation_policy ON customer_data
USING (tenant_id = current_setting('app.current_tenant_id')::integer);
-- Application sets the tenant context for each connection
-- In application code before query execution:
await client.query("SET app.current_tenant_id = $1", [tenantId]);
-- Create policy for user-specific data
CREATE POLICY user_data_policy ON user_documents
USING (user_id = current_setting('app.current_user_id')::integer);
For development/testing environments or reporting:
-- PostgreSQL dynamic data masking
CREATE VIEW masked_customers AS
SELECT
id,
name,
-- Mask SSN except last 4 digits
CONCAT('XXX-XX-', RIGHT(ssn, 4)) AS ssn,
-- Mask credit card
CONCAT('XXXX-XXXX-XXXX-', RIGHT(credit_card, 4)) AS credit_card,
email,
created_at
FROM customers;
-- Grant access to the masked view instead of the table
REVOKE ALL ON customers FROM reporting_role;
GRANT SELECT ON masked_customers TO reporting_role;
// Node.js example with pg pool
const { Pool } = require('pg');
const pool = new Pool({
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
host: process.env.DB_HOST,
database: process.env.DB_NAME,
port: process.env.DB_PORT,
ssl: {
rejectUnauthorized: true,
ca: fs.readFileSync('/path/to/server-ca.pem').toString(),
key: fs.readFileSync('/path/to/client-key.pem').toString(),
cert: fs.readFileSync('/path/to/client-cert.pem').toString(),
},
// Security settings
max: 20, // Maximum connections
idleTimeoutMillis: 30000, // Close idle connections
connectionTimeoutMillis: 2000, // Connection timeout
allowExitOnIdle: true // Clean shutdown
});
// Node.js with PostgreSQL
async function getUserData(userId) {
// SECURE: Parameterized query
const query = 'SELECT id, name, email FROM users WHERE id = $1';
const result = await pool.query(query, [userId]);
return result.rows[0];
}
// INSECURE - DO NOT DO THIS:
// const query = `SELECT id, name, email FROM users WHERE id = ${userId}`;
Using Prisma ORM (TypeScript):
// Define schema in schema.prisma
// model User {
// id Int @id @default(autoincrement())
// email String @unique
// name String?
// }
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
// Secure: ORM handles SQL parameterization
async function getUserData(userId: number) {
return await prisma.user.findUnique({
where: {
id: userId
},
select: {
id: true,
name: true,
email: true
}
});
}
Secure connection string handling:
// .env file (not in version control)
DATABASE_URL=postgresql://username:password@localhost:5432/mydb?sslmode=require
// Load with dotenv
require('dotenv').config();
// Access securely
const connectionString = process.env.DATABASE_URL;
Enable database audit logging to track sensitive operations:
-- PostgreSQL auditing with pgaudit extension
CREATE EXTENSION pgaudit;
-- Configure in postgresql.conf
-- pgaudit.log = 'write,ddl'
-- pgaudit.log_catalog = on
-- pgaudit.log_parameter = on
-- pgaudit.log_relation = on
-- Create audit role for specific tables
CREATE ROLE audit_admin;
GRANT audit_admin TO postgres;
-- Audit specific tables
SELECT audit.audit_table('users');
SELECT audit.audit_table('financial_transactions');
Implement real-time query monitoring for:
- Slow queries that could indicate issues
- Unusual access patterns
- Failed login attempts
- Privileged operations
-- Enable slow query logging in MySQL
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1; -- Log queries taking more than 1 second
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow-query.log';
-
Encrypt backups:
# PostgreSQL backup with encryption pg_dump dbname | gpg -c > backup.sql.gpg # Restore gpg -d backup.sql.gpg | psql dbname
-
Secure transport to offsite storage:
# Secure copy to backup server scp -i ~/.ssh/id_rsa backup.sql.gpg backup_user@backup-server:/backups/
-
Implement least privilege for backup processes:
-- Create backup-specific user with minimal permissions CREATE USER backup_user WITH PASSWORD 'strong_password'; GRANT CONNECT ON DATABASE myapp TO backup_user; GRANT SELECT ON ALL TABLES IN SCHEMA public TO backup_user;
-
Enable encryption:
- Select "Enable encryption" during instance creation
- Use AWS KMS keys for management
-
Network security:
# Security Group Configuration Type: PostgreSQL Protocol: TCP Port: 5432 Source: 10.0.0.0/16 (VPC CIDR only)
-
IAM authentication:
// AWS SDK v3 example import { RDSDataClient, ExecuteStatementCommand } from "@aws-sdk/client-rds-data"; const client = new RDSDataClient({ region: "us-west-2" }); const params = { secretArn: "arn:aws:secretsmanager:us-west-2:123456789012:secret:my-rds-secret", resourceArn: "arn:aws:rds:us-west-2:123456789012:cluster:my-cluster", sql: "SELECT * FROM users WHERE id = :id", parameters: [{ name: "id", value: { longValue: 42 } }], database: "mydb" }; const command = new ExecuteStatementCommand(params); const response = await client.send(command);
- Configure private IP and authorized networks
- Enable Cloud SQL Auth Proxy for secure connections
- Use service account with minimal permissions
- Use parameterized queries / prepared statements
- Implement input validation with whitelist approach
- Use ORM with security features
- Apply principle of least privilege to database users
- Enable WAF to block common SQL injection patterns
- Regularly patch database software
Implement monitoring for:
-
Unusual query patterns:
- Multiple failed queries in succession
- Queries with SQL comments or typical injection patterns
- Queries attempting to access system tables
-
Unusual access patterns:
- Access from new locations/IP addresses
- Access outside normal business hours
- Sudden increase in query volume
-
Privilege escalation attempts:
- Failed attempts to execute administrative commands
- Attempts to change user permissions
// Secure Prisma implementation
import { PrismaClient } from '@prisma/client'
// Avoid exposing error details
const prisma = new PrismaClient({
errorFormat: 'minimal',
log: [
{ level: 'query', emit: 'event' },
{ level: 'error', emit: 'stdout' },
],
})
// Log queries to backend only (not client)
prisma.$on('query', (e) => {
console.log('Query executed:', {
query: e.query,
duration: e.duration,
timestamp: e.timestamp,
})
})
// Use transactions for data integrity
async function transferFunds(fromId, toId, amount) {
return await prisma.$transaction(async (tx) => {
// Deduct from source account
const fromAccount = await tx.account.update({
where: { id: fromId },
data: { balance: { decrement: amount } },
})
if (fromAccount.balance < 0) {
throw new Error('Insufficient funds')
}
// Add to target account
await tx.account.update({
where: { id: toId },
data: { balance: { increment: amount } },
})
// Record transaction
await tx.transaction.create({
data: {
fromAccountId: fromId,
toAccountId: toId,
amount,
status: 'COMPLETED',
},
})
})
}
// Configure with secure options
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection"),
sqlOptions =>
{
sqlOptions.EnableRetryOnFailure(
maxRetryCount: 5,
maxRetryDelay: TimeSpan.FromSeconds(30),
errorNumbersToAdd: null);
sqlOptions.CommandTimeout(30);
}
));
// Use parameterized LINQ queries
public async Task<UserDTO> GetUserDataAsync(int userId)
{
// Safe: Parameters handled securely
var user = await _context.Users
.Where(u => u.Id == userId)
.Select(u => new UserDTO
{
Id = u.Id,
Name = u.Name,
Email = u.Email
})
.FirstOrDefaultAsync();
return user;
}
// Use explicit loading for sensitive relationships
public async Task<IActionResult> GetUserWithRoles(int userId)
{
var user = await _context.Users.FindAsync(userId);
if (user == null)
return NotFound();
// Only load specific data if authorized
if (User.IsInRole("Admin"))
{
await _context.Entry(user)
.Collection(u => u.Roles)
.LoadAsync();
}
return Ok(user);
}
- [OWASP Database Security Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Database_Security_Cheat_Sheet.html)
- [PostgreSQL Security Documentation](https://www.postgresql.org/docs/current/security.html)
- [MySQL Security Guide](https://dev.mysql.com/doc/refman/8.0/en/security.html)
- [Prisma Security Documentation](https://www.prisma.io/docs/concepts/components/prisma-client/security)
- [AWS RDS Security Best Practices](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_BestPractices.Security.html)