π Secure Coding
π― Introduction to Secure Coding
Secure coding is the practice of developing computer software in a way that guards against the accidental introduction of security vulnerabilities. It involves following a set of rules and best practices that help eliminate the most common coding errors that lead to security breaches.
- Prevention over Remediation: It's cheaper and more effective to prevent vulnerabilities than to fix them later
- Defense in Depth: Multiple layers of security controls
- Principle of Least Privilege: Grant minimum necessary permissions
- Input Validation: Never trust user input
- Secure by Default: Default configurations should be secure
π Secrets Management
π― Why Secrets Management Matters
Hardcoded secrets in source code are one of the most common security vulnerabilities. API keys, passwords, tokens, and certificates must be properly managed to prevent unauthorized access.
π Common Secret Types:
- API Keys and Tokens
- Database Credentials
- Encryption Keys
- SSL/TLS Certificates
- Service Account Credentials
- Third-party Integration Keys
β Vulnerable Secrets Management
π¨ Bad Practice
# β NEVER DO THIS - Hardcoded secrets
import requests
class DatabaseConnector:
def __init__(self):
# Hardcoded credentials - MAJOR RISK
self.db_host = "prod-db.company.com"
self.username = "admin"
self.password = "SuperSecret123!"
self.api_key = "sk-1234567890abcdef"
def connect(self):
conn_str = f"postgresql://{self.username}:" + \
f"{self.password}@{self.db_host}/mydb"
return conn_str
# Hardcoded API key in source code
def call_external_api():
headers = {
'Authorization': 'Bearer sk-1234567890abcdef',
'Content-Type': 'application/json'
}
return requests.get('https://api.example.com/data',
headers=headers)
# Embedded encryption key
ENCRYPTION_KEY = "my-super-secret-key-2023"
β Secure Practice
# β
SECURE - Environment variables
import os
import boto3
from cryptography.fernet import Fernet
import logging
class SecureConnector:
def __init__(self):
# Load from environment variables
self.db_host = os.getenv('DB_HOST')
self.username = os.getenv('DB_USERNAME')
self.password = self._get_secret('DB_PASSWORD')
self.api_key = self._get_secret('API_KEY')
if not all([self.db_host, self.username,
self.password, self.api_key]):
raise ValueError("Missing required env vars")
def _get_secret(self, secret_name):
"""Retrieve secret from AWS Secrets Manager"""
try:
client = boto3.client('secretsmanager')
response = client.get_secret_value(
SecretId=secret_name)
return response['SecretString']
except Exception as e:
logging.error(f"Failed to retrieve secret: "
f"{secret_name}")
raise
def connect(self):
# Use connection pooling and SSL
return {
'host': self.db_host,
'username': self.username,
'password': self.password,
'sslmode': 'require'
}
# Secure API calling
def call_external_api():
api_key = os.getenv('EXTERNAL_API_KEY')
if not api_key:
raise ValueError("API key not configured")
headers = {
'Authorization': f'Bearer {api_key}',
'Content-Type': 'application/json'
}
try:
response = requests.get(
'https://api.example.com/data',
headers=headers, timeout=30)
response.raise_for_status()
return response.json()
except requests.RequestException as e:
logging.error("API call failed", exc_info=True)
raise
# Dynamic key generation
def generate_encryption_key():
return Fernet.generate_key()
π οΈ Secrets Management Tools
βοΈ Cloud-Based Solutions
- AWS Secrets Manager: Managed service with automatic rotation
- Azure Key Vault: Secure key and secret storage
- Google Secret Manager: Global secret management
- HashiCorp Vault: Multi-cloud secret management
π§ Development Tools
- dotenv: Load environment variables from .env files
- git-secrets: Prevent committing secrets to git
- pre-commit hooks: Scan for secrets before commits
- Kubernetes Secrets: Native secret management in K8s
π Detection Tools
- TruffleHog: Search for secrets in git repos
- GitLeaks: Detect and prevent secrets in git
- detect-secrets: Pre-commit hook for secret detection
- GitHub Secret Scanning: Built-in secret detection
β οΈ Error Handling and Information Disclosure
π¨ Information Disclosure Through Error Messages
Poor error handling can expose sensitive information about your system architecture, database structure, file paths, and internal logic to attackers. This information can be used to plan more sophisticated attacks.
π Secure Error Handling Patterns
π¨ Vulnerable Error Handling
# β BAD - Exposing sensitive information
import sqlite3
import traceback
def get_user_data(user_id):
try:
conn = sqlite3.connect(
'/var/www/app/database/users.db')
cursor = conn.cursor()
# SQL injection vulnerability + info disclosure
query = f"SELECT * FROM users WHERE id = {user_id}"
cursor.execute(query)
result = cursor.fetchone()
if not result:
raise Exception(f"User {user_id} not found in "
f"database table 'users'")
return result
except sqlite3.Error as e:
# Exposing database errors to client
raise Exception(f"Database error: {str(e)}")
except Exception as e:
# Exposing full stack trace
raise Exception(f"Internal error: "
f"{traceback.format_exc()}")
def authenticate_user(username, password):
try:
conn = sqlite3.connect('/home/admin/production.db')
cursor = conn.cursor()
# Vulnerable query with detailed error
query = f"SELECT password_hash FROM users " + \
f"WHERE username = '{username}'"
cursor.execute(query)
result = cursor.fetchone()
if not result:
raise Exception(f"Username '{username}' does not "
f"exist in our system")
stored_hash = result[0]
if not check_password(password, stored_hash):
raise Exception(f"Invalid password for user "
f"'{username}'. Password should be "
f"at least 8 characters.")
return True
except Exception as e:
# Logging sensitive data
print(f"Authentication failed for {username} "
f"with password {password}: {str(e)}")
raise
β Secure Error Handling
# β
SECURE - Proper error handling and logging
import sqlite3
import logging
import hashlib
import secrets
from typing import Optional, Dict, Any
# Configure secure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - '
'%(message)s',
handlers=[
logging.FileHandler('/var/log/app/security.log'),
logging.StreamHandler()
]
)
logger = logging.getLogger(__name__)
class SecurityError(Exception):
"""Custom exception for security-related errors"""
pass
class DatabaseManager:
def __init__(self, db_path: str):
self.db_path = db_path
self._setup_database()
def _setup_database(self):
"""Initialize database with proper security"""
try:
with sqlite3.connect(self.db_path) as conn:
conn.execute("PRAGMA foreign_keys = ON")
conn.execute("PRAGMA secure_delete = ON")
except sqlite3.Error as e:
logger.error("Database initialization failed",
exc_info=True)
raise SecurityError("Database config error")
def get_user_data(self, user_id: int) -> Optional[Dict]:
"""Retrieve user data with secure error handling"""
try:
with sqlite3.connect(self.db_path) as conn:
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
# Parameterized query prevents SQL injection
cursor.execute("SELECT * FROM users WHERE id = ?",
(user_id,))
result = cursor.fetchone()
if result:
return dict(result)
else:
# Generic error message
logger.warning(f"User lookup failed for "
f"ID: {user_id}")
return None
except sqlite3.Error as e:
# Log technical details internally
logger.error(f"Database query failed for "
f"user_id {user_id}", exc_info=True)
# Return generic error to client
raise SecurityError("Data retrieval failed")
except Exception as e:
logger.error("Unexpected error in get_user_data",
exc_info=True)
raise SecurityError("Internal server error")
class AuthenticationManager:
def __init__(self, db_manager: DatabaseManager):
self.db_manager = db_manager
self.max_attempts = 5
self.lockout_duration = 300 # 5 minutes
def authenticate_user(self, username: str,
password: str) -> bool:
"""Secure user authentication"""
# Input validation
if not username or not password:
logger.warning("Authentication attempt with "
"empty credentials")
raise SecurityError("Invalid credentials")
if len(username) > 255 or len(password) > 255:
logger.warning(f"Authentication attempt with "
f"oversized input from "
f"{username[:50]}...")
raise SecurityError("Invalid credentials")
try:
# Check for account lockout
if self._is_account_locked(username):
logger.warning(f"Authentication attempt on "
f"locked account: {username}")
raise SecurityError("Account temporarily locked")
# Retrieve user data
user_data = self._get_user_by_username(username)
if not user_data:
# Same error for non-existent users
self._log_failed_attempt(username)
raise SecurityError("Invalid credentials")
# Verify password
if self._verify_password(password,
user_data['password_hash']):
logger.info(f"Successful authentication for "
f"user: {username}")
self._clear_failed_attempts(username)
return True
else:
self._log_failed_attempt(username)
raise SecurityError("Invalid credentials")
except SecurityError:
# Re-raise security errors as-is
raise
except Exception as e:
# Log unexpected errors
logger.error("Unexpected authentication error",
exc_info=True)
raise SecurityError("Authentication service unavailable")
def _get_user_by_username(self, username: str) -> Optional[Dict]:
"""Retrieve user by username using parameterized query"""
try:
with sqlite3.connect(self.db_manager.db_path) as conn:
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
cursor.execute(
"SELECT id, username, password_hash FROM users WHERE username = ?",
(username,)
)
result = cursor.fetchone()
return dict(result) if result else None
except sqlite3.Error as e:
logger.error("Database query failed", exc_info=True)
return None
def _verify_password(self, password: str, stored_hash: str) -> bool:
"""Secure password verification"""
try:
# Use secure comparison to prevent timing attacks
return secrets.compare_digest(
hashlib.pbkdf2_hmac('sha256', password.encode(), b'salt', 100000),
bytes.fromhex(stored_hash)
)
except Exception as e:
logger.error("Password verification error", exc_info=True)
return False
def _log_failed_attempt(self, username: str):
"""Log failed authentication attempt"""
# Log without exposing password
logger.warning(f"Failed authentication attempt for username: {username}")
def _is_account_locked(self, username: str) -> bool:
"""Check if account is locked due to failed attempts"""
# Implementation would check failed attempt count and timing
return False
def _clear_failed_attempts(self, username: str):
"""Clear failed attempt counter after successful login"""
pass
# Usage example
def main():
try:
db_manager = DatabaseManager("/secure/path/app.db")
auth_manager = AuthenticationManager(db_manager)
result = auth_manager.authenticate_user("john_doe", "secure_password")
if result:
print("Authentication successful")
except SecurityError as e:
# Return generic error to client
print(f"Error: {str(e)}")
except Exception as e:
# Log unexpected errors, return generic message
logger.error("Unexpected application error", exc_info=True)
print("Service temporarily unavailable")
π Secure Password Handling
# β
SECURE - Proper password hashing and verification
import bcrypt
import secrets
import hashlib
from typing import Tuple
class SecurePasswordManager:
def __init__(self):
self.min_length = 12
self.max_length = 128
def hash_password(self, password: str) -> str:
"""Hash password using bcrypt with salt"""
if not self._validate_password_strength(password):
raise ValueError("Password does not meet "
"security requirements")
# Generate salt and hash password
salt = bcrypt.gensalt(rounds=12)
hashed = bcrypt.hashpw(password.encode('utf-8'), salt)
return hashed.decode('utf-8')
def verify_password(self, password: str, hashed: str) -> bool:
"""Verify password against hash using constant-time"""
try:
return bcrypt.checkpw(password.encode('utf-8'),
hashed.encode('utf-8'))
except Exception:
return False
def _validate_password_strength(self, password: str) -> bool:
"""Validate password meets security requirements"""
if (len(password) < self.min_length or
len(password) > self.max_length):
return False
has_upper = any(c.isupper() for c in password)
has_lower = any(c.islower() for c in password)
has_digit = any(c.isdigit() for c in password)
has_special = any(c in "!@#$%^&*()_+-=[]{}|;:,.<>?"
for c in password)
return all([has_upper, has_lower, has_digit, has_special])
def generate_secure_token(self, length: int = 32) -> str:
"""Generate cryptographically secure random token"""
return secrets.token_urlsafe(length)
def generate_api_key(self) -> Tuple[str, str]:
"""Generate API key pair (public ID and secret key)"""
key_id = f"ak_{secrets.token_urlsafe(16)}"
secret_key = secrets.token_urlsafe(32)
# Hash the secret key for storage
secret_hash = hashlib.sha256(
secret_key.encode()).hexdigest()
return key_id, secret_key, secret_hash
# Example usage
password_manager = SecurePasswordManager()
# Hash a password
password = "MySecureP@ssw0rd123!"
hashed_password = password_manager.hash_password(password)
print(f"Hashed password: {hashed_password}")
# Verify password
is_valid = password_manager.verify_password(password,
hashed_password)
print(f"Password verification: {is_valid}")
# Generate secure tokens
token = password_manager.generate_secure_token()
print(f"Secure token: {token}")
# Generate API key
key_id, secret, secret_hash = password_manager.generate_api_key()
print(f"API Key ID: {key_id}")
print(f"Secret Key: {secret}")
print(f"Secret Hash: {secret_hash}")
π― Security in CI/CD Pipeline
# Example GitHub Actions workflow for security
name: Security Pipeline
on: [push, pull_request]
jobs:
security-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
# Secret scanning
- name: Secret Scan
uses: trufflesecurity/trufflehog@main
with:
path: ./
base: main
head: HEAD
# Static Application Security Testing (SAST)
- name: Run Bandit Security Linter
uses: jpetrucciani/bandit-check@main
with:
path: "."
# Dependency vulnerability scanning
- name: Python Security Check
uses: pyupio/safety@2.3.1
with:
api-key: ${{ secrets.SAFETY_API_KEY }}
# Dynamic Application Security Testing (DAST)
- name: ZAP Scan
uses: zaproxy/action-full-scan@v0.4.0
with:
target: 'https://your-app.com'
rules_file_name: '.zap/rules.tsv'
cmd_options: '-a'
# Infrastructure as Code security
- name: Checkov Security Scan
uses: bridgecrewio/checkov-action@v12
with:
directory: ./terraform
framework: terraform
output_format: sarif
output_file_path: checkov.sarif
# Upload results to GitHub Security tab
- name: Upload SARIF file
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: checkov.sarif
π Conclusion
Secure coding is not just about following a checklistβit's about developing a security mindset and making security an integral part of your development process. The key principles include:
π― Key Takeaways
- Defense in Depth: Implement multiple layers of security controls
- Secure by Default: Make secure configuration the default
- Principle of Least Privilege: Grant minimum necessary permissions
- Input Validation: Never trust user input
- Fail Securely: Ensure failures don't compromise security
- Keep it Simple: Complex systems are harder to secure
- Stay Updated: Keep dependencies and frameworks current
- Security Testing: Test security throughout the development lifecycle
Security is everyone's responsibility, and by following these practices, you'll be well-equipped to build more secure applications and protect against common vulnerabilities.