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}")

5. Authentication & Authorization#

Authentication vs Authorization#

Authentication (Who are you?)

  • Login with username/password

  • Multi-factor authentication (MFA)

  • Social login (OAuth)

  • API keys, tokens

Authorization (What can you do?)

  • Role-based access control (RBAC)

  • Permission checks

  • Resource-level permissions

JWT (JSON Web Tokens)#

# Install: pip install pyjwt
import jwt
from datetime import datetime, timedelta

SECRET_KEY = "your-secret-key-keep-this-safe"  # Should be in environment variable!

def create_token(user_id, username):
    """Create JWT token for user."""
    payload = {
        'user_id': user_id,
        'username': username,
        'exp': datetime.utcnow() + timedelta(hours=24),  # Expires in 24 hours
        'iat': datetime.utcnow()  # Issued at
    }
    
    token = jwt.encode(payload, SECRET_KEY, algorithm='HS256')
    return token

def verify_token(token):
    """Verify and decode JWT token."""
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
        return payload
    except jwt.ExpiredSignatureError:
        raise ValueError("Token has expired")
    except jwt.InvalidTokenError:
        raise ValueError("Invalid token")

# Usage
token = create_token(1, 'alice')
print(f"Token: {token}")

decoded = verify_token(token)
print(f"Decoded: {decoded}")

Rate Limiting#

from time import time
from collections import defaultdict

class RateLimiter:
    """Simple rate limiter."""
    
    def __init__(self, max_requests=5, window=60):
        """
        Args:
            max_requests: Maximum requests allowed
            window: Time window in seconds
        """
        self.max_requests = max_requests
        self.window = window
        self.requests = defaultdict(list)
    
    def is_allowed(self, identifier):
        """Check if request is allowed."""
        now = time()
        
        # Clean old requests
        self.requests[identifier] = [
            req_time for req_time in self.requests[identifier]
            if now - req_time < self.window
        ]
        
        # Check limit
        if len(self.requests[identifier]) >= self.max_requests:
            return False
        
        # Record request
        self.requests[identifier].append(now)
        return True

# Usage
limiter = RateLimiter(max_requests=3, window=10)  # 3 requests per 10 seconds

def login(username, password):
    if not limiter.is_allowed(username):
        return "Rate limit exceeded. Try again later."
    
    # ... actual login logic ...
    return "Login successful"

# Test
for i in range(5):
    print(f"Attempt {i+1}: {login('alice', 'password')}")

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:

  1. Network layer: Firewall, VPN

  2. Application layer: Input validation, authentication

  3. Database layer: Parameterized queries, least privilege

  4. 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#

  1. Whatโ€™s the difference between authentication and authorization?

  2. What is SQL injection and how do you prevent it?

  3. Why should you never store passwords in plaintext?

  4. What hashing algorithm should you use for passwords?

  5. What is XSS and how do you prevent it?

  6. Why use HTTPS instead of HTTP?

  7. What is the principle of least privilege?

  8. Where should you store API keys and secrets?

  9. What is rate limiting and why is it important?

  10. 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#

  1. Security by default - Make secure choice the easy choice

  2. Fail securely - Deny access on errors

  3. Donโ€™t roll your own crypto - Use tested libraries

  4. Keep it simple - Complex code has more vulnerabilities

  5. Assume breach - Plan for when (not if) youโ€™re hacked

  6. Security reviews - Code review for security

  7. Educate team - Everyone needs security awareness

  8. Stay updated - New vulnerabilities discovered daily


๐Ÿ“š Resources#

Essential Reading:

Tools:

Learning Platforms:


โ€œSecurity is not a product, but a process.โ€ - Bruce Schneier

Build security into everything you create! ๐Ÿ”’โœจ