Security Essentials for Developers#
Security is everyoneโs responsibility. A single security flaw can compromise an entire system. Every developer needs to know security basics.
What Youโll Learn#
Why security matters
Common vulnerabilities (OWASP Top 10)
Input validation and sanitization
Authentication and authorization
Password security and hashing
Secure coding practices
HTTPS and encryption
API security
Secrets management
Security tools and resources
๐ก Real-World Analogy#
Security is like locking your house:
๐ Locks = Authentication (who can enter?)
๐ช Doors = Access points that need protection
๐ Keys = Credentials that must be protected
๐น Cameras = Logging and monitoring
๐ฐ Fence = Firewall and network security
One weak point compromises everything!
1. Why Security Matters#
Real-World Breaches#
Equifax (2017)
147 million peopleโs data stolen
Cause: Unpatched vulnerability
Cost: $700+ million in settlements
Sony PlayStation (2011)
77 million accounts compromised
23 days of downtime
Cost: $171 million
Target (2013)
40 million credit cards stolen
Entry point: HVAC vendorโs credentials
Cost: $252 million
The Cost of Insecurity#
Direct Costs:
Fines and penalties
Legal fees
Notification costs
Credit monitoring for victims
Indirect Costs:
Brand damage
Customer trust lost
Stock price impact
Employee morale
Your Responsibility#
As a developer:
โ You handle user data
โ You write code that could be exploited
โ You make architectural decisions
โ Youโre the first line of defense
โSecurity is not a feature, itโs a requirement.โ
2. OWASP Top 10 Vulnerabilities#
OWASP (Open Web Application Security Project) maintains the Top 10 most critical web application security risks.
1. Broken Access Control#
Problem: Users can access resources they shouldnโt.
Example:
# INSECURE: Anyone can access any user's data
def get_user_data(user_id):
# No check if current user can access this user_id!
return database.get_user(user_id)
# SECURE: Verify authorization
def get_user_data_secure(user_id, current_user):
# Check if user is authorized
if current_user.id != user_id and not current_user.is_admin:
raise PermissionError("Not authorized to access this user's data")
return database.get_user(user_id)
2. Cryptographic Failures#
Problem: Sensitive data not encrypted or poorly encrypted.
Example:
# INSECURE: Plaintext passwords!
users = {
"alice": {"password": "secret123"} # โ NEVER DO THIS!
}
# SECURE: Hash passwords
import hashlib
import secrets
def hash_password(password, salt=None):
"""Securely hash a password with salt."""
if salt is None:
salt = secrets.token_hex(16)
# Use a strong hashing algorithm
hash_obj = hashlib.pbkdf2_hmac(
'sha256',
password.encode('utf-8'),
salt.encode('utf-8'),
100000 # iterations
)
return salt + hash_obj.hex()
# Store this instead!
hashed = hash_password("secret123")
print(f"Hashed password: {hashed[:50]}...")
3. Injection#
Problem: Untrusted data executed as code.
SQL Injection Example:
# INSECURE: SQL Injection vulnerability!
def get_user_insecure(username):
query = f"SELECT * FROM users WHERE username = '{username}'"
# If username = "admin' OR '1'='1" โ returns ALL users!
return database.execute(query)
# SECURE: Use parameterized queries
def get_user_secure(username):
query = "SELECT * FROM users WHERE username = ?"
return database.execute(query, (username,))
# Even better: Use an ORM
# user = User.objects.get(username=username)
4. Insecure Design#
Problem: Architecture lacks security from the start.
Prevention:
Threat modeling during design
Security requirements
Defense in depth (multiple layers)
Principle of least privilege
5. Security Misconfiguration#
Problem: Default settings, unnecessary features, verbose errors.
Example:
# INSECURE: Debug mode in production!
app.config['DEBUG'] = True # โ Shows stack traces to attackers!
# SECURE: Production configuration
import os
app.config['DEBUG'] = os.getenv('DEBUG', 'False') == 'True'
app.config['SECRET_KEY'] = os.getenv('SECRET_KEY') # From environment
app.config['DATABASE_URL'] = os.getenv('DATABASE_URL')
6. Vulnerable and Outdated Components#
Problem: Using libraries with known vulnerabilities.
Prevention:
# Check for vulnerabilities
# pip install safety
# safety check
# Or use pip-audit
# pip install pip-audit
# pip-audit
# Keep dependencies updated
# pip list --outdated
# pip install --upgrade package-name
7. Identification and Authentication Failures#
Problem: Weak authentication, session management issues.
Prevention:
Multi-factor authentication (MFA)
Strong password requirements
Rate limiting on login attempts
Secure session management
8. Software and Data Integrity Failures#
Problem: Code and infrastructure donโt verify integrity.
Example: CI/CD pipeline without verification, deserializing untrusted data.
9. Security Logging and Monitoring Failures#
Problem: Canโt detect or respond to breaches.
Prevention:
import logging
# Log security events
security_logger = logging.getLogger('security')
def login_attempt(username, success):
if success:
security_logger.info(f"Successful login: {username}")
else:
security_logger.warning(f"Failed login attempt: {username}")
def access_denied(user, resource):
security_logger.error(
f"Access denied: {user} attempted to access {resource}"
)
10. Server-Side Request Forgery (SSRF)#
Problem: Application fetches remote resource without validation.
Example:
import requests
from urllib.parse import urlparse
# INSECURE: User can make server request internal resources!
def fetch_url_insecure(url):
return requests.get(url).text # โ url could be http://localhost:6379
# SECURE: Validate URL
def fetch_url_secure(url):
parsed = urlparse(url)
# Block internal/private IPs
if parsed.hostname in ['localhost', '127.0.0.1', '0.0.0.0']:
raise ValueError("Cannot access internal resources")
# Whitelist allowed domains
allowed_domains = ['example.com', 'api.example.com']
if parsed.hostname not in allowed_domains:
raise ValueError("Domain not allowed")
return requests.get(url, timeout=5).text
3. Input Validation#
Never trust user input! Always validate, sanitize, and escape.
Validation Strategies#
import re
def validate_email(email):
"""Validate email format."""
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
if not re.match(pattern, email):
raise ValueError("Invalid email format")
return email.lower()
def validate_username(username):
"""Validate username."""
# Whitelist approach: only allow specific characters
if not re.match(r'^[a-zA-Z0-9_-]{3,20}$', username):
raise ValueError(
"Username must be 3-20 characters, letters/numbers/underscore/dash only"
)
return username
def validate_age(age):
"""Validate age input."""
try:
age = int(age)
except ValueError:
raise ValueError("Age must be a number")
if age < 0 or age > 150:
raise ValueError("Age must be between 0 and 150")
return age
# Test validations
print(validate_email("alice@example.com"))
print(validate_username("alice_123"))
print(validate_age("25"))
Using Pydantic for Validation#
from pydantic import BaseModel, EmailStr, Field, validator
from typing import Optional
class UserInput(BaseModel):
"""Validated user input model."""
username: str = Field(..., min_length=3, max_length=20, regex=r'^[a-zA-Z0-9_-]+$')
email: EmailStr
age: int = Field(..., ge=0, le=150)
website: Optional[str] = None
@validator('username')
def username_no_admin(cls, v):
if 'admin' in v.lower():
raise ValueError('Username cannot contain "admin"')
return v
# Valid input
user = UserInput(
username="alice123",
email="alice@example.com",
age=25
)
print(user)
# Invalid input will raise ValidationError
# UserInput(username="al", email="bad-email", age=200)
Sanitization - Preventing XSS#
import html
def sanitize_html(text):
"""Escape HTML to prevent XSS."""
return html.escape(text)
# User input
user_comment = '<script>alert("XSS")</script>'
# INSECURE: Direct insertion
print(f"<div>{user_comment}</div>") # โ Script would execute!
# SECURE: Sanitized
safe_comment = sanitize_html(user_comment)
print(f"<div>{safe_comment}</div>") # โ
Rendered as text
# Modern frameworks (React, Vue, etc.) do this automatically!
# But be careful with dangerouslySetInnerHTML or v-html
4. Password Security#
What NOT to Do#
โ Store passwords in plaintext โ Use simple hashing (MD5, SHA1) โ Use encryption (passwords should be one-way) โ Implement your own crypto
What to Do#
โ Use battle-tested libraries โ Use bcrypt, scrypt, or Argon2 โ Use a unique salt per password โ Enforce strong password requirements
# Install: pip install bcrypt
import bcrypt
def hash_password_bcrypt(password: str) -> bytes:
"""Hash password using bcrypt."""
# Generate salt and hash
salt = bcrypt.gensalt()
hashed = bcrypt.hashpw(password.encode('utf-8'), salt)
return hashed
def verify_password(password: str, hashed: bytes) -> bool:
"""Verify password against hash."""
return bcrypt.checkpw(password.encode('utf-8'), hashed)
# Usage
password = "MySecurePassword123!"
hashed = hash_password_bcrypt(password)
print(f"Hashed: {hashed}")
print(f"Correct password: {verify_password(password, hashed)}")
print(f"Wrong password: {verify_password('wrong', hashed)}")
Password Strength Requirements#
import re
def check_password_strength(password):
"""
Check password strength.
Requirements:
- At least 8 characters
- At least one uppercase letter
- At least one lowercase letter
- At least one number
- At least one special character
"""
if len(password) < 8:
return False, "Password must be at least 8 characters"
if not re.search(r'[A-Z]', password):
return False, "Password must contain uppercase letter"
if not re.search(r'[a-z]', password):
return False, "Password must contain lowercase letter"
if not re.search(r'[0-9]', password):
return False, "Password must contain a number"
if not re.search(r'[!@#$%^&*(),.?":{}|<>]', password):
return False, "Password must contain special character"
# Check against common passwords (simplified)
common_passwords = ['Password123!', 'Admin123!', 'Welcome123!']
if password in common_passwords:
return False, "Password is too common"
return True, "Password is strong"
# Test
passwords = [
"weak",
"Password123",
"MySecureP@ss123"
]
for pwd in passwords:
valid, message = check_password_strength(pwd)
print(f"{pwd}: {message}")
6. Secrets Management#
Never Hardcode Secrets!#
โ Donโt do this:
DATABASE_URL = "postgresql://user:password@localhost/db"
API_KEY = "sk-1234567890abcdef"
SECRET_KEY = "my-secret-key"
Use Environment Variables#
import os
from dotenv import load_dotenv
# Load from .env file (don't commit this file!)
load_dotenv()
# Get secrets from environment
DATABASE_URL = os.getenv('DATABASE_URL')
API_KEY = os.getenv('API_KEY')
SECRET_KEY = os.getenv('SECRET_KEY')
# With defaults for development
DEBUG = os.getenv('DEBUG', 'False') == 'True'
PORT = int(os.getenv('PORT', '5000'))
# Validate required secrets
if not SECRET_KEY:
raise ValueError("SECRET_KEY environment variable not set!")
.env File Example#
# .env (add to .gitignore!)
DATABASE_URL=postgresql://user:pass@localhost/db
API_KEY=sk-1234567890abcdef
SECRET_KEY=randomly-generated-secret-key
DEBUG=False
.env.example for Documentation#
# .env.example (commit this!)
DATABASE_URL=postgresql://user:pass@localhost/dbname
API_KEY=your-api-key-here
SECRET_KEY=your-secret-key-here
DEBUG=False
Production Secrets Management#
For production, use:
AWS Secrets Manager
HashiCorp Vault
Azure Key Vault
Google Secret Manager
Kubernetes Secrets
7. HTTPS and Encryption#
Why HTTPS?#
HTTP (unencrypted):
Passwords visible in plaintext
Data can be modified in transit
Man-in-the-middle attacks
HTTPS (encrypted):
โ Encrypted communication
โ Verified server identity
โ Data integrity
Force HTTPS#
from flask import Flask, request, redirect
app = Flask(__name__)
@app.before_request
def force_https():
"""Redirect HTTP to HTTPS."""
if not request.is_secure and not app.debug:
url = request.url.replace('http://', 'https://', 1)
return redirect(url, code=301)
# Or use HSTS (HTTP Strict Transport Security)
@app.after_request
def set_hsts(response):
response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains'
return response
Security Headers#
@app.after_request
def set_security_headers(response):
"""Set security headers."""
# Prevent clickjacking
response.headers['X-Frame-Options'] = 'SAMEORIGIN'
# Prevent MIME sniffing
response.headers['X-Content-Type-Options'] = 'nosniff'
# Enable XSS protection
response.headers['X-XSS-Protection'] = '1; mode=block'
# Content Security Policy
response.headers['Content-Security-Policy'] = "default-src 'self'"
# Referrer policy
response.headers['Referrer-Policy'] = 'strict-origin-when-cross-origin'
return response
8. Secure Coding Practices#
Principle of Least Privilege#
Give minimum permissions needed:
# BAD: Database user has all permissions
# db_user = "admin" # Can DROP tables!
# GOOD: Different users for different tasks
# read_only_user = "app_readonly" # SELECT only
# app_user = "app_write" # SELECT, INSERT, UPDATE
# admin_user = "db_admin" # Only for migrations
Defense in Depth#
Multiple layers of security:
Network layer: Firewall, VPN
Application layer: Input validation, authentication
Database layer: Parameterized queries, least privilege
Logging layer: Audit trails, monitoring
Fail Securely#
def check_permission(user, resource):
"""Check if user has permission."""
try:
# Query permission system
has_permission = permission_db.check(user, resource)
return has_permission
except Exception as e:
# FAIL SECURELY: Deny access on error
logging.error(f"Permission check failed: {e}")
return False # โ
Deny by default
# return True # โ NEVER do this!
Keep Security Simple#
Complex code โ More bugs โ More vulnerabilities
# Complex (harder to audit)
def auth(u,p): return h(p)==db.get(u).get('h') if db.get(u) else False
# Simple (easier to audit)
def authenticate(username, password):
"""Authenticate user with password."""
user = database.get_user(username)
if user is None:
return False
password_hash = hash_password(password)
return password_hash == user.password_hash
9. Security Tools#
Static Analysis#
# Install security linters
# pip install bandit safety semgrep
# Bandit - Python security linter
# bandit -r .
# Safety - Check dependencies for vulnerabilities
# safety check
# Semgrep - Static analysis
# semgrep --config=auto .
Dependency Scanning#
# pip-audit - Scan for known vulnerabilities
# pip install pip-audit
# pip-audit
# Or use GitHub's Dependabot
# Automatically creates PRs for vulnerable dependencies
Secret Scanning#
# gitleaks - Scan for secrets in git history
# brew install gitleaks
# gitleaks detect
# truffleHog - Find secrets in code
# pip install truffleHog
# trufflehog filesystem .
๐ Exercises#
Exercise 1: Fix Security Issues#
Find and fix all security issues:
# This code has multiple security issues!
def login(username, password):
query = f"SELECT * FROM users WHERE username='{username}' AND password='{password}'"
user = database.execute(query)
return user is not None
# Issues:
# 1. SQL injection
# 2. Plaintext password comparison
# 3. No rate limiting
# 4. No logging
# Your fixed version:
Exercise 2: Input Validation#
Create a validation function for user registration:
def validate_registration(username, email, password):
"""
Validate user registration data.
Requirements:
- Username: 3-20 chars, alphanumeric + underscore
- Email: Valid email format
- Password: Strong (8+ chars, mixed case, numbers, special)
Returns:
(bool, str): (is_valid, error_message)
"""
# Your code here
pass
Exercise 3: Secure API Key Storage#
Refactor this code to use environment variables:
# INSECURE CODE
API_KEY = "sk-1234567890abcdef"
DATABASE_URL = "postgresql://admin:password123@localhost/mydb"
def connect_to_api():
return requests.get("https://api.example.com", headers={"Authorization": API_KEY})
# Your secure version using environment variables:
โ Self-Check Quiz#
Whatโs the difference between authentication and authorization?
What is SQL injection and how do you prevent it?
Why should you never store passwords in plaintext?
What hashing algorithm should you use for passwords?
What is XSS and how do you prevent it?
Why use HTTPS instead of HTTP?
What is the principle of least privilege?
Where should you store API keys and secrets?
What is rate limiting and why is it important?
Name 3 security headers you should set.
๐ฏ Key Takeaways#
Security is everyoneโs job - Not just security team
Never trust user input - Always validate and sanitize
Use parameterized queries - Prevent SQL injection
Hash passwords - Never store plaintext (use bcrypt)
Use HTTPS - Encrypt all communication
Manage secrets properly - Environment variables, not code
Principle of least privilege - Minimum permissions needed
Defense in depth - Multiple security layers
Keep dependencies updated - Patch vulnerabilities
Log security events - Monitor and respond
๐ Next Steps#
Security is a journey, not a destination!
Continue learning:
OWASP Top 10 - Read in detail
Security testing - Pen testing, fuzzing
Secure DevOps - CI/CD security
Cloud security - AWS/Azure/GCP best practices
Practice:
CTF challenges (Capture The Flag)
HackTheBox, TryHackMe
OWASP WebGoat (intentionally vulnerable app)
๐ก Pro Tips#
Security by default - Make secure choice the easy choice
Fail securely - Deny access on errors
Donโt roll your own crypto - Use tested libraries
Keep it simple - Complex code has more vulnerabilities
Assume breach - Plan for when (not if) youโre hacked
Security reviews - Code review for security
Educate team - Everyone needs security awareness
Stay updated - New vulnerabilities discovered daily
๐ Resources#
Essential Reading:
Tools:
Bandit - Python security linter
Safety - Dependency scanner
Semgrep - Static analysis
Gitleaks - Secret scanner
Learning Platforms:
โSecurity is not a product, but a process.โ - Bruce Schneier
Build security into everything you create! ๐โจ