Files
OmniRoute/tests/unit/bug-8029-promptql-issuer-substring.test.ts
Diego Rodrigues de Sa e Souza 898e2bfcaa fix(sse): replace spoofable .includes() PromptQL issuer check with hostname comparison (#8029) (#8042)
isDdnProjectPromptQlToken() (jwt.ts) and isLikelyDdnToken() (usage/promptql.ts) used
`iss.includes("auth.pro.hasura.io")`, which a spoofed issuer like
"https://auth.pro.hasura.io.evil.com/ddn/token" also satisfies
(js/incomplete-url-substring-sanitization, 2 open CodeQL high alerts).

Adds a shared issuerHostIsTrusted() helper in jwt.ts that parses `iss` with `new URL()`
and compares the hostname (exact match or trusted subdomain), and points both call
sites at it, de-duplicating the previously copy-pasted predicate.
2026-07-21 21:42:36 -03:00

106 lines
4.6 KiB
TypeScript

// Regression test for #8029 — PromptQL issuer checks used a bare `String.includes()`
// against the JWT `iss` claim, which is an incomplete-url-substring-sanitization bug
// (CodeQL js/incomplete-url-substring-sanitization). A spoofed issuer such as
// `https://auth.pro.hasura.io.evil.com/ddn/token` satisfied `.includes("auth.pro.hasura.io")`
// even though its actual host is `auth.pro.hasura.io.evil.com`, not `auth.pro.hasura.io`.
//
// Two call sites shared the same flawed predicate (copy-pasted):
// - open-sse/services/promptql/jwt.ts::isDdnProjectPromptQlToken()
// - open-sse/services/usage/promptql.ts::isLikelyDdnToken()
//
// The fix replaces both with a shared `issuerHostIsTrusted()` helper that parses the
// issuer with `new URL()` and compares the hostname (exact match or a `.`-delimited
// subdomain of a trusted host), never a raw substring check.
import { describe, it } from "node:test";
import assert from "node:assert/strict";
const jwtMod = await import("../../open-sse/services/promptql/jwt.ts");
const usageMod = await import("../../open-sse/services/usage/promptql.ts");
function makeFakeJwt(claims: Record<string, unknown>): string {
const header = Buffer.from(JSON.stringify({ alg: "none", typ: "JWT" })).toString("base64url");
const payload = Buffer.from(JSON.stringify(claims)).toString("base64url");
return `${header}.${payload}.sig`;
}
const NON_UUID_AUD = "not-a-project-uuid";
const spoofedDdnJwt = makeFakeJwt({
iss: "https://auth.pro.hasura.io.evil.com/ddn/token",
aud: NON_UUID_AUD,
exp: Math.floor(Date.now() / 1000) + 3600,
});
const spoofedQlAppJwt = makeFakeJwt({
iss: "https://auth.pro.ql.app.evil.com/ddn/token",
aud: NON_UUID_AUD,
exp: Math.floor(Date.now() / 1000) + 3600,
});
const legitimateDdnJwt = makeFakeJwt({
iss: "https://auth.pro.hasura.io/ddn/token",
aud: NON_UUID_AUD,
exp: Math.floor(Date.now() / 1000) + 3600,
});
const legitimateSubdomainJwt = makeFakeJwt({
iss: "https://eu.auth.pro.hasura.io/ddn/token",
aud: NON_UUID_AUD,
exp: Math.floor(Date.now() / 1000) + 3600,
});
const garbageIssuerJwt = makeFakeJwt({
iss: "not a url at all",
aud: NON_UUID_AUD,
exp: Math.floor(Date.now() / 1000) + 3600,
});
describe("BUG #8029 — PromptQL issuer host check (js/incomplete-url-substring-sanitization)", () => {
it("jwt.ts::isDdnProjectPromptQlToken rejects a spoofed host that merely CONTAINS the trusted substring", () => {
assert.equal(jwtMod.isDdnProjectPromptQlToken(spoofedDdnJwt), false);
assert.equal(jwtMod.isDdnProjectPromptQlToken(spoofedQlAppJwt), false);
});
it("jwt.ts::isDdnProjectPromptQlToken still accepts the real trusted host and its subdomains", () => {
assert.equal(jwtMod.isDdnProjectPromptQlToken(legitimateDdnJwt), true);
assert.equal(jwtMod.isDdnProjectPromptQlToken(legitimateSubdomainJwt), true);
});
it("jwt.ts::isDdnProjectPromptQlToken rejects a non-URL issuer instead of throwing", () => {
assert.equal(jwtMod.isDdnProjectPromptQlToken(garbageIssuerJwt), false);
});
it("jwt.ts::issuerHostIsTrusted is exported and shared", () => {
assert.equal(typeof jwtMod.issuerHostIsTrusted, "function");
assert.equal(jwtMod.issuerHostIsTrusted("https://auth.pro.hasura.io.evil.com/x"), false);
assert.equal(jwtMod.issuerHostIsTrusted("https://auth.pro.hasura.io/x"), true);
assert.equal(jwtMod.issuerHostIsTrusted("https://eu.auth.pro.hasura.io/x"), true);
assert.equal(jwtMod.issuerHostIsTrusted("https://auth.pro.ql.app/x"), true);
assert.equal(jwtMod.issuerHostIsTrusted("garbage"), false);
});
it("usage/promptql.ts collectCreditsTokens sorts a spoofed-issuer token as non-DDN (via getPromptQlUsage token ordering)", async () => {
// isLikelyDdnToken is not exported directly; exercise it indirectly through
// getPromptQlUsage's "onlyEnrich" fallback message, which flips to the DDN-required
// message only when at least one token is classified as DDN-shaped.
const originalFetch = globalThis.fetch;
globalThis.fetch = (async () =>
new Response(JSON.stringify({ errors: [{ message: "access-denied" }] }), {
status: 200,
})) as typeof fetch;
try {
const result = await usageMod.getPromptQlUsage(spoofedDdnJwt, {
projectId: "01a0fe61-baf4-4e31-9311-8cc0bb3eba91",
});
// A spoofed-host token must NOT be treated as DDN-shaped, so the fallback
// "onlyEnrich" branch (which requires isLikelyDdnToken to be false for every
// token) is taken and the DDN-specific instructional message is returned.
assert.ok("message" in result);
assert.match(String(result.message), /DDN\/project JWT/);
} finally {
globalThis.fetch = originalFetch;
}
});
});