docs(security): put the SQL injection invariant where it is enforced (#202)
#180 cleared the three `typescript:S2077` hotspots and marked them Reviewed/Safe on the dashboard, but the repository half never reached main — the branch carrying it was deleted before merge, so the markers are cleared, the issue is closed, and nothing in the code said why. That is the exact state #180 set out to avoid: "the justification has to live in the repository, not only in SonarQube's UI". The three call-site comments are restored, with two corrections a review of the original found. They were in the wrong file. `buildItemFilterSql` is where the rule actually lives: both callers splice its clauses straight into query text, so only a placeholder index may ever be interpolated into one and every value must go onto `params`. That function's header said nothing about it, and it is where a seventh clause would be added. This matters more than ordinary comment placement because of how a cleared hotspot behaves. Reviewed/Safe stays marked and does not re-raise when a *different* file changes, so the one edit that would break this — interpolating a filter value in `itemFilters.ts` — was the one edit that would have got neither a warning nor a fresh marker. `items.ts` gets the same note. It builds `${PUBLIC_ITEM_SELECT} WHERE ${where}` from the identical construct and is reachable without signing in, but SonarQube never flagged it, so the higher-exposure copy was the undocumented one. It also records why joining with AND cannot weaken `EXCLUDE_PENDING`: no fragment carries a top-level OR for the join to re-associate against. The wording was slightly false. "The single interpolation is `$${next}`" — the tags clause also interpolates `$${next + 1}`. Same category, so the argument is untouched, but a reader checking it literally finds a counter-example immediately, and a comment asserting safety cannot afford that. Two tests make the invariant fail a build rather than depend on being read. One feeds values built by hand rather than parsed — `"1); DROP TABLE items; --"` in every field — and asserts none of it reaches the clause text, which states directly that these literals are safe with no parser at all. The other asserts two disjoint filter sets produce byte-identical SQL, which catches a value that happens not to look hostile. Both were mutation-tested rather than assumed: interpolating `filters.minPriceCents` into the price clause — the precise edit the comment forbids — fails both, and one pre-existing test besides. Reverted, and the diff against main for `itemFilters.ts` is comment-only. Verified: backend build clean, 280 unit tests pass. Closes #202 Refs #180 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -73,6 +73,14 @@ router.get('/', asyncRoute(async (req: Request, res: Response) => {
|
||||
};
|
||||
|
||||
const { clauses, params } = buildItemFilterSql(effectiveFilters, 1, req.customerId ?? null);
|
||||
// The same construct SonarQube flagged as S2077 in admin.ts and which is
|
||||
// marked Reviewed/Safe there (#180) — and this is the copy reachable without
|
||||
// signing in, so it is worth saying here too rather than relying on the
|
||||
// reader having seen the other one. It holds for the same reason: the clauses
|
||||
// are literals from buildItemFilterSql carrying only placeholder indices, and
|
||||
// EXCLUDE_PENDING is a module constant. Joining with AND cannot weaken
|
||||
// EXCLUDE_PENDING either, because no fragment contains a top-level OR for the
|
||||
// join to re-associate against.
|
||||
const where = [EXCLUDE_PENDING, ...clauses].join(' AND ');
|
||||
const { rows } = await pool.query<PublicItemRow>(
|
||||
`${PUBLIC_ITEM_SELECT} WHERE ${where} ORDER BY i.created_at DESC`,
|
||||
|
||||
Reference in New Issue
Block a user