T05 · Unauthorized Access and Privilege Escalation
- Location
scripts/run.js:30- Finding
Caller-Controlled Selection of Local Agents Bypasses Intended Least-Privilege Boundaries
- Content
View full analysis
Vulnerability Details
File Location:
scripts/run.js, lines 30-84
Vulnerability Type: Unrestricted selection and invocation of locally configured agents
Risk Level: HighVulnerable Code
js function callAgent(agentId, query) { return new Promise((resolve, reject) => { const proc = spawn('openclaw', ['agent', '--agent', agentId, '--message', query, '--json'], { stdio: ['ignore', 'pipe', 'pipe'] }); let stdout = ''; let stderr = ''; proc.stdout.on('data', d => stdout += d); proc.stderr.on('data', d => stderr += d); proc.on('close', (code) => { if (code === 0) { try { const result = JSON.parse(stdout); resolve({ agentId, response: result.text || result.response || stdout, error: null }); } catch (e) { resolve({ agentId, response: null, error: `parse error: ${e.message}\nraw: ${stdout}` }); } } else { resolve({ agentId, response: null, error: `exit ${code}: ${stderr.trim()}` }); } }); proc.on('error', (err) => { resolve({ agentId, response: null, error: err.message }); }); }); } async function main() { try { const input = await readInput(); const { query, agents = ['strategist', 'data-analyst', 'finance', 'expert-coder', 'researcher'], synthesizer_agent = 'synthesizer' } = input; // Parallel calls to all expert agents const calls = agents.map(agentId => callAgent(agentId, query)); const results = await Promise.all(calls); // ... // Call synthesizer const synthResult = await callAgent(synthesizer_agent, synthPrompt);Technical Analysis
The
agentsarray andsynthesizer_agentidentifier are read directly from attacker-controlled JSON input. Each identifier is passed to the OpenClaw CLI through the--agentargument without an allowlist or an authorization check.The ...[truncated 1838 chars]
- Remediation
View remediation
Remediation Suggestions
- Define a server-side allowlist containing only the intended specialist agents and synthesizer.
- Reject unknown, malformed, empty, or duplicate agent identifiers before starting any child process.
- Do not allow callers to override
synthesizer_agentunless an explicit authorization policy permits it. - Apply per-caller authorization checks when different callers are permitted to access different agents.
- Run invoked agents with the minimum tools, credentials, filesystem access, and network permissions required for their assigned purpose.
- Record the authenticated caller, requested agent IDs, authorization decision, and invocation result in security audit logs.
- Keep use of
spawnwith a fixed executable and an argument array; do not replace it with shell-based command construction.
Example validation approach:
js const ALLOWED_EXPERTS = new Set([ 'strategist', 'data-analyst', 'finance', 'expert-coder', 'researcher' ]); const ALLOWED_SYNTHESIZERS = new Set(['synthesizer']); function validateAgentList(agents) { if (!Array.isArray(agents)) { throw new Error('agents must be an array'); } const uniqueAgents = [...new Set(agents)]; if (uniqueAgents.some(id => typeof id !== 'string' || !ALLOWED_EXPERTS.has(id))) { throw new Error('An unauthorized agent was requested'); } return uniqueAgents; }
