⏱️ Reading time: ~10 minutes | 📅 Updated: December 17, 2025
SQL Injection has remained in the OWASP Top 10 for over 20 years. In November 2025, OWASP published an updated ranking where Injection holds 5th place (A05:2025). The FBI and CISA have officially labeled SQLi an "unforgivable defect" — a vulnerability that shouldn't exist in modern software.
But it does exist. At Hostiserver, we see this regularly: clients come to us after breaches, with damaged databases and leaked user data. In most cases, the cause is missing prepared statements or legacy code without validation.
This guide isn't a theoretical overview. We've compiled configurations and approaches we actually use on Hostiserver managed servers: from ModSecurity setup to MySQL hardening. Everything tested in practice.
⚠️ Important: If your site accepts any user input (forms, search, filters, URL parameters) — it's potentially vulnerable. Even a "simple blog" on WordPress can become a victim through a vulnerable plugin.
SQL Injection is an attack technique where an attacker inserts malicious SQL code into input fields. If the application doesn't validate input, this code executes on the database server.
// ❌ UNSAFE — never do this!
$username = $_POST['username'];
$query = "SELECT * FROM users WHERE username = '$username'";
$result = mysqli_query($conn, $query);
If an attacker enters in the username field:
' OR '1'='1' --
The query becomes:
SELECT * FROM users WHERE username = '' OR '1'='1' --'
Result: the attacker gains access to all records in the table.
| Type | Mechanism | Detection Difficulty |
|---|---|---|
| Classic (In-band) | Result visible on page | Low |
| Union-based | UNION extracts data from other tables | Low |
| Error-based | Data through error messages | Medium |
| Blind SQLi | No visible result, "guessing" | High |
| Time-based Blind | SLEEP() determines condition truth | High |
| Out-of-band | Data sent to external server | Very High |
SQLi isn't an archaic problem. Critical vulnerabilities are found even in modern enterprise software. Here are examples we've been tracking:
Ironic case: SQL Injection in a WAF itself. CVSS 9.6 Critical. The vulnerability allowed unauthenticated attackers to execute SQL commands via HTTP requests. (Source).
Critical vulnerability in PostgreSQL escaping functions. CVSS 8.1 High. Bypass of prepared statements through incorrect multibyte character handling. Affected all versions up to 17.3. (Source).
CVSS 9.9 Critical. Any user with API access could exploit SQLi and escalate privileges. Zabbix is used by thousands of companies for infrastructure monitoring. (Source).
This case is still referenced as an example of scale. SQLi led to compromise of over 2,500 organizations. (Source).
Prepared Statements (parameterized queries) are the most effective protection against SQL Injection. They separate SQL code from data. We recommend this as baseline standard for all projects.
// ✅ SAFE — PDO with prepared statements
$pdo = new PDO('mysql:host=localhost;dbname=app_db;charset=utf8mb4', $user, $pass);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); // Important!
$stmt = $pdo->prepare('SELECT * FROM users WHERE username = :username AND status = :status');
$stmt->execute([
':username' => $username,
':status' => 'active'
]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
// ✅ SAFE — MySQLi with prepared statements
$mysqli = new mysqli('localhost', $user, $pass, 'app_db');
$mysqli->set_charset('utf8mb4');
$stmt = $mysqli->prepare('SELECT * FROM users WHERE username = ? AND status = ?');
$stmt->bind_param('ss', $username, $status);
$stmt->execute();
$result = $stmt->get_result();
$user = $result->fetch_assoc();
When using prepared statements:
' OR '1'='1 becomes just a string✅ Our recommendation: ALWAYS use prepared statements for SQL queries with user input. No exceptions.
In February 2025, it was discovered that even prepared statements can be bypassed through incorrect multibyte character handling in PostgreSQL. Solution is simple: keep software updated (PostgreSQL 17.3+, 16.7+, 15.11+).
Modern frameworks have SQL Injection protection out of the box. On Hostiserver servers we support all popular frameworks — Laravel, Django, Node.js stack.
// ✅ SAFE — Eloquent auto-parameterizes
$users = User::where('username', $username)
->where('status', 'active')
->get();
// ✅ SAFE — Query Builder
$users = DB::table('users')
->where('username', $username)
->get();
// ❌ UNSAFE — raw queries without bindings
// DB::select("SELECT * FROM users WHERE username = '$username'");
# ✅ SAFE — Django ORM
users = User.objects.filter(username=username, status='active')
# ✅ SAFE — raw query with parameters
users = User.objects.raw('SELECT * FROM users WHERE username = %s', [username])
# ❌ UNSAFE — string formatting
# User.objects.raw(f"SELECT * FROM users WHERE username = '{username}'")
// ✅ SAFE — Sequelize
const users = await User.findAll({
where: { username: username, status: 'active' }
});
// ✅ SAFE — Prisma
const users = await prisma.user.findMany({
where: { username: username, status: 'active' }
});
⚠️ Warning: ORM protects only when used correctly. Raw SQL inside ORM can still be vulnerable. We often see this when auditing client projects.
Validation is additional protection, not a replacement for prepared statements. We recommend applying both approaches simultaneously.
| Type | Description | Example |
|---|---|---|
| Whitelist | Allow only expected values | Sorting: only 'asc' or 'desc' |
| Type casting | Force type conversion | $id = (int) $_GET['id']; |
| Format validation | Check data format | Email, date, UUID |
| Length limits | Restrict length | Username: max 50 chars |
// ✅ Whitelist for sorting (ORDER BY can't be parameterized)
$allowed_columns = ['created_at', 'username', 'email'];
$sort_column = in_array($_GET['sort'], $allowed_columns) ? $_GET['sort'] : 'created_at';
$allowed_directions = ['ASC', 'DESC'];
$sort_dir = in_array(strtoupper($_GET['dir']), $allowed_directions) ? strtoupper($_GET['dir']) : 'DESC';
// ✅ Type casting for ID
$user_id = filter_var($_GET['id'], FILTER_VALIDATE_INT);
if ($user_id === false) {
throw new InvalidArgumentException('Invalid user ID');
}
// ✅ Regex for specific formats
if (!preg_match('/^[a-zA-Z0-9_]{3,20}$/', $username)) {
throw new InvalidArgumentException('Invalid username format');
}
💡 From our experience: Whitelist is always better than Blacklist. Instead of blocking dangerous characters — allow only expected ones.
WAF analyzes HTTP requests and blocks suspicious patterns before they reach the application. Especially important for legacy code that's hard to rewrite.
| Solution | Level | Use |
|---|---|---|
| ModSecurity | Server (Apache/Nginx) | Deep request inspection |
| Cloudflare WAF | DNS proxy | Edge protection, DDoS |
Here's an example of rules we configure for clients:
# Rule 1: Detect SQLi patterns
SecRule ARGS|REQUEST_BODY \
"@rx (?i)(union\s+select|sleep\(|benchmark\(|or\s+1=1)" \
"id:1001002,phase:2,pass,log,tag:'attack-sqli',setvar:'tx.inbound_anomaly_score=+5',msg:'SQLi pattern detected'"
# Rule 2: Block when anomaly score exceeded
SecRule TX:INBOUND_ANOMALY_SCORE "@ge 5" \
"id:1001099,phase:2,deny,status:403,log,msg:'Inbound anomaly score exceeded'"
UNION SELECT — extracting data from other tablesSLEEP() — time-based blind SQLiBENCHMARK() — alternative time-based attackOR 1=1 — classic boolean injectionFor full protection we install OWASP CRS — ruleset covering SQLi, XSS, LFI and other attacks:
# Installing OWASP CRS for Apache
sudo apt install libapache2-mod-security2
sudo mv /etc/modsecurity/modsecurity.conf-recommended /etc/modsecurity/modsecurity.conf
# Download CRS
cd /etc/modsecurity
sudo git clone https://github.com/coreruleset/coreruleset.git
sudo cp coreruleset/crs-setup.conf.example coreruleset/crs-setup.conf
✅ Our approach: We combine ModSecurity on server with Cloudflare WAF. Cloudflare blocks bulk of attacks at edge, ModSecurity catches what gets through.
Even if attacker finds SQLi, properly configured database minimizes damage. Here's what we do on Hostiserver managed servers.
# /etc/mysql/mysql.conf.d/mysqld.cnf
# MySQL listens only on localhost — CRITICAL!
bind-address = 127.0.0.1
# Port 3306 NOT open to public internet
# Access allowed only from localhost or specific IPs via firewall
Each application gets separate user with minimal permissions:
-- ✅ Separate user for application
CREATE USER 'app_user'@'localhost' IDENTIFIED BY 'STRONG_RANDOM_PASSWORD';
-- Only needed permissions on specific database
GRANT SELECT, INSERT, UPDATE, DELETE ON app_db.* TO 'app_user'@'localhost';
-- ❌ DON'T grant: GRANT ALL ON *.*
-- ❌ DON'T grant: SUPER, FILE, PROCESS, SHUTDOWN
FLUSH PRIVILEGES;
| What We Do | How |
|---|---|
| Remove anonymous users | DELETE FROM mysql.user WHERE User=''; |
| Root local only | Prohibit root@'%' |
| Strong passwords | Password policy, min 16 chars |
| Database isolation | User sees only their DB |
| TLS for remote | REQUIRE SSL |
# /etc/mysql/mysql.conf.d/mysqld.cnf
max_connections = 150
max_user_connections = 50
# Timeouts — close idle connections
wait_timeout = 300
interactive_timeout = 300
⚠️ Why this matters: If attacker finds SQLi, they're limited to app_user permissions. Without FILE — can't write files. Without SUPER — can't change server config.
Preventive protection is important, but you also need to see what's happening in real-time.
# /etc/mysql/mysql.conf.d/mysqld.cnf
# Error log — always enabled
log_error = /var/log/mysql/error.log
# Slow query log — detect suspicious queries
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 2
For compliance and forensics we use:
# MariaDB Audit Plugin
INSTALL SONAME 'server_audit';
SET GLOBAL server_audit_logging = ON;
SET GLOBAL server_audit_events = 'CONNECT,QUERY,TABLE';
For enterprise clients we offer integration with Elastic Security (ELK SIEM) — centralized log collection, event correlation, automatic threat detection.
💡 Tip: Slow query log isn't just about performance. Abnormally slow queries can indicate time-based SQLi (SLEEP, BENCHMARK).
Outdated PHP and MySQL versions have known vulnerabilities. Here's what we recommend:
| Version | Status | Our Recommendation |
|---|---|---|
| PHP 8.4 | ✅ Active Support | Best Choice |
| PHP 8.3 | ✅ Active Support | Recommended |
| PHP 8.2 | ⚠️ Security Only | Minimum Version |
| PHP 8.1 and below | ❌ End of Life | Update urgently! |
| Version | Status | Our Recommendation |
|---|---|---|
| MySQL 8.4 LTS | ✅ Long Term Support | Best Choice |
| MySQL 8.0.3x+ | ✅ Active Support | Recommended |
| MariaDB 10.11 LTS | ✅ Long Term Support | Recommended |
| MySQL 5.7 | ❌ End of Life | Critical risk! |
If using PostgreSQL — update to versions 17.3+, 16.7+, 15.11+, 14.16+, or 13.19+.
🔴 Critical: MySQL 5.7 and PHP 7.x no longer receive security updates. If on these versions — contact us, we'll help with migration.
ATTR_EMULATE_PREPARES = falsebind-address = 127.0.0.1We can audit your project, configure WAF, database hardening and monitoring.
WordPress core uses $wpdb->prepare() and is well protected. But plugins and themes are different story. From our experience, most WordPress breaches happen through vulnerable plugins.
We recommend: Use verified plugins, update regularly, install WAF.
Prepared statements protect against value injection. But some elements can't be parameterized: table names, ORDER BY, LIMIT. For those — whitelist validation.
Also consider CVE-2025-1094: PostgreSQL had vulnerability in escaping functions. Keep software updated.
No. WAF is important layer but not silver bullet. CVE-2025-25257 showed even Fortinet FortiWeb (a WAF!) had critical SQLi vulnerability.
Right approach: prepared statements + validation + WAF + database hardening.
Both safe when used correctly:
Important for PDO: ATTR_EMULATE_PREPARES = false.
Tools:
Important: Only test your own sites. Testing others without permission is crime.
If need help with incident response — contact our support.
Yes. Isolation is critical. If one site breached — other databases stay protected. We configure this by default on all managed servers.