SQL Injection Defense in Practice: Parameterized Queries and WAF Rule Configuration Guide
Code-layer parameterized queries, input allowlists, then network WAF rules—three steps to a complete SQL injection defense. PHP/Node.js/Python samples and ModSecurity config.
Scope
For web engineers, security ops, and CTF players building SQL injection defenses in real projects. In-house apps or forked CMS: the parameterized-query and WAF rule configs here drop in as-is. Keywords: SQL injection defense, parameterized queries, WAF rule config.
Prerequisites
- Linux server (CentOS 7+ / Ubuntu 20.04+), root or sudo
- Nginx + PHP (or any backend) already deployed
- ModSecurity (Nginx WAF module) or OpenResty installed
- Basic SQL and regex
How It Works
SQL injection concatenates malicious SQL into user input and runs unauthorized queries when the backend does not bind parameters. Full defense covers three layers:
- Code: parameterized queries (Prepared Statement)—no string concat
- Input: strict allowlist validation and special-character filtering
- Network: WAF rules to block anomalous requests
Steps
Step 1: Code Layer — Parameterized Queries
PHP PDO—never concatenate SQL:
<?php
// ❌ Wrong — SQL injection vuln
$stmt = $pdo->query("SELECT * FROM users WHERE username = '{$_GET['username']}'");
// ✅ Correct — parameterized query
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = :username");
$stmt->execute(['username' => $_GET['username']]);
$user = $stmt->fetch();
?>
Python (Flask + SQLAlchemy):
from flask import Flask, request
from sqlalchemy import text
@app.route('/user')
def get_user():
# ❌ Wrong — string concat
# query = f"SELECT * FROM users WHERE id = {request.args.get('id')}"
# ✅ Correct — parameterized
query = text("SELECT * FROM users WHERE id = :uid")
result = db.session.execute(query, {"uid": request.args.get('id')})
return result.fetchone()
Node.js (mysql2):
const mysql = require('mysql2/promise');
const pool = mysql.createPool({/* config */});
// ✅ Use ? placeholders
const [rows] = await pool.execute(
'SELECT * FROM users WHERE email = ?',
[req.query.email]
);
Step 2: Input Layer — Allowlist Validation
Define allowed formats per parameter type. Reject everything else:
// PHP allowlist validation example
function validateInput($input, $type) {
switch ($type) {
case 'integer':
return filter_var($input, FILTER_VALIDATE_INT) !== false;
case 'email':
return filter_var($input, FILTER_VALIDATE_EMAIL) !== false;
case 'alphanumeric':
return preg_match('/^[a-zA-Z0-9_]+$/', $input) === 1;
default:
return false;
}
}
Step 3: Network Layer — ModSecurity WAF Rules
Install ModSecurity and enable OWASP Core Rule Set (CRS):
# Install ModSecurity for Nginx
cd /usr/local/src
git clone --depth 1 https://github.com/SpiderLabs/ModSecurity-nginx.git
git clone --depth 1 https://github.com/SpiderLabs/owasp-modsecurity-crs.git
# Enable CRS rules
cp owasp-modsecurity-crs/crs-setup.conf.example /etc/nginx/modsec/crs-setup.conf
cp -r owasp-modsecurity-crs/rules /etc/nginx/modsec/
# Load in Nginx config
# nginx.conf add:
# modsecurity on;
# modsecurity_rules_file /etc/nginx/modsec/modsecurity.conf;
Add custom SQLi rules for typical payloads:
# /etc/nginx/modsec/custom_sqli.conf
SecRule REQUEST_ARGS|REQUEST_BODY "@rx (union.*select|select.*from|insert.*into|delete.*from|drop\s+table|or\s+1=1|or\s+'1'='1)" \
"id:10001,phase:2,t:lowercase,deny,status:403,msg:'SQL Injection blocked'"
SecRule REQUEST_URI "@rx (%27|%22|--|;%20)" \
"id:10002,phase:1,t:urlDecode,deny,status:403,msg:'SQL special chars blocked'"
Reference in the Nginx server block:
server {
listen 443 ssl;
server_name yourdomain.com;
# Enable ModSecurity
modsecurity on;
modsecurity_rules_file /etc/nginx/modsec/custom_sqli.conf;
location / {
proxy_pass http://backend;
}
}
Verify
After deploy, curl simulated SQLi to confirm blocks:
# Should return 403 Forbidden
curl -v "https://yourdomain.com/user?id=1%20OR%201=1"
# Should return 403
curl -v -X POST "https://yourdomain.com/login" \
-d "username=admin'--&password=test"
# Normal request should return 200
curl -v "https://yourdomain.com/user?id=42"
FAQ
Q1: WAF false-positives on legitimate requests?
Check the ModSecurity audit log (/var/log/modsec_audit.log) for the triggering parameter. If legitimate input hits the regex, exclude it: SecRuleRemoveById 10001 "ENV:REQUEST_URI", or raise the detection threshold for that URL.
Q2: Do parameterized queries fully stop SQL injection?
Parameterized queries stop most injection. Still need extra controls for: dynamic table/column names (Prepared Statement cannot bind identifiers), dynamic SQL in stored procedures, ORDER BY sort columns. Pair those with a strict allowlist.
Summary
SQL injection defense is three layers: parameterized queries, allowlist validation, WAF rules. Never trust user input. Wire SAST (SonarQube, Semgrep) into CI/CD to catch SQL concat, plus WAF for defense in depth.