Skip to content

Injection Mechanics: Shell & SQL

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.


Shell injection occurs when an application passes unsafe user-supplied data (forms, cookies, HTTP headers) to a system shell.

In Python, the subprocess module is the standard way to spawn new processes. The danger arises when using shell=True combined with string formatting.

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)

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)

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.

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()
}
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()
}

SQL Injection happens when untrusted data is concatenated into a database query string. The database engine parses this string and executes modified logic.

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()

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()
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)
}

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)
}

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=False can sometimes execute strings if they point to an executable, but argument parsing rules are complex. Always use a list for shell=False.

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.

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.

  1. Never use f-strings or string concatenation (+, fmt.Sprintf) to build SQL queries or Shell commands.
  2. Always use the parameterization/binding features provided by your database driver (?, $1, %s).
  3. Always prefer executing binaries directly (subprocess.run(["cmd", "arg"]) or exec.Command("cmd", "arg")) over invoking a shell.
  4. Sanitize input, but treat sanitization as a defense-in-depth measure, not the primary fix. Parameterization is the primary fix.