Summary
Probus spawns Claude Code agents with permissionMode: 'bypassPermissions' while exposing an Express HTTP server on localhost with no authentication, CORS policy, or CSRF protection. Any webpage a developer opens in their browser can make cross-origin requests to http://127.0.0.1:<port>/api/scans and trigger a full agent run — giving an attacker unrestricted read/write access to the developer's entire filesystem and the ability to execute arbitrary shell commands on the host.
This is a complete workstation compromise vector that activates the moment a developer runs probus and visits any malicious website.
Verdict from review panel: REJECT-UNTIL-REMEDIATED (4 reviewers, Supreme Judge, unanimous P0)
Affected Files
| File |
Lines |
Issue |
src/claude-agent.ts |
L57 |
permissionMode: 'bypassPermissions' passed unconditionally |
src/server/chat-agent.ts |
L56 |
Same bypass in the chat/fix agent |
src/server/index.ts |
L16, L94 |
Server binds to 127.0.0.1 with no CORS/CSRF middleware |
src/server/routes.ts |
L128–L296 |
All scan-start and key-write routes are unauthenticated |
Root Cause — Code Evidence
1. bypassPermissions in scanner agent (src/claude-agent.ts, line 57):
const options: Options = {
cwd,
model,
env: { ...process.env, ...env } as Record<string, string | undefined>,
abortController,
includePartialMessages: true,
permissionMode: 'bypassPermissions', // ← grants full host access
settingSources: [],
};
2. bypassPermissions in chat/fix agent (src/server/chat-agent.ts, line 56):
const options: Options = {
cwd,
model: runtime.modelForSDK,
env: { ...process.env, ...runtime.env } as Record<string, string | undefined>,
abortController,
includePartialMessages: true,
permissionMode: 'bypassPermissions', // ← same bypass
settingSources: [],
};
3. Express server — no CORS, no auth, no CSRF (src/server/index.ts, line 29–34):
const app = express();
app.disable('etag');
const api = createApiRouter();
app.use('/api', api);
// ↑ No cors(), no CSRF token middleware, no Origin check
4. Scan-start route — public, no auth (src/server/routes.ts, lines 246–296):
router.post('/scans', (req, res) => {
// No token, no Origin check — any cross-origin request from browser can start a scan
const runner = registry.start({ ... });
res.json({ ok: true, slug: runner.slug, state: runner.snapshot });
});
Attack Scenario
- Developer runs
probus — server starts on http://127.0.0.1:9090.
- Developer visits
https://attacker.example.
- Attacker page runs:
fetch('http://127.0.0.1:9090/api/scans', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ repoPath: '/Users/victim', provider: 'anthropic', effort: 'high' })
});
- The
bypassPermissions agent spawns, reads ~/.ssh/id_rsa, ~/.aws/credentials, source code, secrets — and can write files or execute shell commands anywhere on the host.
DNS Rebinding variant: An attacker can also cycle DNS records to bypass browser same-origin policy entirely, removing even the browser's implicit loopback guard.
Remediation Steps
Step 1 — Generate a startup secret token
In src/index.ts (before startServer), generate a cryptographic token:
import { randomBytes } from 'node:crypto';
const PROBUS_TOKEN = randomBytes(32).toString('hex');
Step 2 — Validate token + Origin on every API request
Add middleware in src/server/index.ts before mounting createApiRouter():
const ALLOWED_ORIGINS = new Set(['http://127.0.0.1', 'http://localhost']);
app.use('/api', (req, res, next) => {
const origin = req.headers.origin ?? '';
const host = req.headers.host ?? '';
const token = req.headers['x-probus-token'] ?? req.query['token'];
// Block DNS rebinding: Host must be 127.0.0.1 or localhost
if (!host.startsWith('127.0.0.1') && !host.startsWith('localhost')) {
return res.status(403).json({ error: 'Forbidden: invalid Host' });
}
// Block CSRF: if Origin header is present it must be loopback
if (origin && !ALLOWED_ORIGINS.has(new URL(origin).origin)) {
return res.status(403).json({ error: 'Forbidden: cross-origin request' });
}
// Token check for state-mutating requests
if (['POST', 'PUT', 'DELETE', 'PATCH'].includes(req.method) && token !== PROBUS_TOKEN) {
return res.status(403).json({ error: 'Forbidden: missing or invalid token' });
}
next();
});
Step 3 — Remove bypassPermissions; use explicit tool allowlists
Replace permissionMode: 'bypassPermissions' with a curated tool allowlist in both src/claude-agent.ts and src/server/chat-agent.ts:
// Instead of:
permissionMode: 'bypassPermissions',
// Use:
permissionMode: 'default',
allowedTools: ['Read', 'Grep', 'Glob', 'LS'], // scanner: read-only
// For chat-agent (fix workflow), add only 'Edit', 'Write', 'Bash'
// and constrain cwd to the target repo path
Step 4 — Consider containerisation
For production use, spawn agents inside a Docker container with no network access and a bind-mounted read-only view of the target repository.
References
Summary
Probus spawns Claude Code agents with
permissionMode: 'bypassPermissions'while exposing an Express HTTP server on localhost with no authentication, CORS policy, or CSRF protection. Any webpage a developer opens in their browser can make cross-origin requests tohttp://127.0.0.1:<port>/api/scansand trigger a full agent run — giving an attacker unrestricted read/write access to the developer's entire filesystem and the ability to execute arbitrary shell commands on the host.This is a complete workstation compromise vector that activates the moment a developer runs
probusand visits any malicious website.Verdict from review panel: REJECT-UNTIL-REMEDIATED (4 reviewers, Supreme Judge, unanimous P0)
Affected Files
src/claude-agent.tspermissionMode: 'bypassPermissions'passed unconditionallysrc/server/chat-agent.tssrc/server/index.ts127.0.0.1with no CORS/CSRF middlewaresrc/server/routes.tsRoot Cause — Code Evidence
1. bypassPermissions in scanner agent (
src/claude-agent.ts, line 57):2. bypassPermissions in chat/fix agent (
src/server/chat-agent.ts, line 56):3. Express server — no CORS, no auth, no CSRF (
src/server/index.ts, line 29–34):4. Scan-start route — public, no auth (
src/server/routes.ts, lines 246–296):Attack Scenario
probus— server starts onhttp://127.0.0.1:9090.https://attacker.example.bypassPermissionsagent spawns, reads ~/.ssh/id_rsa, ~/.aws/credentials, source code, secrets — and can write files or execute shell commands anywhere on the host.DNS Rebinding variant: An attacker can also cycle DNS records to bypass browser same-origin policy entirely, removing even the browser's implicit loopback guard.
Remediation Steps
Step 1 — Generate a startup secret token
In
src/index.ts(beforestartServer), generate a cryptographic token:Step 2 — Validate token + Origin on every API request
Add middleware in
src/server/index.tsbefore mountingcreateApiRouter():Step 3 — Remove bypassPermissions; use explicit tool allowlists
Replace
permissionMode: 'bypassPermissions'with a curated tool allowlist in bothsrc/claude-agent.tsandsrc/server/chat-agent.ts:Step 4 — Consider containerisation
For production use, spawn agents inside a Docker container with no network access and a bind-mounted read-only view of the target repository.
References