What SQL injection actually is
SQL injection is an attack in which an attacker supplies input that changes the meaning of a SQL query the application sends to the database. If the application builds queries by dropping user input directly into a string, the database cannot tell where the developer's query ends and the attacker's code begins.
The result can be anything from reading another user's data to deleting entire tables or bypassing authentication entirely. It is not a database bug; it is an application-layer bug that lives in how the app composes SQL.
When it happens
SQL injection happens whenever an application treats user input as part of the SQL grammar instead of data.
- String concatenation to build a query from form fields, URL parameters, or JSON bodies.
- Dynamic sorting or column names interpolated from client input without an allow-list.
- Stored procedures that use EXEC or dynamic SQL with unsanitized arguments.
- Legacy reporting tools that expose raw SQL boxes to end users.
A concrete attack walkthrough
Imagine a login endpoint that builds a query like this:
const query = "SELECT * FROM users WHERE email = '" + email + "' AND password = '" + password + "'";
An attacker sends the following email:
' OR '1'='1
The database receives:
SELECT * FROM users WHERE email = '' OR '1'='1' AND password = '...'
The condition '1'='1' is always true, so the query returns every user. With a small change the attacker can dump the whole schema:
' UNION SELECT null, table_name, null FROM information_schema.tables --
Types of SQL injection
Not every SQLi attack returns data on the page. Attackers adapt to what the application reveals.
| Type | How it works | Typical use case |
|---|---|---|
| In-band / Classic | Results appear directly in the application's response, often via UNION SELECT. | Dumping tables when error output or page content is visible. |
| Error-based | Forces the database to throw an error that leaks schema or data details. | Enumerating table or column names through error messages. |
| Boolean-based blind | Sends payloads that make the query true or false, then observes page differences. | Exfiltrating one bit at a time when no direct output exists. |
| Time-based blind | Uses database functions like SLEEP or pg_sleep to delay responses conditionally. | Extracting data when the page looks identical either way. |
| Out-of-band | Triggers the database to make an external network call with stolen data. | Bypassing strict output controls by exfiltrating via DNS/HTTP. |
| Stored / Second-order | Malicious input is saved safely first, then executed later in a different query. | Attacking reports, exports, or admin dashboards that reuse stored values. |
How to prevent it
1. Parameterized queries / prepared statements
This is the single most effective defense. The query template is sent to the database first, and parameters are bound separately. The database treats them as values, never as executable SQL.
// Node.js with pg const result = await pool.query( 'SELECT * FROM users WHERE email = $1 AND password_hash = $2', [email, passwordHash] );
2. Use ORMs, but do not trust them blindly
ORMs parameterize by default, but they often expose raw query methods. Avoid those for user input. Also avoid passing strings into orderBy or select without an allow-list.
3. Allow-lists for dynamic identifiers
You cannot parameterize table names, column names, or sort directions. If you need them, keep an allow-list and reject anything not in it.
const allowedDirections = ['ASC', 'DESC']; const direction = allowedDirections.includes(userDirection) ? userDirection : 'ASC';
4. Input validation
Validate shape, length, and type before the value reaches the database. An email should look like an email, a UUID like a UUID. Validation is a defense-in-depth layer, not a replacement for parameterization.
Defense in depth
Even with prepared statements, add extra layers so a single mistake does not become a breach.
- Least privilege: The application DB user should only have the permissions it needs. It should not be able to drop tables, read unrelated schemas, or write to admin tables.
- Stored procedures with care: Stored procedures can help, but only if they do not build dynamic SQL from their arguments.
- Web Application Firewall (WAF): A WAF can catch obvious SQLi signatures, but it is a safety net, not the primary defense. Skilled attackers craft payloads that bypass rules.
- Disable verbose errors: Never expose raw database errors to end users. Log them internally and return a generic message.
- Static analysis and dependency scanning: Tools like Semgrep, CodeQL, and dependency checkers can find unsafe query construction during development.
How to answer it in an interview
Interviewers want to hear that you understand the mechanism, the risk, and the layered fix.
- Define it simply: "SQL injection is when user input is interpreted as part of the SQL query instead of as data, letting an attacker alter what the database executes."
- Give an example: Walk through a login query with
' OR '1'='1and explain how the condition becomes always-true. - Explain the fix: Prepared statements / parameterized queries are the primary defense. Show the before-and-after code.
- Mention edge cases: Dynamic identifiers cannot be parameterized, so use allow-lists. Stored procedures help only if they avoid dynamic SQL.
- Talk about defense in depth: Least-privilege DB users, WAFs, input validation, and hiding detailed errors.
- Relate it to system design: In a large system, centralize data access behind repositories or query builders, never let services concatenate SQL, and audit raw-query usage in code review.
Summary
SQL injection survives because it is easy to introduce and devastating when exploited. The fix is not a single tool; it is a discipline: parameterize every query, validate input, allow-list dynamic identifiers, restrict database privileges, and add monitoring. If you can explain the mechanism and the layered defense with a short code example, you have already answered the interview question well.