June 21, 2026
The Quote That Opened the Whole Store
One apostrophe broke the database. The first lab of my SQL injection series.
morgan_hack
4 min read
A new series starts today. I closed out Information Disclosure — secrets left lying around. Now I go after something louder: making the database itself betray the app.
This is SQL injection. Lab 1. And it all turned on a single character — '.
What the bug actually is
Websites store their data in a database. To fetch it, the app writes a question in a language called SQL (Structured Query Language). That question is a query.
When you click a category on a shop, the app builds a query like this:
SELECT * FROM products WHERE category = 'Pets' AND released = 1
In plain words: "Give me all products where the category is Pets, and the product is released." The released = 1 part hides unreleased items.
Here's the flaw. The word Pets came from you — the URL. If the app pastes your text straight into that query without cleaning it, you stop sending a category and start sending commands. That's SQL injection: your input becomes part of the database's question.
What I Noticed
I started where every test starts — looking, not attacking.
I opened the lab in Burp Suite (a proxy tool that sits between your browser and the site, recording every request). I used Burp's built-in browser so all traffic flows through it automatically.
I clicked around the shop. Each category click changed the URL:
GET /filter?category=GiftsGET /filter?category=GiftsThat category=Gifts is a parameter — a value the app reads. And it almost certainly feeds a database lookup. That was my target.
I sent the request to Burp Repeater (a tool to resend one request over and over, tweaking it each time and watching the response).
First, the baseline. I sent the normal Pets request and wrote down what normal looks like:
The action, step by step
Step 1 — Poke it with a quote.
The single quote ' is the probe. In SQL, a quote ends a piece of text. If the app is vulnerable, my quote ends the string early and breaks the query.
I changed the value to Gifts' and sent it:
GET /filter?category=Gifts' HTTP/2GET /filter?category=Gifts' HTTP/2I hit Send. The response:
HTTP/2 500 Internal Server ErrorHTTP/2 500 Internal Server Error
There it was. The bug said hello.
That 500 means the database choked. My query had become:
SELECT * FROM products WHERE category = 'Gifts'' AND released = 1SELECT * FROM products WHERE category = 'Gifts'' AND released = 1See the two quotes? 'Gifts''. That extra quote has no partner. The database can't read it — broken syntax — so it errors out. And it only errors because my raw input reached the SQL engine. A cleaned input would never do this. Injection confirmed.
Step 2 — Turn the break into control.
Breaking the query proves the door is unlocked. Now I walk through it. I want every product to show — including the unreleased ones the app tries to hide.
I changed the value to Gifts' OR 1=1-- and sent it:
GET /filter?category=Gifts' OR 1=1-- HTTP/2
GET /filter?category=Gifts' OR 1=1-- HTTP/2
This builds:
SELECT * FROM products WHERE category = 'Gifts' OR 1=1--' AND released = 1
SELECT * FROM products WHERE category = 'Gifts' OR 1=1--' AND released = 1
Read it piece by piece:
- ' closes the
Giftstext cleanly. OR 1=1— and 1 always equals 1. Every single row now matches.- -- comments out the leftover
' AND released = 1, so the released filter dies.
I hit Send.
What fell out
The page exploded with products.
Items from every category. Items marked unreleased — things no normal customer was ever meant to see. The released = 1 gate was gone, and the whole catalog spilled out.
The banner flipped green: solved.
One apostrophe and a true statement opened a door the developer thought was locked.
Why This Works
The root cause is one mistake: the app built its SQL query by gluing my text directly into it.
Developers do this because it's the easy way. You take the category from the URL, drop it into a string, run the query. It works perfectly — until someone sends a ' instead of a normal word. Then your text isn't data anymore; it's part of the command.
The fix is called parameterized queries (also "prepared statements"). The query structure is fixed first, and your input is passed in separately as pure data — never as code. The database treats Gifts' OR 1=1-- as a literal category name to search for, finds nothing, and moves on. No injection possible.
The vulnerable version asks the database a question written partly by the attacker. The safe version asks a fixed question and hands your input over as a sealed envelope.
Real Bug Bounty Payouts
SQL injection is old, but it still pays — because one bug often unlocks the entire database.
$3,000 — HackerOne. A researcher found SQLi in a search parameter on a SaaS platform. A single quote threw a database error, and a UNION attack dumped the users table. Chained into account takeover via leaked password hashes.
$15,000 — private program. SQLi in an API filter parameter exposed a backend PostgreSQL database. The hunter extracted internal API keys and customer PII, escalating to a full data-breach report.
$5,500 — Bugcrowd. A sort parameter (?order=) was injectable into an ORDER BY clause — the kind devs forget to protect. Blind extraction revealed admin credentials.
$20,000+ — multiple programs. Where SQLi reaches a database function that can write files or run commands, it escalates from data theft to full server takeover. Those reports cross five figures fast.
Small character. Big payout. Every time it reaches the database.
Where to Hunt This in Real Apps
A practical checklist for live targets:
Probe every input that looks up, filters, searches, sorts, or logs in — those build SQL queries. Category filters, search boxes, login fields, ?id= parameters.
Don't stop at the URL bar. Check POST bodies, JSON API values, cookies (tracking IDs especially), and headers the app might log (User-Agent, Referer, X-Forwarded-For).
Always grab a baseline first — status code and response length. You compare against it.
Send a single '. Watch for any change: a 500 error, a different length, missing or extra results, or a delay. No visible error doesn't mean no bug — blind SQLi is silent.
When you confirm it, escalate carefully. '-- to drop filters, OR 1=1 to force-match, then UNION to pull data. Be careful with OR 1=1 on inputs that might write or delete — it can wipe a table.