0x55aa
Back to Blog

💉 SQL Injection: The Vulnerability That Refuses to Die

|
6 min read

💉 SQL Injection: The Vulnerability That Refuses to Die

Let me set the scene.

It's 2008. A security researcher pastes ' OR '1'='1 into a login form. The database coughs up every user record it has. The researcher writes a blog post. Everyone gasps. The industry vows to do better.

Cut to 2026. SQL injection is still sitting comfortably at the top of the OWASP Top 10, still responsible for countless data breaches, and still being introduced by developers who are absolutely certain it won't happen to them.

It will happen to them.

Let's talk about why SQLi is so sticky, what it actually looks like in the wild, and — most importantly — how to make sure your code isn't the next cautionary tale on HackerNews.


What Is SQL Injection, Actually?

At its core, SQL injection happens when user-supplied input is concatenated directly into a SQL query. The database can't tell the difference between "data you put in" and "SQL commands you wrote," so it happily executes whatever shows up.

Here's the classic bad code you've probably seen a hundred times (Python, but the sin is universal):

# 🚨 DO NOT DO THIS — ever
def get_user(username):
    query = f"SELECT * FROM users WHERE username = '{username}'"
    return db.execute(query)

Now imagine the attacker passes this as the username:

' OR '1'='1' --

Your query becomes:

SELECT * FROM users WHERE username = '' OR '1'='1' --'

The OR '1'='1' is always true, so the WHERE clause matches every row. The -- comments out the rest of the query. You just handed over your entire users table.

And that's just the dumbest version. Skilled attackers use UNION SELECT to pivot into other tables, SLEEP() to do blind time-based extraction, and INTO OUTFILE to write arbitrary files to the server. Some even chain SQLi into RCE. One bad f-string and you're filing an incident report at 2 AM.


The Fix Is Not That Hard (Seriously)

The solution has been known for decades: parameterized queries (also called prepared statements). Never concatenate user input into SQL. Ever. Full stop.

Here's the same function, done correctly:

# ✅ Safe — the database treats the input as data, not code
def get_user(username):
    query = "SELECT * FROM users WHERE username = %s"
    return db.execute(query, (username,))

With a parameterized query, the database driver sends the SQL template and the data separately. The database engine knows the username value is data — it cannot be interpreted as SQL syntax, no matter what the attacker types. '; DROP TABLE users; -- becomes literally just a string that doesn't match any username.

Most modern ORMs (SQLAlchemy, Django ORM, ActiveRecord, Hibernate, Prisma) do this automatically. If you're using an ORM and writing raw SQL strings with string interpolation, you're doing extra work to make your code less safe. Stop.


The Sneaky Cases People Miss

The obvious WHERE username = [input] example is well-known. But SQLi hides in subtler places that catch experienced developers off guard.

ORDER BY clauses — Most DB drivers can't parameterize column names or sort directions. If you're doing:

# Still dangerous!
query = f"SELECT * FROM products ORDER BY {user_sort_column} {user_sort_direction}"

You need to explicitly whitelist the allowed values before using them:

ALLOWED_COLUMNS = {"price", "name", "created_at"}
ALLOWED_DIRECTIONS = {"ASC", "DESC"}

def get_products(sort_col: str, sort_dir: str):
    if sort_col not in ALLOWED_COLUMNS or sort_dir not in ALLOWED_DIRECTIONS:
        raise ValueError("Invalid sort parameters")
    query = f"SELECT * FROM products ORDER BY {sort_col} {sort_dir}"
    return db.execute(query)

Search/LIKE patterns — Parameterized queries handle the quote escaping, but % and _ are still wildcard characters inside a LIKE pattern. If a user searches for 50%, they might match more than expected. Escape the wildcards:

search_term = user_input.replace("%", r"\%").replace("_", r"\_")
query = "SELECT * FROM products WHERE name LIKE %s ESCAPE '\\'"
db.execute(query, (f"%{search_term}%",))

Raw query fallback in ORMs — Every ORM has an escape hatch for raw SQL. Django has .raw(), SQLAlchemy has text(), Prisma has $queryRaw. These are fine when used correctly — but developers in a hurry drop user input straight into them. The ORM's safety guarantees disappear the moment you touch the raw query interface with unsanitized input.


Defense in Depth: Don't Stop at Parameterization

Parameterized queries are your first line of defense and they should be non-negotiable. But a layered approach is better:

  • Least privilege: Your app's database user shouldn't have DROP TABLE permissions, or access to tables it doesn't need. If the app only reads from products, give it SELECT on products — nothing more.
  • Input validation: Not as a substitute for parameterization, but in addition to it. If a field expects an integer ID, reject anything that isn't an integer before the query ever runs.
  • WAF rules: A Web Application Firewall can catch obvious injection patterns before they reach your app layer. It's not foolproof, but it's a useful safety net.
  • Automated scanning: Tools like sqlmap, Snyk, or SAST scanners in your CI pipeline can catch injection-prone code before it ships. Add them to your pipeline and thank yourself later.

Why Is This Still Happening in 2026?

Honestly? A few reasons:

  1. Legacy code — decade-old codebases written before ORMs were ubiquitous, where nobody has had the budget or courage to refactor.
  2. Tutorial rot — StackOverflow answers from 2010 that show string concatenation still rank on the first page of Google.
  3. Speed over security — "I'll fix it later" is the most expensive phrase in software.
  4. Overconfidence — "We're a small startup, nobody's targeting us." Automated scanners don't care about your startup size.

The fix is simple. The knowledge is widely available. The only thing missing is doing it.


Your Action Items

  1. Audit your queries today — grep your codebase for string formatting patterns near SQL keywords. f"SELECT, "WHERE " +, format( — these are your red flags.
  2. Enable SAST in CI — Snyk, Semgrep, or Bandit (Python) can catch SQLi-prone patterns automatically.
  3. Review your ORM's raw query usage — every .raw() or $queryRaw call deserves a second look.
  4. Apply least privilege to your DB users — it won't prevent injection, but it limits the blast radius when something slips through.

SQL injection is not a mystery. It's not sophisticated. It's a solved problem that keeps showing up because developers keep skipping the solution. Don't be that developer.


Got a war story about SQLi in the wild, or a sneaky case I missed? I'd love to hear it — reach out on GitHub or drop it in the comments. And if this saved your database from a '; DROP TABLE users; --, share it with a dev who needs it. 🛡️

Thanks for reading!

Back to all posts