SQL injection via template-literal interpolation of req.query.q into raw Sequelize query
`searchProducts` builds a raw `SELECT * FROM Products ...` string by template-interpolating the unsanitized `req.query.q` value (only truncated to 200 chars) and hands it to `sequelize.query`, allowing classic UNION-based SQL injection.
Lines 19–23 of routes/search.ts:
let criteria: any = req.query.q === 'undefined' ? '' : req.query.q ?? ''
criteria = (criteria.length <= 200) ? criteria : criteria.substring(0, 200)
models.sequelize.query(`SELECT * FROM Products WHERE ((name LIKE '%${criteria}%' OR description LIKE '%${criteria}%') AND deletedAt IS NULL) ORDER BY name`)
The value comes straight from the request query string and is dropped into the SQL with no parameter binding, no replacements/bind option, and no escaping. The 200-char truncation does nothing for safety — UNION-based exfiltration fits well under that limit. The downstream code even has an if (notSolved(unionSqlInjectionChallenge)) block confirming the pattern is reachable.
Safe form would be sequelize.query("SELECT * FROM Products WHERE name LIKE :q OR description LIKE :q ...", { replacements: { q: %${criteria}% } }).
GET /rest/products/search?q=')) UNION SELECT id,email,password,role,deluxeToken,lastLoginIp,profileImage,totpSecret,1,2 FROM Users-- returns every Users row inside the products JSON response — the canonical Juice Shop union-SQLi exploit. No authentication required.
Unauthenticated full-database read (and, with the right payload, write/delete) — the documented Juice Shop union-SQLi challenge: an attacker can exfiltrate the entire Users table (emails, MD5 password hashes, TOTP secrets) via a single GET request.
The line models.sequelize.query(\SELECT * FROM Products WHERE ((name LIKE '%${criteria}%' ...)\) interpolates req.query.q directly into a raw SQL string with no replacements/bind parameters. The 200-char truncation is irrelevant for UNION-based payloads, and there is no auth middleware on this route. The PoC matches the canonical Juice Shop union-SQLi challenge (the surrounding unionSqlInjectionChallenge block confirms reachability), enabling unauthenticated full-table exfiltration.
The searchProducts handler is exposed as an unauthenticated HTTP GET endpoint (/rest/products/search?q=...), so the sink is reachable remotely with no privileges or user interaction (AV:N, PR:N, UI:N, AC:L). req.query.q is interpolated directly into the models.sequelize.query template literal with no binding or escaping — the only "filter" is a 200-char truncation that fits typical UNION payloads — giving an attacker arbitrary SQL execution against the application's DB user. Because Sequelize runs the statement with the app's DB credentials, the attacker can read every table (Users emails, password hashes, TOTP secrets — C:H) and, depending on the DB driver/multi-statement support, perform UPDATE/DELETE or stack writes (I:H), as well as DROP/heavy queries causing denial of service (A:H). The impact stays within the database/app security authority, so Scope is Unchanged.
- CWE-89
- OWASP A03:2021 Injection