VYPR
High severity7.3NVD Advisory· Published Jun 1, 2026

CVE-2026-10281

CVE-2026-10281

Description

Enderfga claw-orchestrator up to 3.5.5 is vulnerable to missing authentication in its API endpoint, allowing remote attackers to control sessions.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Enderfga claw-orchestrator up to 3.5.5 is vulnerable to missing authentication in its API endpoint, allowing remote attackers to control sessions.

Vulnerability

A missing authentication vulnerability exists in the EmbeddedServer function within src/embedded-server.ts of Enderfga claw-orchestrator up to version 3.5.5. By default, when the OPENCLAW_SERVER_TOKEN environment variable is not set, the this.authToken is initialized to null, bypassing authentication checks for all critical API endpoints. This issue is identified as CWE-306 [3].

Exploitation

An attacker can exploit this vulnerability by starting the server without configuring the OPENCLAW_SERVER_TOKEN environment variable. Once the server is running in this vulnerable state, unauthenticated attackers, potentially remotely if the server binds to 0.0.0.0, can interact with the API endpoints. Proof-of-concept commands demonstrate listing, creating, and controlling arbitrary sessions, as well as making unauthenticated API calls [3].

Impact

Successful exploitation allows unauthenticated attackers to gain control over the claw-orchestrator's sessions. This includes the ability to list, create, stop, and control any session. Additionally, attackers can steal sensitive session history through specific API endpoints. If the server is exposed to the network, remote attackers can perform these actions [3].

Mitigation

This vulnerability is mitigated in Enderfga claw-orchestrator version 3.5.6, released on 2026-06-01 [4]. The fix, identified by commit d0b02a800aa0689d9428cc4cc170e0b6589fb2c3 [1], enforces authentication by default on all endpoints except /health. Users should upgrade to version 3.5.6 or later. The server now auto-generates a token and writes it to ~/.openclaw/server-token with mode 0o600 if OPENCLAW_SERVER_TOKEN is not explicitly set or disabled [1, 4].

AI Insight generated on Jun 1, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected products

2

Patches

1
d0b02a800aa0

fix(server): require auth by default (closes #61)

https://github.com/enderfga/claw-orchestratorGuian FangMay 11, 2026via nvd-ref
6 files changed · +217 27
  • bin/cli.ts+28 4 modified
    @@ -10,11 +10,35 @@
     
     import { Command } from 'commander';
     import { createRequire } from 'node:module';
    +import * as fs from 'node:fs';
    +import * as path from 'node:path';
    +import * as os from 'node:os';
     
     function getBaseUrl(): string {
       return process.env.CLAWO_API_URL || process.env.CLAUDE_CODE_API_URL || 'http://127.0.0.1:18796';
     }
     
    +/**
    + * Locate the auth token the embedded server requires (3.5.6+):
    + *   1. CLAWO_AUTH_TOKEN env (explicit override)
    + *   2. OPENCLAW_SERVER_TOKEN env (same env the server reads — handy when both
    + *      processes share the same shell)
    + *   3. ~/.openclaw/server-token file (the server writes this at startup)
    + * Returns null if nothing is found — caller falls through to an unauthenticated
    + * request, which the server will reject with 401 unless `OPENCLAW_SERVER_TOKEN=disabled`.
    + */
    +function getAuthToken(): string | null {
    +  const envToken = process.env.CLAWO_AUTH_TOKEN || process.env.OPENCLAW_SERVER_TOKEN;
    +  if (envToken && envToken !== 'disabled') return envToken;
    +  try {
    +    const filePath = path.join(os.homedir(), '.openclaw', 'server-token');
    +    const t = fs.readFileSync(filePath, 'utf-8').trim();
    +    return t || null;
    +  } catch {
    +    return null;
    +  }
    +}
    +
     function getCliVersion(): string {
       try {
         const _require = createRequire(import.meta.url);
    @@ -29,10 +53,10 @@ function getCliVersion(): string {
     // ─── HTTP Client ─────────────────────────────────────────────────────────────
     
     async function api(path: string, method = 'GET', body?: unknown): Promise<Record<string, unknown>> {
    -  const opts: RequestInit = {
    -    method,
    -    headers: { 'Content-Type': 'application/json' },
    -  };
    +  const headers: Record<string, string> = { 'Content-Type': 'application/json' };
    +  const token = getAuthToken();
    +  if (token) headers.Authorization = `Bearer ${token}`;
    +  const opts: RequestInit = { method, headers };
       if (body) opts.body = JSON.stringify(body);
       try {
         const base = getBaseUrl();
    
  • CHANGELOG.md+35 0 modified
    @@ -5,6 +5,41 @@ All notable changes to this project will be documented in this file.
     The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
     and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
     
    +## [3.5.6] - 2026-05-11
    +
    +### Fixed — embedded HTTP server auth-by-default (closes #61)
    +
    +The embedded HTTP server now requires authentication on every endpoint
    +except `/health`. Previously it ran unauthenticated unless `OPENCLAW_SERVER_TOKEN`
    +was explicitly set (CWE-306).
    +
    +| Mode | Trigger |
    +|---|---|
    +| **Auto-generate** (new default) | unset env var → server writes a fresh 32-byte token to `~/.openclaw/server-token` (mode 0600) at startup. |
    +| **Explicit token** (unchanged) | `OPENCLAW_SERVER_TOKEN=<value>` |
    +| **Disabled** (opt-out, new) | `OPENCLAW_SERVER_TOKEN=disabled` — single-user host only; logs a loud warning |
    +
    +Three ways to authenticate (all equivalent):
    +
    +1. `Authorization: Bearer <token>` header — for CLIs / scripts.
    +2. `clawo_auth=<token>` cookie — set automatically when a browser hits
    +   `/dashboard?token=<token>`. Subsequent same-origin fetches and
    +   `EventSource` connections inherit the cookie.
    +3. `?token=<token>` query string — the bootstrap path for the dashboard;
    +   the server upgrades it to the cookie on the same response.
    +
    +The `clawo` CLI now reads the token automatically (env vars
    +`CLAWO_AUTH_TOKEN` / `OPENCLAW_SERVER_TOKEN`, falling back to
    +`~/.openclaw/server-token`). The dashboard URL printed at server start
    +contains the token query — clicking it in a browser establishes the
    +cookie, after which the URL can be bookmarked at plain `/dashboard`.
    +
    +### Changed
    +
    +- 4 new tests in `src/__tests__/embedded-server.test.ts` cover the
    +  query-token → cookie handoff, cookie-only auth, the new auto-generate
    +  default, and the `disabled` sentinel.
    +
     ## [3.5.5] - 2026-05-11
     
     ### Added — three-agent autoloop architecture
    
  • CLAUDE.md+15 0 modified
    @@ -97,8 +97,23 @@ Follow this checklist for every release. Do not skip steps.
     
     ```bash
     npm run build && npm run lint && npm run format:check && npm run test   # Must all pass
    +
    +# Tarball + leak sweep (added after the 3.5.0-3.5.4 incident, see git history)
    +npm pack --dry-run                                                       # Inspect file list
    +grep -rEn '<personal-or-internal-string>' src configs skills bin scripts README.md CHANGELOG.md package.json  # Tree
    +git log --all -p -S '<personal-or-internal-string>'                      # History
     ```
     
    +The `build` script must clean `dist/` first (it does: `rm -rf dist && tsc && ...`).
    +Without that, source-tree refactors leave stale paths in the tarball and ship
    +content the new source already removed.
    +
    +**Hard rule on leak-fix releases**: if the patch is sanitising something
    +previously hard-coded, the commit message / CHANGELOG entry / release title /
    +release body **must NOT name what was leaked**, list affected versions, or
    +include words like "security" / "🔒" / "hard-coded" / "sanitize". Frame as a
    +bland refactor. The CHANGELOG entry ships in the tarball — same treatment.
    +
     ### 2. Version bump
     
     Update version in `package.json`. Follow semver:
    
  • package.json+1 1 modified
    @@ -1,6 +1,6 @@
     {
       "name": "@enderfga/claw-orchestrator",
    -  "version": "3.5.5",
    +  "version": "3.5.6",
       "description": "Claw Orchestrator — run Claude Code, Codex, Gemini, Cursor Agent, OpenCode and custom coding CLIs as one unified runtime for claw-style agent systems. Runs standalone, with first-class OpenClaw plugin support. Persistent sessions, multi-agent council, tool orchestration.",
       "type": "module",
       "main": "./dist/src/index.js",
    
  • src/embedded-server.ts+70 19 modified
    @@ -11,6 +11,7 @@ import * as http from 'node:http';
     import * as fs from 'node:fs';
     import * as path from 'node:path';
     import * as os from 'node:os';
    +import * as crypto from 'node:crypto';
     import { SessionManager } from './session-manager.js';
     import { sanitizeCwd, validateRegex } from './validation.js';
     import type { EffortLevel } from './types.js';
    @@ -51,23 +52,45 @@ export class EmbeddedServer {
         return recent.length <= this._rateLimit;
       }
     
    +  private _writeTokenFile(token: string): void {
    +    const tokenDir = path.join(os.homedir(), '.openclaw');
    +    try {
    +      if (!fs.existsSync(tokenDir)) fs.mkdirSync(tokenDir, { recursive: true });
    +      fs.writeFileSync(path.join(tokenDir, 'server-token'), token, { mode: 0o600 });
    +    } catch (err) {
    +      console.warn(`[embedded-server] failed to write server-token file: ${(err as Error).message}`);
    +    }
    +  }
    +
       async start(): Promise<number> {
    -    // Auth token: opt-in via OPENCLAW_SERVER_TOKEN env var.
    -    // When set, all requests (except /health) must include Authorization: Bearer <token>.
    -    // Default: no auth (localhost-only is the primary security boundary).
    +    // Auth token policy (changed in 3.5.6 — closes CWE-306 from issue #61):
    +    //
    +    //   default                    → auto-generate 32-byte random token,
    +    //                                  write to ~/.openclaw/server-token mode 0600.
    +    //                                  Required on every non-/health request via
    +    //                                  Bearer header OR `clawo_auth` cookie OR
    +    //                                  ?token=<v> query.
    +    //   OPENCLAW_SERVER_TOKEN=<v>  → use the explicit token (legacy behaviour).
    +    //   OPENCLAW_SERVER_TOKEN=disabled → opt out of auth entirely. Only safe on
    +    //                                  a single-user host; loud warning at start.
    +    //
    +    // The file is mode 0600 (owner-read-only). Same-user CLI + dashboard read it;
    +    // other users on the same box cannot. Browsers reach /dashboard via the
    +    // `?token=<v>` query once; the server replies with a Set-Cookie so subsequent
    +    // requests authenticate via the cookie.
         const envToken = process.env.OPENCLAW_SERVER_TOKEN;
    -    if (envToken) {
    +    if (envToken === 'disabled') {
    +      this.authToken = null;
    +      console.warn(
    +        '[embedded-server] OPENCLAW_SERVER_TOKEN=disabled — authentication is OFF. ' +
    +          'All endpoints are reachable to any process that can connect. Only safe on a trusted single-user host.',
    +      );
    +    } else if (envToken) {
           this.authToken = envToken;
    -      // Write token to file for CLI to read
    -      const tokenDir = path.join(os.homedir(), '.openclaw');
    -      try {
    -        if (!fs.existsSync(tokenDir)) fs.mkdirSync(tokenDir, { recursive: true });
    -        fs.writeFileSync(path.join(tokenDir, 'server-token'), this.authToken, { mode: 0o600 });
    -      } catch {
    -        /* best effort */
    -      }
    +      this._writeTokenFile(this.authToken);
         } else {
    -      this.authToken = null;
    +      this.authToken = crypto.randomBytes(32).toString('hex');
    +      this._writeTokenFile(this.authToken);
         }
     
         this._rateLimitCleanupTimer = setInterval(() => {
    @@ -95,9 +118,15 @@ export class EmbeddedServer {
           });
     
           this.server.listen(this.port, this.host, () => {
    -        console.log(
    -          `[embedded-server] Listening on http://${this.host}:${this.port}${this.authToken ? ' (auth enabled)' : ''}`,
    -        );
    +        if (this.authToken) {
    +          console.log(`[embedded-server] Listening on http://${this.host}:${this.port} (auth enabled)`);
    +          console.log(`[embedded-server] Token file: ${path.join(os.homedir(), '.openclaw', 'server-token')}`);
    +          console.log(
    +            `[embedded-server] Dashboard:  http://${this.host}:${this.port}/dashboard?token=${this.authToken}`,
    +          );
    +        } else {
    +          console.log(`[embedded-server] Listening on http://${this.host}:${this.port} (AUTH DISABLED)`);
    +        }
             resolve(this.port);
           });
         });
    @@ -143,14 +172,36 @@ export class EmbeddedServer {
         const url = new URL(req.url || '/', `http://localhost:${this.port}`);
         const path = url.pathname;
     
    -    // Bearer token auth (skip for health checks)
    +    // Auth: accept Bearer header, `clawo_auth` cookie, or `?token=` query
    +    // (auth-skip allow-list: /health for monitoring).
         if (this.authToken && path !== '/health') {
           const authHeader = req.headers.authorization || '';
    -      if (authHeader !== `Bearer ${this.authToken}`) {
    +      const queryToken = url.searchParams.get('token');
    +      const cookieHeader = req.headers.cookie || '';
    +      const cookieToken = /(?:^|;\s*)clawo_auth=([^;]+)/.exec(cookieHeader)?.[1];
    +
    +      const bearerOk = authHeader === `Bearer ${this.authToken}`;
    +      const queryOk = queryToken === this.authToken;
    +      const cookieOk = cookieToken === this.authToken;
    +
    +      if (!bearerOk && !queryOk && !cookieOk) {
             res.writeHead(401, { 'Content-Type': 'application/json' });
    -        res.end(JSON.stringify({ ok: false, error: 'Unauthorized — provide Authorization: Bearer <token>' }));
    +        res.end(
    +          JSON.stringify({
    +            ok: false,
    +            error: 'Unauthorized',
    +            hint: 'Send Authorization: Bearer <token> (token at ~/.openclaw/server-token), or visit /dashboard?token=<token> in a browser to set the cookie.',
    +          }),
    +        );
             return;
           }
    +
    +      // First-touch via query token → persist as cookie so subsequent same-origin
    +      // requests (including EventSource) authenticate without exposing the token
    +      // in URLs / referrers / access logs.
    +      if (queryOk && !cookieOk) {
    +        res.setHeader('Set-Cookie', `clawo_auth=${this.authToken}; HttpOnly; SameSite=Strict; Path=/; Max-Age=86400`);
    +      }
         }
     
         // Rate limiting
    
  • src/__tests__/embedded-server.test.ts+68 3 modified
    @@ -5,7 +5,7 @@
      * and verify responses. SessionManager methods are mocked.
      */
     
    -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
    +import { describe, it, expect, vi, beforeEach, afterEach, afterAll } from 'vitest';
     import * as http from 'node:http';
     import * as net from 'node:net';
     import { EmbeddedServer } from '../embedded-server.js';
    @@ -134,8 +134,11 @@ describe('EmbeddedServer', () => {
       let port: number;
     
       beforeEach(async () => {
    -    // Clear env vars to prevent interference
    -    delete process.env.OPENCLAW_SERVER_TOKEN;
    +    // 3.5.6+: server auto-generates a token by default. Most routing /
    +    // rate-limit / CORS tests don't care about auth and shouldn't have to
    +    // know about it, so opt out for these tests via the explicit sentinel.
    +    // Auth-specific tests below override this env back to a literal token.
    +    process.env.OPENCLAW_SERVER_TOKEN = 'disabled';
         delete process.env.OPENCLAW_RATE_LIMIT;
         delete process.env.OPENCLAW_CORS_ORIGINS;
     
    @@ -144,6 +147,10 @@ describe('EmbeddedServer', () => {
         server = new EmbeddedServer(manager, port);
       });
     
    +  afterAll(() => {
    +    delete process.env.OPENCLAW_SERVER_TOKEN;
    +  });
    +
       afterEach(async () => {
         await server.stop();
       });
    @@ -214,6 +221,64 @@ describe('EmbeddedServer', () => {
           });
           expect(res.status).toBe(401);
         });
    +
    +    it('accepts ?token=<v> query param and sets cookie for follow-up requests', async () => {
    +      process.env.OPENCLAW_SERVER_TOKEN = 'secret-token';
    +      port = await getFreePort();
    +      server = new EmbeddedServer(manager, port);
    +      await server.start();
    +
    +      const res = await request(port, '/health?token=secret-token');
    +      expect(res.status).toBe(200);
    +      // /health skips auth, no cookie expected. Hit a real route to verify
    +      // the query→cookie handoff.
    +      const res2 = await request(port, '/agents?token=secret-token');
    +      expect(res2.status).toBe(200);
    +      const setCookie = res2.headers['set-cookie'];
    +      const cookieStr = Array.isArray(setCookie) ? setCookie[0] : setCookie;
    +      expect(cookieStr).toBeDefined();
    +      expect(String(cookieStr)).toMatch(/clawo_auth=secret-token/);
    +      expect(String(cookieStr)).toMatch(/HttpOnly/);
    +      expect(String(cookieStr)).toMatch(/SameSite=Strict/);
    +    });
    +
    +    it('accepts clawo_auth cookie in lieu of Bearer header', async () => {
    +      process.env.OPENCLAW_SERVER_TOKEN = 'secret-token';
    +      port = await getFreePort();
    +      server = new EmbeddedServer(manager, port);
    +      await server.start();
    +
    +      const res = await request(port, '/session/list', {
    +        method: 'POST',
    +        body: {},
    +        headers: { Cookie: 'clawo_auth=secret-token' },
    +      });
    +      expect(res.status).toBe(200);
    +    });
    +
    +    it('auto-generates a token by default (no env var set)', async () => {
    +      delete process.env.OPENCLAW_SERVER_TOKEN;
    +      port = await getFreePort();
    +      server = new EmbeddedServer(manager, port);
    +      await server.start();
    +
    +      // No token sent → 401
    +      const res = await request(port, '/session/list', { method: 'POST', body: {} });
    +      expect(res.status).toBe(401);
    +      // /health still works
    +      const health = await request(port, '/health');
    +      expect(health.status).toBe(200);
    +    });
    +
    +    it('OPENCLAW_SERVER_TOKEN=disabled disables auth entirely', async () => {
    +      process.env.OPENCLAW_SERVER_TOKEN = 'disabled';
    +      port = await getFreePort();
    +      server = new EmbeddedServer(manager, port);
    +      await server.start();
    +
    +      const res = await request(port, '/session/list', { method: 'POST', body: {} });
    +      expect(res.status).toBe(200);
    +    });
       });
     
       describe('rate limiting', () => {
    

Vulnerability mechanics

Root cause

"The embedded HTTP server defaulted to having no authentication enabled."

Attack vector

An attacker can initiate requests remotely to the API endpoint without any authentication if the server is not configured with an authentication token. This allows unauthenticated attackers to list, create, stop, and control arbitrary sessions, and to steal sensitive session history via grep endpoints [ref_id=3]. The vulnerability is present when the OPENCLAW_SERVER_TOKEN environment variable is not configured, leading to `this.authToken = null` [ref_id=3].

Affected code

The vulnerability resides in the `EmbeddedServer` class within the `src/embedded-server.ts` file. Specifically, the `start` method's logic for handling the `OPENCLAW_SERVER_TOKEN` environment variable and the subsequent authentication checks in the request handling logic are affected [ref_id=1, ref_id=3].

What the fix does

The patch modifies the authentication policy for the embedded server. By default, it now auto-generates a 32-byte random token and writes it to a file in the user's home directory. This token is required on every non-/health request via a Bearer header, `clawo_auth` cookie, or a `?token=` query parameter. This change ensures that authentication is enabled by default, closing the missing authentication vulnerability [ref_id=1].

Preconditions

  • configThe `OPENCLAW_SERVER_TOKEN` environment variable is not set or is explicitly set to `disabled`.
  • networkThe server is accessible over the network.

Reproduction

# POC (Proof of Concept) 2.1. Trigger Vulnerable State Start the server without setting OPENCLAW_SERVER_TOKEN (default vulnerable configuration) [ref_id=3] 2.2. Unauthenticated Attack Commands # List all sessions without authentication curl http://target:18796/session/list

# Create malicious session curl -X POST http://target:18796/session/start \ -H "Content-Type: application/json" \ -d '{"name":"evil-session","cwd":"/tmp","engine":"claude"}'

# Abuse API quota without permission curl -X POST http://target:18796/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{"model":"opus","messages":[{"role":"user","content":"costly request"}]}' [ref_id=3]

Generated on Jun 1, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

7

News mentions

0

No linked articles in our index yet.