fix(security): resolve open CodeQL alerts (#10188)

Alert 806 (js/insecure-randomness, open-sse/executors/tinycms.ts): the
TinyCMS nonce is signed into x-secure-signature and reused as
x-secure-nonce / x-session-id, so the Math.random() fallback made a
signed request predictable and replayable. Use randomUUID() from
node:crypto unconditionally.

Alert 811 (js/double-escaping, chatgpt-web adapters/environment.ts):
decodeXmlText() decoded & before " / ', so the bare & it
produced was re-consumed and the text was unescaped twice
(" collapsed to "). These values become the trusted Codex
sandbox cwd / workspace_roots, so the double-unescape silently rewrote
the workspace boundary. Decode & last.

Alerts 813/814 (js/incomplete-url-substring-sanitization, test files):
replace the includes() URL checks with exact comparisons
(new URL(url).hostname === ... and an explicit === over the recorded
URL array). Both assertions get strictly tighter.

Regression guards: tests/unit/tinycms-secure-nonce-randomness.test.ts
and tests/unit/chatgpt-web-environment-double-unescape.test.ts, both
failing before the fix and passing after.

Co-authored-by: backryun <bakryun0718@proton.me>
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-08-12 15:58:58 -03:00
committed by GitHub
parent 423b616503
commit c19da4db46
6 changed files with 264 additions and 55 deletions

View File

@@ -17,10 +17,7 @@ import assert from "node:assert/strict";
import { WEB_COOKIE_PROVIDERS } from "../../src/shared/constants/providers/web-cookie.ts";
import { REGISTRY } from "../../open-sse/config/providers/index.ts";
import { getExecutor, TinyCmsExecutor } from "../../open-sse/executors/index.ts";
import {
setupDomMocks,
type DomMockRestore,
} from "../../open-sse/executors/tinycmsSigner.ts";
import { setupDomMocks, type DomMockRestore } from "../../open-sse/executors/tinycmsSigner.ts";
// tinycmsSigner.ts intentionally does NOT install its window/document/canvas
// shims as a module-load side effect (see setupDomMocks() there) — doing so
@@ -41,9 +38,10 @@ after(() => {
// ── Catalog / WEB_COOKIE_PROVIDERS ────────────────────────────────────────────
test("tinycms-web is present in WEB_COOKIE_PROVIDERS", () => {
const p = (WEB_COOKIE_PROVIDERS as Record<string, unknown>)[
"tinycms-web"
] as Record<string, unknown>;
const p = (WEB_COOKIE_PROVIDERS as Record<string, unknown>)["tinycms-web"] as Record<
string,
unknown
>;
assert.ok(p, "WEB_COOKIE_PROVIDERS['tinycms-web'] must exist");
assert.equal(p.id, "tinycms-web");
assert.equal(p.alias, "tcw");
@@ -51,9 +49,10 @@ test("tinycms-web is present in WEB_COOKIE_PROVIDERS", () => {
});
test("tinycms-web WEB_COOKIE_PROVIDERS entry is marked as free-tier", () => {
const p = (WEB_COOKIE_PROVIDERS as Record<string, unknown>)[
"tinycms-web"
] as Record<string, unknown>;
const p = (WEB_COOKIE_PROVIDERS as Record<string, unknown>)["tinycms-web"] as Record<
string,
unknown
>;
assert.equal(p.hasFree, true);
assert.ok(typeof p.freeNote === "string" && (p.freeNote as string).length > 0);
assert.ok(typeof p.authHint === "string" && (p.authHint as string).length > 0);
@@ -79,10 +78,7 @@ test("tinycms-web registry has all expected models", () => {
assert.ok(ids.includes("gpt-5-free"), "gpt-5-free must be registered");
assert.ok(ids.includes("gpt-5.3-free"), "gpt-5.3-free must be registered");
assert.ok(
ids.includes("gpt-5.3-thinking-free"),
"gpt-5.3-thinking-free must be registered"
);
assert.ok(ids.includes("gpt-5.3-thinking-free"), "gpt-5.3-thinking-free must be registered");
assert.ok(ids.includes("deepseek-v4-flash"), "deepseek-v4-flash must be registered");
assert.ok(ids.includes("claude-sonnet-5"), "claude-sonnet-5 must be registered");
assert.ok(ids.includes("gemini-3.5-flash"), "gemini-3.5-flash must be registered");
@@ -140,10 +136,7 @@ test("TinyCmsExecutor returns 401 when UUID is missing", async () => {
assert.equal(result.response.status, 401);
const body = await result.response.json();
const errMsg = body?.error?.message || "";
assert.ok(
errMsg.includes("Invalid or missing device UUID"),
"error must mention missing UUID"
);
assert.ok(errMsg.includes("Invalid or missing device UUID"), "error must mention missing UUID");
// Hard Rule #12: must NOT leak stack traces
assert.ok(!errMsg.includes("at /"), "error must not contain a stack trace path");
});
@@ -161,10 +154,7 @@ test("TinyCmsExecutor returns 401 when UUID does not start with 'R'", async () =
assert.equal(result.response.status, 401);
const body = await result.response.json();
const errMsg = body?.error?.message || "";
assert.ok(
errMsg.includes("Invalid or missing device UUID"),
"error must mention missing UUID"
);
assert.ok(errMsg.includes("Invalid or missing device UUID"), "error must mention missing UUID");
assert.ok(!errMsg.includes("at /"), "error must not contain a stack trace path");
});
@@ -172,7 +162,7 @@ test("TinyCmsExecutor returns the standard executor response envelope on success
const originalFetch = globalThis.fetch;
globalThis.fetch = async (input) => {
const url = String(input);
if (url.includes("api64.ipify.org")) {
if (new URL(url).hostname === "api64.ipify.org") {
return new Response(JSON.stringify({ ip: "127.0.0.1" }), {
headers: { "Content-Type": "application/json" },
});
@@ -217,10 +207,7 @@ test("TinyCmsExecutor returns the standard executor response envelope on success
test("initTinyCmsWasm module exports expected functions", async () => {
const signer = await import("../../open-sse/executors/tinycmsSigner.ts");
assert.ok(
typeof signer.initTinyCmsWasm === "function",
"must export initTinyCmsWasm function"
);
assert.ok(typeof signer.initTinyCmsWasm === "function", "must export initTinyCmsWasm function");
assert.ok(
typeof signer.generateSecurePayload === "function",
"must export generateSecurePayload function"
@@ -254,10 +241,7 @@ test("TinyCmsExecutor sanitizes errors (no stack traces in error response)", asy
assert.ok(result.response, "response must be present");
const body = await result.response.json();
const errMsg = body?.error?.message || "";
assert.ok(
errMsg.includes("Invalid or missing device UUID"),
"error must mention missing UUID"
);
assert.ok(errMsg.includes("Invalid or missing device UUID"), "error must mention missing UUID");
assert.ok(!errMsg.includes("at /"), "error must not contain a stack trace path (Hard Rule #12)");
});
@@ -274,12 +258,6 @@ test("tinycms-web credential requirement is kind: token with app-config-uuid", a
assert.equal(req.credentialName, "app-config-uuid");
assert.equal(req.acceptsFullCookieHeader, false);
assert.ok(Array.isArray(req.storageKeys), "must have storageKeys array");
assert.ok(
(req.storageKeys as string[]).includes("apiKey"),
"apiKey must be in storageKeys"
);
assert.ok(
(req.storageKeys as string[]).includes("uuid"),
"uuid must be in storageKeys"
);
assert.ok((req.storageKeys as string[]).includes("apiKey"), "apiKey must be in storageKeys");
assert.ok((req.storageKeys as string[]).includes("uuid"), "uuid must be in storageKeys");
});