Back

A3:2017 – Sensitive Data Exposure: Prevention

Defence in Layers

No single control prevents Sensitive Data Exposure, because the category spans the whole data lifecycle. Effective prevention stacks independent layers so that a failure in one does not become a breach. The ordering below is deliberate: it starts by reducing what you must protect, then protects it in transit, at rest, and in use, and finally governs the copies and their disposal.

Classify & minimise   -> you cannot leak data you never collected or already deleted
        +
Protect in transit    -> TLS everywhere, HSTS, no weak protocols, no mixed content
        +
Protect at rest       -> encrypt DB/files/backups; keys in a KMS, not beside the data
        +
Hash passwords        -> salted, slow hashing so a DB leak is not a password leak
        +
Close in-use leaks    -> no secrets in URLs, no caching of sensitive responses, redacted logs
        +
Manage secrets/backups-> secret store, private buckets, encrypted backups outside the web root
        +
Retain & dispose      -> delete on schedule across every copy
        =
Defence in depth: one failure is contained, not catastrophic

Layer 1: Classify and Minimise

The cheapest data to protect is the data you do not hold. Before any encryption decision, build an inventory and apply minimisation.

# Example: reduce what you keep at the point of collection (Python)
def store_payment(card_number: str, cvv: str) -> str:
    # DO NOT persist the PAN or CVV yourself.
    token = payment_gateway.tokenize(card_number, cvv)   # gateway holds the PAN
    last4 = card_number[-4:]
    db.save(payment_token=token, last4=last4)             # you keep only a token + last4
    return token   # CVV is never written anywhere

Layer 2: Protect Data In Transit

Every network hop that carries sensitive data must be encrypted with modern TLS, and the browser must be told never to fall back to HTTP. The server configuration below is the backbone control for A3.

Nginx: HTTPS, HSTS, redirect, modern protocols

# Redirect all HTTP to HTTPS - no cleartext leg to intercept or strip
server {
    listen 80;
    server_name app.example.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    server_name app.example.com;

    ssl_certificate     /etc/ssl/app.example.com/fullchain.pem;
    ssl_certificate_key /etc/ssl/app.example.com/privkey.pem;

    # Modern protocols only - no SSLv3, TLS 1.0/1.1
    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers off;   # let TLS 1.3 negotiate; strong 1.2 suites below
    ssl_ciphers         ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;

    # Force HTTPS on every future request, including the first, for a year + subdomains
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;

    location / {
        proxy_pass http://127.0.0.1:8000;
    }
}

Also encrypt the internal legs. TLS that terminates at the load balancer and travels cleartext to the app or database still exposes data to anyone inside the network. Use TLS (or mTLS) for service-to-service and database connections too — "internal" is not "safe."

Layer 3: Protect Data At Rest

Encryption at rest limits what an attacker gains from a stolen disk, an exfiltrated dump, or a compromised backup. The critical rule is key separation: the key must not live in the same place as the data it protects.

Application/field-level encryption with a managed key (Python)

import os
from cryptography.fernet import Fernet

# Key comes from a KMS / secret manager at runtime - NEVER hard-coded, NEVER in the DB.
# In production, prefer a cloud KMS (envelope encryption) over a raw local key.
key = os.environ["FIELD_ENCRYPTION_KEY"]   # injected by the secret store
cipher = Fernet(key)

def encrypt_field(plaintext: str) -> bytes:
    return cipher.encrypt(plaintext.encode())      # AES-128-CBC + HMAC under the hood

def decrypt_field(ciphertext: bytes) -> str:
    return cipher.decrypt(ciphertext).decode()

# Store only ciphertext; an attacker who dumps the table gets nothing usable
db.save(ssn_enc=encrypt_field(user_ssn))

Key management principles

For the deeper treatment of algorithm and mode selection (AES-GCM, ChaCha20-Poly1305, avoiding ECB, IV handling), see the dedicated Cryptographic Failures lesson.

Layer 4: Store Passwords as Hashes

Passwords are never encrypted — they are hashed with a salted, deliberately slow algorithm so that even a full database leak does not hand over the plaintext. This is the control that keeps "database breach" from automatically meaning "password breach."

# Argon2id (preferred) - Python
from argon2 import PasswordHasher
ph = PasswordHasher()                 # salted automatically, tunable work factor

hash = ph.hash(user_password)         # store this
# ... later, at login:
try:
    ph.verify(hash, submitted_password)   # constant-time verify, no "decrypt"
except Exception:
    reject_login()

Layer 5: Close In-Use Leaks (Caching, URLs, Logs)

These are the leaks that no amount of transport or storage encryption catches, and they are the heart of the broad 2017 framing.

Keep sensitive responses out of caches

# Node/Express: mark sensitive responses uncacheable
app.get('/account/statement', (req, res) => {
    res.set('Cache-Control', 'no-store');   // do not write to any cache
    res.set('Pragma', 'no-cache');          // legacy proxies
    res.json(getStatement(req.user));
});

Never put secrets in URLs

Redact sensitive fields before logging

# Python logging filter that scrubs sensitive fields
import logging, re

SENSITIVE = re.compile(r'(password|cvv|card|ssn|token)=([^&\s]+)', re.I)

class RedactFilter(logging.Filter):
    def filter(self, record):
        record.msg = SENSITIVE.sub(r'\1=[REDACTED]', str(record.msg))
        return True

logging.getLogger().addFilter(RedactFilter())
# Now "card=4111111111111111" is logged as "card=[REDACTED]"

Layer 6: Manage Secrets and Backups

Secrets

# Pre-commit / CI gate that blocks secrets from entering the repo
gitleaks protect --staged --redact        # pre-commit
gitleaks detect --source . --exit-code 1   # CI: fail the build on any finding

Backups and object storage

Layer 7: Retention and Disposal

Data held forever is data waiting to leak. Define and enforce retention, and account for every copy when you delete.

-- Automated retention: purge expired sensitive records on a schedule
DELETE FROM password_reset_tokens WHERE created_at < NOW() - INTERVAL '1 hour';
DELETE FROM audit_pii            WHERE created_at < NOW() - INTERVAL '90 days';

Prevention Checklist

LayerControlDone?
ClassifyCurrent data inventory with sensitivity labels and locations[ ]
MinimiseCollect and retain only what is needed; tokenise PAN; never store CVV[ ]
TransitHTTPS everywhere, HSTS (preload), TLS 1.2+ only, no mixed content[ ]
TransitInternal service and DB connections encrypted too[ ]
At restSensitive data encrypted; keys in a KMS, separate from data[ ]
PasswordsSalted, slow hashing (Argon2/bcrypt/scrypt); no MD5/SHA-1[ ]
In useCache-Control: no-store on sensitive responses[ ]
In useNo secrets in URLs; restrictive Referrer-Policy[ ]
In useLogs redact sensitive fields; generic client errors[ ]
SecretsSecret manager; secret scanning in CI; rotation[ ]
BackupsEncrypted, access-controlled, outside web root; storage private by default[ ]
RetentionAutomated deletion across all copies; crypto-shred where needed[ ]

Next Steps

---

Part of the OWASP Top 10 Educational Repository