Skip to content

CWE-312: Cleartext Storage of Sensitive Information

Overview

Cleartext storage occurs when applications store sensitive information without encryption, making it readable to anyone who gains access to the storage medium. Unlike cleartext transmission (CWE-319), this vulnerability affects data at rest - in databases, files, logs, cache, backups, and memory dumps.

Common examples are credentials such as passwords and API keys, personal information such as SSNs and health records, card numbers, and cryptographic keys, stored unencrypted in databases, configuration files, logs, cloud storage, or version control.

Relationship to Other CWEs

OWASP Classification

A06:2025 - Insecure Design

Risk

High: Anyone who reaches the storage medium reads the data:

  • A database compromise exposes every sensitive record at once. Unencrypted dumps are what ransomware operators exfiltrate before they encrypt, because the plaintext is what they extort with
  • DBAs, developers, and support staff can read the data as part of ordinary access
  • Backup tapes, dumps, and snapshots carry the same plaintext wherever they go
  • A publicly accessible S3 bucket or Azure blob exposes it to everyone
  • Stolen API keys and passwords enable further compromise; exposed PII enables fraud and identity theft
  • Storing cardholder data in cleartext breaches PCI DSS outright. HIPAA treats encryption at rest as addressable and GDPR Article 32 is risk-based, so the exposure there is failing a control your own risk assessment called for - which is harder to argue away, not easier
  • Fines, lawsuits, and mandatory breach disclosure follow

Common Vulnerable Data and Storage Locations

Data that is commonly stored without protection:

  • Credentials: Passwords, API keys, secret tokens, private keys
  • Personal information: SSN, passport numbers, dates of birth, addresses
  • Financial data: Credit card numbers, bank accounts, payment tokens
  • Health records: Medical history, diagnoses, prescriptions (PHI/ePHI)
  • Cryptographic keys: Encryption keys, signing keys, certificates
  • Session data: Authentication tokens, session identifiers
  • Business secrets: Proprietary algorithms, pricing data, customer lists

Where it ends up in cleartext:

  • Databases (primary and backup)
  • Configuration files
  • Log files
  • Temporary files and cache
  • Memory dumps and swap files
  • Version control systems
  • Cloud storage buckets
  • Mobile app local storage

Remediation Steps

Core Principle: Do not persist sensitive information in cleartext; encrypt recoverable sensitive data, hash passwords with password-hashing algorithms, tokenize where possible, and redact data that does not need to be stored.

Locate the cleartext storage of sensitive data

  • Identify the sensitive data the finding refers to and what kind it is: credentials such as passwords and API keys, PII such as SSNs and passport numbers, financial data, health records, or cryptographic keys
  • Identify where it is stored: database columns, configuration files, log files, cache, temporary files, cloud storage
  • Trace how it gets there: where the value is written and in what form

Encrypt sensitive data at rest (Primary Defense)

  • Know which layer protects what: Full-disk or volume encryption (BitLocker, FileVault, LUKS) and database transparent data encryption (TDE) both defend the same thing: a disk, a volume image or a backup medium that leaves the building. On a running system the volume is mounted and the database is serving, so the application, the database account, a DBA and anyone who reaches either read plaintext exactly as before. Neither closes an application-level finding on its own - they are defense-in-depth underneath the fix, not the fix
  • Database encryption: Use TDE for the stolen-medium case above, and application-level or column-level encryption for fields that must stay protected from a database dump, a compromised application credential, or broad read access to the database. A logical export (mysqldump, pg_dump, bcp) of a TDE-protected database contains plaintext, because it leaves through the SQL layer that TDE decrypts for
  • File encryption: Encrypt files before writing to disk using authenticated encryption such as AES-GCM or ChaCha20-Poly1305; avoid sensitive information in file names
  • Cloud storage encryption: Enable server-side encryption (AWS S3: SSE-KMS, Azure: Storage Service Encryption, Google Cloud: default encryption at rest). Provider-managed keys are the same shape as TDE - the storage service decrypts for any caller it authorizes - so where the threat is a leaked credential or an over-broad policy rather than a stolen drive, encrypt client-side before upload
  • Key management: Store encryption keys separate from the data, use key management services (AWS KMS, Azure Key Vault, GCP KMS, HashiCorp Vault), rotate keys regularly. "Separate" means a different trust boundary, not a different row - a key in the same config file, database, repository or bucket as the ciphertext is recovered by whoever recovered the data, and the finding is unchanged. What the application should hold is a credential that lets it use the key, ideally a workload identity rather than a static secret
  • Never hard-code keys: Hold them in a KMS or secrets manager and fetch them at runtime, so the key is never a literal in source or config - a hard-coded key is CWE-321. Envelope encryption is the pattern that makes this practical at scale: data keys are encrypted under a master key the KMS holds, so only the wrapped key travels with the data

Use application-level encryption for sensitive database fields

  • Encrypt before storing: encryptedData = encrypt(sensitiveData, encryptionKey) then db.execute("INSERT INTO users (ssn_encrypted) VALUES (?)", encryptedData)
  • Use authenticated encryption: AES-GCM, ChaCha20-Poly1305, or another vetted AEAD mode; generate a unique nonce for each encryption under the same key
  • Consider searchability: Use tokenization, keyed hashes for exact-match lookup, or carefully reviewed searchable-encryption designs if you need to search protected data
  • Do not encrypt passwords: Hash them with bcrypt, Argon2, or scrypt instead
  • Format-preserving encryption: Use FPE only when compatibility requires it and after reviewing the leakage and compliance tradeoffs

Minimize storage of sensitive data

  • Use tokenization: Store a payment gateway token from a provider such as Stripe or PayPal instead of the card number
  • Just-in-time data: Request the data when it is needed, keep it only for the session, and clear it after use rather than persisting it
  • Redaction: Store only the display-safe portion of data, such as the last 4 digits of a card; use keyed hashes or tokens for lookup instead of plaintext
  • Retention policies: Delete sensitive data after the required retention period, automate purging of old records, and use cryptographic erasure or controlled backup expiry where direct deletion from backups is not practical

Secure logs and temporary storage

  • Never log sensitive data: Do not log passwords, tokens, card numbers, PII, or session IDs; where a log line has to refer to one, redact it
  • Sanitize error messages: Keep sensitive data out of exception messages and stack traces
  • Secure temporary files: Avoid writing sensitive data to shared temp directories; if unavoidable, encrypt temp files, use restrictive permissions, and delete them as soon as possible
  • Memory protection: Avoid memory dumps with sensitive data and clear sensitive buffers where the language/runtime gives reliable control

Test the encryption implementation

  • Inspect the database and files directly and confirm they hold ciphertext, not plaintext
  • Confirm decryption works for legitimate access
  • Confirm keys are stored separately from the encrypted data
  • Check that logs contain no sensitive plaintext
  • Confirm a backup and restore round-trip keeps the data encrypted
  • Re-scan to confirm the finding is resolved

Secure Patterns

Logging Best Practices

// SECURE - pseudo-code
log_info("User login attempt: " + username)                 // identifier, not credential
log_debug("Payment processed: card ending " + last4(card))  // enough to trace, not to use
log_error("Query failed: INSERT INTO users (ssn) VALUES ('***')")  // shape kept, value dropped

Why this works: The sensitive value never reaches the log in the first place. A username identifies the account without exposing a credential; the last four digits identify a card to a support agent without being enough to use it; a query is logged with the parameter elided rather than interpolated. That matters because a log entry outlives the request by months, is copied to aggregation services, appears in backups, and is readable by people who would never be granted access to the underlying record.

Truncation is preferable to redaction wherever it is possible. Storing the last four digits is a decision made once at the call site; scrubbing the full number afterwards is a decision that has to keep working forever.

Log filters are a backstop, not the control. Use them to scrub patterns that slip through - credit cards with (\d{4}[-\s]?){3}\d{4}, SSNs with \d{3}-\d{2}-\d{4}, and API keys, tokens, and passwords - and never log request bodies that carry sensitive fields. A pattern matches the formats you anticipated: it will miss a card number written with unusual separators, a token in a format you did not know about, or a value nested inside a serialized object. Relying on the filter means every new field is exposed by default until someone notices. Not logging the value is the control; the filter is what catches the case where someone did anyway.

Temporary Files Best Practices

  • Encrypt temp files containing sensitive data
  • Create them with restrictive permissions from the outset, not afterwards
  • Delete in a finally block or equivalent, so an exception does not leave the file behind
  • Avoid shared temporary directories such as /tmp for sensitive data unless file permissions and lifecycle controls are explicit

Why this works: A temporary file is only temporary if nothing goes wrong. It survives a crash, a kill signal, and a full disk, so the cleanup has to be on a path that runs when the operation fails rather than only when it succeeds. Permissions have to be set as the file is created - a file created world-readable and then chmod-ed is readable for the interval in between, which is exactly the window a local attacker watches for. On a shared directory the filename is also predictable enough to matter, which is why an attacker-controllable temp path is a distinct weakness (CWE-377).

Testing & Verification

Database Inspection

-- Check database for cleartext sensitive data
SELECT password, api_key, ssn, credit_card 
FROM users 
LIMIT 10;

-- Expected: 
-- Passwords are hashed (not encrypted): $2b$12$...
-- Other fields are encrypted: binary/base64 gibberish
-- OR tokenized: tok_1234567890
-- NOT plaintext: "password123", "4111-1111-1111-1111"
--
-- "Looks like gibberish" is not the assertion. Base64-encode a card number
-- and it looks like gibberish too, so decode one sample and confirm the
-- result is not the plaintext value. Then read the same row back through
-- the application and confirm it decrypts to the original - a column of
-- unreadable bytes that nothing can recover is a broken write path, not a
-- fix, and it passes the inspection above either way.

Log File Audit

# Search logs for sensitive data patterns
grep -r "password" /var/log/app/
grep -r "[0-9]\{3\}-[0-9]\{2\}-[0-9]\{4\}" /var/log/  # SSN pattern
grep -r "[0-9]\{4\}[- ][0-9]\{4\}[- ][0-9]\{4\}[- ][0-9]\{4\}" /var/log/  # CC pattern

# Expected: No matches or only redacted versions (***-**-1234)
#
# A clean run narrows the problem rather than closing it: the CC pattern
# above requires a separator, so 4111111111111111 matches neither it nor
# anything else here. Grep finds the formats you thought of, which is the
# same limitation the log filters have.

File System Check

# Check for sensitive data in config files
cat config/database.yml  # Should not contain committed plaintext passwords
cat .env  # Should not be committed to git; prefer a secrets manager for deployed systems
find . -name "*.key" -o -name "*.pem"  # Private keys should be encrypted or tightly access-controlled

# Check file permissions
ls -la config/ logs/ tmp/

# Expected: Restrictive permissions (600 or 640), not world-readable

Backup and Export Testing

# Create database backup
mysqldump -u root -p database > backup.sql

# Inspect backup file
grep -i "credit_card\|ssn\|password" backup.sql

# Expected: 
# - Passwords are hashed
# - Sensitive fields are encrypted (binary data or base64)
# - NOT plaintext sensitive values

Cloud Storage Verification

# AWS S3 encryption check - ask which encryption, not whether
aws s3api head-object --bucket mybucket --key sensitive-file.txt

# Expected: ServerSideEncryption: aws:kms, with an SSEKMSKeyId naming a key
# whose policy you control. Every new S3 object has been encrypted with
# SSE-S3 by default since 5 January 2023, so a bare "AES256" is
# indistinguishable from the default. It may have been asked for deliberately;
# the result reads the same either way, which is what makes it useless as
# evidence that anything was configured. SSE-S3 also protects the disks in the
# data center, not the object from anyone holding s3:GetObject.

# Check public access - get-bucket-acl is the wrong question
aws s3api get-public-access-block --bucket mybucket
aws s3api get-bucket-policy-status --bucket mybucket

# Expected: BlockPublicAcls, IgnorePublicAcls, BlockPublicPolicy and
# RestrictPublicBuckets all true, and IsPublic false. ACLs are disabled by
# default on new buckets (Object Ownership: bucket owner enforced), so
# get-bucket-acl returns only the owner grant whether or not a bucket policy
# has made the bucket world-readable.

Additional Resources