From db1f435f553ac477efc893cf69e2b170fba5a22b Mon Sep 17 00:00:00 2001 From: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> Date: Thu, 23 Jul 2026 16:56:55 -0300 Subject: [PATCH] fix(security): remove quadratic trailing-slash trim in Alibaba endpoint normalization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CodeQL js/polynomial-redos (alerts #765/#766). `/\/+$/` has no left anchor, so the engine retries the match at every start offset and each attempt re-walks the whole slash run before failing `$` — O(n^2) on a connection baseUrl made of many slashes. Measured on the vulnerable code: 10k slashes = 102ms, 30k = 968ms, 60k = 4028ms (clean quadratic); through the public resolvers the same input took 22s. providerSpecificData.baseUrl is operator-supplied config and reaches the trim via isFamilyPresetUrl() -> normalizeEndpoint() and both resolve*Url() helpers, so the input is reachable. Replaces all three identical occurrences with a linear charCodeAt scan. CodeQL only flagged two of them; the third (resolveAlibabaProviderModelsUrl) carries the same defect and is fixed here rather than left behind. Behavior is unchanged: every trailing slash is still removed (not a bounded subset), interior slashes are preserved, and an all-slash string still collapses to empty — asserted by the new behavior test. --- .../constants/alibabaProviderRegions.ts | 33 ++++++++---- tests/unit/alibaba-provider-regions.test.ts | 54 +++++++++++++++++++ 2 files changed, 77 insertions(+), 10 deletions(-) diff --git a/src/shared/constants/alibabaProviderRegions.ts b/src/shared/constants/alibabaProviderRegions.ts index 613302ad32..cd8b61a464 100644 --- a/src/shared/constants/alibabaProviderRegions.ts +++ b/src/shared/constants/alibabaProviderRegions.ts @@ -52,10 +52,24 @@ function canonicalProviderFamily(providerId: string): AlibabaProviderFamily | nu return null; } +const SLASH_CHAR_CODE = 47; + +/** + * Strip every trailing "/" in linear time. + * + * Deliberately not a regex: `/\/+$/` has no left anchor, so the engine retries at + * every start offset and each attempt re-walks the slash run before failing `$` — + * quadratic on an endpoint made of many slashes (CodeQL js/polynomial-redos). + * Connection base URLs are operator-supplied, so they reach this trim directly. + */ +function stripTrailingSlashes(value: string): string { + let end = value.length; + while (end > 0 && value.charCodeAt(end - 1) === SLASH_CHAR_CODE) end--; + return end === value.length ? value : value.slice(0, end); +} + function normalizeEndpoint(value: string): string { - return value - .trim() - .replace(/\/+$/, "") + return stripTrailingSlashes(value.trim()) .replace(/\/(?:chat\/completions|messages)$/i, "") .toLowerCase(); } @@ -138,10 +152,9 @@ export function resolveAlibabaProviderModelsUrl( providerSpecificData?: unknown, fallback = "" ): string { - const baseUrl = resolveAlibabaProviderBaseUrl(providerId, providerSpecificData, fallback) - .trim() - .replace(/\/+$/, "") - .replace(/\/(?:chat\/completions|messages|models)$/i, ""); + const baseUrl = stripTrailingSlashes( + resolveAlibabaProviderBaseUrl(providerId, providerSpecificData, fallback).trim() + ).replace(/\/(?:chat\/completions|messages|models)$/i, ""); return baseUrl ? `${baseUrl}/models` : ""; } @@ -154,9 +167,9 @@ export function resolveAlibabaProviderMediaBaseUrl( providerSpecificData?: unknown, fallback = "" ): string { - return resolveAlibabaProviderBaseUrl(providerId, providerSpecificData, fallback) - .trim() - .replace(/\/+$/, "") + return stripTrailingSlashes( + resolveAlibabaProviderBaseUrl(providerId, providerSpecificData, fallback).trim() + ) .replace(/\/compatible-mode\/v1(?:\/(?:chat\/completions|models))?$/i, "/api/v1") .replace(/\/apps\/anthropic(?:\/v1)?(?:\/messages)?$/i, "/api/v1"); } diff --git a/tests/unit/alibaba-provider-regions.test.ts b/tests/unit/alibaba-provider-regions.test.ts index 7764124324..a9e8a65094 100644 --- a/tests/unit/alibaba-provider-regions.test.ts +++ b/tests/unit/alibaba-provider-regions.test.ts @@ -12,6 +12,7 @@ import { isAlibabaRegionalProvider, normalizeAlibabaProviderRegion, resolveAlibabaProviderBaseUrl, + resolveAlibabaProviderMediaBaseUrl, resolveAlibabaProviderModelsUrl, } from "../../src/shared/constants/alibabaProviderRegions.ts"; import { APIKEY_PROVIDERS } from "../../src/shared/constants/providers.ts"; @@ -302,3 +303,56 @@ test("dashboard folds legacy China connections into the unified Alibaba card", ( assert.equal(isAlibabaRegionalProvider(providerId), true); } }); + +/** + * Regression guard for CodeQL `js/polynomial-redos` (alerts #765/#766). + * + * The trailing-slash trim used to be `/\/+$/`. That regex has no left anchor, so + * the engine retries the match at every start offset and each attempt walks the + * whole run of slashes before failing `$` — O(n^2) on a connection baseUrl made + * of many slashes. Measured on the vulnerable version: 10k slashes = 102ms, + * 30k = 968ms, 60k = 4028ms (clean quadratic). The linear strip runs in <1ms. + * + * `providerSpecificData.baseUrl` is operator-supplied config, so this input + * reaches the trim through isFamilyPresetUrl() -> normalizeEndpoint() and + * through both resolve*Url() helpers. + */ +test("trailing-slash normalization is linear on pathological slash runs (ReDoS guard)", () => { + const pathological = { baseUrl: `${"/".repeat(60_000)}x` }; + const budgetMs = 500; + + const started = performance.now(); + resolveAlibabaProviderModelsUrl("alibaba", pathological); + resolveAlibabaProviderMediaBaseUrl("alibaba", pathological); + resolveAlibabaProviderBaseUrl("alibaba", pathological); + const elapsed = performance.now() - started; + + assert.ok( + elapsed < budgetMs, + `trailing-slash trim must stay linear; took ${elapsed.toFixed(0)}ms (budget ${budgetMs}ms). ` + + "A quadratic trim (/\\/+$/) takes seconds on this input." + ); +}); + +test("trailing-slash normalization keeps its exact behavior", () => { + // Every trailing slash is removed (not a bounded subset), and only trailing ones. + const withSlashes = { baseUrl: "https://example.test/v1////" }; + assert.equal( + resolveAlibabaProviderModelsUrl("alibaba", withSlashes), + "https://example.test/v1/models" + ); + + // Interior slashes are preserved. + const interior = { baseUrl: "https://example.test//deep//path/" }; + assert.equal( + resolveAlibabaProviderBaseUrl("alibaba", interior), + "https://example.test//deep//path/" + ); + assert.equal( + resolveAlibabaProviderModelsUrl("alibaba", interior), + "https://example.test//deep//path/models" + ); + + // A string that is nothing but slashes collapses to empty. + assert.equal(resolveAlibabaProviderModelsUrl("alibaba", { baseUrl: "////" }), ""); +});