CWE-798: Hard-coded Credentials - Python
Overview
Hard-coded credentials in Python code create serious security vulnerabilities. Never embed passwords, API keys, database credentials, or encryption keys in source code or configuration files committed to version control. Use environment variables, configuration files, or secrets managers.
Primary Defence: Use environment variables with os.getenv(), python-dotenv for local development, or cloud secrets managers (AWS Secrets Manager, Azure Key Vault) for production.
Common Vulnerable Patterns
Hard-coded Database Credentials
# VULNERABLE - Credentials in source code
import psycopg2
class DatabaseConnection:
DB_HOST = "localhost"
DB_NAME = "mydb"
DB_USER = "admin"
DB_PASSWORD = "P@ssw0rd123" # DANGEROUS!
def get_connection(self):
return psycopg2.connect(
host=self.DB_HOST,
database=self.DB_NAME,
user=self.DB_USER,
password=self.DB_PASSWORD
)
Why this is vulnerable: Hard-coded database passwords in Python source files are visible to anyone with repository access, persist in git history forever, are deployed to production servers where they can be extracted, and cannot be rotated without code changes and redeployment.
Hard-coded API Keys
# VULNERABLE - API key in code
import requests
class ApiClient:
API_KEY = "sk_live_51H7x8y9z10a11b12c" # DANGEROUS!
API_SECRET = "whsec_abcdef123456" # DANGEROUS!
def make_request(self):
headers = {
"Authorization": f"Bearer {self.API_KEY}"
}
response = requests.get("https://api.example.com/data", headers=headers)
return response.json()
Why this is vulnerable: API keys in Python code are exposed to all developers with repository access, remain in version control history permanently, enable unauthorized API usage and billing charges, and can be easily extracted from .pyc bytecode files or deployed applications.
Hard-coded Encryption Keys
# VULNERABLE - Encryption key in code
from cryptography.fernet import Fernet
class Encryptor:
SECRET_KEY = b'MySecretKey12345678901234567890=' # DANGEROUS!
def encrypt(self, data: str) -> bytes:
f = Fernet(self.SECRET_KEY)
return f.encrypt(data.encode())
Why this is vulnerable: Hard-coded encryption keys defeat encryption's purpose since anyone with code access can decrypt data, keys cannot be rotated without code changes, and compromised keys expose all historical encrypted data permanently.
Credentials in config.py (Committed to Git)
# VULNERABLE - config.py with real credentials
DATABASE_URL = "postgresql://admin:P@ssw0rd123@localhost/mydb"
AWS_ACCESS_KEY = "AKIAIOSFODNN7EXAMPLE"
AWS_SECRET_KEY = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
STRIPE_API_KEY = "sk_live_51H7x8y9z10a11b12c"
Why this is vulnerable: Config files with credentials committed to git expose secrets permanently in version control history, make them accessible to all repository clones and forks, are often accidentally pushed to public repositories, and remain visible even after deletion through git history.
Secure Patterns
Environment Variables with os.environ
# SECURE - Read from environment variables
import os
import psycopg2
class DatabaseConnection:
def __init__(self):
self.db_host = os.environ.get("DB_HOST")
self.db_name = os.environ.get("DB_NAME")
self.db_user = os.environ.get("DB_USER")
self.db_password = os.environ.get("DB_PASSWORD")
if not all([self.db_host, self.db_name, self.db_user, self.db_password]):
raise ValueError("Database credentials not configured")
def get_connection(self):
return psycopg2.connect(
host=self.db_host,
database=self.db_name,
user=self.db_user,
password=self.db_password
)
# Set environment variables:
# export DB_HOST=localhost
# export DB_NAME=mydb
# export DB_USER=admin
# export DB_PASSWORD=SecurePassword123
Why this works: os.environ.get() retrieves credentials from environment variables set at the OS/container/platform level, keeping them out of source code. The validation ensures the application fails fast if credentials are missing (raising ValueError). Environment variables can be configured per environment (dev, staging, prod) without code changes. Credentials can be rotated by updating environment variables and restarting the application, with no code deployment.
python-dotenv (.env files)
# SECURE - Use python-dotenv for environment variables
import os
from dotenv import load_dotenv
import requests
# Load environment variables from .env file
load_dotenv()
class ApiClient:
def __init__(self):
self.api_key = os.getenv("API_KEY")
self.api_secret = os.getenv("API_SECRET")
if not self.api_key or not self.api_secret:
raise ValueError("API credentials not configured")
def make_request(self):
headers = {
"Authorization": f"Bearer {self.api_key}"
}
response = requests.get("https://api.example.com/data", headers=headers)
return response.json()
# .env file (NOT committed to version control):
"""
API_KEY=sk_live_51H7x8y9z10a11b12c
API_SECRET=whsec_abcdef123456
DB_PASSWORD=SecurePassword123
"""
# Install: pip install python-dotenv
Why this works: python-dotenv loads environment variables from .env files for local development convenience. The .env file is excluded from version control via .gitignore, preventing secrets from entering Git history. load_dotenv() populates os.getenv() at runtime, not compile time. The validation (or operator with raise ValueError) ensures required credentials are present. Different .env files can be used per environment without modifying code.
AWS Secrets Manager
# SECURE - AWS Secrets Manager
import boto3
import json
from botocore.exceptions import ClientError
class SecretsManager:
def __init__(self):
self.client = boto3.client('secretsmanager')
def get_database_credentials(self):
secret_name = "prod/database/credentials"
try:
response = self.client.get_secret_value(SecretId=secret_name)
except ClientError as e:
raise Exception(f"Failed to retrieve secret: {e}")
# Parse the secret
secret = json.loads(response['SecretString'])
return {
'host': secret['host'],
'database': secret['database'],
'username': secret['username'],
'password': secret['password']
}
def get_connection(self):
creds = self.get_database_credentials()
import psycopg2
return psycopg2.connect(
host=creds['host'],
database=creds['database'],
user=creds['username'],
password=creds['password']
)
# Install: pip install boto3
# AWS credentials from environment or IAM role
# export AWS_ACCESS_KEY_ID=your_access_key
# export AWS_SECRET_ACCESS_KEY=your_secret_key
# export AWS_DEFAULT_REGION=us-east-1
Why this works: AWS Secrets Manager provides centralized secret storage with KMS encryption, automatic rotation, and IAM-based access control. The boto3 client uses AWS credentials from environment variables or IAM roles (EC2, ECS, Lambda) - no credentials in code. Secrets are retrieved at runtime as needed, not stored in the application. CloudTrail logs all access for auditing. Supports secret versioning for gradual rollout of rotated credentials. The JSON structure allows storing related credentials together.
HashiCorp Vault
# SECURE - HashiCorp Vault integration
import hvac
import os
class VaultClient:
def __init__(self):
vault_url = os.getenv("VAULT_ADDR", "https://vault.example.com:8200")
vault_token = os.getenv("VAULT_TOKEN")
if not vault_token:
raise ValueError("VAULT_TOKEN not configured")
self.client = hvac.Client(url=vault_url, token=vault_token)
if not self.client.is_authenticated():
raise Exception("Failed to authenticate with Vault")
def get_database_password(self):
secret_path = "secret/data/database"
response = self.client.secrets.kv.v2.read_secret_version(path="database")
return response['data']['data']['password']
def get_api_key(self):
response = self.client.secrets.kv.v2.read_secret_version(path="api")
return response['data']['data']['api_key']
# Install: pip install hvac
Why this works: HashiCorp Vault provides enterprise-grade secret management with dynamic secrets, lease management, and fine-grained access policies. The VAULT_TOKEN comes from environment (not code), allowing authentication without hardcoded credentials. Vault supports secret versioning, automatic rotation, detailed audit logs, and encryption as a service. The KV v2 API retrieves secrets on-demand. Vault's dynamic secrets can auto-generate database credentials that expire automatically.
Framework-Specific Guidance
Django
# SECURE - Django with environment variables
# settings.py
import os
from pathlib import Path
# Read from environment variables
SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY')
DEBUG = os.environ.get('DEBUG', 'False') == 'True'
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': os.environ.get('DB_NAME'),
'USER': os.environ.get('DB_USER'),
'PASSWORD': os.environ.get('DB_PASSWORD'),
'HOST': os.environ.get('DB_HOST', 'localhost'),
'PORT': os.environ.get('DB_PORT', '5432'),
}
}
# Email configuration
EMAIL_HOST_USER = os.environ.get('EMAIL_USER')
EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_PASSWORD')
# AWS S3 settings
AWS_ACCESS_KEY_ID = os.environ.get('AWS_ACCESS_KEY_ID')
AWS_SECRET_ACCESS_KEY = os.environ.get('AWS_SECRET_ACCESS_KEY')
AWS_STORAGE_BUCKET_NAME = os.environ.get('AWS_STORAGE_BUCKET_NAME')
# Alternative: Use django-environ
from environ import Env
env = Env()
env.read_env() # Reads .env file
SECRET_KEY = env('DJANGO_SECRET_KEY')
DEBUG = env.bool('DEBUG', default=False)
DATABASES = {
'default': env.db() # Reads DATABASE_URL
}
# Install: pip install django-environ
Flask
# SECURE - Flask with environment variables
# config.py
import os
class Config:
SECRET_KEY = os.environ.get('SECRET_KEY') or os.urandom(32)
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL')
# API keys
STRIPE_API_KEY = os.environ.get('STRIPE_API_KEY')
SENDGRID_API_KEY = os.environ.get('SENDGRID_API_KEY')
# AWS
AWS_ACCESS_KEY_ID = os.environ.get('AWS_ACCESS_KEY_ID')
AWS_SECRET_ACCESS_KEY = os.environ.get('AWS_SECRET_ACCESS_KEY')
class DevelopmentConfig(Config):
DEBUG = True
SQLALCHEMY_DATABASE_URI = os.environ.get('DEV_DATABASE_URL')
class ProductionConfig(Config):
DEBUG = False
# app.py
from flask import Flask
from dotenv import load_dotenv
load_dotenv()
app = Flask(__name__)
app.config.from_object('config.ProductionConfig')
# Access configuration
stripe_key = app.config['STRIPE_API_KEY']
FastAPI
# SECURE - FastAPI with Pydantic Settings
# config.py
from pydantic import BaseSettings, Field
class Settings(BaseSettings):
# Database
db_host: str = Field(..., env='DB_HOST')
db_name: str = Field(..., env='DB_NAME')
db_user: str = Field(..., env='DB_USER')
db_password: str = Field(..., env='DB_PASSWORD')
# API Keys
api_key: str = Field(..., env='API_KEY')
api_secret: str = Field(..., env='API_SECRET')
# JWT
jwt_secret: str = Field(..., env='JWT_SECRET')
class Config:
env_file = '.env'
env_file_encoding = 'utf-8'
# main.py
from fastapi import FastAPI, Depends
from functools import lru_cache
app = FastAPI()
@lru_cache()
def get_settings():
return Settings()
@app.get("/")
async def root(settings: Settings = Depends(get_settings)):
# Use settings.api_key, etc.
return {"status": "ok"}
# Install: pip install pydantic[dotenv]
SQLAlchemy
# SECURE - SQLAlchemy with environment variables
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
import os
class Database:
def __init__(self):
# Build connection string from environment variables
db_user = os.environ.get('DB_USER')
db_password = os.environ.get('DB_PASSWORD')
db_host = os.environ.get('DB_HOST', 'localhost')
db_port = os.environ.get('DB_PORT', '5432')
db_name = os.environ.get('DB_NAME')
if not all([db_user, db_password, db_name]):
raise ValueError("Database credentials not configured")
# PostgreSQL
database_url = f"postgresql://{db_user}:{db_password}@{db_host}:{db_port}/{db_name}"
# Or use DATABASE_URL directly:
# database_url = os.environ.get('DATABASE_URL')
self.engine = create_engine(database_url, echo=False)
self.SessionLocal = sessionmaker(bind=self.engine)
def get_session(self):
return self.SessionLocal()
Encryption Keys Management
# SECURE - Key management with AWS KMS
import boto3
import base64
class EncryptionService:
def __init__(self):
self.kms_client = boto3.client('kms')
self.key_id = os.environ.get('KMS_KEY_ID')
if not self.key_id:
raise ValueError("KMS_KEY_ID not configured")
def encrypt(self, plaintext: str) -> str:
response = self.kms_client.encrypt(
KeyId=self.key_id,
Plaintext=plaintext.encode()
)
# Return base64-encoded ciphertext
return base64.b64encode(response['CiphertextBlob']).decode()
def decrypt(self, ciphertext: str) -> str:
ciphertext_blob = base64.b64decode(ciphertext)
response = self.kms_client.decrypt(
CiphertextBlob=ciphertext_blob
)
return response['Plaintext'].decode()
# Alternative: Use Fernet with key from environment
from cryptography.fernet import Fernet
class FernetEncryption:
def __init__(self):
# Key should be in environment variable
key = os.environ.get('ENCRYPTION_KEY')
if not key:
raise ValueError("ENCRYPTION_KEY not configured")
self.cipher = Fernet(key.encode())
def encrypt(self, data: str) -> bytes:
return self.cipher.encrypt(data.encode())
def decrypt(self, token: bytes) -> str:
return self.cipher.decrypt(token).decode()
# Generate key once: Fernet.generate_key()
# Store in environment: export ENCRYPTION_KEY=generated_key
Testing with Test Credentials
# SECURE - Use test credentials for unit tests
import pytest
import os
from unittest.mock import patch
class TestDatabaseConnection:
@pytest.fixture
def mock_env_vars(self, monkeypatch):
"""Provide test credentials via environment variables"""
monkeypatch.setenv("DB_HOST", "localhost")
monkeypatch.setenv("DB_NAME", "test_db")
monkeypatch.setenv("DB_USER", "test_user")
monkeypatch.setenv("DB_PASSWORD", "test_password")
def test_connection(self, mock_env_vars):
"""Test database connection with test credentials"""
db = DatabaseConnection()
assert db.db_host == "localhost"
assert db.db_user == "test_user"
@patch.dict(os.environ, {
"API_KEY": "test_api_key",
"API_SECRET": "test_secret"
})
def test_api_client(self):
"""Test API client with mocked credentials"""
client = ApiClient()
assert client.api_key == "test_api_key"
# Using testcontainers for integration tests
from testcontainers.postgres import PostgresContainer
import psycopg2
def test_with_postgres_container():
with PostgresContainer("postgres:13") as postgres:
# Container provides test credentials
conn = psycopg2.connect(
host=postgres.get_container_host_ip(),
port=postgres.get_exposed_port(5432),
database=postgres.POSTGRES_DB,
user=postgres.POSTGRES_USER,
password=postgres.POSTGRES_PASSWORD
)
# Test database operations
cursor = conn.cursor()
cursor.execute("SELECT 1")
assert cursor.fetchone()[0] == 1
# Install: pip install pytest testcontainers
.gitignore Best Practices
# Add these patterns to .gitignore
# Environment files
.env
.env.local
.env.*.local
# Configuration files with secrets
config/secrets.py
config/local_settings.py
# Django
local_settings.py
db.sqlite3
# Flask
instance/
# Credentials
*.pem
*.key
credentials.json
service-account.json
# IDE
.vscode/
.idea/
*.swp
Creating .env.example Template
# .env.example (committed to version control)
# Copy this to .env and fill in real values
# Database
DB_HOST=localhost
DB_NAME=mydb
DB_USER=admin
DB_PASSWORD=your_password_here
# API Keys
API_KEY=your_api_key_here
API_SECRET=your_api_secret_here
# AWS
AWS_ACCESS_KEY_ID=your_access_key
AWS_SECRET_ACCESS_KEY=your_secret_key
# Django
DJANGO_SECRET_KEY=your_secret_key_here
DEBUG=False
Detecting Hard-coded Secrets
Using detect-secrets
# Install detect-secrets
pip install detect-secrets
# Scan repository
detect-secrets scan > .secrets.baseline
# Audit findings
detect-secrets audit .secrets.baseline
# Add pre-commit hook
# .pre-commit-config.yaml:
repos:
- repo: https://github.com/Yelp/detect-secrets
rev: v1.4.0
hooks:
- id: detect-secrets
args: ['--baseline', '.secrets.baseline']
Using TruffleHog
# Install truffleHog
pip install truffleHog
# Scan repository
trufflehog --regex --entropy=True .
# Scan Git history
trufflehog --regex --entropy=True file:///path/to/repo
Verification
After implementing the recommended secure patterns, verify the fix through multiple approaches:
- Manual testing: Submit malicious payloads relevant to this vulnerability and confirm they're handled safely without executing unintended operations
- Code review: Confirm all instances use the secure pattern (parameterized queries, safe APIs, proper encoding) with no string concatenation or unsafe operations
- Static analysis: Use security scanners to verify no new vulnerabilities exist and the original finding is resolved
- Regression testing: Ensure legitimate user inputs and application workflows continue to function correctly
- Edge case validation: Test with special characters, boundary conditions, and unusual inputs to verify proper handling
- Framework verification: If using a framework or library, confirm the recommended APIs are used correctly according to documentation
- Authentication/session testing: Verify security controls remain effective and cannot be bypassed (if applicable to the vulnerability type)
- Rescan: Run the security scanner again to confirm the finding is resolved and no new issues were introduced
Cloud-Native Secrets Management (Production-Grade)
For production applications, use dedicated secrets management services instead of environment variables:
AWS Secrets Manager
import boto3
from botocore.exceptions import ClientError
import json
class AWSSecretsManager:
"""
Retrieve secrets from AWS Secrets Manager.
Prerequisites:
- pip install boto3
- EC2/Lambda must have IAM role with secretsmanager:GetSecretValue permission
- For local development, configure AWS credentials via aws configure
"""
def __init__(self, region_name="us-east-1"):
self.client = boto3.session.Session().client(
service_name='secretsmanager',
region_name=region_name
)
def get_secret(self, secret_name: str) -> dict:
"""
Retrieve secret from AWS Secrets Manager.
Args:
secret_name: Name or ARN of the secret (e.g., "prod/database/credentials")
Returns:
dict: Parsed JSON secret value
Raises:
Exception: If secret not found or access denied
"""
try:
response = self.client.get_secret_value(SecretId=secret_name)
# Secrets can be string or binary
if 'SecretString' in response:
return json.loads(response['SecretString'])
else:
# Binary secret (less common)
import base64
return json.loads(base64.b64decode(response['SecretBinary']))
except ClientError as e:
error_code = e.response['Error']['Code']
if error_code == 'ResourceNotFoundException':
raise Exception(f"Secret '{secret_name}' not found in Secrets Manager")
elif error_code == 'AccessDeniedException':
raise Exception(f"Access denied to secret '{secret_name}'. Check IAM permissions.")
else:
raise Exception(f"Error retrieving secret: {e}")
# Usage example - Database credentials
secrets_manager = AWSSecretsManager(region_name="us-west-2")
db_creds = secrets_manager.get_secret("prod/database/postgres")
# db_creds contains: {"host": "db.example.com", "username": "app_user", "password": "..."}
import psycopg2
connection = psycopg2.connect(
host=db_creds["host"],
database=db_creds["database"],
user=db_creds["username"],
password=db_creds["password"]
)
# Usage example - API keys
api_secrets = secrets_manager.get_secret("prod/external-apis")
stripe_api_key = api_secrets["stripe_key"]
sendgrid_api_key = api_secrets["sendgrid_key"]
Caching for Performance:
from functools import lru_cache
@lru_cache(maxsize=128)
def get_cached_secret(secret_name: str, region: str = "us-east-1") -> dict:
"""Cache secrets for 1 hour to reduce API calls (default LRU cache)"""
manager = AWSSecretsManager(region_name=region)
return manager.get_secret(secret_name)
# Or use AWS Secrets Manager Caching Client (recommended)
# pip install aws-secretsmanager-caching
from aws_secretsmanager_caching import SecretCache
cache = SecretCache() # Caches secrets for 1 hour by default
secret_json = cache.get_secret_string('prod/database/credentials')
Kubernetes Secrets
For applications running in Kubernetes:
import os
class KubernetesSecrets:
"""
Read secrets from Kubernetes Secrets mounted as volumes or environment variables.
Secrets are defined in Kubernetes manifests and automatically injected:
apiVersion: v1
kind: Secret
metadata:
name: database-credentials
type: Opaque
data:
password: BASE64_ENCODED_PASSWORD
username: BASE64_ENCODED_USERNAME
"""
@staticmethod
def get_from_volume(secret_name: str, mount_path: str = "/etc/secrets") -> str:
"""
Read secret mounted as a volume.
Kubernetes pod spec:
volumes:
- name: db-secret
secret:
secretName: database-credentials
volumeMounts:
- name: db-secret
mountPath: /etc/secrets/db
readOnly: true
Args:
secret_name: Name of the secret file (matches key in Secret)
mount_path: Base path where secrets are mounted
Returns:
str: Secret value
"""
secret_file = os.path.join(mount_path, secret_name)
try:
with open(secret_file, 'r') as f:
return f.read().strip()
except FileNotFoundError:
raise Exception(f"Secret '{secret_name}' not found at {secret_file}")
@staticmethod
def get_from_env(env_var_name: str) -> str:
"""
Read secret from environment variable injected by Kubernetes.
Kubernetes pod spec:
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: database-credentials
key: password
Args:
env_var_name: Name of the environment variable
Returns:
str: Secret value
"""
value = os.getenv(env_var_name)
if not value:
raise Exception(f"Environment variable '{env_var_name}' not set")
return value
# Usage example - Volume-mounted secrets (recommended for large secrets)
k8s_secrets = KubernetesSecrets()
db_password = k8s_secrets.get_from_volume("password", mount_path="/etc/secrets/db")
db_username = k8s_secrets.get_from_volume("username", mount_path="/etc/secrets/db")
# Usage example - Environment variable secrets (simpler for small secrets)
db_password = k8s_secrets.get_from_env("DB_PASSWORD")
api_key = k8s_secrets.get_from_env("STRIPE_API_KEY")
# Complete database connection example
import psycopg2
connection = psycopg2.connect(
host=os.getenv("DB_HOST"), # Non-secret config can use env vars
database=os.getenv("DB_NAME"),
user=k8s_secrets.get_from_env("DB_USERNAME"),
password=k8s_secrets.get_from_env("DB_PASSWORD")
)
External Secrets Operator (Advanced):
# For syncing secrets from AWS/Azure/GCP into Kubernetes
# Install: https://external-secrets.io/
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: aws-secrets-manager
spec:
provider:
aws:
service: SecretsManager
region: us-west-2
auth:
jwt:
serviceAccountRef:
name: external-secrets-sa
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: database-credentials
spec:
refreshInterval: 1h
secretStoreRef:
name: aws-secrets-manager
target:
name: database-credentials # Kubernetes Secret name
data:
- secretKey: password # Key in K8s Secret
remoteRef:
key: prod/database/credentials # AWS Secrets Manager secret
property: password # JSON key within the secret
Best Practices for Cloud Secrets Management
Use IAM Roles/Service Accounts (No Access Keys)
- AWS: Assign IAM role to EC2/ECS/Lambda with
secretsmanager:GetSecretValue - Kubernetes: Use ServiceAccount with IRSA (IAM Roles for Service Accounts)
- Never hard-code AWS access keys to access Secrets Manager!
Enable Secret Rotation
- AWS Secrets Manager supports automatic rotation (Lambda-based)
- Rotate database passwords, API keys every 30-90 days
- Application should handle rotation gracefully (cache TTL < rotation interval)
Audit Secret Access
- Enable AWS CloudTrail for Secrets Manager API calls
- Monitor for unusual access patterns
- Alert on
GetSecretValuefrom unexpected sources
Separate Secrets by Environment
- Never share secrets across environments
- Use IAM policies to restrict access by environment
Cache Secrets (Reduce API Calls)
- Cache for 15-60 minutes depending on rotation frequency
- Reduces cost and latency
- Use libraries like
aws-secretsmanager-caching