Testing for Injection
By the end of this lesson you will be able to recognise where an application might mix user input into a query or command, check for the problem safely in your own lab, explain clearly why it happens, and show the developer a fix that removes it for good.
Injection happens when user input is treated as part of a command instead of as plain data. It is category A05 in the OWASP Top 10:2025. The theory is covered in the Cyber Security lesson SQL Injection. Here the focus is the tester's workflow and, above all, the fix.
The idea in one example
Imagine a clerk in Pune who fills a form by copying whatever a customer writes on a slip. The form reads "Pay ___ to Priya." A customer writes "100 to Priya. Also pay 50,000." A careless clerk copies it word for word, and the form now says something nobody intended. The clerk mixed instructions with data.
Databases and shells behave the same way. If a program pastes user input directly into a SQL query or a shell command, certain characters in that input can change the command itself. The cure is never to mix the two: keep code as code and data as data.
Why string-built queries are unsafe
The root cause is building a query by joining text. Look at this deliberately unsafe pattern, like the one in Juice Shop's own training code:
# UNSAFE: never do this
email = user_input # comes from the request
query = "SELECT * FROM Users WHERE email = '" + email + "'"If email contains a single quote, it ends the string early, and the rest of the input lands in the query rather than staying data. The database then runs something the developer never wrote. A tester confirms this safely by sending one harmless character, a single quote, and watching for a database error. An error from a single quote is the classic sign that input reaches the query unescaped.
You can see the mechanism without any database, right in Python, by printing the query a vulnerable builder would produce:
def unsafe_query(email):
return "SELECT * FROM Users WHERE email = '" + email + "'"
print("Normal input:")
print(" ", unsafe_query("aarav@lab.test"))
print("Input with a quote (breaks the syntax):")
print(" ", unsafe_query("x'"))Normal input: SELECT * FROM Users WHERE email = 'aarav@lab.test' Input with a quote (breaks the syntax): SELECT * FROM Users WHERE email = 'x''
Notice the unbalanced quotes in the second line. The database cannot parse it, so it errors. That broken syntax is what a tester detects first. From there, the weakness can let someone read rows they should not, which is why injection is rated so highly. You confirm the behaviour in your lab, record it, and move straight to the fix.
The fix: parameterised queries
The correct way is a parameterised query (also called a prepared statement). You write the SQL with a placeholder, and pass the data separately. The database driver then keeps the data as data, whatever it contains. Python's sqlite3 lets you prove this safely:
import sqlite3
db = sqlite3.connect(":memory:")
db.execute("CREATE TABLE Users (email TEXT, role TEXT)")
db.execute("INSERT INTO Users VALUES ('aarav@lab.test', 'customer')")
db.execute("INSERT INTO Users VALUES ('admin@lab.test', 'admin')")
def safe_lookup(email):
cur = db.execute("SELECT email, role FROM Users WHERE email = ?", (email,))
return cur.fetchall()
print("Normal lookup:", safe_lookup("aarav@lab.test"))
print("Quote as input:", safe_lookup("x'"))
print("Tricky input: ", safe_lookup("' OR '1'='1"))Run it and read the output below. The tricky input returns no rows, because the driver looked for a user whose email is literally that whole string. The data never became part of the SQL.
Normal lookup: [('aarav@lab.test', 'customer')]
Quote as input: []
Tricky input: []That ? placeholder is the entire fix. The OWASP advice is the same: use a safe API such as parameterised queries or an ORM, so the interpreter never sees user input as code.
Input validation as a second layer
Validation does not replace parameterised queries, but it adds defence in depth and rejects nonsense early. For an email field, allow only what a real email looks like:
import re
EMAIL = re.compile(r"[\w.+-]+@[\w-]+\.[\w.-]+")
def valid_email(value):
return EMAIL.fullmatch(value) is not None
for value in ["aarav@lab.test", "priya.k@shop.example.in", "x'", "' OR '1'='1"]:
print(f"{value!r:<22} {'accept' if valid_email(value) else 'reject'}")'aarav@lab.test' accept 'priya.k@shop.example.in' accept "x'" reject "' OR '1'='1" reject
OWASP calls this positive validation: describe what is allowed, not what is banned. A blocklist of "bad characters" is fragile because attackers find characters you forgot. Never use validation alone, though; parameterised queries remain the real defence.
Command injection, and its fix
The same disease affects shell commands. A page that pings a host by building "ping " + host is in danger, because shell characters like ; chain commands. DVWA's "Command Injection" page, at the low level, shows this clearly in your lab. The fix is to avoid the shell entirely and pass arguments as a list, so input is always one argument, never new commands:
import shlex
def build_command(host):
# Safe: arguments as a list, no shell involved
return ["ping", "-c", "1", host]
for host in ["192.168.56.103", "192.168.56.103; rm file"]:
argv = build_command(host)
print("argv:", argv)
print("as one shell word:", shlex.quote(host))argv: ['ping', '-c', '1', '192.168.56.103'] as one shell word: 192.168.56.103 argv: ['ping', '-c', '1', '192.168.56.103; rm file'] as one shell word: '192.168.56.103; rm file'
In the second case the whole string, semicolon and all, stays one argument to ping, which simply fails to resolve it as a host. No second command runs. In real code you would use subprocess.run(argv) with a list and never shell=True. On DVWA, compare the low and impossible levels to see this fix applied in PHP.
Testing checklist
When you test an input for injection in the lab:
- Note where the input goes (a search, a login, a host field).
- Send one harmless probe (a single quote for SQL, a lone special character for shells) and watch for errors or changed behaviour.
- If it reacts, record the request and response as evidence. Stop there; you have proof.
- Read the code if you have it, confirm the cause, and write the fix.
- Re-test after the fix to confirm the error is gone.
Common mistakes
- Escaping quotes by hand. It is error-prone and misses cases. Use parameterised queries.
- Relying on validation alone. Validation helps, but a safe API is the real fix.
- Going too far in the test. Confirm the weakness; do not read or change real data.
- Using
shell=Truewith user input. Pass arguments as a list and avoid the shell.
Key points
- Injection occurs when input is treated as code; SQL and command injection are the common forms.
- A single quote causing a database error is the classic first sign of SQL injection.
- Parameterised queries (placeholders with separate data) are the real fix, confirmed here with
sqlite3. - Positive input validation adds defence in depth but does not replace parameterised queries.
- For shells, pass arguments as a list and never use
shell=Truewith user input.
Exercise
Using the in-memory sqlite3 example, add a products table and a search(term) function that uses a parameterised LIKE query (name LIKE ?) with a wildcard. Show that searching for apple works and that searching for a single quote returns no rows and raises no error.
Show solution
import sqlite3
db = sqlite3.connect(":memory:")
db.execute("CREATE TABLE products (name TEXT)")
db.executemany("INSERT INTO products VALUES (?)", [("Apple Juice",), ("Banana Juice",)])
def search(term):
cur = db.execute("SELECT name FROM products WHERE name LIKE ?", ("%" + term + "%",))
return [row[0] for row in cur.fetchall()]
print(search("Apple"))
print(search("'"))search("Apple") returns ['Apple Juice'], and search("'") returns [] with no error. The wildcard is added to the data, not the SQL, so the query stays safe.
Quick Quiz
1. What is the root cause of injection?
Mixing instructions and data lets input change the command.
2. Which is the real fix for SQL injection?
Placeholders with separate data stop input from becoming code.
3. A tester sends a single quote to a field and the app returns a database error. What does this suggest?
A syntax error from one quote shows input is breaking the query.
4. Why pass command arguments as a list instead of a single shell string?
Avoiding the shell keeps user input from being read as extra commands.
Learn Ethical Hacking with a mentor
Free lessons get you started. Our Ethical Hacking & Cyber Security training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
