Labs · Cyber Security
Lab: Fix-It: Turn an Unsafe SQL Query in a Small Training App into a Parameterised Query and Prove It Works
Course: Cyber Security · Chapter 29: OWASP Top 10 Web Vulnerabilities
Chapter 29 explains the OWASP Top 10; this lab fixes the root cause of injection in real code.
Chala mitrano! SQL injection sounds scary, but the fix is one small habit: never paste user text into a SQL string, pass it as a parameter. Today a customer called O'Brien breaks our app, and that apostrophe teaches us everything. We find the bug, fix it in two lines, and prove it. Chala, developer banuya!
चला मित्रांनो! SQL injection भीतीदायक वाटतं, पण fix म्हणजे एक छोटी सवय: user चा text कधीच SQL string मध्ये चिकटवू नका, parameter म्हणून पाठवा. आज O'Brien नावाचा customer आपलं app बिघडवतो, आणि ते apostrophe आपल्याला सगळं शिकवतं. आपण bug शोधणार, दोन lines मध्ये fix करणार, आणि सिद्ध करणार. चला, developer बनूया!
चलो दोस्तों! SQL injection डरावना लगता है, पर fix एक छोटी आदत है: user का text कभी SQL string में मत चिपकाओ, parameter की तरह भेजो। आज O'Brien नाम का customer हमारा app तोड़ता है, और वो apostrophe हमें सब सिखाता है। हम bug ढूँढेंगे, दो lines में fix करेंगे, और साबित करेंगे। चलो, developer बनते हैं!
Suppose we are…
Suppose we are a junior developer at Freshworks. A support agent reports: "Customer search works for Asha and Rahul, but searching O'Brien gives an error page with SQL in it." That error is a warning sign: if one apostrophe can change the meaning of the query, an attacker can do the same on purpose. That is SQL injection. We fix the root cause in a small training app on our own laptop.
Goal of this lab
By the end you will be able to:
- Run a small Flask app on
127.0.0.1and reproduce the bug safely with a normal name. - Explain why string-building SQL is unsafe.
- Fix it with a parameterised query (
?placeholder), hide internal error details, and prove both.
What you need (all free)
- Python 3 on your laptop (
python --versionon Windows,python3 --versionon Mac/Linux). - A text editor (VS Code or Notepad). The training app below. About 35 minutes.
Safety and ethics
This app is a training copy that runs only on 127.0.0.1. Use the "O'Brien" test only on your own code. Typing quotes or SQL into other people's websites to "test" them is unauthorised testing.
Steps
-
Put
search_app.pyin a new folder, open a terminal there and install Flask:python -m pip install flask(On Mac/Linux use
python3 -m pip install flask; if pip complains about a managed environment, first runpython3 -m venv venvandsource venv/bin/activate.) -
Start the app:
python search_app.pyWhat you should see:
Running on http://127.0.0.1:5000. -
In your browser open
http://127.0.0.1:5000/search?name=Asha.What you should see:
{"results":[{"city":"Pune","name":"Asha"}]}. -
Now search for a real customer with an apostrophe:
http://127.0.0.1:5000/search?name=O'BrienWhat you should see: an error:
near "Brien": syntax error, and the full SQL... WHERE name = 'O'Brien'. The apostrophe ended the text early, so the rest was read as SQL code. -
Open
search_app.pyand find the bug (the line under# BUG):sql = f"SELECT name, city FROM customers WHERE name = '{name}'" rows = con.execute(sql).fetchall() -
Fix it with a placeholder. The database receives the SQL and the value separately, so the value can never become code:
sql = "SELECT name, city FROM customers WHERE name = ?" rows = con.execute(sql, (name,)).fetchall()Note the comma in
(name,): it makes a one-item tuple. -
Fix the second problem: the error response shows internal SQL. Replace the
exceptblock's return line with:app.logger.error("search failed: %s", e) return jsonify(error="Search failed. Please try again."), 500 -
Stop the app (Ctrl + C), start it again and repeat step 4.
What you should see:
{"results":[{"city":"Bengaluru","name":"O'Brien"}]}. The name is treated purely as data. -
Repeat step 3 to confirm normal searches still work, and try a name that does not exist (
?name=Zara): you get{"results":[]}, not an error. -
Search your own projects for the risky pattern:
grep -rniE "f[\"'](SELECT|INSERT|UPDATE|DELETE)|[\"'](SELECT|INSERT|UPDATE|DELETE)[^\"']*[\"'] *(\+|%)" --include=*.py --exclude-dir=venv .What you should see: no match for your fixed
search_app.py. Any match (an f-string,+or%building SQL) is a place to switch to placeholders.
Ravindra Bagale's Tip
Every language has the same fix: PHP PDO uses ? or :name, Java uses PreparedStatement, Node uses ? or $1. Escaping quotes by hand is not a fix; parameters are. And keep O'Brien in your test data forever. Ha test kadhi visru naka!
Ravindra Bagale's Tip – मराठी
प्रत्येक भाषेत fix तोच आहे: PHP PDO मध्ये ? किंवा :name, Java मध्ये PreparedStatement, Node मध्ये ? किंवा $1. हाताने quotes escape करणं हा fix नाही; parameters हा fix आहे. आणि O'Brien तुमच्या test data मध्ये कायम ठेवा. हा test कधी विसरू नका!
Ravindra Bagale's Tip – हिंदी
हर भाषा में fix वही है: PHP PDO में ? या :name, Java में PreparedStatement, Node में ? या $1। हाथ से quotes escape करना fix नहीं है; parameters fix हैं। और O'Brien को अपने test data में हमेशा रखो। ये test कभी मत भूलना!
Common mistakes
| Mistake | What happens | Fix |
|---|---|---|
Writing (name) instead of (name,) |
Python passes a string, not a tuple, and you get a binding error | Add the comma |
Putting quotes around the placeholder '?' |
The query looks for the literal text ? |
Write = ? with no quotes |
| "Fixing" by removing apostrophes from input | Real names like O'Brien and D'Souza break | Use parameters, keep the data |
| Showing database errors to users | Attackers learn table and column names | Log details, show a generic message |
Running Flask with debug=True on a network |
The debugger can run code | Keep debug off and bind to 127.0.0.1 |
Self-check checklist
0 of 5 done
Try-at-home challenge
Add a second route /city?city=Pune that returns all customers in a city. Write it safely from the start, and test it with D'Souza Nagar as the city.
Check your answer
Use con.execute("SELECT name, city FROM customers WHERE city = ?", (city,)).fetchall() with the same try/except that logs errors and returns a generic message. ?city=D'Souza Nagar must return {"results":[]} (no such city), not an error.
Samjla ka? User text is data, never code: pass it as a parameter. Aata pudhe jaauya: Chapter 30 secures the AWS account itself.