NewApp Monitoring is liveSign up free
AgentbeamBeta
Course outline

Learn / Level 3 / lesson 3

SQL injection

How pasting user input into a database query lets the input rewrite the query, why parameterized queries end the problem, and how it showed up in Kaseya VSA. Includes a safe in-memory lab.

8 min read · Yash Thakker · updated 2026-10-24

SQL injection · parameterized queries · CWE-89 · secure coding

A login query where typed text spills out of its quotes and rewrites the database command

What you will learn

  • Explain how string-built SQL lets data change a query
  • Rewrite a vulnerable query with placeholders
  • Recognise risky query building in code review
  • Describe the real-world impact seen in CVE-2017-18362

Before this: Never trust input, Path traversal

A database answers questions written in a language called SQL. When a website checks your login, shows your orders or searches a catalogue, some program on its server is writing a SQL sentence and sending it to the database. SQL injection is what happens when you can reach into that sentence.

A query is a sentence with a hole in it

Here is a table of users and a query that finds one of them. The query is text:

SELECT id, name FROM users WHERE name = 'sam';

Read it plainly: from the users table, get the id and name of rows whose name equals sam. The quotes mark where the value starts and ends. Everything outside them is the database's own vocabulary.

A web app does not know the name in advance, so it fills in the hole from the request. How it fills it in is the whole story.

The vulnerable way: gluing strings

def find_user(db, name):
    query = "SELECT id, name FROM users WHERE name = '" + name + "'"
    return db.execute(query).fetchall()

Step by step: typed text is glued into the query string, the closing quote is taken early, and the rest becomes part of the command

With name = "sam" this builds the sentence above. With input that itself contains a quote mark, the picture changes. Suppose someone types:

x' OR '1'='1

The program glues it in and produces:

SELECT id, name FROM users WHERE name = 'x' OR '1'='1'

The first quote the visitor typed closed the value early. What followed became the database's own words: "or one equals one". That is always true, so the query now matches every row. The program intended to look up one person and returned the entire table.

That is , weakness class CWE-89. The input did not trick the database; the database did exactly what the sentence said. The program wrote a sentence the visitor could finish.

What the visitor can finish depends on the account the app uses. Reading private rows is common. Changing or deleting rows happens where the database account is allowed to write. A few database products can also reach the server's files or run commands, and that is why this bug class can rise from "leaked table" to "server compromised".

The fix: parameterized queries

The lasting fix is to stop gluing. Write the query with a placeholder and hand the value over separately. This is a , also called a prepared statement.

def find_user(db, name):
    return db.execute(
        "SELECT id, name FROM users WHERE name = ?", (name,)
    ).fetchall()

Now the database receives two things: a finished sentence with a hole, and a value for the hole. It parses the sentence first, so its structure is already fixed, and then plugs the value in as data. Whatever the visitor types, including quote marks, is only ever the text to compare against name. The input from before is searched for as a literal name, and nobody has that name.

The fixed flow: the query shape is sent first and the value is sent separately, so the value stays data

This works because it removes the mixing, not because it filters bad characters. Compare with the alternatives:

ApproachWhy it holds or fails
Parameterized queriesValue never parsed as SQL. Holds.
Escaping quotes by handRules differ by database and context, and one missed case reopens the hole. Fragile.
Rejecting words like ORA denylist. Breaks real names and still gets bypassed.
Allowlist validation (digits only for an id)Good second layer for values with a known shape.
Least-privilege database accountLimits damage if something else fails.

Two cases need care. Placeholders work for values but not for table names or the direction of a sort. For those, choose from a fixed list in your code (ORDER BY an allowlist of column names), never from the request. And ORMs, which build queries for you, are safe by default but offer "raw SQL" escape hatches; every use of them deserves the same suspicion.

Why do placeholders stop injection when escaping often does not?

With a placeholder the database parses the command before it sees the value, so the value can never change the command's structure. Escaping tries to neutralise characters inside a mixed string, which depends on getting every rule right.

Spotting it in a code review

  • Search for execute, query, raw, exec and similar database calls.
  • Look at the argument. A string built with +, an f-string, format or a template literal that includes anything from a request is the red flag.
  • Check ORM raw modes and query builders for string concatenation inside them.
  • Check dynamic parts: sort columns, table names, IN lists. These need allowlists or a safe builder, because placeholders do not cover them.
  • Check the database account. An app that connects as an administrator turns every small bug into a large one.

A real case

The lesson is about reach. The bug was SQL injection (CWE-89), but the product was a management tool that controlled many other computers. A flaw in the database layer of a tool with that much authority became a path to every machine it managed. The more power a system holds, the less room it has for unchecked input.

How AI changes this

Attackers. SQL injection was one of the first bug classes to be automated, so scanners have long tried it. AI adds adaptation: a model can read an error message or a slightly different response, reason about what the underlying query looks like, and try the next variation, instead of replaying a fixed list. It can also read source code to find the string-built queries directly, without probing a live site. The defensive answer is unchanged: if the query is parameterized, there is nothing to adapt to.

Defenders. Assistants are good at converting string-built queries to parameterized ones and at listing every raw query call in a codebase. They are also known to produce vulnerable code themselves, including gluing request data into SQL, so generated database code should get the review in the section above. Run a static analyser (a tool that scans code for risky patterns) in CI as a second opinion.

AI agents. Agents are often given a database tool: "answer questions by running SQL". Two risks follow. First, the SQL is written by a model that reads untrusted text, so a page or ticket the agent processes can steer the query it writes; this is arriving as injection into your database. Second, the agent's connection usually has far more rights than the question needs. The controls are the same old ones: a read-only database account scoped to the tables the agent needs, row limits, and approval before any write or delete. Beam's enterprise data agent post works through these permissions in more depth.

Try it

This lab uses SQLite, which ships with Python, in memory. It creates no files and touches no real data.

Save as sqli_lab.py:

import sqlite3

db = sqlite3.connect(":memory:")
db.execute("CREATE TABLE users (id INTEGER, name TEXT, secret TEXT)")
db.executemany("INSERT INTO users VALUES (?, ?, ?)", [
    (1, "sam", "sam-private-note"),
    (2, "ana", "ana-private-note"),
])

def unsafe(name):
    q = "SELECT id, name FROM users WHERE name = '" + name + "'"
    return db.execute(q).fetchall()

def safe(name):
    return db.execute("SELECT id, name FROM users WHERE name = ?", (name,)).fetchall()

for name in ["sam", "x' OR '1'='1"]:
    print("input:", name)
    print("  unsafe ->", unsafe(name))
    print("  safe   ->", safe(name))

Run python3 sqli_lab.py. The normal name behaves the same in both. The second input makes the unsafe version return both users while the safe version returns nothing. Try adding a print(q) inside unsafe to see the final sentence the database was given. Delete the file when you are done.

Check yourself

In one sentence, what is the root cause of SQL injection?

The program builds the query by joining strings, so characters in the input can end the value early and become part of the command.

A developer says 'we replace every single quote with two single quotes, so we are safe'. What would you tell them?

Escaping is fragile: it depends on the database and the context, such as numbers that are not in quotes, and one missed case reopens the hole. Use parameterized queries so values are never parsed as SQL, and keep escaping out of the design.

Why can't a placeholder be used for a column name in ORDER BY, and what do you do instead?

Placeholders carry values, not parts of the command. Pick the column from a fixed allowlist in your code and put only that vetted name into the query.

Next

SQL injection let input rewrite a message to the database. The next lesson looks at the same mistake aimed at the visitor's browser, where input becomes script on a page.