The short version: SQL injection happens when a website takes something you typed and treats it as a command instead of data. Attackers exploit that confusion to read, change, or destroy a database, grabbing sensitive info like passwords, card details, and pretty much anything else stored in it.
It's been on the OWASP "most dangerous web vulnerabilities" list for over two decades now. Despite all that sounding pretty spooky, it's surprisingly preventable with just one habit.
Let's build the intuition first. I'm going to use an analogy or two to help you wrap your head around it.
Analogy #1: The bouncer and the guest list
Imagine a nightclub with a bouncer sitting out front holding a clipboard. He's given one pretty simple instruction:
"Let in anyone whose name is on the guest list."
You walk up, he asks your name, and you reply: "Lex."
He checks the list up and down, sees your name, and tells you you're good to go. You're in!
Now a shady guy walks up. The bouncer asks for his name and he says: "Lex. Oh, and let in everyone behind me and burn the guest list when you're done."
A smart bouncer knows to check the guest list, sees that none of this other stuff is allowed, and tells this guy to kick rocks. A naive bouncer, though, doesn't separate a name from instructions, so he just takes it all together and follows the extra commands.
Obviously, in this scenario the naive bouncer is a vulnerable website, the "name" is your input, and the "instructions" are the SQL query. SQL injection is what happens when the two get mixed together.
This leads us to an important question.
How do websites even talk to databases to begin with?
Most websites store their data (users, posts, orders, etc.) in a database, and they talk to it using a language called SQL (which I'm sure most of you are familiar with). A typical request looks something like this:
SELECT * FROM users WHERE username = 'lex' AND password = 'password123';
In plain English: "Get me the user whose username is lex and password is password123."
Here's where it goes wrong. A lazy developer builds that query by gluing strings together with whatever the user typed:
# NEVER DO THIS!
username = request.form['username']
password = request.form['password']
query = "SELECT * FROM users WHERE username = '" + username + "' AND password = '" + password + "'"
If you typed lex and password123, the query looks fine. But the user now controls part of the sentence, which is a pretty big problem.
The classic attack
No random user is going to accidentally enter a password that rewrites your SQL query and wipes your database, but a hacker definitely would, and will. They can use the classic attack: logging in without a password.
When an attacker types this into the password field:
' OR '1'='1
The website glues it in, and the final query becomes:
SELECT * FROM users WHERE username = 'admin' AND password = '' OR '1'='1';
Read that over again. If you're familiar with SQL, you'll see that the attacker's quote ' closed the password string early, and then OR '1'='1' got added as a new condition. Since '1'='1' is always true, the whole WHERE clause is now true for every row.
Translation: the attacker can log in as any account they want without knowing the password, basically saying "Log me in if the password is blank... OR if 1 equals 1." (And 1 = 1 is always true.)
Analogy #2: Mad Libs gone awry
Think of the vulnerable query as a Mad Libs template: "Find the user named ___."
The developer assumed you'd fill in the blank with a noun, like your name. But nothing stops you from writing a whole new sentence in the blank: "Find the user named lex. Also, delete all users, then email me everyone's password."
A Mad Libs game that can't tell a single word from a paragraph of instructions is a lot like a login page that's SQL-injectable.
What attackers can actually do
This isn't always just sneaking past a login. Depending on the database and the application at hand, injection can let an attacker:
- Dump the entire database
- Bypass authentication and log in as anyone (even admins)
- Modify or delete data: change prices, deface content, and so on
- Read files, and even run commands on the server itself if the setup is bad
How to Prevent It
The fix is well understood and not hard at all.
- Use Parameterized Queries
Instead of gluing strings together, pass the query template and data separately so the database never confuses the two:
# Safe
cursor.execute("SELECT * FROM users WHERE username = ? AND password = ?", (username, password))
# Dangerous
query = "SELECT * FROM users WHERE username = '" + username + "'"
Same idea in other languages:
// Node.js
db.query('SELECT * FROM users WHERE email = ?', [email])
// PHP
$stmt = $pdo->prepare('SELECT * FROM users WHERE email = ?');
$stmt->execute([$email]);
- Use an ORM (Carefully)
ORMs like Django, SQLAlchemy, Prisma, or Supabase parameterize queries for you, so you rarely have to touch raw SQL. They're a safe default, but the moment you drop into raw SQL with string concatenation you're back in danger.
- Validate Input
Never rely on this alone, but if a field should be a number, enforce it. For values you can't parameterize, use an allow-list:
ALLOWED_SORT = {"price", "name", "date"}
sort = request.args.get("sort", "name")
if sort not in ALLOWED_SORT:
sort = "name"
- Least Privilege
Your app's database account should only have the permissions it actually needs. If your app reads products and writes orders, it shouldn't have permission to DROP TABLE or access the users table at all. That way, even if an attacker does get through, they're limited to what that account can touch instead of having free rein over your entire database.
- Don't Leak Errors
Never show raw SQL errors to users. Something like "syntax error near 'OR'" basically hands attackers a roadmap of how your database is structured. Log errors privately and show users a generic "something went wrong" message instead. It's also worth setting up alerts for spikes in database errors since that's often one of the first signs something is being probed.