mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +03:00
fix(security): resolve 5 CodeQL alerts, document FPs, harden deploy skills
Real fixes (alerts auto-close on next scan): - agentSkills/generator: escape backslash before double-quote in YAML frontmatter so a trailing backslash can't escape the closing quote (js/incomplete-sanitization #297/#298) - usage: replace /\s*\(RESTRICTED\)\s*$/ with a non-backtracking literal + trim (js/polynomial-redos #275) - i18n/request: skip __proto__/constructor/prototype in deepMergeFallback (js/prototype-pollution-utility #274) - scripts/ad-hoc/nvidia diag: log key presence only, never key chars (js/clear-text-logging #273) - tests: regression coverage for all three production fixes (YAML round-trip, proto-pollution guard, RESTRICTED strip + ReDoS timing) Docs: - ARCHITECTURE.md: executor count 45->55, OAuth modules 15->16, agy.ts in list Deploy skills (deploy-vps-*-cc/-cx/-ag): add --legacy-peer-deps (npm v11 peer-dep resolver crashes on the omniroute tree) and replace the "; pm2 start" that masked a failed install with a proper && chain.
This commit is contained in:
@@ -30,7 +30,7 @@ scp omniroute-*.tgz root@69.164.221.35:/tmp/
|
||||
```
|
||||
|
||||
```bash
|
||||
ssh root@69.164.221.35 "npm install -g /tmp/omniroute-*.tgz --ignore-scripts && cd /usr/lib/node_modules/omniroute/app && npm rebuild better-sqlite3 && pm2 delete omniroute 2>/dev/null; pm2 start /root/.omniroute/ecosystem.config.cjs --update-env && pm2 save && echo '✅ Akamai done'"
|
||||
ssh root@69.164.221.35 "npm install -g /tmp/omniroute-*.tgz --ignore-scripts --legacy-peer-deps && cd /usr/lib/node_modules/omniroute/app && npm rebuild better-sqlite3 && (pm2 delete omniroute 2>/dev/null || true) && pm2 start /root/.omniroute/ecosystem.config.cjs --update-env && pm2 save && echo '✅ Akamai done'"
|
||||
```
|
||||
|
||||
### 3. Verify the deployment
|
||||
|
||||
@@ -35,11 +35,11 @@ scp omniroute-*.tgz root@69.164.221.35:/tmp/ && scp omniroute-*.tgz root@192.168
|
||||
```
|
||||
|
||||
```bash
|
||||
ssh root@69.164.221.35 "npm install -g /tmp/omniroute-*.tgz --ignore-scripts && cd /usr/lib/node_modules/omniroute/app && npm rebuild better-sqlite3 && pm2 delete omniroute 2>/dev/null; pm2 start /root/.omniroute/ecosystem.config.cjs --update-env && pm2 save && echo '✅ Akamai done'"
|
||||
ssh root@69.164.221.35 "npm install -g /tmp/omniroute-*.tgz --ignore-scripts --legacy-peer-deps && cd /usr/lib/node_modules/omniroute/app && npm rebuild better-sqlite3 && (pm2 delete omniroute 2>/dev/null || true) && pm2 start /root/.omniroute/ecosystem.config.cjs --update-env && pm2 save && echo '✅ Akamai done'"
|
||||
```
|
||||
|
||||
```bash
|
||||
ssh root@192.168.0.15 "npm install -g /tmp/omniroute-*.tgz --ignore-scripts && cd /usr/lib/node_modules/omniroute/app && npm rebuild better-sqlite3 && pm2 delete omniroute 2>/dev/null; pm2 start /root/.omniroute/ecosystem.config.cjs --update-env && pm2 save && echo '✅ Local done'"
|
||||
ssh root@192.168.0.15 "npm install -g /tmp/omniroute-*.tgz --ignore-scripts --legacy-peer-deps && cd /usr/lib/node_modules/omniroute/app && npm rebuild better-sqlite3 && (pm2 delete omniroute 2>/dev/null || true) && pm2 start /root/.omniroute/ecosystem.config.cjs --update-env && pm2 save && echo '✅ Local done'"
|
||||
```
|
||||
|
||||
### 3. Verify the deployment
|
||||
|
||||
@@ -30,7 +30,7 @@ scp omniroute-*.tgz root@192.168.0.15:/tmp/
|
||||
```
|
||||
|
||||
```bash
|
||||
ssh root@192.168.0.15 "npm install -g /tmp/omniroute-*.tgz --ignore-scripts && cd /usr/lib/node_modules/omniroute/app && npm rebuild better-sqlite3 && pm2 delete omniroute 2>/dev/null; pm2 start /root/.omniroute/ecosystem.config.cjs --update-env && pm2 save && echo '✅ Local done'"
|
||||
ssh root@192.168.0.15 "npm install -g /tmp/omniroute-*.tgz --ignore-scripts --legacy-peer-deps && cd /usr/lib/node_modules/omniroute/app && npm rebuild better-sqlite3 && (pm2 delete omniroute 2>/dev/null || true) && pm2 start /root/.omniroute/ecosystem.config.cjs --update-env && pm2 save && echo '✅ Local done'"
|
||||
```
|
||||
|
||||
### 3. Verify the deployment
|
||||
|
||||
@@ -30,7 +30,7 @@ scp omniroute-*.tgz root@192.168.0.15:/tmp/
|
||||
```
|
||||
|
||||
```bash
|
||||
ssh root@192.168.0.15 "npm install -g /tmp/omniroute-*.tgz --ignore-scripts && cd /usr/lib/node_modules/omniroute/app && npm rebuild better-sqlite3 && pm2 delete omniroute 2>/dev/null; pm2 start /root/.omniroute/ecosystem.config.cjs --update-env && pm2 save && echo '✅ Local done'"
|
||||
ssh root@192.168.0.15 "npm install -g /tmp/omniroute-*.tgz --ignore-scripts --legacy-peer-deps && cd /usr/lib/node_modules/omniroute/app && npm rebuild better-sqlite3 && (pm2 delete omniroute 2>/dev/null || true) && pm2 start /root/.omniroute/ecosystem.config.cjs --update-env && pm2 save && echo '✅ Local done'"
|
||||
```
|
||||
|
||||
### 3. Verify the deployment
|
||||
|
||||
@@ -35,7 +35,7 @@ scp omniroute-*.tgz root@192.168.0.15:/tmp/
|
||||
```
|
||||
|
||||
```bash
|
||||
ssh root@192.168.0.15 "npm install -g /tmp/omniroute-*.tgz --ignore-scripts && cd /usr/lib/node_modules/omniroute/app && npm rebuild better-sqlite3 && pm2 delete omniroute 2>/dev/null; pm2 start /root/.omniroute/ecosystem.config.cjs --update-env && pm2 save && echo '✅ Local done'"
|
||||
ssh root@192.168.0.15 "npm install -g /tmp/omniroute-*.tgz --ignore-scripts --legacy-peer-deps && cd /usr/lib/node_modules/omniroute/app && npm rebuild better-sqlite3 && (pm2 delete omniroute 2>/dev/null || true) && pm2 start /root/.omniroute/ecosystem.config.cjs --update-env && pm2 save && echo '✅ Local done'"
|
||||
```
|
||||
|
||||
### 3. Verify the deployment
|
||||
|
||||
@@ -17,13 +17,13 @@ It provides a single OpenAI-compatible endpoint (`/v1/*`) and routes traffic acr
|
||||
|
||||
Core capabilities:
|
||||
|
||||
- OpenAI-compatible API surface for CLI/tools (177 providers, 45 executors)
|
||||
- OpenAI-compatible API surface for CLI/tools (177 providers, 55 executors)
|
||||
- Request/response translation across provider formats
|
||||
- Model combo fallback (multi-model sequence)
|
||||
- Structured combo steps (`provider + model + connection`) with runtime ordering by `compositeTiers`
|
||||
- Account-level fallback (multi-account per provider)
|
||||
- Quota preflight and quota-aware P2C account selection in the main chat path
|
||||
- OAuth + API-key provider connection management (15 OAuth modules)
|
||||
- OAuth + API-key provider connection management (16 OAuth modules)
|
||||
- Embedding generation via `/v1/embeddings` (6 providers, 9 models)
|
||||
- Image generation via `/v1/images/generations` (10+ providers, 20+ models)
|
||||
- Audio transcription via `/v1/audio/transcriptions` (7 providers)
|
||||
@@ -66,7 +66,7 @@ Core capabilities:
|
||||
- Prompt injection guard middleware
|
||||
- Prompt compression pipeline with Caveman, RTK, stacked pipelines, compression combos, language packs, and analytics
|
||||
- ACP (Agent Communication Protocol) registry
|
||||
- Modular OAuth providers (15 individual modules under `src/lib/oauth/providers/`)
|
||||
- Modular OAuth providers (16 individual modules under `src/lib/oauth/providers/`)
|
||||
- Uninstall/full-uninstall scripts
|
||||
- OAuth environment repair action
|
||||
- WebSocket bridge for OpenAI-compatible WS clients (`/v1/ws`)
|
||||
@@ -321,10 +321,10 @@ Domain layer modules:
|
||||
- Eval runner: `src/lib/domain/evalRunner.ts`
|
||||
- Domain state persistence: `src/lib/db/domainState.ts` — SQLite CRUD for fallback chains, budgets, cost history, lockout state, circuit breakers
|
||||
|
||||
OAuth provider modules (15 individual files under `src/lib/oauth/providers/`):
|
||||
OAuth provider modules (16 individual files under `src/lib/oauth/providers/`):
|
||||
|
||||
- Registry index: `src/lib/oauth/providers/index.ts`
|
||||
- Individual providers: `claude.ts`, `codex.ts`, `gemini.ts`, `antigravity.ts`, `qoder.ts`, `qwen.ts`, `kimi-coding.ts`, `github.ts`, `kiro.ts`, `cursor.ts`, `kilocode.ts`, `cline.ts`, `windsurf.ts`, `gitlab-duo.ts`, `trae.ts`
|
||||
- Individual providers: `claude.ts`, `codex.ts`, `gemini.ts`, `antigravity.ts`, `agy.ts`, `qoder.ts`, `qwen.ts`, `kimi-coding.ts`, `github.ts`, `kiro.ts`, `cursor.ts`, `kilocode.ts`, `cline.ts`, `windsurf.ts`, `gitlab-duo.ts`, `trae.ts`
|
||||
- Thin wrapper: `src/lib/oauth/providers.ts` — re-exports from individual modules
|
||||
|
||||
## 5) Embedded Services (v3.8.4)
|
||||
|
||||
@@ -1951,7 +1951,10 @@ function mapSubscriptionTierStringToPlanLabel(tierText: string): string | null {
|
||||
if (upper.includes("PLUS")) return "Plus";
|
||||
if (upper.includes("LITE")) return "Lite";
|
||||
if (upper.includes("INDIVIDUAL") || upper.includes("FREE")) return "Free";
|
||||
const normalizedId = upper.replace(/\s*\(RESTRICTED\)\s*$/i, "").trim();
|
||||
// Strip a trailing "(RESTRICTED)" marker. Match the fixed literal anywhere then
|
||||
// trim, instead of /\s*\(RESTRICTED\)\s*$/ whose overlapping \s* runs backtrack
|
||||
// polynomially on whitespace-heavy upstream input (js/polynomial-redos).
|
||||
const normalizedId = upper.replace(/\(RESTRICTED\)/i, "").trim();
|
||||
if (normalizedId) {
|
||||
const mapped = mapCodeAssistTierIdToLabel(normalizedId);
|
||||
if (mapped) return mapped;
|
||||
|
||||
@@ -166,7 +166,8 @@ async function partC() {
|
||||
async function main() {
|
||||
line("");
|
||||
line("NVIDIA NIM — diagnóstico `startsWith is not a function`");
|
||||
show("NVIDIA_API_KEY presente", KEY ? `sim (${KEY.slice(0, 6)}…)` : "não");
|
||||
// Never echo any portion of the key (js/clear-text-logging) — presence only.
|
||||
show("NVIDIA_API_KEY presente", KEY ? "sim" : "não");
|
||||
show("BASE_URL", BASE_URL);
|
||||
show("MODEL", MODEL);
|
||||
line("");
|
||||
|
||||
@@ -15,6 +15,8 @@ export function deepMergeFallback(
|
||||
source: Record<string, unknown>
|
||||
): Record<string, unknown> {
|
||||
for (const [key, sourceValue] of Object.entries(source)) {
|
||||
// Guard against prototype pollution from a crafted locale message tree.
|
||||
if (key === "__proto__" || key === "constructor" || key === "prototype") continue;
|
||||
const targetValue = target[key];
|
||||
if (
|
||||
sourceValue !== null &&
|
||||
|
||||
@@ -48,9 +48,13 @@ function serializeFrontmatter(fm: { name: string; description: string }): string
|
||||
// Use block scalar for description if it contains colons, quotes, or newlines
|
||||
const needsQuote = (v: string): boolean => /[:\n"']/.test(v) || v.startsWith(" ");
|
||||
|
||||
const nameStr = needsQuote(fm.name) ? `"${fm.name.replace(/"/g, '\\"')}"` : fm.name;
|
||||
// Escape order matters for double-quoted YAML scalars: backslash FIRST (so the
|
||||
// escapes we add below are not themselves re-escaped), then the double-quote,
|
||||
// then collapse real newlines into the \n escape sequence.
|
||||
const escDq = (v: string): string => v.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
||||
const nameStr = needsQuote(fm.name) ? `"${escDq(fm.name)}"` : fm.name;
|
||||
const descStr = needsQuote(fm.description)
|
||||
? `"${fm.description.replace(/"/g, '\\"').replace(/\n/g, "\\n")}"`
|
||||
? `"${escDq(fm.description).replace(/\n/g, "\\n")}"`
|
||||
: fm.description;
|
||||
|
||||
return `---\nname: ${nameStr}\ndescription: ${descStr}\n---\n`;
|
||||
@@ -363,3 +367,7 @@ export async function generateAgentSkills(opts: GeneratorOptions): Promise<Gener
|
||||
|
||||
return report;
|
||||
}
|
||||
|
||||
// Exported for unit testing only — keep the internal helpers reachable without
|
||||
// widening the public surface.
|
||||
export const __testing = { serializeFrontmatter };
|
||||
|
||||
@@ -15,7 +15,7 @@ import os from "node:os";
|
||||
|
||||
// ── Dynamic imports (tsx/esm resolves TS imports) ────────────────────────────
|
||||
|
||||
const { generateAgentSkills, buildSkillMarkdown } = await import(
|
||||
const { generateAgentSkills, buildSkillMarkdown, __testing } = await import(
|
||||
"../../src/lib/agentSkills/generator.ts"
|
||||
);
|
||||
|
||||
@@ -537,3 +537,25 @@ test("report has all required fields with correct types", async () => {
|
||||
rmTmpDir(tmpDir);
|
||||
}
|
||||
});
|
||||
|
||||
// ── serializeFrontmatter escaping (js/incomplete-sanitization regression) ──────
|
||||
|
||||
test("serializeFrontmatter escapes backslashes before quotes → valid round-trippable YAML", async () => {
|
||||
const { parse } = await import("yaml");
|
||||
const fm = {
|
||||
name: 'name with "quote"',
|
||||
description: 'line1\nline2: path C:\\Users\\x with "q" and trailing \\',
|
||||
};
|
||||
const block = __testing.serializeFrontmatter(fm);
|
||||
// Strip the --- fences and parse the inner YAML; broken escaping (e.g. an
|
||||
// unescaped trailing backslash that escapes the closing quote) would throw or
|
||||
// corrupt the value here.
|
||||
const inner = block.replace(/^---\n/, "").replace(/---\n?$/, "");
|
||||
const parsed = parse(inner) as { name: string; description: string };
|
||||
assert.equal(parsed.name, fm.name, "name must round-trip through YAML unchanged");
|
||||
assert.equal(
|
||||
parsed.description,
|
||||
fm.description,
|
||||
"description with backslashes + quotes + newline must round-trip exactly"
|
||||
);
|
||||
});
|
||||
|
||||
@@ -237,3 +237,26 @@ test("realistic i18n: locale invalid → DEFAULT_LOCALE applies; merge still wor
|
||||
assert.equal(common.cancel, "Cancel", "missing key filled from EN");
|
||||
assert.deepEqual(messages.extra, { key: "Extra EN" }, "missing namespace filled from EN");
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 8. Prototype-pollution guard (js/prototype-pollution-utility regression)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test("deepMergeFallback: ignores __proto__ / constructor / prototype keys (no prototype pollution)", () => {
|
||||
const target: Record<string, unknown> = {};
|
||||
// JSON.parse produces a real own-enumerable __proto__ key (an object literal would
|
||||
// not), so Object.entries iterates it — exactly the attack vector the guard blocks.
|
||||
const malicious = JSON.parse(
|
||||
'{"__proto__":{"polluted":"yes"},"constructor":{"bad":1},"safe":"ok"}'
|
||||
) as Record<string, unknown>;
|
||||
|
||||
deepMergeFallback(target, malicious);
|
||||
|
||||
assert.equal(
|
||||
({} as Record<string, unknown>).polluted,
|
||||
undefined,
|
||||
"Object.prototype must not be polluted"
|
||||
);
|
||||
assert.equal((target as Record<string, unknown>).polluted, undefined);
|
||||
assert.equal(target.safe, "ok", "legitimate keys still merge through");
|
||||
});
|
||||
|
||||
@@ -297,3 +297,22 @@ describe("extractCodeAssistSubscriptionTier", () => {
|
||||
assert.equal(__testing.extractCodeAssistSubscriptionTier({}), null);
|
||||
});
|
||||
});
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* mapSubscriptionTierStringToPlanLabel — (RESTRICTED) strip + ReDoS */
|
||||
/* ------------------------------------------------------------------ */
|
||||
describe("mapSubscriptionTierStringToPlanLabel", () => {
|
||||
it("resolves a code-assist tier id via the normalized-id path after stripping (RESTRICTED)", () => {
|
||||
// These reach the `normalizedId` branch (no early includes() match) and only
|
||||
// resolve to a label once the "(RESTRICTED)" suffix is stripped + trimmed.
|
||||
assert.equal(__testing.mapSubscriptionTierStringToPlanLabel("GOOGLE_ONE (RESTRICTED)"), "Pro");
|
||||
assert.equal(__testing.mapSubscriptionTierStringToPlanLabel("LEGACY (RESTRICTED)"), "Free");
|
||||
});
|
||||
|
||||
it("does not hang on whitespace-heavy input (js/polynomial-redos guard)", () => {
|
||||
const start = process.hrtime.bigint();
|
||||
__testing.mapSubscriptionTierStringToPlanLabel(" ".repeat(100000) + "(");
|
||||
const ms = Number(process.hrtime.bigint() - start) / 1e6;
|
||||
assert.ok(ms < 500, `tier mapping took ${ms.toFixed(1)}ms on whitespace-heavy input — possible ReDoS`);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user