Injection Attacks Deep Dive

SQL, NoSQL, Command, LDAP Injection & Path Traversal

Introduction

Injection attacks occur when untrusted data is sent to an interpreter as part of a command or query. The attacker's hostile data tricks the interpreter into executing unintended commands or accessing unauthorized data. Injection has been in the OWASP Top 10 for over a decade. This lesson shows exactly how each injection type works, with vulnerable Python code, and the precise fix for each.

SQL Injection

Vulnerable Code

# sql_injection_vulnerable.py
import sqlite3

conn = sqlite3.connect(":memory:")
conn.execute("CREATE TABLE users (id INTEGER, username TEXT, password TEXT, is_admin INTEGER)")
conn.execute("INSERT INTO users VALUES (1, 'alice', 'hash1', 0)")
conn.execute("INSERT INTO users VALUES (2, 'admin', 'secrethash', 1)")
conn.commit()

# ❌ VULNERABLE, String formatting with user input
def login_vulnerable(username: str, password_hash: str):
    query = f"""
        SELECT * FROM users
        WHERE username = '{username}' AND password = '{password_hash}'
    """
    print(f"  Query: {query.strip()}")
    row = conn.execute(query).fetchone()
    return row

print("=== SQL Injection Demo ===")

# Normal login
print("\n[1] Normal login:")
result = login_vulnerable("alice", "hash1")
print(f"  Result: {result}")

# SQL injection, bypass password check
print("\n[2] SQL Injection attack: username = alice'--")
# The -- comments out the rest of the query (password check)
result = login_vulnerable("alice'--", "anything")
print(f"  Result: {result}  ← LOGGED IN without correct password!")

# Dump all users
print("\n[3] Dump all users: username = ' OR '1'='1'--")
result = login_vulnerable("' OR '1'='1'--", "")
print(f"  Result: {result}  ← Got first user (can iterate to get all)")
Expected Output:
=== SQL Injection Demo ===

[1] Normal login:
  Query: SELECT * FROM users WHERE username = 'alice' AND password = 'hash1'
  Result: (1, 'alice', 'hash1', 0)

[2] SQL Injection attack: username = alice'--
  Query: SELECT * FROM users WHERE username = 'alice'-- AND password = 'anything'
  Result: (1, 'alice', 'hash1', 0)  ← LOGGED IN without correct password!

[3] Dump all users: username = ' OR '1'='1'--
  Query: SELECT * FROM users WHERE username = '' OR '1'='1'-- AND password = ''
  Result: (1, 'alice', 'hash1', 0)  ← Got first user

Parameterized Query Fix

Install: pip install sqlalchemy

# sql_injection_fixed.py
# ✅ FIXED, Always use parameterized queries
def login_safe(username: str, password_hash: str):
    query = "SELECT * FROM users WHERE username = ? AND password = ?"
    # The ? placeholders are filled in by the database driver
    # User input is NEVER interpolated into the query string
    row = conn.execute(query, (username, password_hash)).fetchone()
    return row

print("=== Safe Query Demo ===")

# Normal login still works
result = login_safe("alice", "hash1")
print(f"Normal login: {result}")   # Works ✅

# SQL injection attempt, treated as literal string, NOT code
result = login_safe("alice'--", "anything")
print(f"Injection attempt: {result}")   # None ✅, no match, not executed as SQL

# ORM approach is equally safe (SQLAlchemy example)
from sqlalchemy import create_engine, text

engine = create_engine("sqlite:///:memory:")
with engine.connect() as c:
    c.execute(text("CREATE TABLE test (id INTEGER, name TEXT)"))
    c.execute(text("INSERT INTO test VALUES (1, 'alice')"))
    c.commit()

    # ✅ SQLAlchemy named parameters
    row = c.execute(
        text("SELECT * FROM test WHERE name = :name"),
        {"name": "alice'--"},   # Safely escaped by SQLAlchemy
    ).fetchone()
    print(f"SQLAlchemy parameterized: {row}")   # None ✅

print("\n✅ Rule: NEVER use string formatting/interpolation in SQL queries")
print("✅ Use ? placeholders (sqlite3), %s (psycopg2), or :name (SQLAlchemy)")
Expected Output:
=== Safe Query Demo ===
Normal login: (1, 'alice', 'hash1', 0)
Injection attempt: None
SQLAlchemy parameterized: None

✅ Rule: NEVER use string formatting/interpolation in SQL queries
✅ Use ? placeholders (sqlite3), %s (psycopg2), or :name (SQLAlchemy)

Command Injection

Command injection occurs when user input is passed to a shell command. The attacker uses shell metacharacters (; & | ` $(...)) to execute additional commands.

import os
import subprocess
import shlex

# ❌ VULNERABLE, os.system with user input
def ping_vulnerable(hostname: str) -> None:
    # Attack payload: "google.com; cat /etc/passwd"
    # Executes: ping -c 1 google.com; cat /etc/passwd
    os.system(f"ping -c 1 {hostname}")

# ❌ ALSO VULNERABLE, shell=True with user input
def ping_also_vulnerable(hostname: str) -> str:
    result = subprocess.run(f"ping -c 1 {hostname}", shell=True, capture_output=True, text=True)
    return result.stdout

# ✅ FIXED, subprocess with list args (no shell interpolation)
def ping_safe(hostname: str) -> str:
    # Validate input first
    import re
    if not re.match(r'^[a-zA-Z0-9.-]+$', hostname):
        raise ValueError(f"Invalid hostname: {hostname!r}")

    # Pass as list, subprocess does NOT pass through a shell
    result = subprocess.run(
        ["ping", "-c", "1", hostname],   # Each arg is separate, no shell needed
        capture_output=True,
        text=True,
        timeout=5,
    )
    return result.stdout

# Demonstrate safe usage
try:
    output = ping_safe("127.0.0.1")
    print(f"Safe ping: {output[:100]}")
except subprocess.TimeoutExpired:
    print("Timeout (expected in some environments)")

# Attack rejected
try:
    ping_safe("127.0.0.1; cat /etc/passwd")
except ValueError as e:
    print(f"Attack rejected: {e}")

print("\n✅ Rule: NEVER use shell=True with user input")
print("✅ Pass command args as a list to subprocess.run()")
print("✅ Validate input with an allowlist regex before use")
Expected Output:
Safe ping: PING 127.0.0.1 (127.0.0.1) 56(84) bytes of data.
Attack rejected: Invalid hostname: '127.0.0.1; cat /etc/passwd'

✅ Rule: NEVER use shell=True with user input
✅ Pass command args as a list to subprocess.run()
✅ Validate input with an allowlist regex before use

Path Traversal

Path traversal (directory traversal) allows attackers to access files outside the intended directory using ../ sequences. Example: ../../etc/passwd.

from pathlib import Path

UPLOADS_DIR = Path("/var/app/uploads").resolve()

# ❌ VULNERABLE, Direct path join with user input
def serve_file_vulnerable(filename: str) -> str:
    file_path = UPLOADS_DIR / filename
    # Attack: filename = "../../etc/passwd"
    # Resolved: /var/app/uploads/../../etc/passwd → /etc/passwd
    return file_path.read_text()

# ✅ FIXED, Verify the resolved path stays within UPLOADS_DIR
def serve_file_safe(filename: str) -> str:
    # Reject any path that contains directory separators
    if "/" in filename or "\\" in filename or ".." in filename:
        raise ValueError(f"Invalid filename: {filename!r}")

    file_path = (UPLOADS_DIR / filename).resolve()

    # Double-check: resolved path must be inside UPLOADS_DIR
    # This catches symlinks and other tricks
    if not str(file_path).startswith(str(UPLOADS_DIR)):
        raise PermissionError(f"Path traversal attempt: {filename!r}")

    if not file_path.exists():
        raise FileNotFoundError(f"File not found: {filename}")

    return file_path.read_text()

# Test
import tempfile, os

with tempfile.TemporaryDirectory() as tmp:
    UPLOADS_DIR = Path(tmp).resolve()
    (UPLOADS_DIR / "report.txt").write_text("Quarterly report content")

    # Valid access
    content = serve_file_safe("report.txt")
    print(f"Valid access: {content}")

    # Path traversal attempt
    try:
        serve_file_safe("../../etc/passwd")
    except ValueError as e:
        print(f"Traversal rejected: {e}")

print("\n✅ Always call .resolve() and verify the path starts with your base dir")
Expected Output:
Valid access: Quarterly report content
Traversal rejected: Invalid filename: '../../etc/passwd'

✅ Always call .resolve() and verify the path starts with your base dir

Key Takeaways

  • SQL: always use parameterized queries, ? or :name placeholders; never f-strings or .format() in SQL
  • Command injection: never use shell=True with user input; pass args as a list to subprocess.run()
  • Path traversal: resolve + verify, call .resolve() and check the path starts with your base directory
  • Input validation is secondary, parameterization/safe APIs are the primary defense; validation is defense-in-depth
  • NoSQL is not injection-proof, MongoDB operators like $where and $regex can also be injected; use strict schemas
  • Test your own code, the payloads shown here are simple; run a DAST scanner to find the less obvious variations
What's Next?

Lesson 14 covers web-specific security vulnerabilities: XSS, CSRF, clickjacking, security headers, and CORS.

  • XSS, reflected, stored, and DOM-based variants with Flask examples
  • CSRF, attack flow and CSRF token implementation
  • Security headers, CSP, HSTS, X-Frame-Options in Python middleware