Skip to content

[P0][SECURITY] Unauthenticated loopback RCE via bypassPermissions + missing CORS/CSRF guards #1

Description

@pratik-saptarshi

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

  1. Developer runs probus — server starts on http://127.0.0.1:9090.
  2. Developer visits https://attacker.example.
  3. 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' })
});
  1. 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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions