Cross-filed from a Stacks-side security audit (stacksjs/stacks#1858) — the bun-query-builder runtime has several SQL-injection vectors when identifiers or operators come from user input. The Stacks framework has shipped TypeScript surface tightening + identifier/operator validators as defense-in-depth (stacksjs/stacks@b2b587d), but the underlying construction paths need to be fixed here.
Reachability: any consumer that passes a column name or operator from req.body / req.query to the methods below produces user-controlled SQL.
Findings
| ID | File:line (dist) | Issue |
|---|---|---|
| Q-1 | dist/src/index.js:7507, 7528, 7556 | whereHas / has / whereDoesntHave quote string values with naked '…' and interpolate operator raw. |
| Q-2 | dist/src/index.js:19494, 19470, 19550, 19567 | ModelQueryBuilder.increment / decrement / pluck / aggregate / count interpolate column + _definition.table raw. |
| Q-3 | dist/src/index.js:7625, 8377, 8707, 8738, 8743 | whereRaw / selectRaw / groupByRaw / havingRaw / orderByRaw accept any string. |
| Q-4 | dist/src/index.js:8383, 8389 | whereColumn / orWhereColumn interpolate left, right, and operator raw. |
| Q-5 | dist/src/index.js:8243-8256 | whereJsonPath interpolates path and op raw on every dialect. |
| Q-6 | dist/src/index.js:8154, 8186, 8454, 8514 | where(string, op, value) parameterises the value but interpolates column + operator raw. Object form where(req.body) exposes column injection. |
| Q-7 | dist/src/index.js:9496 | SQLite identifier quoting is a no-op ((id) => id). SQLite is the typical default driver. |
| Q-8 | dist/src/index.js:7306-7308 | applyCondition is / is not cases interpolate val directly. |
| Q-9 | dist/src/index.js:8210-8220 | whereNull / whereNotNull interpolate column raw. |
| Q-10 | dist/src/index.js:8573-8606 | orderBy / orderByDesc / latest / oldest / reorder / inRandomOrder interpolate column raw. |
Recommended fixes
- Identifier paths: route through
validateIdentifier(already defined internally) or accept only model-attribute keys; reject anything else. - Operator paths: constrain at runtime to the documented
WhereOperatorunion. *Rawmethods: change the public TS signatures to accept only aRawBuilder<T>tagged-template fragment, never a barestring. Stacks shipped this on its ORM-side facade in stacksjs/stacks@b2b587d—whereRaw: (raw: RawBuilder<unknown>) => M.- SQLite quoting: emit
"-quoted identifiers with internal-quote doubling (id.replace(/"/g, '""')) — same shape as the Postgres branch.
INVESTIGATE
- The bundled runtime uses
sql([subquerySQL])(e.g.dist/src/index.js:7994, 8029, 8062, 8095) to re-flag previously-built raw strings as a Bun.SQL fragment. Confirm Bun.SQL's behaviour on array contents — if it's tightened to escape values in the array form, everywhereHas/has/doesntHavepath breaks.
Filed upstream so the dist code gets the real fix.