The Pick ’n’ Mix search is the first flaw in FreeFungi because it is the first flaw most people ever learn: a SQL injection caused by building a query out of string concatenation. You will read a customer’s password hash out of the database using nothing but the search box.
New to this? What is SQL injection?
A website talks to its database using queries written in a language called SQL. A SQL injection happens when text you type gets pasted into one of those queries instead of being treated as plain data.
That lets you change what the query does: read tables you were never meant to see, slip past a login, or dump the customer list. The fix is to keep your input and the query strictly apart, which is what a parameterised query does.
You need FreeFungi running locally first. If you have Docker, it is one line:
docker run -p 3000:3000 ghcr.io/bad-adventure/freefungi 1 · Find it
Open http://localhost:3000 and use the search box at the top of the
shop. Search for fudge and you get the fudge. Now search for a single
apostrophe:
' The page breaks with a database error instead of an empty result. That error is the tell: your apostrophe reached the SQL engine as syntax, not as data. The query was built by pasting your text straight into it.
2 · Exploit it
The search runs something shaped like
SELECT name, price FROM sweets WHERE name LIKE '%yourtext%'.
Two columns come back. A UNION SELECT lets you append a second query
with the same column count, and choose what fills those two columns.
Paste this into the search box:
' UNION SELECT email, password_hash FROM customers--
The results table now lists every customer’s email and hashed password where
it expected sweet names and prices. The trailing -- (with a space)
comments out the rest of the original query so it does not trip over the leftover
%'.
Reward: one of those hashes belongs to owner@example.com,
the shop owner. It is unsalted MD5, so any lookup cracks it in seconds. Take the password
to /login, sign in, and the staff back office at /admin is yours.
A search box that ends in an admin session.
3 · Understand it
Here is the actual handler. The problem is the whole query being assembled with a template string, so your input becomes code:
app.get('/api/sweets/search', (req, res) => {
const q = req.query.q ?? '';
// The query is built by gluing user input straight in:
const sql = `SELECT name, price FROM sweets
WHERE name LIKE '%${q}%'`;
const rows = db.prepare(sql).all();
res.json(rows);
}); Nothing distinguishes the data you typed from the SQL around it. The database receives one flat string and does exactly what it says.
4 · Fix it
The fix isn't escaping quotes. It's never letting your input be code in the
first place. Use a parameterised query, where ? is a placeholder the
driver binds safely:
const sql =
`SELECT name, price FROM sweets
WHERE name LIKE '%${q}%'`;
db.prepare(sql).all(); const sql =
`SELECT name, price FROM sweets
WHERE name LIKE ?`;
db.prepare(sql).all(`%${q}%`);
Now the wildcards are part of the value bound to the placeholder, and the
query structure is fixed before your input is ever seen. A UNION SELECT
in the search box becomes a literal search for sweets called
“' UNION SELECT...”, of which there are none.
The general rule: user input is data, never code. Parameterise every query. An ORM does this for you, as long as you steer clear of its raw-SQL escape hatch, which is where this class of bug usually sneaks back in.