SQL Injection and NoSQL Injection
SQL Injection and NoSQL Injection
SQL Injection (SQLi) is one of the oldest and most dangerous web application vulnerabilities. Despite being well-understood, it remains prevalent, ranking #3 in the OWASP Top 10 under "Injection." NoSQL databases, while different in architecture, face similar injection vulnerabilities. This lesson covers both SQL and NoSQL injection attacks and their prevention.
Understanding SQL Injection
SQL Injection occurs when untrusted data is sent to an interpreter as part of a command or query. The attacker's hostile data tricks the interpreter into executing unintended commands or accessing unauthorized data.
<?php
// DANGEROUS: Direct concatenation of user input
$username = $_POST['username'];
$password = $_POST['password'];
$query = "SELECT * FROM users WHERE username = '$username' AND password = '$password'";
$result = mysqli_query($conn, $query);
?>
Attack payload:
Username: admin'--
Password: anything
Resulting query:
SELECT * FROM users WHERE username = 'admin'--' AND password = 'anything'
The -- comments out the password check, allowing login without a password!
Types of SQL Injection
1. Classic SQL Injection
Direct manipulation of SQL queries through user input to extract or modify data:
Username: ' OR '1'='1
Password: ' OR '1'='1
Query: SELECT * FROM users WHERE username='' OR '1'='1' AND password='' OR '1'='1'
Result: Always true, returns all users
// Example 2: Data extraction
Product ID: 1' UNION SELECT username, password, NULL FROM users--
Query: SELECT name, price, description FROM products WHERE id='1' UNION SELECT username, password, NULL FROM users--'
Result: Extracts all usernames and passwords alongside product data
2. Blind SQL Injection
When the application doesn't display database errors or query results, attackers use boolean-based or time-based techniques:
// Testing if first character of admin password is 'a'
Username: admin' AND SUBSTRING(password,1,1)='a'--
If page behaves normally → first char is 'a'
If page behaves differently → first char is not 'a'
// Time-based blind SQLi
Product ID: 1' AND IF(SUBSTRING(password,1,1)='a', SLEEP(5), 0)--
If response delays 5 seconds → first char is 'a'
If response is immediate → first char is not 'a'
// Automated extraction (pseudo-code)
password = ""
for position in 1 to 32:
for char in 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789':
if test_char(position, char):
password += char
break
3. Second-Order SQL Injection
Malicious data is stored safely but later used unsafely in a different part of the application:
// Safely stored using parameterized query
$stmt = $pdo->prepare("INSERT INTO users (username, email) VALUES (?, ?)");
$stmt->execute(['admin'--', 'attacker@evil.com']);
Step 2: Profile update uses the stored username unsafely
$username = $row['username']; // Retrieves: admin'--
$query = "UPDATE users SET bio='$bio' WHERE username='$username'";
// Query becomes: UPDATE users SET bio='...' WHERE username='admin'--'
// Comments out rest of query, potentially affecting wrong users
SQL Injection Prevention
1. Parameterized Queries (Prepared Statements)
The most effective defense against SQL injection is using parameterized queries, where SQL code and data are kept separate:
<?php
$pdo = new PDO('mysql:host=localhost;dbname=mydb', $user, $pass);
// Prepared statement with named placeholders
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = :username AND password = :password");
$stmt->execute([
':username' => $_POST['username'],
':password' => hash('sha256', $_POST['password'])
]);
$user = $stmt->fetch();
// Prepared statement with positional placeholders
$stmt = $pdo->prepare("INSERT INTO products (name, price) VALUES (?, ?)");
$stmt->execute([$name, $price]);
?>
// Node.js with MySQL
const mysql = require('mysql2/promise');
const [rows] = await connection.execute(
'SELECT * FROM users WHERE username = ? AND email = ?',
[username, email]
);
2. ORM Usage and Safe APIs
Object-Relational Mappers provide an additional layer of protection when used correctly:
$users = User::where('username', $request->input('username'))
->where('active', true)
->get();
// Safe: Query builder with bindings
$users = DB::table('users')
->where('votes', '>', 100)
->get();
// DANGEROUS: Raw queries without bindings
$users = DB::select("SELECT * FROM users WHERE name = '" . $name . "'"); // Vulnerable!
// Safe: Raw queries WITH bindings
$users = DB::select("SELECT * FROM users WHERE name = ?", [$name]);
// Safe: Named bindings
$users = DB::select("SELECT * FROM users WHERE name = :name", ['name' => $name]);
3. Input Validation and Sanitization
While not sufficient alone, input validation adds defense-in-depth:
// Whitelist validation for expected types
function validateProductId($id) {
if (!is_numeric($id) || $id < 1) {
throw new InvalidArgumentException("Invalid product ID");
}
return (int) $id;
}
// Whitelist for column names (can't use parameters for these)
$allowedSortColumns = ['name', 'price', 'created_at'];
$sortColumn = $_GET['sort'] ?? 'name';
if (!in_array($sortColumn, $allowedSortColumns)) {
$sortColumn = 'name';
}
$stmt = $pdo->prepare("SELECT * FROM products ORDER BY $sortColumn DESC");
// Limit input length
$username = substr($_POST['username'], 0, 50);
// Reject dangerous patterns (last resort, easily bypassed)
$blacklist = ['union', 'select', 'drop', 'insert', 'update', 'delete', '--', '/*'];
foreach ($blacklist as $keyword) {
if (stripos($input, $keyword) !== false) {
die("Suspicious input detected");
}
}
?>
4. Least Privilege Database Access
Limit the damage of successful injection by restricting database user permissions:
CREATE USER 'webapp_user'@'localhost' IDENTIFIED BY 'strong_password';
-- Grant only necessary permissions
GRANT SELECT, INSERT, UPDATE ON myapp.products TO 'webapp_user'@'localhost';
GRANT SELECT, INSERT ON myapp.orders TO 'webapp_user'@'localhost';
GRANT SELECT ON myapp.users TO 'webapp_user'@'localhost';
-- DO NOT grant:
-- DROP, CREATE, ALTER (structure modification)
-- FILE (read/write server files)
-- SUPER, PROCESS (server administration)
-- Access to mysql.user or other system tables
-- Separate users for different functions
CREATE USER 'webapp_readonly'@'localhost' IDENTIFIED BY 'password';
GRANT SELECT ON myapp.* TO 'webapp_readonly'@'localhost';
NoSQL Injection
NoSQL databases like MongoDB, CouchDB, and Cassandra use different query languages, but they're not immune to injection attacks:
MongoDB Injection Examples
app.post('/login', async (req, res) => {
const user = await db.collection('users').findOne({
username: req.body.username,
password: req.body.password
});
});
// Attack payload (sent as JSON)
{
"username": {"$ne": null},
"password": {"$ne": null}
}
// Resulting query
db.users.findOne({
username: {$ne: null}, // Not equal to null (always true)
password: {$ne: null} // Not equal to null (always true)
})
// Returns first user in database, bypassing authentication!
// Another attack: $gt (greater than)
{
"username": "admin",
"password": {"$gt": ""}
}
// Matches any password, since all strings are greater than empty string
NoSQL Injection Prevention
app.post('/login', async (req, res) => {
// Ensure inputs are strings, not objects
const username = String(req.body.username);
const password = String(req.body.password);
const user = await db.collection('users').findOne({
username: username,
password: password
});
});
// 2. Input validation with schema
const Joi = require('joi');
const loginSchema = Joi.object({
username: Joi.string().alphanum().min(3).max(30).required(),
password: Joi.string().min(8).required()
});
const { error, value } = loginSchema.validate(req.body);
if (error) {
return res.status(400).json({ error: error.details[0].message });
}
// 3. Use ORM/ODM with built-in protection
// Mongoose for MongoDB
const UserSchema = new mongoose.Schema({
username: { type: String, required: true },
password: { type: String, required: true }
});
const User = mongoose.model('User', UserSchema);
// Safe query using Mongoose
const user = await User.findOne({ username: username });
// 4. Disable operator query parsing in Express
// Using express-mongo-sanitize
const mongoSanitize = require('express-mongo-sanitize');
app.use(mongoSanitize());
// This automatically removes any keys starting with $ or containing .
Other NoSQL Injection Types
// Vulnerable:
db.view('users', 'by_username', {
key: req.query.username // Could be object with startkey/endkey
});
// Redis injection via Lua scripts
// Vulnerable:
const script = `return redis.call('GET', '${req.body.key}')`;
redis.eval(script);
// Attack: key = ") redis.call('FLUSHALL') --"
// Cassandra CQL injection
// Vulnerable:
const query = `SELECT * FROM users WHERE username='${username}'`;
// Safe: Use parameterized queries
const query = 'SELECT * FROM users WHERE username=?';
client.execute(query, [username]);
Advanced Protection Techniques
1. Stored Procedures (SQL)
DELIMITER //
CREATE PROCEDURE GetUserByUsername(IN p_username VARCHAR(50))
BEGIN
SELECT user_id, username, email FROM users WHERE username = p_username;
END //
DELIMITER ;
// PHP calling stored procedure
<?php
$stmt = $pdo->prepare("CALL GetUserByUsername(?)");
$stmt->execute([$username]);
$user = $stmt->fetch();
?>
2. Web Application Firewalls (WAF)
WAFs can detect and block SQL injection attempts, but should not be the only defense:
- Multiple SQL keywords (SELECT, UNION, FROM)
- SQL comments (--, /* */)
- Boolean operators in unexpected contexts
- UNION attacks
- Time-based attack patterns (SLEEP, BENCHMARK)
Limitations:
- Can be bypassed with encoding (URL encoding, Unicode, hex)
- False positives may block legitimate traffic
- Zero-day bypass techniques
- Performance overhead
3. Database Activity Monitoring
- Unusual query patterns (UNION, excessive JOINs)
- Queries to system tables (information_schema, mysql.user)
- Multiple failed login attempts
- Queries with sleep/delay functions
- Access to administrative procedures
- Data exfiltration (large result sets)
// Example: Logging suspicious queries
<?php
$logFile = '/var/log/app/suspicious_queries.log';
if (preg_match('/union|information_schema|benchmark|sleep/i', $query)) {
error_log(date('Y-m-d H:i:s') . " - Suspicious query: " . $query . "\n", 3, $logFile);
// Also alert security team
}
?>
Testing for Injection Vulnerabilities
1. Single quote: ' (Should cause error if vulnerable)
2. Boolean test: ' OR '1'='1 and ' OR '1'='2
3. Time-based: ' AND SLEEP(5)--
4. UNION test: ' UNION SELECT NULL--
5. Comment injection: --+, #, /*
// Automated tools
- SQLMap (comprehensive SQL injection tool)
- Burp Suite (with SQLi scanner)
- NoSQLMap (for NoSQL databases)
- Havij, jSQL Injection
// SQLMap example
sqlmap -u "http://example.com/product?id=1" --batch --dbs
sqlmap -u "http://example.com/product?id=1" -D mydb --tables
sqlmap -u "http://example.com/product?id=1" -D mydb -T users --dump
1. Build a basic PHP/MySQL login page with deliberate SQL injection vulnerability
2. Test it with at least 5 different injection payloads
3. Rewrite using prepared statements
4. Add input validation layer
5. Implement rate limiting to prevent brute force
6. Create a MongoDB version and secure it against NoSQL injection
7. Document the differences between SQL and NoSQL injection prevention
Bonus: Set up SQLMap and run it against your vulnerable version, then verify it fails against the secured version.
Real-World Impact
- 2020 - Freepik SQL Injection: Exposed 8.3 million user records including emails and hashed passwords
- 2019 - GoDaddy SQL Injection: Compromised 28,000 customer accounts through hosting control panel vulnerability
- 2017 - Equifax Breach: SQL injection in web application framework led to theft of 147 million records
- 2014 - Yahoo Voices: SQL injection exposed 450,000 plaintext passwords
Summary
SQL and NoSQL injection remain critical threats despite being well-understood vulnerabilities. The primary defense is always using parameterized queries or prepared statements that separate code from data. ORMs provide good protection when used properly. Input validation, least privilege database access, and monitoring add additional security layers. NoSQL databases require type validation and operator sanitization. Regular security testing and code review help ensure injection vulnerabilities are caught before production deployment.