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:
@@ -378,6 +378,23 @@ export function generateApiKey(mode: ApiKeyMode = 'live'): { key: string; hash:
|
||||
return { key, hash, prefix }
|
||||
}
|
||||
|
||||
/**
|
||||
* SHA-256, deliberately, and NOT a slow KDF like bcrypt/argon2.
|
||||
*
|
||||
* CodeQL flags this as js/insufficient-password-hash. That rule exists for
|
||||
* user-chosen passwords, which are low-entropy and brute-forceable, so the
|
||||
* defence is to make each guess expensive. This input is not a password: keys
|
||||
* come from generateApiKey as 32 CSPRNG bytes (`gnubok_sk_<base64url>`), and no
|
||||
* work factor moves the needle on a 256-bit random secret.
|
||||
*
|
||||
* A slow KDF would also be actively worse here: this runs on the hot path of
|
||||
* every MCP request, where the hash is the primary-key lookup used to find the
|
||||
* row, so per-request cost is real latency for zero security gain.
|
||||
*
|
||||
* Do NOT "fix" this by changing the algorithm. The hash IS the stored
|
||||
* credential, so a different function invalidates every live `gnubok_sk_` key,
|
||||
* breaking existing MCP connections with no migration path.
|
||||
*/
|
||||
export function hashApiKey(key: string): string {
|
||||
return crypto.createHash('sha256').update(key).digest('hex')
|
||||
}
|
||||
|
||||
@@ -307,7 +307,27 @@ function formatAmount(amount: number): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape double quotes in SIE strings
|
||||
* Escape double quotes in SIE strings.
|
||||
*
|
||||
* Quotes only. A literal backslash is deliberately NOT doubled, and it must stay
|
||||
* that way. SIE 4B defines the backslash purely as a marker placed before a
|
||||
* quotation mark and defines no `\\` sequence at all: "Quotation marks in export
|
||||
* fields are to be preceded by a backslash (ASCII 92)", and for the checksum,
|
||||
* "Quotation marks within fields are marked with a 'backslash'. However, only
|
||||
* the quotation marks are to be included in the calculation of the control
|
||||
* total" -- the marker is excluded from #KSUMMA, which is only coherent if it is
|
||||
* not itself data.
|
||||
*
|
||||
* Emitting `\\` for a literal backslash would invent a sequence the format does
|
||||
* not define, land as a doubled backslash in every reader that implements the
|
||||
* spec's single rule (Fortnox, Visma, BL), and skew #KSUMMA. That corrupts a
|
||||
* file kept under BFL 7-year retention.
|
||||
*
|
||||
* Round-tripping is already correct: `a\"b` exports as `a\\"b`, and the parser's
|
||||
* /\\"/g rule (lib/import/sie-parser.ts) recovers `a\"b`.
|
||||
*
|
||||
* CodeQL flags this as js/incomplete-sanitization; it is a false positive here,
|
||||
* because the rule assumes a grammar in which backslash escapes itself.
|
||||
*/
|
||||
function escapeQuotes(str: string): string {
|
||||
return str.replace(/"/g, '\\"')
|
||||
|
||||
Reference in New Issue
Block a user