Two years into my career as a developer, I shipped a login endpoint. The query looked like every other query I had written. It worked in testing. It passed review. We moved on.
Years later, sitting in front of OWASP Juice Shop with Burp Suite running in the background, I typed a four-word payload into a login form and watched it hand me full admin access. No username required. No password. Just a string that broke the query’s logic from the inside.
Same pattern. Different side of the keyboard. That is SQL injection.
What the developer writes
When you’re building a login form, you think in flows: user submits credentials, server queries the database, session begins. The SQL that powers that flow might look something like this:
SELECT * FROM users
WHERE email = '" + email + "'
AND password = '" + password + "'"
It works. Every test passes. No alarms fire in code review. You ship it and move on to the next ticket.
What you don’t see, unless you have spent time thinking like an attacker, is that you just handed anyone who can reach that input field a direct line into the database engine itself.
You are not just accepting a string. You are accepting a command.
What the attacker sees
An attacker doesn’t see a login form. They see a string that gets concatenated into a SQL statement. And if that string is not sanitized before it reaches the query, the attacker controls the query.
The payload goes into the email field:
' OR 1=1--
The resulting database query becomes:
SELECT * FROM users
WHERE email = '' OR 1=1--'
AND password = '...'
Three things happen simultaneously. The single quote closes the email string early. OR 1=1 is always true, so the WHERE clause matches every row in the table. The double dash comments out everything after it, including the password check entirely. The database returns the first user record, typically the admin account. Authentication bypassed. Zero credentials required.
Every form input is a potential injection point. The first question isn’t “does this look exploitable?” It’s “is user input ever concatenated into a query?” If the answer is yes, the rest is methodology.
Lab: OWASP Juice Shop login bypass
OWASP Juice Shop is the industry-standard intentionally vulnerable web application for security training. It replicates the vulnerabilities you actually encounter in real engagements, not theoretical ones. This is a platform I use regularly to sharpen technique and document attack chains.
Open Burp Suite with your browser proxy set to 127.0.0.1:8080. Navigate to the Juice Shop login page and submit any credentials. Intercept the POST request to /rest/user/login before it reaches the server.
The request body contains email and password parameters in JSON. The email field is the target. Forward the request to Burp Repeater for controlled, repeatable testing.
Replace the email value with ' OR 1=1-- and put any string in the password field. Send the modified request from Repeater.
HTTP 200. The response body contains a valid JWT token for the admin account, with the email address exposed in plaintext. The application treats the injection as a legitimate admin login. Full application access granted without a single valid credential.
POST /rest/user/login HTTP/1.1 Host: localhost:3000 Content-Type: application/json Content-Length: 47 {"email":"user@example.com","password":"pass123"}
POST /rest/user/login HTTP/1.1 Host: localhost:3000 Content-Type: application/json Content-Length: 43 {"email":"' OR 1=1--","password":"anything"}
HTTP/1.1 200 OK Content-Type: application/json; charset=utf-8 { "authentication": { "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdGF0dXMiOi...", "bid": 1, "umail": "admin@juice-sh.op" } }
Going deeper: extracting the whole database
Login bypass is the most visible SQLi technique. It is rarely the most damaging one. Once injection is confirmed, the real extraction begins.
UNION-based injection appends a second SELECT statement to the original query, pulling data from other tables directly into the HTTP response:
' UNION SELECT null, email, password FROM Users--
If the column count matches and data types align, the database executes both statements and returns the second result set in the same HTTP response. Emails. Password hashes. Account metadata. The entire Users table, served back over the wire to whoever is holding the proxy.
That is what SQL injection means in a real engagement report. Not “an attacker could log in as admin.” It means full credential exposure across the entire user base. Every account. Every hash. The blast radius is the whole system.
The fix: parameterized queries
There is one fix and it is non-negotiable: parameterized queries, also called prepared statements. Input sanitization, length limits, and regex filtering are mitigations at best. Parameterized queries are a structural solution because they eliminate the vulnerability at the architectural level.
Instead of building the query by concatenating user input into the string, you send the query structure and the data as separate operations:
# Vulnerable
query = "SELECT * FROM users WHERE email = '" + email + "'"
# Fixed
cursor.execute(
"SELECT * FROM users WHERE email = ?",
(email,)
)
The database engine receives the query template first, compiles it, and only then substitutes the user-supplied value into a designated placeholder. The input is treated as data, never as code. There is no string to inject into, because the structure of the query was finalized before the user’s input was ever involved.
SQL injection becomes structurally impossible. The vulnerability doesn’t get caught by a filter. It can’t exist.
Why the developer background changes everything
Most automated scanners will flag SQL injection. What they won’t do is read the ORM configuration, trace the data flow through three middleware layers, find the one unparameterized raw query buried in a legacy auth module, and explain precisely which business logic depends on that module and why the fix has downstream consequences.
That requires someone who has actually built systems. Who knows what a migration looks like. Who understands why raw queries end up in codebases in the first place, and how developers think about performance versus security tradeoffs when shipping under deadline pressure.
The developer background is not a nice-to-have in offensive security. It is the difference between finding a flag and writing a finding that actually gets fixed.
When I report a SQL injection in a web application, I don’t just flag it. I trace the query back to its origin in the codebase. I identify every other endpoint that follows the same pattern. I write a remediation path that the development team can implement without rewriting the application. That is what depth looks like in a pentest report.
Automated tools produce output. Security engineers who understand the code produce findings.
Next in the series
Episode 2: Broken Authentication. The login page worked perfectly. The session management did not. How attackers bypass authentication without ever touching a password, and what developers consistently miss when they build session logic from scratch.
Same code.
Different eyes.
Everything changes.