Storing SSNs, health records, or payment card data as plaintext in a database means a SQL injection, a misconfigured backup, or a rogue employee with SELECT privileges can access the raw values immediately. CWE-311 (Missing Encryption of Sensitive Data) and OWASP A02 (Cryptographic Failures) classify this as a high-severity failure. NIST 800-53 SC-28 mandates encryption of information at rest for sensitive categories. Infrastructure-level disk encryption (RDS encryption, full-disk) does not protect against application-layer attacks or insider queries — field-level AES-256-GCM encryption is the only control that survives a database dump.
High because plaintext sensitive fields are fully exposed to any party with database read access — a single SQL injection or insider query bypasses all other controls.
Encrypt sensitive fields before writing them to the database using AES-256-GCM with a randomly generated IV per record:
import crypto from 'crypto'
const KEY = Buffer.from(process.env.ENCRYPTION_KEY!, 'hex') // 32-byte hex key
function encrypt(text: string): string {
const iv = crypto.randomBytes(16)
const cipher = crypto.createCipheriv('aes-256-gcm', KEY, iv)
const enc = Buffer.concat([cipher.update(text, 'utf8'), cipher.final()])
const tag = cipher.getAuthTag()
return `${iv.toString('hex')}:${tag.toString('hex')}:${enc.toString('hex')}`
}
Store ENCRYPTION_KEY in AWS Secrets Manager or HashiCorp Vault — not in a .env file alongside the data it protects. Fields eligible for a HIPAA or PCI skip (e.g., Stripe-tokenized cards) need documented evidence of third-party handling.
ID: security-hardening.input-validation.encryption-at-rest
Severity: high
What to look for: List all sensitive data storage locations (database columns with PII, passwords, tokens, payment data). For each, identify what sensitive data the application stores (personal data, health records, payment information, private keys, security questions). Check whether this data is encrypted before being written to the database. Look for encryption calls using AES-256-GCM or AES-256-CBC, field-level encryption libraries, or database-level encryption configuration. PII stored as plaintext in a database that is otherwise encrypted at the infrastructure level (e.g., RDS encryption) counts as a partial pass only.
Pass criteria: Sensitive fields (SSNs, health data, payment card data, private keys, security question answers) are encrypted at the application layer using AES-256-GCM or equivalent before being stored. Encryption keys are stored separately from the encrypted data — 100% of sensitive fields must use AES-256 or equivalent encryption. Report: "X sensitive data fields found, all Y encrypted at rest."
Fail criteria: Sensitive personal data, health records, or payment information is stored as plaintext in the database. Or encryption keys are stored in the same database table as the encrypted data.
Skip (N/A) when: The application stores no sensitive data beyond public profile information, or all sensitive data (e.g., payment cards) is handled exclusively by a PCI-compliant third party (Stripe tokenization) with no raw data touching your database.
Detail on fail: Describe what is stored and how. Example: "Social security numbers stored as plaintext in users table — field-level encryption not applied" or "Payment card last-4 digits and expiry stored unencrypted in billing_info table"
Remediation: Implement field-level encryption for sensitive columns:
import crypto from 'crypto'
const ALGORITHM = 'aes-256-gcm'
const KEY = Buffer.from(process.env.ENCRYPTION_KEY!, 'hex') // 32-byte key
function encrypt(text: string): string {
const iv = crypto.randomBytes(16)
const cipher = crypto.createCipheriv(ALGORITHM, KEY, iv)
const encrypted = Buffer.concat([cipher.update(text, 'utf8'), cipher.final()])
const tag = cipher.getAuthTag()
return `${iv.toString('hex')}:${tag.toString('hex')}:${encrypted.toString('hex')}`
}
function decrypt(encryptedText: string): string {
const [ivHex, tagHex, dataHex] = encryptedText.split(':')
const iv = Buffer.from(ivHex, 'hex')
const tag = Buffer.from(tagHex, 'hex')
const data = Buffer.from(dataHex, 'hex')
const decipher = crypto.createDecipheriv(ALGORITHM, KEY, iv)
decipher.setAuthTag(tag)
return decipher.update(data) + decipher.final('utf8')
}
// Store before writing to DB
await db.user.update({ where: { id }, data: { ssn: encrypt(ssn) } })
Manage the ENCRYPTION_KEY in a secrets manager (AWS Secrets Manager, HashiCorp Vault), not as a plain environment variable in a flat .env file.