fix(gateway): restore default operator scopes for pure HTTP token auth#57596
Conversation
Greptile SummaryThis 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 Key changes:
One minor finding: On line 36 of Confidence Score: 5/5Safe 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.
|
| 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
🔒 Aisle Security AnalysisWe found 1 potential security issue(s) in this PR:
Vulnerabilities1. 🟠 Privilege escalation: missing x-openclaw-scopes header grants full CLI operator scopes
Description
Impact:
Vulnerable code: const raw = getHeader(req, OPERATOR_SCOPES_HEADER)?.trim();
if (raw === undefined) {
return [...CLI_DEFAULT_OPERATOR_SCOPES];
}RecommendationDo not derive effective authorization scopes from an attacker-controlled request header. Safer options:
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);
}
Additionally, consider validating/whitelisting scopes in the header against the known Analyzed PR: #57596 at commit Last updated on: 2026-03-30T09:44:55Z Latest run failed. Keeping previous successful results. Trace ID: Last updated on: 2026-03-30T16:00:24Z |
|
Pushed a follow-up commit to drop the dead For anyone reviewing: the core idea here is a three-way distinction on the
I deliberately used the The PoC test for the CVE bypass still asserts 403 when |
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 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 For context:
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. |
d80e939 to
8e5f4e7
Compare
obviyus
left a comment
There was a problem hiding this comment.
Reviewed latest changes; landing now.
Summary
/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 affectscurl, OpenAI SDKs, and other headless callers. The issue is located insrc/gateway/http-auth-helpers.tswhereresolveGatewayRequestedOperatorScopes()returns an empty array when thex-openclaw-scopesheader is missing.requiredOperatorMethod) for HTTP endpoints. However, the scope resolution logic assumed that a missingx-openclaw-scopesheader 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.resolveGatewayRequestedOperatorScopes()to returnCLI_DEFAULT_OPERATOR_SCOPESwhen thex-openclaw-scopesheader 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.src/gateway/http-auth-helpers.ts: ModifiedresolveGatewayRequestedOperatorScopesto return default scopes when the header is undefined/null.src/gateway/http-auth-helpers.test.ts: Added comprehensive unit tests forresolveGatewayRequestedOperatorScopes.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.message-handler.ts) and device pairing scope bindings remain completely untouched. The CVE security patch logic that preventsoperator.approvalsfrom bypassingoperator.writeremains fully intact when scopes are explicitly declared.Reproduction
x-openclaw-scopesheader:missing scope: operator.write).Risk / Mitigation
undefinedornullheader 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 tohttp-auth-helpers.test.tsto 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)
Scope (select all touched areas)
Linked Issue/PR
Fixes #57550