Cleartext Storage of Sensitive Information in an Environment Variable
Description
Cleartext Storage of Sensitive Information in an Environment Variable is a vulnerability where a product stores unencrypted sensitive data in environment variables, exposing it to unauthorized access by other processes within the same execution context. Environment variables are accessible to processes sharing the execution context, including child processes and cloud serverless functions. Their contents may be exposed through system messages, error outputs, process listings, log files, or other outputs. Third-party dependencies often inherit environment access without requiring it, expanding the attack surface beyond intended boundaries.
Risk
Storing secrets in environment variables without encryption creates multiple exposure vectors. The ps command can reveal environment variables in process listings. Child processes inherit parent environment variables, potentially including secrets. Log aggregation systems may capture environment variable contents. Error messages and stack traces often dump environment context. In containerized environments, environment variables are often visible in orchestration tools. Debug modes may expose all environment variables. Cloud serverless platforms may log environment variables in function invocations. The cumulative effect is that secrets stored in plain text environment variables are likely to be exposed through multiple channels.
Solution
Encrypt sensitive information before storing in environment variables, or better, avoid storing secrets in environment variables entirely. Use dedicated secrets management systems like HashiCorp Vault, AWS Secrets Manager, or Azure Key Vault. Implement proper secret injection at runtime rather than static environment configuration. Remove or empty environment variables after use when possible. Use separate, restricted processes for handling sensitive operations. Audit log outputs to ensure environment variables are not captured. In serverless environments, use platform-specific secrets management rather than environment variables.
Common Consequences
| Impact | Details |
|---|---|
| Confidentiality | Scope: Confidentiality Read Application Data - Unauthorized exposure of sensitive information stored in environment variables, including API keys, database credentials, encryption keys, or other secrets. |
Example Code
Vulnerable Code
# Vulnerable: Secrets in environment variables (shell script)
#!/bin/bash
# Vulnerable: Database credentials in environment
export DB_PASSWORD="super_secret_password_123"
export DB_USER="admin"
# Vulnerable: API keys in environment
export API_KEY="sk_live_abcdef123456789"
export SECRET_TOKEN="ghp_xxxxxxxxxxxxxxxxxxxx"
# Vulnerable: Encryption keys in cleartext
export ENCRYPTION_KEY="AES256-key-in-plain-text"
# Run application - inherits all environment variables
./myapp
# Vulnerable: Can be seen with ps command
# ps auxe | grep myapp
# Vulnerable: Python using cleartext environment variables
import os
import subprocess
# Vulnerable: Reading secrets from environment
DATABASE_URL = os.environ.get('DATABASE_URL')
# Could be: "postgresql://admin:[email protected]:5432/mydb"
API_KEY = os.environ.get('API_KEY')
SECRET_KEY = os.environ.get('SECRET_KEY')
# Vulnerable: Passing to subprocess
def run_backup():
subprocess.run([
'backup_script.sh',
], env={
**os.environ, # Vulnerable: All secrets passed to subprocess
'BACKUP_PASSWORD': 'backup_secret_123'
})
# Vulnerable: Error handling exposes environment
try:
connect_to_database()
except Exception as e:
# Stack trace might include environment context
print(f"Error: {e}")
print(f"Environment: {os.environ}") # Exposes all secrets!
# Vulnerable: Logging environment
import logging
logging.info(f"Starting with config: {dict(os.environ)}")
// Vulnerable: Node.js with cleartext environment secrets
const express = require('express');
const app = express();
// Vulnerable: Secrets read from environment
const DB_PASSWORD = process.env.DB_PASSWORD;
const API_SECRET = process.env.API_SECRET;
const JWT_KEY = process.env.JWT_SECRET;
// Vulnerable: Debug endpoint exposes environment
app.get('/debug', (req, res) => {
// Extremely dangerous!
res.json({
env: process.env, // Exposes all secrets
config: {
dbPassword: DB_PASSWORD,
apiSecret: API_SECRET
}
});
});
// Vulnerable: Error handler leaks environment
app.use((err, req, res, next) => {
console.error('Error:', err);
console.error('Environment:', process.env); // Logs secrets!
res.status(500).json({ error: err.message });
});
// Vulnerable: Spawning child process
const { spawn } = require('child_process');
spawn('worker.js', [], {
env: process.env // All secrets passed to child
});
# Vulnerable: Dockerfile with environment secrets
FROM node:16
# Vulnerable: Secrets baked into image
ENV DATABASE_PASSWORD=production_password_123
ENV API_KEY=sk_live_xxxxxxxxxxxx
ENV ENCRYPTION_KEY=super_secret_key
# These are visible with: docker inspect <image>
# And persist in image layers
WORKDIR /app
COPY . .
RUN npm install
CMD ["node", "server.js"]
# Vulnerable: Kubernetes deployment with cleartext secrets
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
template:
spec:
containers:
- name: myapp
image: myapp:latest
env:
# Vulnerable: Secrets in plaintext
- name: DB_PASSWORD
value: "production_password_123"
- name: API_KEY
value: "sk_live_xxxxxxxxxxxx"
- name: JWT_SECRET
value: "jwt_secret_key_here"
Fixed Code
# Fixed: Using secrets manager instead of environment variables
#!/bin/bash
# Fixed: Retrieve secrets from vault at runtime
DB_PASSWORD=$(vault kv get -field=password secret/database)
API_KEY=$(vault kv get -field=key secret/api)
# Fixed: Clear sensitive vars after use
export DB_PASSWORD
./myapp
unset DB_PASSWORD
# Fixed: Use secret files instead of environment
./myapp --db-password-file=/run/secrets/db_password
# Alternative: Use process substitution to avoid env vars
./myapp --config <(vault kv get -format=json secret/myapp/config)
# Fixed: Secure secrets management in Python
import os
from functools import lru_cache
from cryptography.fernet import Fernet
# Fixed: Secrets manager integration
class SecretsManager:
def __init__(self):
# Use proper secrets management service
self._client = self._init_secrets_client()
def _init_secrets_client(self):
# AWS Secrets Manager, HashiCorp Vault, etc.
import boto3
return boto3.client('secretsmanager')
@lru_cache(maxsize=1)
def get_secret(self, secret_name):
response = self._client.get_secret_value(SecretId=secret_name)
return response['SecretString']
secrets = SecretsManager()
# Fixed: Get secrets from manager, not environment
DATABASE_PASSWORD = secrets.get_secret('myapp/database/password')
API_KEY = secrets.get_secret('myapp/api/key')
# Fixed: If environment vars must be used, encrypt them
class EncryptedEnvVar:
def __init__(self, key):
self.fernet = Fernet(key)
def get(self, var_name):
encrypted = os.environ.get(var_name)
if encrypted:
return self.fernet.decrypt(encrypted.encode()).decode()
return None
def set(self, var_name, value):
encrypted = self.fernet.encrypt(value.encode()).decode()
os.environ[var_name] = encrypted
# Fixed: Never log environment variables
import logging
class SanitizedLogger(logging.Logger):
SENSITIVE_PATTERNS = ['password', 'secret', 'key', 'token', 'credential']
def _sanitize(self, msg):
# Remove any potential secrets from log messages
for pattern in self.SENSITIVE_PATTERNS:
if pattern.lower() in str(msg).lower():
return '[REDACTED - Sensitive data detected]'
return msg
def info(self, msg, *args, **kwargs):
super().info(self._sanitize(msg), *args, **kwargs)
# Fixed: Secure subprocess handling
def run_backup_secure():
import subprocess
# Fixed: Only pass required, non-sensitive environment
clean_env = {
'PATH': os.environ.get('PATH', ''),
'HOME': os.environ.get('HOME', ''),
}
# Get password from secrets manager, pass via stdin
password = secrets.get_secret('backup/password')
proc = subprocess.Popen(
['backup_script.sh'],
env=clean_env, # Minimal, clean environment
stdin=subprocess.PIPE,
text=True
)
proc.communicate(input=password)
// Fixed: Secure secrets management in Node.js
const express = require('express');
const { SecretsManagerClient, GetSecretValueCommand } = require('@aws-sdk/client-secrets-manager');
const app = express();
const secretsClient = new SecretsManagerClient({});
// Fixed: Secrets from manager, not environment
async function getSecret(secretName) {
const command = new GetSecretValueCommand({ SecretId: secretName });
const response = await secretsClient.send(command);
return JSON.parse(response.SecretString);
}
// Fixed: Initialize secrets securely
let secrets = {};
async function initSecrets() {
secrets = await getSecret('myapp/secrets');
}
initSecrets();
// Fixed: No debug endpoint exposing secrets
// Completely removed /debug endpoint
// Fixed: Error handler doesn't leak secrets
app.use((err, req, res, next) => {
// Log error ID only, not details
const errorId = generateErrorId();
console.error(`Error ${errorId}: ${err.message}`);
// Never log process.env or secrets
res.status(500).json({
error: 'Internal server error',
errorId: errorId
});
});
// Fixed: Child processes get minimal environment
const { spawn } = require('child_process');
function spawnWorker() {
spawn('worker.js', [], {
env: {
// Fixed: Only non-sensitive vars
NODE_ENV: process.env.NODE_ENV,
PATH: process.env.PATH
}
});
// Pass secrets via IPC, not environment
worker.on('message', (msg) => {
if (msg.type === 'GET_SECRET') {
worker.send({ type: 'SECRET', value: secrets[msg.name] });
}
});
}
# Fixed: Dockerfile without embedded secrets
FROM node:16
# Fixed: No secrets in environment
# Secrets provided at runtime via secrets manager
WORKDIR /app
COPY . .
RUN npm install
# Fixed: Use secrets from mounted volume or external service
# Not from environment variables baked into image
CMD ["node", "server.js"]
# Fixed: Kubernetes with proper secrets management
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
template:
spec:
containers:
- name: myapp
image: myapp:latest
env:
# Fixed: Reference Kubernetes secrets (encrypted at rest)
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: myapp-secrets
key: db-password
# Fixed: Or use external secrets operator
- name: API_KEY
valueFrom:
secretKeyRef:
name: external-secrets
key: api-key
volumeMounts:
# Fixed: Mount secrets as files
- name: secrets-volume
mountPath: "/run/secrets"
readOnly: true
volumes:
- name: secrets-volume
secret:
secretName: myapp-file-secrets
---
# Fixed: Kubernetes Secret (stored encrypted)
apiVersion: v1
kind: Secret
metadata:
name: myapp-secrets
type: Opaque
data:
# Base64 encoded (Kubernetes encrypts at rest)
db-password: cHJvZHVjdGlvbl9wYXNzd29yZF8xMjM=
CVE Examples
- CVE-2022-43691: CMS exposes sensitive server-side information from environment variables when Debug mode is enabled.
- CVE-2022-27195: Jenkins plugin inserts environment variable contents including secrets into build XML files.
- CVE-2022-25264: CI/CD tool logs password-related environment variables in build logs.
References
- MITRE Corporation. "CWE-526: Cleartext Storage of Sensitive Information in an Environment Variable." https://cwe.mitre.org/data/definitions/526.html
- OWASP. "Secrets Management Cheat Sheet."
- CIS Controls. "Secure Configuration for Hardware and Software."