fix(security): remove quadratic trailing-slash trim in Alibaba endpoint normalization

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.
This commit is contained in:
diegosouzapw
2026-07-23 16:56:55 -03:00
parent c525a0f452
commit db1f435f55
2 changed files with 77 additions and 10 deletions

View File

@@ -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");
}