Every web application needs to store and retrieve data โ user accounts, transactions, content, settings. This is the job of databases. Understanding how databases work and how applications interact with them is absolutely critical for security professionals, because SQL Injection remains one of the most dangerous and prevalent web application vulnerabilities.
Most web applications use relational databases (RDBMS) like MySQL, PostgreSQL, Microsoft SQL Server, or SQLite. Data is organized into tables with rows and columns, and tables can have relationships with each other. SQL (Structured Query Language) is the standard language for interacting with these databases.
-- A typical users table in a web application
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(100) NOT NULL,
password_hash VARCHAR(255) NOT NULL,
role ENUM('user', 'admin') DEFAULT 'user',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Inserting a user (after registration)
INSERT INTO users (username, email, password_hash, role)
VALUES ('alice', 'alice@example.com', '$2b$12$LJ3m...', 'user');
-- Authenticating a user (login)
SELECT id, username, role FROM users
WHERE username = 'alice' AND password_hash = '$2b$12$LJ3m...';Web applications construct SQL queries dynamically, often incorporating user input. This is where the danger lies. Let's see how a typical application builds a query:
# Python/Flask example - Login function
@app.route('/api/login', methods=['POST'])
def login():
username = request.form['username']
password = request.form['password']
# The application builds a SQL query with user input
query = f"SELECT * FROM users WHERE username = '{username}'"
result = db.execute(query)
if result and verify_password(result['password_hash'], password):
session['user_id'] = result['id']
return jsonify({"status": "success"})
return jsonify({"status": "invalid credentials"}), 401SQL Injection occurs when user input is concatenated directly into SQL queries without proper sanitization or parameterization. An attacker can craft input that changes the query's logic:
-- Normal input: username = 'alice'
SELECT * FROM users WHERE username = 'alice'
-- Malicious input: username = "' OR '1'='1' --"
SELECT * FROM users WHERE username = '' OR '1'='1' --'
-- This returns ALL users because '1'='1' is always true
-- The -- comments out the rest of the queryโ ๏ธ SQL Injection can lead to complete database compromise โ reading any data, modifying data, deleting tables, and in some cases, executing operating system commands on the database server. It is consistently ranked in the OWASP Top 10.
| Type | Description | Example |
|---|---|---|
| In-band (Classic) | Results visible directly in the response | UNION SELECT to extract data |
| Error-based | Database errors reveal information | Triggering type conversion errors |
| Blind Boolean | True/false questions via response differences | AND 1=1 vs AND 1=2 behavior |
| Blind Time-based | Using delays to infer information | SLEEP(5) or WAITFOR DELAY |
| Out-of-band | Data sent via DNS/HTTP to attacker server | LOAD_FILE with UNC path to attacker |
The definitive defense against SQL Injection is parameterized queries (also called prepared statements). Instead of concatenating user input into the SQL string, placeholders are used and the database engine handles the separation of code and data.
# โ VULNERABLE: String concatenation
query = f"SELECT * FROM users WHERE username = '{username}'"
result = db.execute(query)
# โ
SAFE: Parameterized query
query = "SELECT * FROM users WHERE username = %s"
result = db.execute(query, (username,))
# โ
SAFE: Using an ORM (SQLAlchemy)
user = User.query.filter_by(username=username).first()// โ VULNERABLE: Node.js with string concatenation
const query = `SELECT * FROM users WHERE id = ${req.params.id}`;
db.query(query, (err, results) => { ... });
// โ
SAFE: Parameterized query in Node.js
db.query('SELECT * FROM users WHERE id = ?', [req.params.id],
(err, results) => { ... });๐ก Parameterized queries work because the database engine compiles the query structure FIRST, then binds the user input as DATA. The input can never change the structure of the query โ it's treated as a literal value, not executable SQL code.
NoSQL databases like MongoDB, CouchDB, and Redis are increasingly popular. They're not immune to injection attacks โ they just have different syntax. NoSQL injection exploits the same fundamental issue: user input being interpreted as code rather than data.
// โ VULNERABLE: MongoDB with user input in query object
app.post('/api/login', (req, res) => {
const { username, password } = req.body;
// If req.body is: {"username": {"$gt": ""}, "password": {"$gt": ""}}
// This matches the first user in the collection!
db.collection('users').findOne({ username, password });
});
// โ
SAFE: Validate and sanitize input types
app.post('/api/login', (req, res) => {
const username = String(req.body.username);
const password = String(req.body.password);
db.collection('users').findOne({ username, password });
});NoSQL injection is particularly dangerous because many developers assume that using MongoDB or similar databases eliminates injection risk. It doesn't โ it just changes the attack syntax. Always validate and sanitize input regardless of database type.
Verify exercises to earn โ 130 XP and unlock next lab level.