Prepared Statements and Security Concerns
Learning Objectives
- Understand SQL injection attacks and their impact
- Master prepared statements in both PDO and MySQLi
- Implement parameter binding correctly
- Apply input validation and sanitization
- Use whitelisting for dynamic queries
- Implement defense-in-depth security strategies
- Handle different data types securely
- Audit and test for security vulnerabilities
Understanding SQL Injection
SQL injection is one of the most dangerous web vulnerabilities. Let's understand how it works and how to prevent it completely! 🛡️
SQL Injection Risks
Prepared Statements Deep Dive
graph TB
INPUT[User Input] --> PREPARE[Prepare Statement]
PREPARE --> COMPILE[SQL Compiled]
COMPILE --> PLAN[Execution Plan Created]
INPUT --> BIND[Bind Parameters]
BIND --> EXECUTE[Execute Statement]
PLAN --> EXECUTE
EXECUTE --> SAFE[Safe Execution]
subgraph "Protection Layer"
BIND --> |"Data separated from code"| SAFE
end
style INPUT fill:#fee2e2
style PREPARE fill:#dcfce7
style SAFE fill:#dbeafe
Complete Prepared Statements Implementation
<?php
// ========================================
// PDO PREPARED STATEMENTS
// ========================================
class SecurePDODatabase {
private $pdo;
private $allowedTables = ['users', 'products', 'orders'];
private $allowedColumns = [];
public function __construct(PDO $pdo) {
$this->pdo = $pdo;
$this->initializeAllowedColumns();
}
/**
* Initialize allowed columns for each table
*/
private function initializeAllowedColumns() {
$this->allowedColumns = [
'users' => ['id', 'username', 'email', 'status', 'created_at'],
'products' => ['id', 'name', 'price', 'category', 'stock'],
'orders' => ['id', 'user_id', 'total', 'status', 'created_at']
];
}
/**
* Secure SELECT with prepared statements
*/
public function secureSelect($table, $conditions = [], $columns = ['*']) {
// Validate table name (whitelist)
if (!in_array($table, $this->allowedTables)) {
throw new Exception("Invalid table name");
}
// Validate columns (whitelist)
$safeColumns = $this->validateColumns($table, $columns);
// Build query
$sql = "SELECT " . implode(', ', $safeColumns) . " FROM $table";
// Add WHERE conditions
if (!empty($conditions)) {
$whereClauses = [];
$params = [];
foreach ($conditions as $column => $value) {
// Validate column name
if (!in_array($column, $this->allowedColumns[$table])) {
throw new Exception("Invalid column: $column");
}
if (is_array($value)) {
// Handle IN clause
$placeholders = array_fill(0, count($value), '?');
$whereClauses[] = "$column IN (" . implode(', ', $placeholders) . ")";
$params = array_merge($params, $value);
} elseif ($value === null) {
// Handle NULL
$whereClauses[] = "$column IS NULL";
} else {
// Regular condition
$whereClauses[] = "$column = ?";
$params[] = $value;
}
}
$sql .= " WHERE " . implode(' AND ', $whereClauses);
}
// Prepare and execute
try {
$stmt = $this->pdo->prepare($sql);
$stmt->execute($params);
return $stmt->fetchAll();
} catch (PDOException $e) {
$this->logSecurityEvent('Query failed', [
'sql' => $sql,
'error' => $e->getMessage()
]);
throw new Exception("Database query failed");
}
}
/**
* Secure INSERT with type checking
*/
public function secureInsert($table, array $data) {
// Validate table
if (!in_array($table, $this->allowedTables)) {
throw new Exception("Invalid table name");
}
// Validate columns
$columns = array_keys($data);
foreach ($columns as $column) {
if (!in_array($column, $this->allowedColumns[$table])) {
throw new Exception("Invalid column: $column");
}
}
// Build query
$placeholders = array_fill(0, count($data), '?');
$sql = "INSERT INTO $table (" . implode(', ', $columns) . ")
VALUES (" . implode(', ', $placeholders) . ")";
try {
$stmt = $this->pdo->prepare($sql);
// Bind parameters with type detection
$i = 1;
foreach ($data as $value) {
$type = $this->getPDOType($value);
$stmt->bindValue($i++, $value, $type);
}
$stmt->execute();
return [
'success' => true,
'id' => $this->pdo->lastInsertId()
];
} catch (PDOException $e) {
$this->logSecurityEvent('Insert failed', [
'table' => $table,
'error' => $e->getMessage()
]);
throw new Exception("Insert operation failed");
}
}
/**
* Secure UPDATE with strict validation
*/
public function secureUpdate($table, array $data, array $where) {
// Validate table
if (!in_array($table, $this->allowedTables)) {
throw new Exception("Invalid table name");
}
// Validate all columns
$allColumns = array_merge(array_keys($data), array_keys($where));
foreach ($allColumns as $column) {
if (!in_array($column, $this->allowedColumns[$table])) {
throw new Exception("Invalid column: $column");
}
}
// Build SET clause
$setClauses = [];
$params = [];
foreach ($data as $column => $value) {
$setClauses[] = "$column = ?";
$params[] = $value;
}
// Build WHERE clause
$whereClauses = [];
foreach ($where as $column => $value) {
$whereClauses[] = "$column = ?";
$params[] = $value;
}
$sql = "UPDATE $table SET " . implode(', ', $setClauses) .
" WHERE " . implode(' AND ', $whereClauses);
try {
$stmt = $this->pdo->prepare($sql);
$stmt->execute($params);
return [
'success' => true,
'affected' => $stmt->rowCount()
];
} catch (PDOException $e) {
$this->logSecurityEvent('Update failed', [
'table' => $table,
'error' => $e->getMessage()
]);
throw new Exception("Update operation failed");
}
}
/**
* Secure DELETE with confirmation
*/
public function secureDelete($table, array $where, $requireConfirmation = true) {
// Validate table
if (!in_array($table, $this->allowedTables)) {
throw new Exception("Invalid table name");
}
// Prevent accidental mass deletion
if (empty($where) && $requireConfirmation) {
throw new Exception("Cannot delete without WHERE clause");
}
// Validate columns
foreach (array_keys($where) as $column) {
if (!in_array($column, $this->allowedColumns[$table])) {
throw new Exception("Invalid column: $column");
}
}
// Build WHERE clause
$whereClauses = [];
$params = [];
foreach ($where as $column => $value) {
$whereClauses[] = "$column = ?";
$params[] = $value;
}
$sql = "DELETE FROM $table WHERE " . implode(' AND ', $whereClauses);
try {
$stmt = $this->pdo->prepare($sql);
$stmt->execute($params);
$affected = $stmt->rowCount();
// Log deletion for audit
$this->logSecurityEvent('Data deleted', [
'table' => $table,
'conditions' => $where,
'affected' => $affected
]);
return [
'success' => true,
'deleted' => $affected
];
} catch (PDOException $e) {
$this->logSecurityEvent('Delete failed', [
'table' => $table,
'error' => $e->getMessage()
]);
throw new Exception("Delete operation failed");
}
}
/**
* Execute stored procedure securely
*/
public function callProcedure($procedure, array $params = []) {
// Whitelist procedures
$allowedProcedures = ['GetUserById', 'UpdateInventory', 'ProcessOrder'];
if (!in_array($procedure, $allowedProcedures)) {
throw new Exception("Invalid procedure");
}
$placeholders = array_fill(0, count($params), '?');
$sql = "CALL $procedure(" . implode(', ', $placeholders) . ")";
try {
$stmt = $this->pdo->prepare($sql);
$stmt->execute($params);
return $stmt->fetchAll();
} catch (PDOException $e) {
$this->logSecurityEvent('Procedure failed', [
'procedure' => $procedure,
'error' => $e->getMessage()
]);
throw new Exception("Procedure execution failed");
}
}
/**
* Validate columns against whitelist
*/
private function validateColumns($table, $columns) {
if ($columns === ['*']) {
return $columns;
}
$validColumns = [];
foreach ($columns as $column) {
if (!in_array($column, $this->allowedColumns[$table])) {
throw new Exception("Invalid column: $column");
}
$validColumns[] = $column;
}
return $validColumns;
}
/**
* Get PDO parameter type
*/
private function getPDOType($value) {
if (is_null($value)) {
return PDO::PARAM_NULL;
} elseif (is_bool($value)) {
return PDO::PARAM_BOOL;
} elseif (is_int($value)) {
return PDO::PARAM_INT;
} else {
return PDO::PARAM_STR;
}
}
/**
* Log security events
*/
private function logSecurityEvent($event, $data) {
$logEntry = [
'timestamp' => date('Y-m-d H:i:s'),
'event' => $event,
'data' => $data,
'ip' => $_SERVER['REMOTE_ADDR'] ?? 'CLI',
'user' => $_SESSION['user_id'] ?? 'anonymous'
];
error_log(json_encode($logEntry), 3, '/var/log/security.log');
}
}
// ========================================
// MYSQLI PREPARED STATEMENTS
// ========================================
class SecureMySQLiDatabase {
private $mysqli;
private $typeMap = [];
public function __construct(mysqli $mysqli) {
$this->mysqli = $mysqli;
}
/**
* Secure query with automatic type detection
*/
public function secureQuery($sql, array $params = []) {
// Check for dangerous patterns
if ($this->containsDangerousSQL($sql)) {
throw new Exception("Potentially dangerous SQL detected");
}
$stmt = $this->mysqli->prepare($sql);
if (!$stmt) {
throw new Exception("Prepare failed: " . $this->mysqli->error);
}
if (!empty($params)) {
// Build type string
$types = $this->buildTypeString($params);
// Bind parameters
$stmt->bind_param($types, ...$params);
}
if (!$stmt->execute()) {
throw new Exception("Execute failed: " . $stmt->error);
}
// Get result if SELECT query
if (stripos($sql, 'SELECT') === 0) {
$result = $stmt->get_result();
$data = [];
while ($row = $result->fetch_assoc()) {
$data[] = $row;
}
$stmt->close();
return $data;
}
// Return affected rows for other queries
$affected = $stmt->affected_rows;
$stmt->close();
return ['affected' => $affected];
}
/**
* Build type string for mysqli
*/
private function buildTypeString(array $params) {
$types = '';
foreach ($params as $param) {
if (is_int($param)) {
$types .= 'i';
} elseif (is_float($param)) {
$types .= 'd';
} elseif (is_string($param)) {
$types .= 's';
} else {
$types .= 'b'; // blob
}
}
return $types;
}
/**
* Check for dangerous SQL patterns
*/
private function containsDangerousSQL($sql) {
$dangerous = [
'/DROP\s+TABLE/i',
'/DROP\s+DATABASE/i',
'/TRUNCATE/i',
'/DELETE\s+FROM\s+\w+\s*$/i', // DELETE without WHERE
'/UPDATE\s+\w+\s+SET\s+[^W]+$/i', // UPDATE without WHERE
'/;.*--;?/i', // Multiple statements
'/UNION.*SELECT/i', // UNION without proper context
'/INTO\s+OUTFILE/i', // File operations
'/LOAD_FILE/i'
];
foreach ($dangerous as $pattern) {
if (preg_match($pattern, $sql)) {
return true;
}
}
return false;
}
}
// ========================================
// INPUT VALIDATION & SANITIZATION
// ========================================
class InputValidator {
/**
* Validate and sanitize input
*/
public static function validate($input, $type, $options = []) {
switch ($type) {
case 'int':
return self::validateInt($input, $options);
case 'float':
return self::validateFloat($input, $options);
case 'email':
return self::validateEmail($input);
case 'url':
return self::validateUrl($input);
case 'alphanumeric':
return self::validateAlphanumeric($input, $options);
case 'date':
return self::validateDate($input, $options);
case 'phone':
return self::validatePhone($input);
case 'uuid':
return self::validateUuid($input);
default:
return self::validateString($input, $options);
}
}
/**
* Validate integer
*/
private static function validateInt($input, $options = []) {
$filtered = filter_var($input, FILTER_VALIDATE_INT);
if ($filtered === false) {
throw new ValidationException("Invalid integer");
}
// Check range
if (isset($options['min']) && $filtered < $options['min']) {
throw new ValidationException("Value below minimum");
}
if (isset($options['max']) && $filtered > $options['max']) {
throw new ValidationException("Value above maximum");
}
return $filtered;
}
/**
* Validate float
*/
private static function validateFloat($input, $options = []) {
$filtered = filter_var($input, FILTER_VALIDATE_FLOAT);
if ($filtered === false) {
throw new ValidationException("Invalid float");
}
return $filtered;
}
/**
* Validate email
*/
private static function validateEmail($input) {
$filtered = filter_var($input, FILTER_VALIDATE_EMAIL);
if ($filtered === false) {
throw new ValidationException("Invalid email address");
}
// Additional checks
if (!checkdnsrr(substr(strrchr($filtered, "@"), 1), 'MX')) {
throw new ValidationException("Email domain does not exist");
}
return $filtered;
}
/**
* Validate URL
*/
private static function validateUrl($input) {
$filtered = filter_var($input, FILTER_VALIDATE_URL);
if ($filtered === false) {
throw new ValidationException("Invalid URL");
}
// Check allowed protocols
$allowedProtocols = ['http', 'https'];
$protocol = parse_url($filtered, PHP_URL_SCHEME);
if (!in_array($protocol, $allowedProtocols)) {
throw new ValidationException("Invalid URL protocol");
}
return $filtered;
}
/**
* Validate alphanumeric
*/
private static function validateAlphanumeric($input, $options = []) {
$allowSpaces = $options['allow_spaces'] ?? false;
$pattern = $allowSpaces ? '/^[a-zA-Z0-9\s]+$/' : '/^[a-zA-Z0-9]+$/';
if (!preg_match($pattern, $input)) {
throw new ValidationException("Invalid alphanumeric value");
}
return $input;
}
/**
* Validate date
*/
private static function validateDate($input, $options = []) {
$format = $options['format'] ?? 'Y-m-d';
$date = DateTime::createFromFormat($format, $input);
if (!$date || $date->format($format) !== $input) {
throw new ValidationException("Invalid date format");
}
return $input;
}
/**
* Validate phone number
*/
private static function validatePhone($input) {
// Remove non-numeric characters
$phone = preg_replace('/[^0-9]/', '', $input);
// Check length (adjust for your country)
if (strlen($phone) < 10 || strlen($phone) > 15) {
throw new ValidationException("Invalid phone number");
}
return $phone;
}
/**
* Validate UUID
*/
private static function validateUuid($input) {
$pattern = '/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i';
if (!preg_match($pattern, $input)) {
throw new ValidationException("Invalid UUID");
}
return $input;
}
/**
* Validate string with sanitization
*/
private static function validateString($input, $options = []) {
$minLength = $options['min_length'] ?? 0;
$maxLength = $options['max_length'] ?? PHP_INT_MAX;
$allowHtml = $options['allow_html'] ?? false;
// Remove null bytes
$input = str_replace(chr(0), '', $input);
// Trim whitespace
$input = trim($input);
// Check length
if (strlen($input) < $minLength) {
throw new ValidationException("String too short");
}
if (strlen($input) > $maxLength) {
throw new ValidationException("String too long");
}
// Sanitize HTML
if (!$allowHtml) {
$input = htmlspecialchars($input, ENT_QUOTES, 'UTF-8');
} else {
// Allow only safe HTML tags
$allowedTags = '
Security Testing and Auditing
Security Testing Framework
<?php
// ========================================
// SECURITY TESTING SUITE
// ========================================
class SQLInjectionTester {
private $testPayloads = [
// Basic injection attempts
"' OR '1'='1",
"1' OR '1' = '1",
"' OR 1=1--",
"' OR 'a'='a",
"' OR ''='",
// Union-based
"' UNION SELECT * FROM users--",
"1 UNION SELECT null, version()--",
// Boolean-based
"1' AND '1'='1",
"1' AND '1'='2",
// Time-based
"1' AND SLEEP(5)--",
"1'; WAITFOR DELAY '0:0:5'--",
// Error-based
"1'",
"\\",
"'\"",
// Stacked queries
"1'; DROP TABLE users--",
"1'; DELETE FROM users--",
// Out-of-band
"1' AND LOAD_FILE('/etc/passwd')--",
// Second-order
"admin'--",
// NoSQL injection (for MongoDB)
'{"$ne": ""}',
'{"$gt": ""}',
// XML injection
"' or count(//user[userid=$userid])>0 or 'a'='b"
];
/**
* Test function for SQL injection vulnerabilities
*/
public function testFunction(callable $function, $paramName = 'input') {
$results = [];
foreach ($this->testPayloads as $payload) {
try {
// Test the function with payload
$result = $function([$paramName => $payload]);
// Analyze result
$analysis = $this->analyzeResult($result, $payload);
$results[] = [
'payload' => $payload,
'vulnerable' => $analysis['vulnerable'],
'reason' => $analysis['reason']
];
} catch (Exception $e) {
// Error might indicate protection
$results[] = [
'payload' => $payload,
'vulnerable' => false,
'reason' => 'Exception thrown (likely protected)'
];
}
}
return $results;
}
/**
* Analyze result for signs of injection
*/
private function analyzeResult($result, $payload) {
// Check for unexpected data volume
if (is_array($result) && count($result) > 100) {
return [
'vulnerable' => true,
'reason' => 'Returned excessive data'
];
}
// Check for database errors in result
if (is_string($result)) {
$errorPatterns = [
'/SQL syntax/',
'/mysql_fetch/',
'/Warning.*mysql/',
'/mysqli::/',
'/PDO::/',
'/PostgreSQL/',
'/valid MySQL result/',
'/mssql_/',
'/Microsoft.*ODBC.*SQL/',
'/Oracle.*error/',
'/SQLite.*error/'
];
foreach ($errorPatterns as $pattern) {
if (preg_match($pattern, $result)) {
return [
'vulnerable' => true,
'reason' => 'Database error exposed'
];
}
}
}
// Check for successful bypass indicators
if (strpos($payload, 'OR') !== false && !empty($result)) {
return [
'vulnerable' => 'possible',
'reason' => 'OR condition might have bypassed filter'
];
}
return [
'vulnerable' => false,
'reason' => 'No injection detected'
];
}
/**
* Generate security report
*/
public function generateReport($results) {
$vulnerable = 0;
$protected = 0;
$warnings = [];
foreach ($results as $result) {
if ($result['vulnerable'] === true) {
$vulnerable++;
$warnings[] = "CRITICAL: Vulnerable to: {$result['payload']}";
} elseif ($result['vulnerable'] === 'possible') {
$warnings[] = "WARNING: Possible vulnerability: {$result['payload']}";
} else {
$protected++;
}
}
return [
'total_tests' => count($results),
'vulnerabilities' => $vulnerable,
'protected' => $protected,
'security_score' => round(($protected / count($results)) * 100, 2),
'warnings' => $warnings,
'recommendation' => $vulnerable > 0 ?
'URGENT: Fix SQL injection vulnerabilities immediately!' :
'Good protection detected, continue monitoring'
];
}
}
// ========================================
// SECURITY AUDIT LOGGER
// ========================================
class SecurityAuditLogger {
private $logFile;
public function __construct($logFile = '/var/log/security_audit.log') {
$this->logFile = $logFile;
}
/**
* Log query execution
*/
public function logQuery($sql, $params, $user, $result) {
$entry = [
'timestamp' => date('Y-m-d H:i:s'),
'user' => $user,
'ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
'sql' => $sql,
'params' => $params,
'result' => $result ? 'success' : 'failed',
'checksum' => $this->generateChecksum($sql, $params)
];
$this->writeLog($entry);
}
/**
* Log suspicious activity
*/
public function logSuspiciousActivity($activity, $details) {
$entry = [
'timestamp' => date('Y-m-d H:i:s'),
'type' => 'SUSPICIOUS',
'activity' => $activity,
'details' => $details,
'ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'unknown',
'alert_level' => $this->determineAlertLevel($activity)
];
$this->writeLog($entry);
// Send immediate alert for critical issues
if ($entry['alert_level'] === 'CRITICAL') {
$this->sendAlert($entry);
}
}
/**
* Determine alert level
*/
private function determineAlertLevel($activity) {
$criticalPatterns = [
'SQL injection attempt',
'Mass deletion attempt',
'Privilege escalation',
'Data exfiltration'
];
foreach ($criticalPatterns as $pattern) {
if (stripos($activity, $pattern) !== false) {
return 'CRITICAL';
}
}
return 'WARNING';
}
/**
* Generate checksum for integrity
*/
private function generateChecksum($sql, $params) {
return hash('sha256', $sql . serialize($params));
}
/**
* Write to log file
*/
private function writeLog($entry) {
$json = json_encode($entry) . PHP_EOL;
file_put_contents($this->logFile, $json, FILE_APPEND | LOCK_EX);
}
/**
* Send security alert
*/
private function sendAlert($entry) {
// Email alert
$to = 'security@example.com';
$subject = 'CRITICAL Security Alert';
$message = "Critical security event detected:\n\n" . print_r($entry, true);
mail($to, $subject, $message);
// You could also send to:
// - Slack webhook
// - SMS via Twilio
// - PagerDuty
// - Security Information and Event Management (SIEM) system
}
/**
* Analyze logs for patterns
*/
public function analyzeLogs($hours = 24) {
$logs = file($this->logFile, FILE_IGNORE_NEW_LINES);
$cutoff = time() - ($hours * 3600);
$analysis = [
'failed_queries' => 0,
'suspicious_activities' => 0,
'unique_ips' => [],
'attack_patterns' => []
];
foreach ($logs as $log) {
$entry = json_decode($log, true);
if (strtotime($entry['timestamp']) < $cutoff) {
continue;
}
if (isset($entry['result']) && $entry['result'] === 'failed') {
$analysis['failed_queries']++;
}
if (isset($entry['type']) && $entry['type'] === 'SUSPICIOUS') {
$analysis['suspicious_activities']++;
$analysis['attack_patterns'][] = $entry['activity'];
}
if (isset($entry['ip'])) {
$analysis['unique_ips'][$entry['ip']] =
($analysis['unique_ips'][$entry['ip']] ?? 0) + 1;
}
}
return $analysis;
}
}
// ========================================
// USAGE EXAMPLES
// ========================================
// Test a function for SQL injection
$tester = new SQLInjectionTester();
// Example vulnerable function (DO NOT USE IN PRODUCTION)
$vulnerableFunction = function($params) use ($pdo) {
$id = $params['input'];
$sql = "SELECT * FROM users WHERE id = $id"; // VULNERABLE!
return $pdo->query($sql)->fetchAll();
};
// Example secure function
$secureFunction = function($params) use ($pdo) {
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$params['input']]);
return $stmt->fetchAll();
};
// Test and compare
echo "Testing vulnerable function:\n";
$results = $tester->testFunction($vulnerableFunction);
$report = $tester->generateReport($results);
print_r($report);
echo "\nTesting secure function:\n";
$results = $tester->testFunction($secureFunction);
$report = $tester->generateReport($results);
print_r($report);
// Security audit logging
$auditLogger = new SecurityAuditLogger();
// Log queries
$auditLogger->logQuery(
"SELECT * FROM users WHERE id = ?",
[123],
$_SESSION['username'] ?? 'anonymous',
true
);
// Log suspicious activity
$auditLogger->logSuspiciousActivity(
"SQL injection attempt detected",
[
'query' => $_GET['search'] ?? '',
'pattern_matched' => "' OR '1'='1"
]
);
// Analyze logs
$analysis = $auditLogger->analyzeLogs(24);
echo "Security analysis for last 24 hours:\n";
print_r($analysis);
Practice Exercise
Security Implementation Challenges