mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-13 10:43:43 +03:00
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 (&quot; 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>
132 lines
4.5 KiB
TypeScript
132 lines
4.5 KiB
TypeScript
import { randomUUID } from "node:crypto";
|
|
|
|
import { BaseExecutor, type ExecuteInput, type ExecutorExecuteResult } from "./base.ts";
|
|
import { makeExecutorErrorResult as makeErrorResult } from "../utils/error.ts";
|
|
import { initTinyCmsWasm, generateSecurePayload } from "./tinycmsSigner.ts";
|
|
|
|
const CHAT_URL = "https://gov.freegpt.win/api/openai/oneapi/v1/chat/completions";
|
|
const CHALLENGE_URL = "https://gov.freegpt.win/api/challenge";
|
|
|
|
let publicIp: string | null = null;
|
|
let lastIpFetch = 0;
|
|
|
|
async function getPublicIp(): Promise<string> {
|
|
const now = Date.now();
|
|
if (publicIp && now - lastIpFetch < 300000) {
|
|
return publicIp;
|
|
}
|
|
try {
|
|
const res = await fetch("https://api64.ipify.org?format=json");
|
|
const json = (await res.json()) as { ip: string };
|
|
publicIp = json.ip;
|
|
lastIpFetch = now;
|
|
return publicIp;
|
|
} catch {
|
|
return publicIp || "127.0.0.1";
|
|
}
|
|
}
|
|
|
|
async function fetchChallenge(uuid: string): Promise<any> {
|
|
const res = await fetch(CHALLENGE_URL, {
|
|
method: "GET",
|
|
headers: {
|
|
uuid: uuid,
|
|
"x-origin": "https://gov.freegpt.win",
|
|
Accept: "application/json",
|
|
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36",
|
|
},
|
|
});
|
|
if (!res.ok) {
|
|
throw new Error(`Failed to fetch challenge: ${res.status}`);
|
|
}
|
|
return await res.json();
|
|
}
|
|
|
|
export class TinyCmsExecutor extends BaseExecutor {
|
|
constructor() {
|
|
super("tinycms-web", { id: "tinycms-web", baseUrl: CHAT_URL });
|
|
}
|
|
|
|
async execute(input: ExecuteInput): Promise<ExecutorExecuteResult> {
|
|
const { body, credentials, signal } = input;
|
|
const bodyObj = (body || {}) as Record<string, any>;
|
|
|
|
// TinyCMS uses 'uuid' header for identification
|
|
const uuid = String(credentials?.apiKey ?? "").trim();
|
|
if (!uuid || !uuid.startsWith("R")) {
|
|
return makeErrorResult(
|
|
401,
|
|
"TinyCMS: Invalid or missing device UUID (must start with 'R')",
|
|
body,
|
|
CHAT_URL
|
|
);
|
|
}
|
|
|
|
try {
|
|
await initTinyCmsWasm();
|
|
|
|
const ip = await getPublicIp();
|
|
const challengeObj = await fetchChallenge(uuid);
|
|
|
|
const timestamp = Date.now().toString();
|
|
// Security context: this nonce is signed into `x-secure-signature` and
|
|
// reused as the session id, so it must be unpredictable. `node:crypto`
|
|
// randomUUID() is always available on the supported runtime — never fall
|
|
// back to Math.random() (CodeQL js/insecure-randomness).
|
|
const nonceJs = randomUUID();
|
|
|
|
const securePayload = generateSecurePayload(
|
|
uuid,
|
|
timestamp,
|
|
nonceJs,
|
|
challengeObj.challenge,
|
|
ip,
|
|
challengeObj.difficulty
|
|
);
|
|
|
|
const signedHeaders: Record<string, string> = {
|
|
uuid: uuid,
|
|
"x-origin": "https://gov.freegpt.win",
|
|
referer: "https://gov.freegpt.win/",
|
|
"x-secure-challenge-id": challengeObj.challengeId,
|
|
"x-secure-challenge-expires-at": String(challengeObj.expiresAt),
|
|
"x-secure-challenge-version": challengeObj.version,
|
|
"x-secure-signature": securePayload.signature,
|
|
"x-secure-fingerprint": securePayload.fingerprint,
|
|
"x-secure-client-ip": securePayload.client_ip,
|
|
"x-secure-pow-seed-nonce": String(securePayload.pow.seed_nonce),
|
|
"x-secure-pow-nonce": String(securePayload.pow.nonce),
|
|
"x-secure-pow-hash": securePayload.pow.hash,
|
|
"x-secure-pow-difficulty": String(securePayload.pow.difficulty),
|
|
"x-secure-timestamp": timestamp,
|
|
"x-secure-nonce": nonceJs,
|
|
"x-secure-version": securePayload.v,
|
|
"x-session-id": nonceJs,
|
|
// Use configurable userid from providerSpecificData if present, otherwise generate one
|
|
// from the UUID (the server uses it for request attribution, not auth).
|
|
userid: String(credentials?.providerSpecificData?.userid ?? "") || uuid.slice(0, 20),
|
|
Accept: bodyObj.stream ? "text/event-stream" : "application/json",
|
|
"Content-Type": "application/json",
|
|
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36",
|
|
};
|
|
|
|
const fetchOptions: RequestInit = {
|
|
method: "POST",
|
|
headers: signedHeaders,
|
|
body: JSON.stringify(bodyObj),
|
|
signal,
|
|
};
|
|
|
|
const response = await fetch(CHAT_URL, fetchOptions);
|
|
return {
|
|
response,
|
|
url: CHAT_URL,
|
|
headers: Object.fromEntries(response.headers.entries()),
|
|
transformedBody: bodyObj,
|
|
};
|
|
} catch (err: any) {
|
|
return makeErrorResult(500, `TinyCMS Error: ${err.message}`, body, CHAT_URL);
|
|
}
|
|
}
|
|
}
|