Injection Mechanics: Shell & SQL
Understanding the Root Cause
Section titled “Understanding the Root Cause”Both Shell and SQL injection stem from a single, fundamental error: Mixing Data with Control Plane Instructions.
When user input is concatenated directly into a command string (whether for the OS shell or a Database engine), the interpreter cannot distinguish between the developer’s original intent and the malicious commands embedded in the input.
1. Shell Injection (OS Command Injection)
Section titled “1. Shell Injection (OS Command Injection)”Shell injection occurs when an application passes unsafe user-supplied data (forms, cookies, HTTP headers) to a system shell.
Python Examples
Section titled “Python Examples”In Python, the subprocess module is the standard way to spawn new processes. The danger arises when using shell=True combined with string formatting.
Vulnerable Code
Section titled “Vulnerable Code”import subprocess
def ping_unsafe(hostname): # DANGER: The shell interprets the entire string. # Input: "8.8.8.8; rm -rf /" will be executed. command = f"ping -c 1 {hostname}"
# shell=True invokes /bin/sh (on Unix) or cmd.exe (on Windows) subprocess.run(command, shell=True)Secure Code
Section titled “Secure Code”The fix is to avoid shell=True and pass the command and arguments as a list. This bypasses the shell entirely; the OS executes the ping binary directly and passes the input strictly as an argument.
import subprocess
def ping_safe(hostname): # SAFE: The input is treated as a raw string argument to 'ping', # not as a shell command. command = ["ping", "-c", "1", hostname]
subprocess.run(command, shell=False)Go Examples
Section titled “Go Examples”In Go, the os/exec package is used. Go is safer by default because exec.Command behaves like Python’s shell=False (it executes the binary directly). You have to go out of your way to be vulnerable by explicitly invoking a shell like /bin/sh.
Vulnerable Code
Section titled “Vulnerable Code”package main
import ( "fmt" "os/exec")
func runUnsafe(userInput string) { // DANGER: Explicitly invoking bash to run a formatted string // Input: "8.8.8.8; cat /etc/passwd" cmdStr := fmt.Sprintf("ping -c 1 %s", userInput)
// Passing the string to sh -c allows injection cmd := exec.Command("sh", "-c", cmdStr) cmd.Run()}Secure Code
Section titled “Secure Code”package main
import "os/exec"
func runSafe(userInput string) { // SAFE: "ping" is the executable, everything else is an argument. // Injection operators like ";" or "&&" are treated as literal text. cmd := exec.Command("ping", "-c", "1", userInput) cmd.Run()}2. SQL Injection (SQLi)
Section titled “2. SQL Injection (SQLi)”SQL Injection happens when untrusted data is concatenated into a database query string. The database engine parses this string and executes modified logic.
Python Examples (using sqlite3)
Section titled “Python Examples (using sqlite3)”Vulnerable Code
Section titled “Vulnerable Code”import sqlite3
def get_user_unsafe(username): conn = sqlite3.connect('example.db') cursor = conn.cursor()
# DANGER: f-string formatting # Input: "' OR '1'='1" results in "SELECT * FROM users WHERE name = '' OR '1'='1'" query = f"SELECT * FROM users WHERE name = '{username}'"
cursor.execute(query) return cursor.fetchall()Secure Code (Parameterization)
Section titled “Secure Code (Parameterization)”Database drivers provide a mechanism to bind parameters. The query structure is compiled separately from the data.
import sqlite3
def get_user_safe(username): conn = sqlite3.connect('example.db') cursor = conn.cursor()
# SAFE: Use '?' as a placeholder (syntax varies by driver: %s, :1, etc.) query = "SELECT * FROM users WHERE name = ?"
# Pass data as a tuple in the second argument cursor.execute(query, (username,)) return cursor.fetchall()Go Examples (using database/sql)
Section titled “Go Examples (using database/sql)”Vulnerable Code
Section titled “Vulnerable Code”import ( "database/sql" "fmt")
func getUserUnsafe(db *sql.DB, username string) { // DANGER: fmt.Sprintf constructs a malicious query string query := fmt.Sprintf("SELECT * FROM users WHERE name = '%s'", username)
db.Query(query)}Secure Code (Parameterization)
Section titled “Secure Code (Parameterization)”Go’s database/sql package supports placeholders (syntax depends on the underlying driver).
import "database/sql"
func getUserSafe(db *sql.DB, username string) { // SAFE: PostgreSQL uses $1, $2 for placeholders query := "SELECT * FROM users WHERE name = $1"
// Pass the variable as a separate argument db.Query(query, username)}3. Key Nuances & Edge Cases
Section titled “3. Key Nuances & Edge Cases”The shell=False Pitfall in Python
Section titled “The shell=False Pitfall in Python”As noted in our conversation, simply setting shell=False in Python while passing a string command (e.g., "ping -c 1 8.8.8.8") will usually result in a FileNotFoundError.
- Why: Python looks for a file named literally
"ping -c 1 8.8.8.8". - Exceptions: On Windows,
shell=Falsecan sometimes execute strings if they point to an executable, but argument parsing rules are complex. Always use a list forshell=False.
”Blind” Injection
Section titled “”Blind” Injection”Sometimes you won’t see the output (stdout/stderr) of your injected command or query.
- Blind SQLi: Attackers use time delays (
SLEEP(10)) or boolean logic to infer data bit by bit. - Blind OS Injection: Attackers might trigger a callback (e.g.,
curl attacker.com) to confirm execution.
Second-Order Injection
Section titled “Second-Order Injection”This occurs when malicious data is stored safely in the database initially (e.g., a username like admin'; --) but is later retrieved and used in an unsafe context, such as being passed to a shell script or another SQL query.
Summary Checklist
Section titled “Summary Checklist”- Never use
f-stringsor string concatenation (+,fmt.Sprintf) to build SQL queries or Shell commands. - Always use the parameterization/binding features provided by your database driver (
?,$1,%s). - Always prefer executing binaries directly (
subprocess.run(["cmd", "arg"])orexec.Command("cmd", "arg")) over invoking a shell. - Sanitize input, but treat sanitization as a defense-in-depth measure, not the primary fix. Parameterization is the primary fix.