ai.cotal/cotal
version 1.1.0
- scanned 2026-10-16
- source github.com/Cotal-AI/Cotal
- commit 2cf01b05df86
- transport streamable-http
- files read 1613
- methodology 0.2.0
- ruleset 0.1.1
No tag matching version 1.1.0 was found in this repository, so the scan read the default branch instead. The findings below describe that code, not necessarily the released version.
0measured on 98% of this axis
3 vendored or minified paths were excluded before scanning; machine-generated bundles are not the server's own code; 17 files could not be parsed and were not scanned
71measured on 85% of this axis
0measured on 99% of this axis
1 vulnerability could not be placed in `03` §6's table and is listed but not scored
100measured on 75% of this axis
77measured on 65% of this axis
The evidence
Every deduction below names the artefact that produced it. Nothing on this page was executed: the source was read, never run, and no running server was contacted.
Code Safety
Showing the 50 most severe of 131 findings on this axis. The score was computed over all of them — findings beyond the fourth of a severity still deduct at a tenth of their weight — so the remainder is omitted from this page, not from the arithmetic.
- bin/smoke/repro-1607.smoke.ts:165mcp-js-shell-exec-nonliteral (critical/medium)
A shell is spawned on a value that is not a string literal. child_process .exec and execSync run their argument through /bin/sh, so any caller-supplied component is shell syntax, not data.
// Recovery run const recoverRun = execSync(`node ${PROOF_TOOL} --recover`, { cwd: fixtureDir, encoding: "utf8" }); const finalGitStatus = execSync("git status --porcelain", { cwd: fixtureDir, encoding: "utf8" }).trim(); const recoveredClean = finalGitStatus === "" && !readFileSync(join(fixtureDir, "src/impl.js"), "utf8").includes("if (false)"); - bin/smoke/repro-1607.smoke.ts:165mcp-js-shell-exec-template-literal (critical/medium)
A shell command is built from a template literal with interpolation. Quoting inside a template does not escape shell metacharacters.
// Recovery run const recoverRun = execSync(`node ${PROOF_TOOL} --recover`, { cwd: fixtureDir, encoding: "utf8" }); const finalGitStatus = execSync("git status --porcelain", { cwd: fixtureDir, encoding: "utf8" }).trim(); const recoveredClean = finalGitStatus === "" && !readFileSync(join(fixtureDir, "src/impl.js"), "utf8").includes("if (false)"); - implementations/auth/smoke/down-manifest-usermode.smoke.ts:174mcp-js-shell-exec-nonliteral (critical/medium)
A shell is spawned on a value that is not a string literal. child_process .exec and execSync run their argument through /bin/sh, so any caller-supplied component is shell syntax, not data.
const mgrPid = Number(readFileSync(canonicalLocalProcessPath(MANAGER_PIDFILE, { root, space: SPACE }), "utf8").trim()); // Children first (the launched worker's process tree), then the manager itself. const kids = (() => { try { return execSync(`pgrep -P ${mgrPid}`, { encoding: "utf8" }).trim().split("\n").filter(Boolean); } catch { return []; } })(); for (const k of kids) { try { execSync(`pkill -9 -P ${k}`); } catch { /* leaf */ } try { process.kill(Number(k), "SIGKILL"); } catch { /* gone */ } } try { process.kill(mgrPid, "SIGKILL"); } catch { /* gone */ } - implementations/auth/smoke/down-manifest-usermode.smoke.ts:174mcp-js-shell-exec-template-literal (critical/medium)
A shell command is built from a template literal with interpolation. Quoting inside a template does not escape shell metacharacters.
const mgrPid = Number(readFileSync(canonicalLocalProcessPath(MANAGER_PIDFILE, { root, space: SPACE }), "utf8").trim()); // Children first (the launched worker's process tree), then the manager itself. const kids = (() => { try { return execSync(`pgrep -P ${mgrPid}`, { encoding: "utf8" }).trim().split("\n").filter(Boolean); } catch { return []; } })(); for (const k of kids) { try { execSync(`pkill -9 -P ${k}`); } catch { /* leaf */ } try { process.kill(Number(k), "SIGKILL"); } catch { /* gone */ } } try { process.kill(mgrPid, "SIGKILL"); } catch { /* gone */ } - implementations/auth/smoke/down-manifest-usermode.smoke.ts:175mcp-js-shell-exec-nonliteral (critical/medium)
A shell is spawned on a value that is not a string literal. child_process .exec and execSync run their argument through /bin/sh, so any caller-supplied component is shell syntax, not data.
// Children first (the launched worker's process tree), then the manager itself. const kids = (() => { try { return execSync(`pgrep -P ${mgrPid}`, { encoding: "utf8" }).trim().split("\n").filter(Boolean); } catch { return []; } })(); for (const k of kids) { try { execSync(`pkill -9 -P ${k}`); } catch { /* leaf */ } try { process.kill(Number(k), "SIGKILL"); } catch { /* gone */ } } try { process.kill(mgrPid, "SIGKILL"); } catch { /* gone */ } await wait(500); - implementations/auth/smoke/down-manifest-usermode.smoke.ts:175mcp-js-shell-exec-template-literal (critical/medium)
A shell command is built from a template literal with interpolation. Quoting inside a template does not escape shell metacharacters.
// Children first (the launched worker's process tree), then the manager itself. const kids = (() => { try { return execSync(`pgrep -P ${mgrPid}`, { encoding: "utf8" }).trim().split("\n").filter(Boolean); } catch { return []; } })(); for (const k of kids) { try { execSync(`pkill -9 -P ${k}`); } catch { /* leaf */ } try { process.kill(Number(k), "SIGKILL"); } catch { /* gone */ } } try { process.kill(mgrPid, "SIGKILL"); } catch { /* gone */ } await wait(500); - implementations/auth/smoke/down-manifest-usermode.smoke.ts:234mcp-js-shell-exec-nonliteral (critical/medium)
A shell is spawned on a value that is not a string literal. child_process .exec and execSync run their argument through /bin/sh, so any caller-supplied component is shell syntax, not data.
// Crash the replacement supervisor + its child, again before any deprovision. The supervise // cmdline carries this run's unique random SPACE token, so the sweep is surgically scoped. const mgrPids2 = (() => { try { return execSync(`pgrep -f "supervise.*${SPACE}"`, { encoding: "utf8" }).trim().split("\n").filter(Boolean); } catch { return []; } })(); check("found the replacement supervisor to crash", mgrPids2.length > 0, mgrPids2); for (const p of mgrPids2) { - implementations/auth/smoke/down-manifest-usermode.smoke.ts:234mcp-js-shell-exec-template-literal (critical/medium)
A shell command is built from a template literal with interpolation. Quoting inside a template does not escape shell metacharacters.
// Crash the replacement supervisor + its child, again before any deprovision. The supervise // cmdline carries this run's unique random SPACE token, so the sweep is surgically scoped. const mgrPids2 = (() => { try { return execSync(`pgrep -f "supervise.*${SPACE}"`, { encoding: "utf8" }).trim().split("\n").filter(Boolean); } catch { return []; } })(); check("found the replacement supervisor to crash", mgrPids2.length > 0, mgrPids2); for (const p of mgrPids2) { - implementations/auth/smoke/down-manifest-usermode.smoke.ts:237mcp-js-shell-exec-nonliteral (critical/medium)
A shell is spawned on a value that is not a string literal. child_process .exec and execSync run their argument through /bin/sh, so any caller-supplied component is shell syntax, not data.
check("found the replacement supervisor to crash", mgrPids2.length > 0, mgrPids2); for (const p of mgrPids2) { const kids2 = (() => { try { return execSync(`pgrep -P ${p}`, { encoding: "utf8" }).trim().split("\n").filter(Boolean); } catch { return []; } })(); for (const k of kids2) { try { execSync(`pkill -9 -P ${k}`); } catch { /* leaf */ } try { process.kill(Number(k), "SIGKILL"); } catch { /* gone */ } } try { process.kill(Number(p), "SIGKILL"); } catch { /* gone */ } - implementations/auth/smoke/down-manifest-usermode.smoke.ts:237mcp-js-shell-exec-template-literal (critical/medium)
A shell command is built from a template literal with interpolation. Quoting inside a template does not escape shell metacharacters.
check("found the replacement supervisor to crash", mgrPids2.length > 0, mgrPids2); for (const p of mgrPids2) { const kids2 = (() => { try { return execSync(`pgrep -P ${p}`, { encoding: "utf8" }).trim().split("\n").filter(Boolean); } catch { return []; } })(); for (const k of kids2) { try { execSync(`pkill -9 -P ${k}`); } catch { /* leaf */ } try { process.kill(Number(k), "SIGKILL"); } catch { /* gone */ } } try { process.kill(Number(p), "SIGKILL"); } catch { /* gone */ } - implementations/auth/smoke/down-manifest-usermode.smoke.ts:238mcp-js-shell-exec-nonliteral (critical/medium)
A shell is spawned on a value that is not a string literal. child_process .exec and execSync run their argument through /bin/sh, so any caller-supplied component is shell syntax, not data.
for (const p of mgrPids2) { const kids2 = (() => { try { return execSync(`pgrep -P ${p}`, { encoding: "utf8" }).trim().split("\n").filter(Boolean); } catch { return []; } })(); for (const k of kids2) { try { execSync(`pkill -9 -P ${k}`); } catch { /* leaf */ } try { process.kill(Number(k), "SIGKILL"); } catch { /* gone */ } } try { process.kill(Number(p), "SIGKILL"); } catch { /* gone */ } } - implementations/auth/smoke/down-manifest-usermode.smoke.ts:238mcp-js-shell-exec-template-literal (critical/medium)
A shell command is built from a template literal with interpolation. Quoting inside a template does not escape shell metacharacters.
for (const p of mgrPids2) { const kids2 = (() => { try { return execSync(`pgrep -P ${p}`, { encoding: "utf8" }).trim().split("\n").filter(Boolean); } catch { return []; } })(); for (const k of kids2) { try { execSync(`pkill -9 -P ${k}`); } catch { /* leaf */ } try { process.kill(Number(k), "SIGKILL"); } catch { /* gone */ } } try { process.kill(Number(p), "SIGKILL"); } catch { /* gone */ } } - packages/lang/smoke/differential.smoke.ts:109mcp-js-dynamic-code-execution (critical/medium)
Caller-influenced input reaches an evaluator. eval, the Function constructor and vm.runInNewContext all execute their argument; none of them sandbox anything by default, vm least of all despite the name.
* The worker's evaluator is the confined one. */ const evaluate = (module: string) => new Function(`return (${module})`)() as never; interface Arm { - scripts/mutation-coverage.mjs:1907mcp-js-shell-exec-nonliteral (critical/medium)
A shell is spawned on a value that is not a string literal. child_process .exec and execSync run their argument through /bin/sh, so any caller-supplied component is shell syntax, not data.
let output; try { output = execSync(cfg.command, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }); } catch (error) { failed++; - scripts/mutation-proof.selftest.mjs:747mcp-js-shell-exec-nonliteral (critical/medium)
A shell is spawned on a value that is not a string literal. child_process .exec and execSync run their argument through /bin/sh, so any caller-supplied component is shell syntax, not data.
const timed = join(root, "src/impl.js"); const OLD_MS = Date.now() - 3600_000; execSync(`touch -d ${JSON.stringify(new Date(OLD_MS).toISOString())} ${JSON.stringify(timed)}`); const mtimeBefore = statSync(timed).mtimeMs; const startedMs = Date.now(); - scripts/mutation-proof.selftest.mjs:747mcp-js-shell-exec-template-literal (critical/medium)
A shell command is built from a template literal with interpolation. Quoting inside a template does not escape shell metacharacters.
const timed = join(root, "src/impl.js"); const OLD_MS = Date.now() - 3600_000; execSync(`touch -d ${JSON.stringify(new Date(OLD_MS).toISOString())} ${JSON.stringify(timed)}`); const mtimeBefore = statSync(timed).mtimeMs; const startedMs = Date.now(); - bin/smoke/backup-conservation-live.smoke.ts:162mcp-js-prototype-pollution-merge-loop (medium/medium)
Keys from a caller-supplied object are copied onto a target without excluding __proto__ / constructor / prototype. A tool argument carrying __proto__ then mutates Object.prototype for the whole process, which can turn an unrelated later property read into attacker-controlled data.
for (const stream of spaceBackupInventory(mesh.space).full) { const { state } = await jsRequest<{ state: StreamState }>(nc, `$JS.API.STREAM.INFO.${stream}`, {}); streams[stream] = { messages: state.messages, first_seq: state.first_seq, last_seq: state.last_seq }; for (let offset = 0;;) { const page = await jsRequest<{ consumers?: RawConsumer[]; total: number; offset: number }>( - bin/smoke/shard-never-ran.smoke.ts:39mcp-js-prototype-pollution-merge-loop (medium/medium)
Keys from a caller-supplied object are copied onto a target without excluding __proto__ / constructor / prototype. A tool argument carrying __proto__ then mutates Object.prototype for the whole process, which can turn an unrelated later property read into attacker-controlled data.
mkdirSync(dirname(join(dir, entry)), { recursive: true }); writeFileSync(join(dir, entry), body); scripts[name] = `tsx ${entry}`; } writeFileSync(join(dir, "package.json"), JSON.stringify({ private: true, packageManager, scripts })); - extensions/connector-claude-code/smoke/launch-env.smoke.ts:149mcp-js-prototype-pollution-merge-loop (medium/medium)
Keys from a caller-supplied object are copied onto a target without excluding __proto__ / constructor / prototype. A tool argument carrying __proto__ then mutates Object.prototype for the whole process, which can turn an unrelated later property read into attacker-controlled data.
delete process.env.PATH; delete process.env.Path; process.env[spelling] = ambientPath; const row = claudeConnector.buildLaunch({ space: "smoke", name: `claude-path-${spelling}`, events: false } as never).env ?? {}; - extensions/connector-core/smoke/egress-guard-differential.smoke.ts:1160mcp-js-prototype-pollution-merge-loop (medium/medium)
Keys from a caller-supplied object are copied onto a target without excluding __proto__ / constructor / prototype. A tool argument carrying __proto__ then mutates Object.prototype for the whole process, which can turn an unrelated later property read into attacker-controlled data.
const arms: Record<string, Arm> = {}; for (const ref of [PREDECESSOR, ADAPTER_FREE, "HEAD"]) { arms[ref] = await loadArm(ref); ok(`${ref}: classifier role bound`, Boolean(arms[ref]), arms[ref]?.role); } - extensions/connector-hermes/bin/install.mjs:142mcp-js-prototype-pollution-merge-loop (medium/medium)
Keys from a caller-supplied object are copied onto a target without excluding __proto__ / constructor / prototype. A tool argument carrying __proto__ then mutates Object.prototype for the whole process, which can turn an unrelated later property read into attacker-controlled data.
} else { const before = meshEnv[k]; meshEnv[k] = before.replace(/127\.0\.0\.1|localhost/g, "host.docker.internal"); warn(`Rewrote ${k}: ${before} → ${meshEnv[k]} (so the container can reach your host).`); } - extensions/pi/pi-sdk.smoke.ts:611mcp-js-prototype-pollution-merge-loop (medium/medium)
Keys from a caller-supplied object are copied onto a target without excluding __proto__ / constructor / prototype. A tool argument carrying __proto__ then mutates Object.prototype for the whole process, which can turn an unrelated later property read into attacker-controlled data.
for (const key of keys) { if (prior[key] === undefined) delete process.env[key]; else process.env[key] = prior[key]; } rmSync(root, { recursive: true, force: true }); - extensions/pi/pi.smoke.ts:817mcp-js-prototype-pollution-merge-loop (medium/medium)
Keys from a caller-supplied object are copied onto a target without excluding __proto__ / constructor / prototype. A tool argument carrying __proto__ then mutates Object.prototype for the whole process, which can turn an unrelated later property read into attacker-controlled data.
const value = saved[key]; if (value === undefined) delete process.env[key]; else process.env[key] = value; } } - implementations/auth/smoke/freeslot-respawn-barrier.smoke.ts:180mcp-js-prototype-pollution-merge-loop (medium/medium)
Keys from a caller-supplied object are copied onto a target without excluding __proto__ / constructor / prototype. A tool argument carrying __proto__ then mutates Object.prototype for the whole process, which can turn an unrelated later property read into attacker-controlled data.
for (const k of ["PATH", "HOME", "TMPDIR", "TMP", "TEMP", "XDG_CACHE_HOME", "LANG", "TERM"]) { const v = process.env[k]; if (v) out[k] = v; } return out; - implementations/auth/smoke/int2-revoke-hold.smoke.ts:149mcp-js-prototype-pollution-merge-loop (medium/medium)
Keys from a caller-supplied object are copied onto a target without excluding __proto__ / constructor / prototype. A tool argument carrying __proto__ then mutates Object.prototype for the whole process, which can turn an unrelated later property read into attacker-controlled data.
for (const k of ["PATH", "HOME", "TMPDIR", "TMP", "TEMP", "XDG_CACHE_HOME", "LANG", "TERM"]) { const v = process.env[k]; if (v) out[k] = v; } return out; - implementations/auth/smoke/user-spawn.smoke.ts:148mcp-js-prototype-pollution-merge-loop (medium/medium)
Keys from a caller-supplied object are copied onto a target without excluding __proto__ / constructor / prototype. A tool argument carrying __proto__ then mutates Object.prototype for the whole process, which can turn an unrelated later property read into attacker-controlled data.
for (const k of ["PATH", "HOME", "TMPDIR", "TMP", "TEMP", "XDG_CACHE_HOME", "LANG", "TERM", "NODE_EXTRA_CA_CERTS"]) { const v = process.env[k]; if (v) out[k] = v; } return out; - implementations/auth/smoke/user-spawn.smoke.ts:727mcp-js-prototype-pollution-merge-loop (medium/medium)
Keys from a caller-supplied object are copied onto a target without excluding __proto__ / constructor / prototype. A tool argument carrying __proto__ then mutates Object.prototype for the whole process, which can turn an unrelated later property read into attacker-controlled data.
check(`precondition: kappa remained alive to receive the ${want} status signal`, signalSent); const reached = signalSent && await until(() => rosterOf("kappa") === want); carried[want] = reached ? meshOf("kappa") : `the roster never reached ${want} (it says ${rosterOf("kappa")})`; } check( - implementations/auth/src/retirement-barrier.ts:789mcp-js-prototype-pollution-merge-loop (medium/medium)
Keys from a caller-supplied object are copied onto a target without excluding __proto__ / constructor / prototype. A tool argument carrying __proto__ then mutates Object.prototype for the whole process, which can turn an unrelated later property read into attacker-controlled data.
throw new EpEnvelopeError("failed-precondition", `the frontier step cannot read the stream ${stream} named by the intent; a missing cutoff never closes a lifecycle interval (SPEC 13.1): ${(e as Error)?.message ?? String(e)}`); } frontiers[stream] = last; } const record: FrontierRecord = { lifecycleUid: intent.lifecycleUid, opId, streams: frontiers }; - implementations/cli/smoke/legacy-packaged-manager.smoke.ts:129mcp-js-prototype-pollution-merge-loop (medium/medium)
Keys from a caller-supplied object are copied onto a target without excluding __proto__ / constructor / prototype. A tool argument carrying __proto__ then mutates Object.prototype for the whole process, which can turn an unrelated later property read into attacker-controlled data.
const path = realHelper(arch); const present = existsSync(path); snapshot[arch] = { present, sha256: present ? createHash("sha256").update(readFileSync(path)).digest("hex") : null, - implementations/cli/smoke/seed.smoke.ts:122mcp-js-prototype-pollution-merge-loop (medium/medium)
Keys from a caller-supplied object are copied onto a target without excluding __proto__ / constructor / prototype. A tool argument carrying __proto__ then mutates Object.prototype for the whole process, which can turn an unrelated later property read into attacker-controlled data.
for (const file of ["symlinked-bin.smoke.ts", "downgrade-hint.smoke.ts"]) { files.splice(files.indexOf(file), 1); excluded[file] = "Unix executable/symlink scenario; unchanged Windows exclusion"; } } - implementations/cli/src/commands/backup.ts:131mcp-js-prototype-pollution-merge-loop (medium/medium)
Keys from a caller-supplied object are copied onto a target without excluding __proto__ / constructor / prototype. A tool argument carrying __proto__ then mutates Object.prototype for the whole process, which can turn an unrelated later property read into attacker-controlled data.
if (streamInfo.error) throw new Error(streamInfo.error.description ?? `JetStream stream info failed for ${stream}`); if (!streamInfo.state) throw new Error(`JetStream omitted stream state for ${stream}`); statesByStream[stream] = validateBackupStreamState(streamInfo.state); const consumers: ConsumerInfo[] = []; try { - implementations/cli/src/commands/backup.ts:145mcp-js-prototype-pollution-merge-loop (medium/medium)
Keys from a caller-supplied object are copied onto a target without excluding __proto__ / constructor / prototype. A tool argument carrying __proto__ then mutates Object.prototype for the whole process, which can turn an unrelated later property read into attacker-controlled data.
throw new Error(`isolated backup cannot enumerate consumers for ${stream}: ${(error as Error).message}`); } consumersByStream[stream] = consumers; } if (selected === "registry") { - implementations/cli/src/lib/agent-skills.ts:197mcp-js-prototype-pollution-merge-loop (medium/medium)
Keys from a caller-supplied object are copied onto a target without excluding __proto__ / constructor / prototype. A tool argument carrying __proto__ then mutates Object.prototype for the whole process, which can turn an unrelated later property read into attacker-controlled data.
mkdirSync(dir, { recursive: true }); writeOwnedFile(destFile, canonical); // write ONLY our file, via rename (never truncates a hard-linked inode); never delete or replace anything else in the dir manifest.skills[name] = digest(canonical); } - implementations/delivery/smoke/hosted-delivery-startup-isolation.smoke.ts:156mcp-js-prototype-pollution-merge-loop (medium/medium)
Keys from a caller-supplied object are copied onto a target without excluding __proto__ / constructor / prototype. A tool argument carrying __proto__ then mutates Object.prototype for the whole process, which can turn an unrelated later property read into attacker-controlled data.
const env: NodeJS.ProcessEnv = { PATH: process.env.PATH, LANG: "C.UTF-8", COTAL_STARTUP_PROOF_CHILD: "1", COTAL_STARTUP_PROOF_INPUT: inputPath, COTAL_DELIVERY_BROKER_GONE_MS: "2500", COTAL_DELIVERY_BROKER_GONE_BACKSTOP_MS: "3000" }; for (const key of ["HOME", "XDG_CONFIG_HOME", "XDG_STATE_HOME", "XDG_CACHE_HOME", "XDG_DATA_HOME", "COTAL_HOME", "COTAL_SEAT_ROOT", "TMPDIR"]) { env[key] = join(root, key.toLowerCase()); mkdirSync(env[key]!, { recursive: true, mode: 0o700 }); } child = spawn(process.execPath, ["--import", "tsx", fileURLToPath(import.meta.url)], { cwd: process.cwd(), env, stdio: ["ignore", "pipe", "pipe", "ipc"] }); - implementations/manager/smoke/remote-authority-expiry.smoke.ts:55mcp-js-prototype-pollution-merge-loop (medium/medium)
Keys from a caller-supplied object are copied onto a target without excluding __proto__ / constructor / prototype. A tool argument carrying __proto__ then mutates Object.prototype for the whole process, which can turn an unrelated later property read into attacker-controlled data.
}); const token = jwtFromCreds(creds)!; liveCredentials[name] = { jwt: token, exp: credsClaims(creds).exp! }; } const liveMaterial = { ...bundleResult, accountPublicKey: auth.account.pub, credentials: liveCredentials, - implementations/manager/smoke/remote-isolated-renewal.smoke.ts:137mcp-js-prototype-pollution-merge-loop (medium/medium)
Keys from a caller-supplied object are copied onto a target without excluding __proto__ / constructor / prototype. A tool argument carrying __proto__ then mutates Object.prototype for the whole process, which can turn an unrelated later property read into attacker-controlled data.
issue: async () => { const credentials = {} as NonNullable<RemoteManagerAuthorityMaterial["credentials"]>; for (const n of names) { const c = await sign(n); credentials[n] = { jwt: jwtFromCreds(c)!, exp: credsClaims(c).exp! }; } return { credentials }; }, - implementations/manager/smoke/remote-start-expiry-recovery.smoke.ts:147mcp-js-prototype-pollution-merge-loop (medium/medium)
Keys from a caller-supplied object are copied onto a target without excluding __proto__ / constructor / prototype. A tool argument carrying __proto__ then mutates Object.prototype for the whole process, which can turn an unrelated later property read into attacker-controlled data.
issue: async () => { const credentials = {} as NonNullable<RemoteManagerAuthorityMaterial["credentials"]>; for (const n of names) { const c = await sign(n); credentials[n] = { jwt: jwtFromCreds(c)!, exp: credsClaims(c).exp! }; } return { credentials }; }, - implementations/manager/src/manager.ts:1175mcp-js-prototype-pollution-merge-loop (medium/medium)
Keys from a caller-supplied object are copied onto a target without excluding __proto__ / constructor / prototype. A tool argument carrying __proto__ then mutates Object.prototype for the whole process, which can turn an unrelated later property read into attacker-controlled data.
for (const bin of requires) { const path = resolveOnPath(bin); if (path) binaries[bin] = path; else missing.push(bin); } - implementations/runtime/src/mesh-handler.ts:2846mcp-js-prototype-pollution-merge-loop (medium/medium)
Keys from a caller-supplied object are copied onto a target without excluding __proto__ / constructor / prototype. A tool argument carrying __proto__ then mutates Object.prototype for the whole process, which can turn an unrelated later property read into attacker-controlled data.
const out: Record<string, unknown> = {}; for (const k of ["name", "owner", "actor", "uid", "readinessDeadlineMs"] as const) { if (floor[k] !== undefined) out[k] = floor[k]; } return out; - packages/core/smoke/history-recent.smoke.ts:200mcp-js-prototype-pollution-merge-loop (medium/medium)
Keys from a caller-supplied object are copied onto a target without excluding __proto__ / constructor / prototype. A tool argument carrying __proto__ then mutates Object.prototype for the whole process, which can turn an unrelated later property read into attacker-controlled data.
const shadowed = ep as unknown as Record<string, unknown>; const real = shadowed[method]; shadowed[method] = async () => { throw new Error(`forced ${method} truncation`); }; let doorThrew: unknown; await ep.channelHistory("talk", { limit: 10 }).then( - packages/core/smoke/history-recent.smoke.ts:206mcp-js-prototype-pollution-merge-loop (medium/medium)
Keys from a caller-supplied object are copied onto a target without excluding __proto__ / constructor / prototype. A tool argument carrying __proto__ then mutates Object.prototype for the whole process, which can turn an unrelated later property read into attacker-controlled data.
(e) => { doorThrew = e; }, ); shadowed[method] = real; check(`channelHistory PROPAGATES a cut-short ${method} (never renders it as an empty channel)`, doorThrew instanceof Error && /forced/.test(String((doorThrew as Error).message)), String(doorThrew)); - packages/core/src/connector-config.ts:202mcp-js-prototype-pollution-merge-loop (medium/medium)
Keys from a caller-supplied object are copied onto a target without excluding __proto__ / constructor / prototype. A tool argument carrying __proto__ then mutates Object.prototype for the whole process, which can turn an unrelated later property read into attacker-controlled data.
const b = base.connectors?.[name]; const o = over.connectors?.[name]; connectors[name] = { ...b, ...o, mcpServers: { ...(b?.mcpServers ?? {}), ...(o?.mcpServers ?? {}) } }; } // `spawn` replaces WHOLESALE rather than merging per key the way servers do. An allow-list is one - packages/core/src/endpoint-cluster.ts:186mcp-js-prototype-pollution-merge-loop (medium/medium)
Keys from a caller-supplied object are copied onto a target without excluding __proto__ / constructor / prototype. A tool argument carrying __proto__ then mutates Object.prototype for the whole process, which can turn an unrelated later property read into attacker-controlled data.
if (typeof v !== "number" || !Number.isSafeInteger(v) || v <= 0) invalid(`command "${name}" admissionCeiling.${f} ${JSON.stringify(v)} is not a positive safe integer`); nums[f] = v; } admissionCeiling = { maxBytes: nums.maxBytes, maxDepth: nums.maxDepth, maxItems: nums.maxItems }; - packages/core/src/endpoint-envelope.ts:254mcp-js-prototype-pollution-merge-loop (medium/medium)
Keys from a caller-supplied object are copied onto a target without excluding __proto__ / constructor / prototype. A tool argument carrying __proto__ then mutates Object.prototype for the whole process, which can turn an unrelated later property read into attacker-controlled data.
// eslint-disable-next-line no-control-regex if (/[\x00-\x1f\x7f]/.test(s)) fail("bad-request", `correlation.${k} carries a control character`); out[k] = s; } return out; - packages/core/src/endpoint-envelope.ts:286mcp-js-prototype-pollution-merge-loop (medium/medium)
Keys from a caller-supplied object are copied onto a target without excluding __proto__ / constructor / prototype. A tool argument carrying __proto__ then mutates Object.prototype for the whole process, which can turn an unrelated later property read into attacker-controlled data.
} if (typeof d !== "string" || !isContractDigest(d)) fail("bad-request", `op.${k} is not a sha256:<hex> digest`); digests[k] = d; } - packages/core/src/remote-manager-authority.ts:480mcp-js-prototype-pollution-merge-loop (medium/medium)
Keys from a caller-supplied object are copied onto a target without excluding __proto__ / constructor / prototype. A tool argument carrying __proto__ then mutates Object.prototype for the whole process, which can turn an unrelated later property read into attacker-controlled data.
const id = (item as { id?: unknown }).id; if (!isUserNkey(id)) fail(`identities.${name}.id must be a user nkey`); identities[name] = { id }; } return identities; - packages/lang/smoke/differential.smoke.ts:1087mcp-js-prototype-pollution-merge-loop (medium/medium)
Keys from a caller-supplied object are copied onto a target without excluding __proto__ / constructor / prototype. A tool argument carrying __proto__ then mutates Object.prototype for the whole process, which can turn an unrelated later property read into attacker-controlled data.
} for (const m of ["spawn", "turn", "ask", "checkpoint", "sleep", "wait", "notify", "monitor", "openConclave", "closeConclave"]) { (RefusesEverything.prototype as unknown as Record<string, unknown>)[m] = () => { throw new Error(`${RefusesEverything.REACHED} (${m})`); }; - packages/lang/smoke/migrate.smoke.ts:65mcp-js-prototype-pollution-merge-loop (medium/medium)
Keys from a caller-supplied object are copied onto a target without excluding __proto__ / constructor / prototype. A tool argument carrying __proto__ then mutates Object.prototype for the whole process, which can turn an unrelated later property read into attacker-controlled data.
const h: Record<string, unknown> = { now: () => (sim as unknown as { now: () => number }).now() }; for (const m of EFFECTS) { h[m] = async (r: unknown, c: EffectContext) => { asked.push(stepKeyString(c.key)); return await sim[m]!(r, c); - packages/lang/src/engine/bridge.ts:271mcp-js-prototype-pollution-merge-loop (medium/medium)
Keys from a caller-supplied object are copied onto a target without excluding __proto__ / constructor / prototype. A tool argument carrying __proto__ then mutates Object.prototype for the whole process, which can turn an unrelated later property read into attacker-controlled data.
} as EffectHandler; for (const m of METHODS) { (handler as Record<Method, (req: unknown, ctx: EffectContext) => Promise<unknown>>)[m] = (req, ctx) => call(m, req, ctx); } - scripts/ci-validator-lock-acquisition.mjs:110mcp-js-prototype-pollution-merge-loop (medium/medium)
Keys from a caller-supplied object are copied onto a target without excluding __proto__ / constructor / prototype. A tool argument carrying __proto__ then mutates Object.prototype for the whole process, which can turn an unrelated later property read into attacker-controlled data.
assert.equal(lstatSync(canonical).mtimeMs, toolStat.mtimeMs); assert.equal(toolBytes.length, toolStat.size); executablePaths[name] = { path: canonical, bytes: toolBytes.length, sha256: sha(toolBytes) }; } assert.equal(executablePaths.node.path, realpathSync(process.execPath));
Auth Posture
- authentication_model
auth enforcement present at a call site in the source (inbound token verification) and documented in the README; "refused without credentials" (`03` §4) established by reading the code rather than by connecting
- transport_security
declared endpoint(s) are HTTPS: https://cotal.ai/mcp; scored 80, not 100: `03` §4 reserves 100 for HTTPS *with HSTS*, and an HSTS header can only be seen by connecting, which `04` §9 does not do. This is an unverifiable band, not an observed absence.
- secret_handling
JSON Web Token at implementations/auth/smoke/login.smoke.ts:301; JSON Web Token at implementations/auth/smoke/login.smoke.ts:641; …and 1154 more matches
- authorization_granularity
no tool registration was recognised anywhere in the source. An MCP server registers tools by definition, so this is a gap in our detection rather than a server with none, and `03` §4's threshold cannot be applied to it. A tool-list handler IS present, so the tools are built in a shape this counter could not enumerate.
Dependency Health
Showing the 50 most severe of 146 findings on this axis. The score was computed over all of them — findings beyond the fourth of a severity still deduct at a tenth of their weight — so the remainder is omitted from this page, not from the arithmetic.
- website/package-lock.jsonastro 7.0.3 — GHSA-26w7-cxv4-gfx2
direct, CVSS 9.8, fixed in 7.2.8
https://osv.dev/vulnerability/GHSA-26w7-cxv4-gfx2 - pnpm-lock.yamlproxy-addr 2.0.7 — GHSA-jqcg-44mw-7w3h
transitive, CVSS 9.1, fixed in 2.0.8
https://osv.dev/vulnerability/GHSA-jqcg-44mw-7w3h - website/package-lock.jsonsharp 0.34.5 — GHSA-rgj7-g3m4-5g8c
direct, CVSS 8.9, fixed in 0.35.4
https://osv.dev/vulnerability/GHSA-rgj7-g3m4-5g8c - website/package-lock.jsonsharp 0.34.5 — GHSA-wq5f-xc86-pv6w
direct, CVSS 8.9, fixed in 0.35.5
https://osv.dev/vulnerability/GHSA-wq5f-xc86-pv6w - website/package-lock.jsonsmol-toml 1.7.0 — GHSA-7w5x-hrqm-74c2
direct, CVSS 8.2, fixed in 1.7.1
https://osv.dev/vulnerability/GHSA-7w5x-hrqm-74c2 - pnpm-lock.yaml@modelcontextprotocol/sdk 1.29.0 — GHSA-6qxp-vccf-f47h
direct, CVSS 7.5, fixed in 1.31.0
https://osv.dev/vulnerability/GHSA-6qxp-vccf-f47h - pnpm-lock.yamlundici 7.29.0 — GHSA-rfgv-xxqx-mfg5
direct, CVSS 7.5, fixed in 7.29.1
https://osv.dev/vulnerability/GHSA-rfgv-xxqx-mfg5 - pnpm-lock.yamlundici 8.5.0 — GHSA-rfgv-xxqx-mfg5
direct, CVSS 7.5, fixed in 8.10.2
https://osv.dev/vulnerability/GHSA-rfgv-xxqx-mfg5 - website/package-lock.jsonundici 7.28.0 — GHSA-rfgv-xxqx-mfg5
direct, CVSS 7.5, fixed in 7.29.1
https://osv.dev/vulnerability/GHSA-rfgv-xxqx-mfg5 - remotion/pnpm-lock.yamlws 8.20.1 — GHSA-96hv-2xvq-fx4p
direct, CVSS 7.5, fixed in 8.21.0
https://osv.dev/vulnerability/GHSA-96hv-2xvq-fx4p - pnpm-lock.yamlundici 8.5.0 — GHSA-4cwx-7wf7-3272
direct, CVSS 7.4, fixed in 8.9.0
https://osv.dev/vulnerability/GHSA-4cwx-7wf7-3272 - website/package-lock.jsonundici 7.28.0 — GHSA-4cwx-7wf7-3272
direct, CVSS 7.4, fixed in 7.29.0
https://osv.dev/vulnerability/GHSA-4cwx-7wf7-3272 - pnpm-lock.yamlundici 7.29.0 — GHSA-w293-vg96-wgc3
direct, CVSS 7.4, fixed in 7.29.1
https://osv.dev/vulnerability/GHSA-w293-vg96-wgc3 - pnpm-lock.yamlundici 8.5.0 — GHSA-w293-vg96-wgc3
direct, CVSS 7.4, fixed in 8.10.2
https://osv.dev/vulnerability/GHSA-w293-vg96-wgc3 - website/package-lock.jsonundici 7.28.0 — GHSA-w293-vg96-wgc3
direct, CVSS 7.4, fixed in 7.29.1
https://osv.dev/vulnerability/GHSA-w293-vg96-wgc3 - website/package-lock.jsonsharp 0.34.5 — GHSA-f88m-g3jw-g9cj
direct, CVSS 7.0, fixed in 0.35.0
https://osv.dev/vulnerability/GHSA-f88m-g3jw-g9cj - pnpm-lock.yamlbraces 3.0.3 — GHSA-vfj7-8cjw-p6xm
transitive, CVSS 8.7
https://osv.dev/vulnerability/GHSA-vfj7-8cjw-p6xm - website/package-lock.jsonbraces 3.0.3 — GHSA-vfj7-8cjw-p6xm
transitive, CVSS 8.7
https://osv.dev/vulnerability/GHSA-vfj7-8cjw-p6xm - website/package-lock.jsonhttp-cache-semantics 4.2.0 — GHSA-ch52-4w7c-c8xp
transitive, CVSS 8.7
https://osv.dev/vulnerability/GHSA-ch52-4w7c-c8xp - remotion/pnpm-lock.yamlsource-map-js 1.2.1 — GHSA-68fv-2mgg-jv7q
transitive, CVSS 8.7, fixed in 1.2.2
https://osv.dev/vulnerability/GHSA-68fv-2mgg-jv7q - website/package-lock.jsonsource-map-js 1.2.1 — GHSA-68fv-2mgg-jv7q
transitive, CVSS 8.7, fixed in 1.2.2
https://osv.dev/vulnerability/GHSA-68fv-2mgg-jv7q - website/package-lock.jsondevalue 5.8.1 — GHSA-mcm9-63f2-9j32
transitive, CVSS 8.2, fixed in 5.9.3
https://osv.dev/vulnerability/GHSA-mcm9-63f2-9j32 - website/package-lock.jsondevalue 5.8.1 — GHSA-r9w8-h9r3-54w4
transitive, CVSS 8.2, fixed in 5.9.3
https://osv.dev/vulnerability/GHSA-r9w8-h9r3-54w4 - website/package-lock.jsondevalue 5.8.1 — GHSA-x5rw-q4pp-hg5g
transitive, CVSS 8.2, fixed in 5.9.3
https://osv.dev/vulnerability/GHSA-x5rw-q4pp-hg5g - remotion/pnpm-lock.yamlnanoid 3.3.12 — GHSA-28wg-ghj8-5hjv
transitive, CVSS 8.2, fixed in 3.3.16
https://osv.dev/vulnerability/GHSA-28wg-ghj8-5hjv - website/package-lock.jsonnanoid 3.3.15 — GHSA-28wg-ghj8-5hjv
transitive, CVSS 8.2, fixed in 3.3.16
https://osv.dev/vulnerability/GHSA-28wg-ghj8-5hjv - remotion/pnpm-lock.yamlnanoid 3.3.12 — GHSA-2v37-7h3g-55p8
transitive, CVSS 8.2, fixed in 3.3.18
https://osv.dev/vulnerability/GHSA-2v37-7h3g-55p8 - website/package-lock.jsonnanoid 3.3.15 — GHSA-2v37-7h3g-55p8
transitive, CVSS 8.2, fixed in 3.3.18
https://osv.dev/vulnerability/GHSA-2v37-7h3g-55p8 - website/package-lock.jsonsvgo 4.0.1 — GHSA-2p49-hgcm-8545
transitive, CVSS 8.2, fixed in 4.0.2
https://osv.dev/vulnerability/GHSA-2p49-hgcm-8545 - website/package-lock.jsonsvgo 4.0.1 — GHSA-w27v-7q3p-w38r
transitive, CVSS 8.2, fixed in 4.1.0
https://osv.dev/vulnerability/GHSA-w27v-7q3p-w38r - pnpm-lock.yamltoml 4.1.1 — GHSA-v5mp-jgw5-2x6j
transitive, CVSS 8.2, fixed in 4.1.2
https://osv.dev/vulnerability/GHSA-v5mp-jgw5-2x6j - pnpm-lock.yamlip-address 10.2.0 — GHSA-mwp4-54f8-5fhr
transitive, CVSS 7.7, fixed in 10.3.1
https://osv.dev/vulnerability/GHSA-mwp4-54f8-5fhr - pnpm-lock.yamlbrace-expansion 5.0.7 — GHSA-6j4f-fj2g-mc7p
transitive, CVSS 7.5, fixed in 5.0.10
https://osv.dev/vulnerability/GHSA-6j4f-fj2g-mc7p - pnpm-lock.yamlbrace-expansion 5.0.7 — GHSA-mh99-v99m-4gvg
transitive, CVSS 7.5, fixed in 5.0.8
https://osv.dev/vulnerability/GHSA-mh99-v99m-4gvg - pnpm-lock.yamlbrace-expansion 5.0.7 — GHSA-qhr7-859c-m2p7
transitive, CVSS 7.5, fixed in 5.0.11
https://osv.dev/vulnerability/GHSA-qhr7-859c-m2p7 - pnpm-lock.yamlbrace-expansion 5.0.7 — GHSA-rgw5-rvv9-x895
transitive, CVSS 7.5, fixed in 5.0.9
https://osv.dev/vulnerability/GHSA-rgw5-rvv9-x895 - remotion/pnpm-lock.yamlbrowserslist 4.28.2 — GHSA-73wf-gq98-2v4g
transitive, CVSS 7.5, fixed in 4.28.7
https://osv.dev/vulnerability/GHSA-73wf-gq98-2v4g - remotion/pnpm-lock.yamlbrowserslist 4.28.2 — GHSA-c83g-rgw3-j3cx
transitive, CVSS 7.5, fixed in 4.28.7
https://osv.dev/vulnerability/GHSA-c83g-rgw3-j3cx - website/package-lock.jsondevalue 5.8.1 — GHSA-j22f-vq7h-c4qm
transitive, CVSS 7.5, fixed in 5.9.3
https://osv.dev/vulnerability/GHSA-j22f-vq7h-c4qm - pnpm-lock.yamlfast-uri 3.1.2 — GHSA-4c8g-83qw-93j6
transitive, CVSS 7.5, fixed in 3.1.3
https://osv.dev/vulnerability/GHSA-4c8g-83qw-93j6 - remotion/pnpm-lock.yamlfast-uri 3.1.2 — GHSA-4c8g-83qw-93j6
transitive, CVSS 7.5, fixed in 3.1.3
https://osv.dev/vulnerability/GHSA-4c8g-83qw-93j6 - pnpm-lock.yamlfast-uri 3.1.2 — GHSA-7p8r-x3mc-p8w7
transitive, CVSS 7.5, fixed in 3.1.5
https://osv.dev/vulnerability/GHSA-7p8r-x3mc-p8w7 - remotion/pnpm-lock.yamlfast-uri 3.1.2 — GHSA-7p8r-x3mc-p8w7
transitive, CVSS 7.5, fixed in 3.1.5
https://osv.dev/vulnerability/GHSA-7p8r-x3mc-p8w7 - pnpm-lock.yamlfast-uri 3.1.2 — GHSA-f65p-4m7j-42xc
transitive, CVSS 7.5, fixed in 3.1.6
https://osv.dev/vulnerability/GHSA-f65p-4m7j-42xc - remotion/pnpm-lock.yamlfast-uri 3.1.2 — GHSA-f65p-4m7j-42xc
transitive, CVSS 7.5, fixed in 3.1.6
https://osv.dev/vulnerability/GHSA-f65p-4m7j-42xc - pnpm-lock.yamlfast-uri 3.1.2 — GHSA-fph4-wmhf-6fwf
transitive, CVSS 7.5, fixed in 3.1.6
https://osv.dev/vulnerability/GHSA-fph4-wmhf-6fwf - remotion/pnpm-lock.yamlfast-uri 3.1.2 — GHSA-fph4-wmhf-6fwf
transitive, CVSS 7.5, fixed in 3.1.6
https://osv.dev/vulnerability/GHSA-fph4-wmhf-6fwf - pnpm-lock.yamlfast-uri 3.1.2 — GHSA-jqff-g426-hqxp
transitive, CVSS 7.5, fixed in 3.1.6
https://osv.dev/vulnerability/GHSA-jqff-g426-hqxp - remotion/pnpm-lock.yamlfast-uri 3.1.2 — GHSA-jqff-g426-hqxp
transitive, CVSS 7.5, fixed in 3.1.6
https://osv.dev/vulnerability/GHSA-jqff-g426-hqxp - pnpm-lock.yamlfast-uri 3.1.2 — GHSA-qw65-cvwx-89v3
transitive, CVSS 7.5, fixed in 3.1.7
https://osv.dev/vulnerability/GHSA-qw65-cvwx-89v3
Maintenance
- recency
last commit 2 day(s) before 2026-10-16
- issue_responsiveness
median over 50 issue(s) in the last 6 months: 2.4 day(s) to first maintainer comment
- release_cadence
released 3 day(s) ago (by releases), inside `03` §5's 30-day window
- bus_factor
this repository's 12-month history is longer than the bounded walk `04` §7 performs, and the authors found so far span more than one of `03` §5's bands. A limit of ours, not a finding about the server.
- repository_signals
`03` §5 scores this by quartile WITHIN the server's category, which needs a distribution across every scanned server and so cannot be computed while scanning one. It is a post-pass over the crawl and that pass does not exist yet.
Transparency
- license
LICENSE present with no SPDX-License-Identifier tag; identified as 'Apache-2.0' from its text
- readme_quality
README.md: installation instructions (+25), configuration documented (+25), states what the server does in the first 200 words (+20)
- declared_scopes
specific scope or access documentation is present, but `03` §7's 100 and 60 bands both compare it against scopes INFERRED from static analysis, which is not built yet. Scoring 100 on an empty inference would be a vacuous truth — 'all ∅ inferred scopes are documented'.
- changelog
the only changelog is bin/CHANGELOG.md, inside a nested sub-project with its own package manifest — it describes that project, and whether it also covers this server cannot be told from the tree
- security_contact
docs/security.md present
Contest a finding
Every number here comes from a rule reading published source, and rules get things wrong. If something on this page is incorrect — or correct but mitigated by a check the rule cannot see — open an issue and we answer it in writing. A finding we got wrong is removed and the rule is fixed; one that stands is answered on the record.
Anyone can file: the maintainer, a researcher who disagrees with the classification, or a user who tested the server and could not reproduce it. Name this server and the rule or advisory ID shown beside the finding above. We aim to reply within five business days and to resolve within ten.