Skip to content

fix(gateway): restore default operator scopes for pure HTTP token auth#57596

Merged
obviyus merged 3 commits into
openclaw:mainfrom
openperf:fix-http-api-token-auth
Mar 30, 2026
Merged

fix(gateway): restore default operator scopes for pure HTTP token auth#57596
obviyus merged 3 commits into
openclaw:mainfrom
openperf:fix-http-api-token-auth

Conversation

@openperf

Copy link
Copy Markdown
Member

Summary

  • Problem: All HTTP API integrations (like /v1/chat/completions) using pure Bearer Token authentication are broken in v3.28+, returning {"ok":false,"error":{"type":"forbidden","message":"missing scope: operator.write"}}. This affects curl, OpenAI SDKs, and other headless callers. The issue is located in src/gateway/http-auth-helpers.ts where resolveGatewayRequestedOperatorScopes() returns an empty array when the x-openclaw-scopes header is missing.
  • Root Cause: The CVE-2026-32919 / CVE-2026-28473 security patches introduced strict method-scope gating (requiredOperatorMethod) for HTTP endpoints. However, the scope resolution logic assumed that a missing x-openclaw-scopes header meant "no scopes requested" (returning []). Pure HTTP Bearer Token callers do not send this header, so despite passing Gateway Token authentication, they were stripped of all operator privileges and subsequently blocked by the method-scope gate.
  • Fix: Updated resolveGatewayRequestedOperatorScopes() to return CLI_DEFAULT_OPERATOR_SCOPES when the x-openclaw-scopes header is completely absent. If the header is present but empty, it still returns [] to honor explicit scope restrictions. This restores the intended behavior where authenticated HTTP clients receive default operator privileges, while preserving the security boundary for callers that explicitly declare restricted scopes.
  • What changed:
    • src/gateway/http-auth-helpers.ts: Modified resolveGatewayRequestedOperatorScopes to return default scopes when the header is undefined/null.
    • src/gateway/http-auth-helpers.test.ts: Added comprehensive unit tests for resolveGatewayRequestedOperatorScopes.
    • src/gateway/server.openai-compatible-http-write-scope-bypass.poc.test.ts: Updated the missing-header test expectation from 403 to 200.
    • src/gateway/sessions-history-http.test.ts: Updated the missing-header test expectation from 403 to 200.
  • What did NOT change (scope boundary): The WebSocket connection auth logic (message-handler.ts) and device pairing scope bindings remain completely untouched. The CVE security patch logic that prevents operator.approvals from bypassing operator.write remains fully intact when scopes are explicitly declared.

Reproduction

  1. Start the OpenClaw gateway with token auth enabled.
  2. Send a pure HTTP request without the x-openclaw-scopes header:
    curl http://127.0.0.1:8080/v1/chat/completions \
      -H "Authorization: Bearer <TOKEN>" \
      -H "Content-Type: application/json" \
      -d '{"model": "openclaw", "messages": [{"role": "user", "content": "hi"}]}'
  3. Before fix: Returns 403 Forbidden (missing scope: operator.write).
  4. After fix: Returns 200 OK.

Risk / Mitigation

  • Risk: The fix might inadvertently grant excessive permissions to HTTP callers that were intentionally restricted.
  • Mitigation: The fix explicitly checks for undefined or null header values. If a caller sends an empty header (x-openclaw-scopes: ), it still resolves to an empty array [], honoring the explicit restriction. Comprehensive unit tests have been added to http-auth-helpers.test.ts to verify this exact parsing behavior, and existing PoC tests were updated to ensure the CVE bypass protection remains active for explicitly declared scopes.

Change Type (select all)

  • Bug fix

Scope (select all touched areas)

  • Gateway

Linked Issue/PR

Fixes #57550

@openperf openperf requested a review from a team as a code owner March 30, 2026 08:49
@openclaw-barnacle openclaw-barnacle Bot added gateway Gateway runtime size: S labels Mar 30, 2026
@greptile-apps

greptile-apps Bot commented Mar 30, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a regression introduced by the CVE-2026-32919 / CVE-2026-28473 security patches that broke all pure Bearer-token HTTP API integrations (curl, OpenAI SDKs, etc.) against /v1/chat/completions and other endpoints. The root cause was that resolveGatewayRequestedOperatorScopes() returned [] when the x-openclaw-scopes header was absent, causing every authenticated HTTP caller to fail the method-scope gate with missing scope: operator.write.

Key changes:

  • src/gateway/http-auth-helpers.ts: resolveGatewayRequestedOperatorScopes now returns a copy of CLI_DEFAULT_OPERATOR_SCOPES (all five operator scopes) when the header is absent, while still returning [] for a present-but-empty header and parsing normally otherwise. The three-way distinction (absent / empty / populated) correctly preserves the CVE boundary for callers that explicitly restrict their own scopes.
  • src/gateway/http-auth-helpers.test.ts: Thorough unit tests added covering all parsing branches including edge cases (whitespace-only, trailing commas, copy-not-reference check).
  • src/gateway/server.openai-compatible-http-write-scope-bypass.poc.test.ts and sessions-history-http.test.ts: Missing-header expectations updated from 403 → 200; the explicit-scope CVE bypass tests remain unchanged at 403, confirming the security boundary is intact.

One minor finding: On line 36 of http-auth-helpers.ts, the raw === null branch is dead code — getHeader() returns string | undefined and ?.trim() converts any undefined to undefined (never null), so raw is always string | undefined.

Confidence Score: 5/5

Safe to merge — the fix is correct, well-tested, and the only finding is a trivial dead-code check that doesn't affect behavior.

The logic correctly handles all three cases (absent header → default scopes, empty header → no scopes, populated header → parsed scopes). Tests are comprehensive, including copy-not-reference verification. The CVE security boundary for explicit scope declarations is verified to remain intact by the unchanged PoC test assertions. The sole finding (dead raw === null branch) is a P2 style nit with zero runtime impact.

No files require special attention beyond the minor dead-code note in src/gateway/http-auth-helpers.ts line 36.

Important Files Changed

Filename Overview
src/gateway/http-auth-helpers.ts Core fix: resolveGatewayRequestedOperatorScopes now returns [...CLI_DEFAULT_OPERATOR_SCOPES] when the x-openclaw-scopes header is absent, restoring default operator privileges for plain Bearer-token HTTP callers. Minor: the raw === null guard is dead code since getHeader returns string
src/gateway/http-auth-helpers.test.ts Adds comprehensive unit tests for resolveGatewayRequestedOperatorScopes covering: absent header (returns CLI defaults as a copy), empty/whitespace header (returns []), comma-separated scopes, whitespace trimming, and trailing/double commas.
src/gateway/server.openai-compatible-http-write-scope-bypass.poc.test.ts Updates the PoC CVE bypass test: missing-header case changed from expected 403 to 200 with a mock agentCommand payload. The original explicit-scope-bypass test (operator.approvals reaching a write-gated endpoint) still expects 403, preserving the CVE security boundary.
src/gateway/sessions-history-http.test.ts Updates the sessions-history integration test: missing-header request to a read-scoped endpoint changed from expected 403 to 200, consistent with the new default-scope behavior.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/gateway/http-auth-helpers.ts
Line: 36

Comment:
**Dead code: `raw === null` is unreachable**

`getHeader()` is typed to return `string | undefined` (never `null`), and the `?.trim()` optional-chain on line 35 converts any `undefined` result to `undefined` — it cannot produce `null`. Therefore `raw` is always `string | undefined`, making the `raw === null` branch permanently unreachable.

The comment in the PR description says "explicitly checks for `undefined` or `null`", but only the `=== undefined` arm can ever match. Leaving the dead arm in can confuse future readers into thinking `getHeader` might return `null`.

```suggestion
  if (raw === undefined) {
```

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "fix(gateway ): restore default operator ..." | Re-trigger Greptile

Comment thread src/gateway/http-auth-helpers.ts Outdated
@aisle-research-bot

aisle-research-bot Bot commented Mar 30, 2026

Copy link
Copy Markdown

🔒 Aisle Security Analysis

We found 1 potential security issue(s) in this PR:

# Severity Title
1 🟠 High Privilege escalation: missing x-openclaw-scopes header grants full CLI operator scopes
Vulnerabilities

1. 🟠 Privilege escalation: missing x-openclaw-scopes header grants full CLI operator scopes

Property Value
Severity High
CWE CWE-285
Location src/gateway/http-auth-helpers.ts:34-47

Description

resolveGatewayRequestedOperatorScopes() treats a missing x-openclaw-scopes header as a signal to grant CLI_DEFAULT_OPERATOR_SCOPES (which include operator.write and operator.admin). Because headers are controlled by the caller, an authenticated HTTP client can omit the header to obtain elevated operator scopes and pass authorizeOperatorScopesForMethod() checks.

Impact:

  • Any bearer-token authenticated HTTP request can bypass method-scope gating by simply omitting the header.
  • This effectively turns the scope gate into an opt-in restriction rather than an enforcement mechanism, enabling unauthorized access to write/admin endpoints.

Vulnerable code:

const raw = getHeader(req, OPERATOR_SCOPES_HEADER)?.trim();
if (raw === undefined) {
  return [...CLI_DEFAULT_OPERATOR_SCOPES];
}

Recommendation

Do not derive effective authorization scopes from an attacker-controlled request header.

Safer options:

  1. Default-deny when the header is absent (treat as no scopes), and require clients to explicitly request scopes:
export function resolveGatewayRequestedOperatorScopes(req: IncomingMessage): string[] {
  const raw = getHeader(req, OPERATOR_SCOPES_HEADER);
  if (raw === undefined) return []; // absent => no scopes
  const trimmed = raw.trim();
  if (!trimmed) return [];
  return trimmed.split(",").map(s => s.trim()).filter(Boolean);
}
  1. If certain clients should receive defaults, derive them from server-side trust signals (e.g., mTLS identity, trusted proxy auth mode, or token claims), not the presence/absence of a caller-controlled header.

Additionally, consider validating/whitelisting scopes in the header against the known OperatorScope set to prevent unrecognized scope strings from being treated as meaningful elsewhere.


Analyzed PR: #57596 at commit d80e939

Last updated on: 2026-03-30T09:44:55Z

Latest run failed. Keeping previous successful results. Trace ID: 019d3ee58cf290bc5dc0de29fa0796d4.

Last updated on: 2026-03-30T16:00:24Z

@openperf

Copy link
Copy Markdown
Member Author

Pushed a follow-up commit to drop the dead raw === null check per Greptile's review — good catch, getHeader() only ever returns string | undefined.

For anyone reviewing: the core idea here is a three-way distinction on the x-openclaw-scopes header:

  • Header absent → caller is a plain HTTP client (curl, OpenAI SDK, etc.) that doesn't know about scopes at all. We grant CLI_DEFAULT_OPERATOR_SCOPES, same as the CLI gets. This is what was broken.
  • Header present but empty → caller explicitly opted into zero scopes. We respect that and return []. This preserves the CVE-2026-32919 / CVE-2026-28473 security boundary.
  • Header present with values → parse as before.

I deliberately used the CLI_DEFAULT_OPERATOR_SCOPES constant rather than hardcoding scope strings, so if new scopes get added upstream they'll be picked up automatically. Also returning a spread copy ([...CLI_DEFAULT_OPERATOR_SCOPES]) to avoid anyone accidentally mutating the shared constant.

The PoC test for the CVE bypass still asserts 403 when operator.approvals is explicitly declared — that path is untouched.

@openperf

openperf commented Mar 30, 2026

Copy link
Copy Markdown
Member Author

🔒 Aisle Security Analysis

We found 1 potential security issue(s) in this PR:

Severity Title

1 🟠 High Privilege escalation: missing x-openclaw-scopes header grants full CLI operator scopes
Vulnerabilities

1. 🟠 Privilege escalation: missing x-openclaw-scopes header grants full CLI operator scopes

Property Value
Severity High
CWE CWE-285
Location src/gateway/http-auth-helpers.ts:34-47

Description

resolveGatewayRequestedOperatorScopes() treats a missing x-openclaw-scopes header as a signal to grant CLI_DEFAULT_OPERATOR_SCOPES (which include operator.write and operator.admin). Because headers are controlled by the caller, an authenticated HTTP client can omit the header to obtain elevated operator scopes and pass authorizeOperatorScopesForMethod() checks.

Impact:

  • Any bearer-token authenticated HTTP request can bypass method-scope gating by simply omitting the header.
  • This effectively turns the scope gate into an opt-in restriction rather than an enforcement mechanism, enabling unauthorized access to write/admin endpoints.

Vulnerable code:

const raw = getHeader(req, OPERATOR_SCOPES_HEADER)?.trim();
if (raw === undefined) {
  return [...CLI_DEFAULT_OPERATOR_SCOPES];
}

Recommendation

Do not derive effective authorization scopes from an attacker-controlled request header.

Safer options:

  1. Default-deny when the header is absent (treat as no scopes), and require clients to explicitly request scopes:
export function resolveGatewayRequestedOperatorScopes(req: IncomingMessage): string[] {
  const raw = getHeader(req, OPERATOR_SCOPES_HEADER);
  if (raw === undefined) return []; // absent => no scopes
  const trimmed = raw.trim();
  if (!trimmed) return [];
  return trimmed.split(",").map(s => s.trim()).filter(Boolean);
}
  1. If certain clients should receive defaults, derive them from server-side trust signals (e.g., mTLS identity, trusted proxy auth mode, or token claims), not the presence/absence of a caller-controlled header.

Additionally, consider validating/whitelisting scopes in the header against the known OperatorScope set to prevent unrecognized scope strings from being treated as meaningful elsewhere.

Analyzed PR: #57596 at commit d80e939

Last updated on: 2026-03-30T09:44:55Z

🔒 Aisle Security Analysis

We found 1 potential security issue(s) in this PR:

Severity Title

1 🟠 High Privilege escalation: missing x-openclaw-scopes header grants full CLI operator scopes
Vulnerabilities

1. 🟠 Privilege escalation: missing x-openclaw-scopes header grants full CLI operator scopes

Property Value
Severity High
CWE CWE-285
Location src/gateway/http-auth-helpers.ts:34-47

Description

resolveGatewayRequestedOperatorScopes() treats a missing x-openclaw-scopes header as a signal to grant CLI_DEFAULT_OPERATOR_SCOPES (which include operator.write and operator.admin). Because headers are controlled by the caller, an authenticated HTTP client can omit the header to obtain elevated operator scopes and pass authorizeOperatorScopesForMethod() checks.

Impact:

  • Any bearer-token authenticated HTTP request can bypass method-scope gating by simply omitting the header.
  • This effectively turns the scope gate into an opt-in restriction rather than an enforcement mechanism, enabling unauthorized access to write/admin endpoints.

Vulnerable code:

const raw = getHeader(req, OPERATOR_SCOPES_HEADER)?.trim();
if (raw === undefined) {
  return [...CLI_DEFAULT_OPERATOR_SCOPES];
}

Recommendation

Do not derive effective authorization scopes from an attacker-controlled request header.

Safer options:

  1. Default-deny when the header is absent (treat as no scopes), and require clients to explicitly request scopes:
export function resolveGatewayRequestedOperatorScopes(req: IncomingMessage): string[] {
  const raw = getHeader(req, OPERATOR_SCOPES_HEADER);
  if (raw === undefined) return []; // absent => no scopes
  const trimmed = raw.trim();
  if (!trimmed) return [];
  return trimmed.split(",").map(s => s.trim()).filter(Boolean);
}
  1. If certain clients should receive defaults, derive them from server-side trust signals (e.g., mTLS identity, trusted proxy auth mode, or token claims), not the presence/absence of a caller-controlled header.

Additionally, consider validating/whitelisting scopes in the header against the known OperatorScope set to prevent unrecognized scope strings from being treated as meaningful elsewhere.

Analyzed PR: #57596 at commit d80e939

Last updated on: 2026-03-30T09:44:55Z

Let me walk through why this finding doesn't represent an actual privilege escalation in practice.

The key thing Aisle's model is missing here is what has already happened before resolveGatewayRequestedOperatorScopes() is ever called. By the time we reach that function, the request has already passed through authorizeGatewayBearerRequestOrReply() — meaning the caller proved they hold a valid gateway Bearer Token. In OpenClaw's auth model, that token is the operator credential. There's no concept of a "low-privilege token" vs. a "high-privilege token" at the HTTP API layer — if you have the token, you're the operator, full stop.

So the scenario Aisle describes — "an attacker omits the header to gain elevated scopes" — requires the attacker to already possess the gateway secret. At that point they don't need to manipulate headers; they already own the box.

The x-openclaw-scopes header was never designed as a security boundary. It's a voluntary self-restriction mechanism: a caller who wants to run with reduced privileges (e.g., a read-only monitoring integration) can opt into that by sending the header. The CVE-2026-32919 / CVE-2026-28473 patches leveraged this mechanism for the WebSocket/UI path where device pairing injects scopes server-side — that's the actual security boundary, and our fix doesn't touch it.

For context:

  • Before v3.28 (pre-CVE patches): no method-scope gate existed on HTTP endpoints at all. Token auth = full access. That was the intended behavior.
  • v3.28+ (post-CVE): the scope gate was added but resolveGatewayRequestedOperatorScopes() returned [] for missing headers, accidentally breaking all headless HTTP callers. That's the regression this PR fixes.
  • Our fix: restores the pre-v3.28 effective behavior for authenticated HTTP callers while preserving the CVE security boundary for explicit scope declarations.

Aisle's second suggestion (deriving scopes from server-side trust signals like token claims) is genuinely a good long-term direction, but it would require changes to the token issuance and validation pipeline — well beyond the scope of a targeted regression fix.

tl;dr — the "attacker" in this scenario already has the keys to the kingdom. We're not granting them anything they didn't already have.

@obviyus obviyus self-assigned this Mar 30, 2026
@obviyus obviyus force-pushed the fix-http-api-token-auth branch from d80e939 to 8e5f4e7 Compare March 30, 2026 13:18

@obviyus obviyus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed latest changes; landing now.

@obviyus obviyus merged commit 9d5c523 into openclaw:main Mar 30, 2026
18 of 20 checks passed
@obviyus

obviyus commented Mar 30, 2026

Copy link
Copy Markdown
Contributor

Landed on main.

Thanks @openperf.

pgondhi987 pushed a commit to pgondhi987/openclaw that referenced this pull request Mar 31, 2026
pgondhi987 pushed a commit to pgondhi987/openclaw that referenced this pull request Mar 31, 2026
lovewanwan pushed a commit to lovewanwan/openclaw that referenced this pull request Apr 28, 2026
Tardisyuan pushed a commit to Tardisyuan/openclaw that referenced this pull request Apr 30, 2026
ogt-redknie pushed a commit to ogt-redknie/OPENX that referenced this pull request May 2, 2026
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 9, 2026
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gateway Gateway runtime size: S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: v3.28+ HTTP API broken: pure Token auth throws "missing scope: operator.write

2 participants