Files
OmniRoute/tests/unit/copilot-web-executor.test.ts
diegosouzapw 6fcc99ba26 fix(security): switch sessionPoolKey to HMAC to clear CodeQL #246
CodeQL re-flagged the SHA-256 inside sessionPoolKey at high severity even
after the rename/dedup in #2391 — its data-flow analysis still tracks the
OAuth bearer (accessToken → token) into createHash and applies the
js/insufficient-password-hash rule. Bcrypt/scrypt/argon2 would be wrong
here (the input is a high-entropy bearer, not a low-entropy human password
that needs brute-force protection).

Switch to HMAC-SHA-256 with a process-scope key generated at startup
(randomBytes(32)). HMAC is a MAC primitive, not a password hash, so the
CodeQL rule no longer applies; uniqueness, determinism-within-process,
and the 16-char hex shape all still hold, and as a small bonus an
off-process attacker can no longer precompute pool keys from a token
alone.

Tests
- Replace the "exact SHA-256 prefix" assertion with shape/uniqueness
  checks and a regression guard that fails if the implementation ever
  reverts to plain createHash.
- All 15 copilot-web tests pass.
2026-05-18 23:53:16 -03:00

104 lines
4.3 KiB
TypeScript

import test from "node:test";
import assert from "node:assert/strict";
const { getCopilotMode, extractAccessToken, sessionPoolKey, solveHashcash } =
await import("../../open-sse/executors/copilot-web.ts");
test("getCopilotMode maps known models to their Copilot modes", () => {
assert.equal(getCopilotMode("copilot"), "chat");
assert.equal(getCopilotMode("gpt-4o"), "chat");
assert.equal(getCopilotMode("copilot-think"), "reasoning");
assert.equal(getCopilotMode("o1"), "reasoning");
assert.equal(getCopilotMode("copilot-smart"), "smart");
assert.equal(getCopilotMode("gpt-5"), "smart");
});
test("getCopilotMode defaults to chat for unknown or missing models", () => {
assert.equal(getCopilotMode("unknown-model"), "chat");
assert.equal(getCopilotMode(undefined), "chat");
assert.equal(getCopilotMode(""), "chat");
});
test("getCopilotMode is case-insensitive", () => {
assert.equal(getCopilotMode("GPT-4O"), "chat");
assert.equal(getCopilotMode("Copilot-Think"), "reasoning");
});
test("extractAccessToken returns direct JWT tokens", () => {
const jwt = "eyJhbGciOiJSUzI1NiJ9." + "x".repeat(200);
assert.equal(extractAccessToken(jwt), jwt);
});
test("extractAccessToken extracts token from cookie string", () => {
const token = "abc123token";
assert.equal(extractAccessToken(`session=xyz; access_token=${token}; other=1`), token);
});
test("extractAccessToken extracts Bearer token from Authorization header", () => {
const token = "my-bearer-token";
assert.equal(extractAccessToken(`Bearer ${token}`), token);
});
test("extractAccessToken returns null for empty input", () => {
assert.equal(extractAccessToken(""), null);
});
test("sessionPoolKey produces unique keys per token preventing session sharing", () => {
const key1 = sessionPoolKey("token-user-alice");
const key2 = sessionPoolKey("token-user-bob");
assert.notEqual(key1, key2);
});
test("sessionPoolKey is deterministic for same token", () => {
const token = "stable-access-token";
assert.equal(sessionPoolKey(token), sessionPoolKey(token));
});
test("sessionPoolKey for undefined returns 'anonymous'", () => {
assert.equal(sessionPoolKey(undefined), "anonymous");
assert.equal(sessionPoolKey(), "anonymous");
});
test("sessionPoolKey never returns 'default' (security regression guard)", () => {
assert.notEqual(sessionPoolKey("any-token"), "default");
assert.notEqual(sessionPoolKey(undefined), "default");
});
test("sessionPoolKey is a 16-char lowercase hex string for any non-empty token", () => {
// HMAC-SHA-256 with a process-scope key, truncated to 16 hex chars (64 bits).
// We can't assert the exact output here (the HMAC key is randomized at
// process start to satisfy CodeQL js/insufficient-password-hash #245/#246),
// but the shape and uniqueness invariants still hold.
assert.match(sessionPoolKey("test-token"), /^[0-9a-f]{16}$/);
assert.match(sessionPoolKey("a"), /^[0-9a-f]{16}$/);
assert.match(sessionPoolKey("x".repeat(1024)), /^[0-9a-f]{16}$/);
});
test("sessionPoolKey output differs from the plain SHA-256 prefix of the token", () => {
// Regression guard for the HMAC migration: if someone ever reverts to
// `createHash("sha256")` the alert resurfaces, and this test catches it
// before CodeQL does.
const token = "regression-guard-token";
const plainSha256Prefix =
"5dd8c5e63dbfd4ccb09362efce82bcc3f5d2bb37f8f1cce03f47d7e57b1b1ec3".slice(0, 16);
assert.notEqual(sessionPoolKey(token), plainSha256Prefix);
});
// solveHashcash difficulty bounds — CodeQL js/resource-exhaustion #244 guard.
test("solveHashcash rejects out-of-range difficulty to avoid resource exhaustion", () => {
// Negative, zero, fractional, NaN, Infinity, and >8 must short-circuit.
assert.equal(solveHashcash("param", 0), null);
assert.equal(solveHashcash("param", -1), null);
assert.equal(solveHashcash("param", 1.5), null);
assert.equal(solveHashcash("param", Number.NaN), null);
assert.equal(solveHashcash("param", Number.POSITIVE_INFINITY), null);
assert.equal(solveHashcash("param", 9), null);
assert.equal(solveHashcash("param", 1_000_000), null);
});
test("solveHashcash succeeds for difficulty=1 (a single leading zero is common)", () => {
// ~1 in 16 chance of leading "0" — well within the 10M iteration budget.
const result = solveHashcash("any-parameter", 1);
assert.ok(typeof result === "number" && result >= 0, "expected a numeric nonce");
});