fix(security): resolve the CodeQL backlog, three fixes and three documented false positives (#1225)

Triage of all 9 CodeQL alerts surfaced on main by #1223. None were introduced by that PR.

Fixed: the compliance-review artifact now unpacks to runner.temp instead of over the trusted checkout (actions/artifact-poisoning, critical); MCP LIKE patterns escape backslash first, which was a real correctness bug returning wrong rows for any search containing a backslash (js/incomplete-sanitization, 2 sites); and the mcp-oauth consent form action is HTML-escaped (js/reflected-xss, not exploitable because WHATWG URL already percent-encodes " < >, but & is not in that encode set).

Dismissed as false positives with reasoning recorded at each site and in DECISIONS.md: sie-export escapeQuotes, where doubling backslashes would violate SIE 4B, corrupt files in conformant readers and skew #KSUMMA under BFL 7-year retention; hashApiKey, where SHA-256 is correct for a 256-bit CSPRNG token and changing it would invalidate every live gnubok_sk_ key; and the DuplicateBookingDialog href, which is a DB UUID behind a literal path prefix.

Regression tests cover both behavioural fixes, including the escape ordering.
This commit is contained in:
Jakob Wennberg
2026-07-27 14:02:25 +02:00
committed by GitHub
parent 4702a63cff
commit 7dde8cac82
8 changed files with 161 additions and 7 deletions
+20 -2
View File
@@ -6589,7 +6589,16 @@ export const tools: McpTool[] = [
// searches the ENTRY description only (documented in the
// schema); the two-leg line+entry union query_journal runs is
// overkill for a write filter.
const escaped = text.replace(/[%]/g, '\\%').replace(/_/g, '\\_')
//
// Backslash is escaped FIRST, and the order matters: `\` is LIKE's
// own escape character, so an unescaped one in the search term
// swallows the character after it (searching `a\b` matched rows
// containing `ab`). Escaping it last would instead double the
// backslashes the % / _ rules just added.
const escaped = text
.replace(/\\/g, '\\\\')
.replace(/%/g, '\\%')
.replace(/_/g, '\\_')
e = e.ilike('description', `%${escaped}%`)
}
return e
@@ -7262,7 +7271,16 @@ export const tools: McpTool[] = [
// separator. The .ilike() path passes the pattern as a parameterised
// filter operand where `,` is a literal: stripping would mangle
// searches for real commas in line descriptions.
const escaped = text.replace(/[%]/g, '\\%').replace(/_/g, '\\_')
//
// Backslash is escaped FIRST, and the order matters: `\` is LIKE's own
// escape character, so an unescaped one in the search term swallows the
// character after it (searching `a\b` matched rows containing `ab`).
// Escaping it last would instead double the backslashes the % / _ rules
// just added.
const escaped = text
.replace(/\\/g, '\\\\')
.replace(/%/g, '\\%')
.replace(/_/g, '\\_')
const pattern = `%${escaped}%`
// Fetch up to 2× limit per leg to reduce global-ordering loss when