fix(security): resolve CodeQL ReDoS + URL sanitization alerts

- Replace replace(/\/+$/, "") with explicit while-endsWith loop to avoid
  polynomial backtracking on inputs with repeated trailing slashes
  (CodeQL js/polynomial-redos #233-240, 8 alerts):
  - @omniroute/opencode-provider/src/index.ts (normalizeBaseURL)
  - bin/cli/api.mjs (stripTrailingSlash)
  - src/lib/cli-helper/config-generator/{claude,cline,codex,continue,
    kilocode,opencode}.ts (6 generators with identical pattern)

- tests/live/deepseek-web-live.test.ts: assert hostname via URL parsing
  instead of String.includes() so the check is exact-match rather than
  substring (CodeQL js/incomplete-url-substring-sanitization #241).

Alert #242 (Array.prototype.includes against fixed needle constant
OPENWEBUI_PARAGRAPH_ANCHORS) dismissed as CodeQL false-positive — not a
URL sanitization callsite.
This commit is contained in:
diegosouzapw
2026-05-17 07:43:32 -03:00
parent ca8f492240
commit a45d9190db
9 changed files with 30 additions and 9 deletions

View File

@@ -107,7 +107,10 @@ export function normalizeBaseURL(rawBaseURL: string): string {
`@omniroute/opencode-provider: baseURL is not a valid URL: ${JSON.stringify(rawBaseURL)}` `@omniroute/opencode-provider: baseURL is not a valid URL: ${JSON.stringify(rawBaseURL)}`
); );
} }
return trimmed.replace(/\/+$/, "").replace(/\/v1$/, "") + "/v1"; let base = trimmed;
while (base.endsWith("/")) base = base.slice(0, -1);
if (base.endsWith("/v1")) base = base.slice(0, -3);
return base + "/v1";
} }
/** /**

View File

@@ -45,7 +45,9 @@ export function getBaseUrl(opts = {}) {
} }
function stripTrailingSlash(value) { function stripTrailingSlash(value) {
return String(value).replace(/\/+$/, ""); let s = String(value);
while (s.endsWith("/")) s = s.slice(0, -1);
return s;
} }
function resolveUrl(path, opts) { function resolveUrl(path, opts) {

View File

@@ -8,7 +8,9 @@ export function generateClaudeConfig(options: {
apiKey: string; apiKey: string;
model?: string; model?: string;
}): string { }): string {
const base = options.baseUrl.replace(/\/+$/, "").replace(/\/v1$/, ""); let base = options.baseUrl;
while (base.endsWith("/")) base = base.slice(0, -1);
if (base.endsWith("/v1")) base = base.slice(0, -3);
const model = options.model || "claude-3-5-sonnet-20241022"; const model = options.model || "claude-3-5-sonnet-20241022";
const config = { const config = {

View File

@@ -8,7 +8,9 @@ export function generateClineConfig(options: {
apiKey: string; apiKey: string;
model?: string; model?: string;
}): string { }): string {
const base = options.baseUrl.replace(/\/+$/, "").replace(/\/v1$/, ""); let base = options.baseUrl;
while (base.endsWith("/")) base = base.slice(0, -1);
if (base.endsWith("/v1")) base = base.slice(0, -3);
const config = { const config = {
openAiBaseUrl: `${base}/v1`, openAiBaseUrl: `${base}/v1`,

View File

@@ -17,7 +17,9 @@ export async function generateCodexConfig(options: {
model?: string; model?: string;
}): Promise<string> { }): Promise<string> {
const y = await loadYaml(); const y = await loadYaml();
const base = options.baseUrl.replace(/\/+$/, "").replace(/\/v1$/, ""); let base = options.baseUrl;
while (base.endsWith("/")) base = base.slice(0, -1);
if (base.endsWith("/v1")) base = base.slice(0, -3);
const config = { const config = {
openai: { openai: {

View File

@@ -17,7 +17,9 @@ export async function generateContinueConfig(options: {
model?: string; model?: string;
}): Promise<string> { }): Promise<string> {
const y = await loadYaml(); const y = await loadYaml();
const base = options.baseUrl.replace(/\/+$/, "").replace(/\/v1$/, ""); let base = options.baseUrl;
while (base.endsWith("/")) base = base.slice(0, -1);
if (base.endsWith("/v1")) base = base.slice(0, -3);
const config = { const config = {
models: [ models: [

View File

@@ -8,7 +8,9 @@ export function generateKilocodeConfig(options: {
apiKey: string; apiKey: string;
model?: string; model?: string;
}): string { }): string {
const base = options.baseUrl.replace(/\/+$/, "").replace(/\/v1$/, ""); let base = options.baseUrl;
while (base.endsWith("/")) base = base.slice(0, -1);
if (base.endsWith("/v1")) base = base.slice(0, -3);
const config = { const config = {
apiKey: options.apiKey, apiKey: options.apiKey,

View File

@@ -8,7 +8,9 @@ export function generateOpencodeConfig(options: {
apiKey: string; apiKey: string;
model?: string; model?: string;
}): string { }): string {
const base = options.baseUrl.replace(/\/+$/, "").replace(/\/v1$/, ""); let base = options.baseUrl;
while (base.endsWith("/")) base = base.slice(0, -1);
if (base.endsWith("/v1")) base = base.slice(0, -3);
const config = { const config = {
provider: "omniroute", provider: "omniroute",

View File

@@ -25,7 +25,11 @@ if (!process.env.DEEPSEEK_WEB_SESSION_COOKIE) {
}); });
assert.ok(result.response, "Should return a response"); assert.ok(result.response, "Should return a response");
assert.ok(result.url.includes("chat.deepseek.com"), "Should target chat.deepseek.com"); assert.equal(
new URL(result.url).hostname,
"chat.deepseek.com",
"Should target chat.deepseek.com"
);
if (result.response.ok) { if (result.response.ok) {
const ct = result.response.headers.get("content-type") || ""; const ct = result.response.headers.get("content-type") || "";