mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
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:
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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: "////" }), "");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user