SQL Injection Testing
SQL injection lets an attacker rewrite the queries your application runs. Learn how string-concatenated SQL becomes an injection point, how in-band, blind and out-of-band variants work, and why parameterized queries are the complete, standard fix.
What is this?
SQL injection (SQLi) is a server-side injection flaw that occurs when an application builds SQL queries by concatenating untrusted input into the query text. An attacker can alter the query's structure — rewriting WHERE clauses, appending UNION selects or stacking statements — to read, modify or delete data they should never touch. It remains one of the most severe web vulnerabilities and is completely preventable.
Why do you need it?
- It can expose entire databases: user tables, credentials and financial records in one request.
- It can escalate to full server compromise via features like xp_cmdshell or COPY ... TO PROGRAM.
- It breaks confidentiality, integrity and availability at the same time.
- The fix — parameterized queries — is simple and free, yet the flaw keeps appearing in new code.
- It is easy to test safely in authorized sandboxes, so teams can prove the defense works.
Mental model
A phone operator who repeats your words directly into the internal order system
SQL built by string concatenation
If the operator literally reads your reply into the system, saying 'cancel order 42 and refund customer 9' changes the instruction. Parameterized queries treat your input as a value, not as part of the instruction being read.
How it works
- The application concatenates user input directly into a SQL statement string.
- An attacker injects metacharacters such as a quote or a comment marker to break out of the expected value.
- The query structure changes: OR 1=1 bypasses checks, UNION appends a second result set, and stacked statements run extra commands.
- The database executes the modified statement and returns attacker-chosen data or changes state.
- Prevention: parameterized queries everywhere, least-privilege database accounts, generic errors, and a WAF as defense-in-depth.
How an injection payload changes the meaning of a query.
Key commands
curl -s -o /dev/null -w "%{http_code}" "http://sandbox.local/WebGoat/login"Confirm the login endpoint is reachable before testing.
200
curl -s "http://sandbox.local/WebGoat/search?q=1%27%20OR%20%271%27%3D%271"Educational smoke test: URL-encoded ' OR '1'='1 payload to observe whether the app changes behavior or leaks an error.
grep -rn "query(" app/ | head -20List every SQL call site so each one can be audited for concatenation.
grep -rn "concat(" app/Find dynamic-SQL helpers that build statements by joining strings.
psql "postgres://app_user@localhost/appdb" -c "SELECT current_user"Verify the application database account runs with least privilege.
current_user\n-------------\n app_user
Examples
Parameterized queries: the complete fix
// ============================================================// EDUCATIONAL PAYLOAD — authorized sandbox only (WebGoat lesson// "SQL Injection"). Never run SQLi payloads against systems you// do not own or lack written permission to test.// ============================================================// Vulnerable login query built by concatenation:// SELECT * FROM users WHERE username = '<input>' AND password = '<input>'// Payload in the username field:// admin' OR '1'='1' --// Resulting query:// SELECT * FROM users WHERE username = 'admin' OR '1'='1' -- ' AND password = '...' // ============================================================// DEFENSE — parameterized queries (node-postgres + bcrypt)// ============================================================const { Pool } = require("pg")const bcrypt = require("bcryptjs")const pool = new Pool() async function login(username, password) { const result = await pool.query( "SELECT id, username, password_hash FROM users WHERE username = $1", [username] ) const user = result.rows[0] if (!user || !bcrypt.compareSync(password, user.password_hash)) return null return user}The payload closes the quote with admin', adds OR '1'='1' which is always true, and comments out the rest with -- so the password check never runs. The defense sends the username as a bound parameter ($1): the driver transmits it as data, never as SQL text, so quotes and keywords have no effect on the statement's structure. Password hashes are compared with bcrypt so even a leaked table does not reveal credentials.
In production
- Parameterized queries or prepared statements are the mandated pattern for every data access layer.
- Database accounts run least-privilege, with scoped read or write roles per service.
- ORM query builders are configured to forbid raw string interpolation.
- A WAF and a generic error handler prevent verbose SQL errors from leaking schema details.
Best practices
- Never build SQL by concatenation or template interpolation with user input.
- Use parameterized queries or the ORM's safe query builder in every layer of the stack.
- Run each service's database connection with the least privilege it needs.
- Return generic error messages so query and schema details never reach the client.
- Add a source-scan rule and a DAST check to catch concatenated queries in CI.
Common mistakes
- Escaping quotes instead of parameterizing — blacklist escaping is bypassable.
- Allowing stacked queries, which turns a read-only flaw into arbitrary commands.
- Exposing raw SQL errors to the client, revealing schema and table names.
- Forgetting second-order injection, where stored data is later used unsafely in another query.
Common issues
column "x" does not existA UNION payload guessed the wrong column count or type.
Probe the column count with ORDER BY in the sandbox only, then align the UNION columns.
TypeError: Cannot read properties of undefinedThe parameter binding order does not match the placeholders.
Align $1, $2 placeholders with the arguments array order and re-run the audit.
The scan finds nothing, but manual testing still triggers errorsThe scanner's canned payloads are blocked by input validation or a WAF.
Test with realistic payloads in the sandbox and audit the query construction path directly in code.
Debugging tips
- Test with a benign marker that forces a clear signal — like appending a comment character — before any UNION payload.
- Map the schema in the WebGoat sandbox first, then verify against the real database with a read-only account.
- Check the ORM or driver logs to confirm parameters are sent separately from the query text.
Interview checkpoint
Hands-on lab
- Identify a concatenated login query as an injection point
- Confirm the flaw with a benign marker before any payload
- Demonstrate a UNION-based data extraction in the sandbox
- Replace the vulnerable pattern with a parameterized query
SQL Injection in WebGoat
40 min · intermediate
Quick quiz
Which of the following is the safe way to pass a search term to a query?
Input validation alone is a complete defense against SQL injection.
An attacker's UNION payload matches the original column count. What can they now do?
Cheat sheet
pool.query(sql, [params])Parameterized, alwaysSELECT ... WHERE id = $1Postgres placeholder styleSELECT ... WHERE id = ?MySQL/Node mysql2 styleknex("items").where("id", id)ORM safe builderSELECT current_userCheck least privilegeSET SEARCH_PATH ... / REVOKELimit table exposure