Raw sequelize.query() with template-literal interpolation of user input
The Sequelize escape-hatch `sequelize.query()` is invoked with a template literal that interpolates `req.query.q` directly into the SQL string instead of using the `replacements`/`bind` option.
routes/search.ts:22:
models.sequelize.query(`SELECT * FROM Products WHERE ((name LIKE '%${criteria}%' OR description LIKE '%${criteria}%') AND deletedAt IS NULL) ORDER BY name`)
This is the exact js-sql-raw brief pattern: Sequelize raw query + ${...} interpolation of a request-derived value (criteria is req.query.q truncated to 200 chars). Sequelize's query() supports parameterization via the { replacements: { ... } } or { bind: [...] } options, neither of which is used. The 200-char clamp is the only filter and is not a defense — it is a length cap, not an escape.
Fix: switch to sequelize.query('SELECT * FROM Products WHERE name LIKE :q OR description LIKE :q AND deletedAt IS NULL ORDER BY name', { replacements: { q: '%' + criteria + '%' }, type: QueryTypes.SELECT }).
GET /rest/products/search?q=%27))%20UNION%20SELECT%20id,email,password,4,5,6,7,8,9,10%20FROM%20Users-- — interpolation closes the string literal, appends a UNION SELECT, and returns Users rows.
Same as the SQL injection finding — full read of the SQLite database via the unprotected raw-query escape hatch, unauthenticated.
Line 22 directly interpolates criteria (derived from req.query.q, only length-clamped to 200 chars) into a models.sequelize.query() template literal with no replacements/bind parameterization. An attacker can break out of the string literal via ' and append UNION SELECT to exfiltrate other tables (e.g., Users), and the response body (res.json(utils.queryResultToJson(products))) reflects the query result back unauthenticated. This is the canonical OWASP Juice Shop UNION SQLi sink and matches the js-sql-raw brief exactly.
The searchProducts handler is mounted on a public REST route (/rest/products/search) with no auth middleware visible in this file and the req.query.q value is interpolated directly into a sequelize.query() template literal at line 22, so any remote, unauthenticated attacker can inject SQL over HTTP (AV:N, AC:L, PR:N, UI:N). The PoC's UNION SELECT id,email,password,... FROM Users-- returns user credentials in the JSON response, giving total disclosure of the SQLite DB (C:H). Although SQLite's sequelize.query default doesn't run multiple statements, an attacker can still meaningfully alter results returned to the app and (via UPDATE/INSERT in single-statement form or stacked queries depending on driver) cause limited integrity/availability impact, so I:L/A:L is the worst plausible code-visible state. Scope stays Unchanged since the impact is confined to the database the app already owns.