openclaw - ✅(Solved) Fix [Bug]: Shared gateway credential exposed in URL query parameter for Control UI media endpoint [1 pull requests, 3 comments, 4 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#77097Fetched 2026-05-05 05:52:14
View on GitHub
Comments
3
Participants
4
Timeline
5
Reactions
2
Author
Timeline (top)
commented ×3closed ×1cross-referenced ×1

Root Cause

This finding crosses the gateway operator-auth boundary because the leaked value is an OpenClaw-owned shared gateway credential that can be replayed to authenticate to operator-protected routes. SECURITY.md explicitly treats exposed OpenClaw-owned credentials as in-scope report material and does not exclude credential-disclosure bugs of this kind. The issue is narrower than originally written, though: the bundled Control UI already sets Referrer-Policy: no-referrer and uses noreferrer link handling, so the proven disclosure path is the credential-bearing request URL itself, not downstream Referer propagation from this route.

Fix Action

Fix / Workaround

The original report correctly identifies the credential-in-URL problem, but one claimed exposure path is overstated in the shipped UI. src/gateway/control-ui.ts:124-129 sets Referrer-Policy: no-referrer, document/video links use rel="noreferrer" in ui/src/ui/chat/grouped-render.ts:907-912 and ui/src/ui/chat/grouped-render.ts:928-933, and image preview opens use window.open(..., "noopener,noreferrer") in ui/src/ui/open-external-url.ts:62-75. Those mitigations do not fix the bug, but they do narrow it: the primary disclosure is the credential-bearing URL itself.

PR fix notes

PR #77111: fix: stop exposing auth tokens in assistant media URLs

Description (problem / solution / changelog)

Summary

  • Problem: Control UI assistant media URLs exposed live auth credentials in the token query parameter.
  • Why it matters: The gateway assistant-media route now accepts only bearer-header auth for Control UI read access, and the Control UI fetches local assistant attachments with authenticated fetch() requests that are converted into blob/object URLs for browser rendering and download flows.
  • What changed: The gateway assistant-media route now accepts only bearer-header auth for Control UI read access, and the Control UI fetches local assistant attachments with authenticated fetch() requests that are converted into blob/object URLs for browser rendering and download flows.
  • What did NOT change (scope boundary): No unrelated defaults, migrations, or compatibility behavior were intentionally changed.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor required for the fix
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

  • Closes #77097
  • Related #
  • This PR fixes a bug or regression

Root Cause (if applicable)

  • Root cause: The gateway assistant-media route now accepts only bearer-header auth for Control UI read access, and the Control UI fetches local assistant attachments with authenticated fetch() requests that are converted into blob/object URLs for browser rendering and download flows.
  • Missing detection / guardrail: No narrower detection note was recorded in the issue bundle.
  • Contributing context (if known): See the linked issue analysis and changed files below.

Regression Test Plan (if applicable)

  • Coverage level that should have caught this: Validation command pnpm test src/gateway/control-ui.http.test.ts && pnpm test ui/src/ui/chat/grouped-render.test.ts && pnpm exec oxfmt --check --threads=1 src/gateway/control-ui.ts src/gateway/control-ui.http.test.ts ui/src/ui/chat/grouped-render.ts ui/src/ui/chat/grouped-render.test.ts docs/web/control-ui.md CHANGELOG.md
  • Target test or file: Not explicitly recorded in the issue bundle.
  • Scenario the test should lock in: Control UI assistant media URLs exposed live auth credentials in the token query parameter.
  • Why this is the smallest reliable guardrail: It validates the failing behavior on the touched path without expanding scope.
  • Existing test that already covers this (if any): Unknown.
  • If no new test is added, why not: The staged bundle did not record a narrower regression target.

User-visible / Behavior Changes

  • None beyond resolving the linked issue's broken behavior.

Diagram (if applicable)

N/A

Security Impact (required)

  • New permissions/capabilities? (Yes/No): No
  • Secrets/tokens handling changed? (Yes/No): Yes
  • New/changed network calls? (Yes/No): Yes
  • Command/tool execution surface changed? (Yes/No): No
  • Data access scope changed? (Yes/No): No
  • If any Yes, explain risk + mitigation: The gateway assistant-media route now accepts only bearer-header auth for Control UI read access, and the Control UI fetches local assistant attachments with authenticated fetch() requests that are converted into blob/object URLs for browser rendering and download flows.. Mitigation: pnpm test src/gateway/control-ui.http.test.ts && pnpm test ui/src/ui/chat/grouped-render.test.ts && pnpm exec oxfmt --check --threads=1 src/gateway/control-ui.ts src/gateway/control-ui.http.test.ts ui/src/ui/chat/grouped-render.ts ui/src/ui/chat/grouped-render.test.ts docs/web/control-ui.md CHANGELOG.md reported passed; note: pnpm build currently fails on latest upstream/main in src/logging/diagnostic-session-context.ts with TS18048, reproduced in a fresh upstream clone.

Repro + Verification

Environment

  • OS: N/A
  • Runtime/container: N/A
  • Model/provider: github-copilot/gpt-5.4
  • Integration/channel (if any): N/A
  • Relevant config (redacted): AI-assisted=yes

Steps

  1. Reproduce the linked issue using the recorded issue bundle.
  2. Apply the fix from this branch.
  3. Run pnpm test src/gateway/control-ui.http.test.ts && pnpm test ui/src/ui/chat/grouped-render.test.ts && pnpm exec oxfmt --check --threads=1 src/gateway/control-ui.ts src/gateway/control-ui.http.test.ts ui/src/ui/chat/grouped-render.ts ui/src/ui/chat/grouped-render.test.ts docs/web/control-ui.md CHANGELOG.md.

Expected

  • Control UI assistant media URLs exposed live auth credentials in the token query parameter.
  • Validation passes for the touched behavior.

Actual

  • Validation status: passed; note: pnpm build currently fails on latest upstream/main in src/logging/diagnostic-session-context.ts with TS18048, reproduced in a fresh upstream clone

Evidence

  • Validation evidence: pnpm test src/gateway/control-ui.http.test.ts && pnpm test ui/src/ui/chat/grouped-render.test.ts && pnpm exec oxfmt --check --threads=1 src/gateway/control-ui.ts src/gateway/control-ui.http.test.ts ui/src/ui/chat/grouped-render.ts ui/src/ui/chat/grouped-render.test.ts docs/web/control-ui.md CHANGELOG.md reported passed; note: pnpm build currently fails on latest upstream/main in src/logging/diagnostic-session-context.ts with TS18048, reproduced in a fresh upstream clone.
  • CVSS v3.1: 7.5 (High)
  • CVSS v4.0: 8.7 (High)
  • Changed files:
  • CHANGELOG.md (+1/-0)
  • docs/web/control-ui.md (+8/-0)
  • src/gateway/control-ui.http.test.ts (+16/-9)
  • src/gateway/control-ui.ts (+3/-35)
  • ui/src/ui/chat/grouped-render.test.ts (+111/-49)
  • ui/src/ui/chat/grouped-render.ts (+287/-63)

Human Verification (required)

  • Verified scenarios: Control UI assistant media URLs exposed live auth credentials in the token query parameter.
  • Edge cases checked: pnpm test src/gateway/control-ui.http.test.ts && pnpm test ui/src/ui/chat/grouped-render.test.ts && pnpm exec oxfmt --check --threads=1 src/gateway/control-ui.ts src/gateway/control-ui.http.test.ts ui/src/ui/chat/grouped-render.ts ui/src/ui/chat/grouped-render.test.ts docs/web/control-ui.md CHANGELOG.md completed with status passed; note: pnpm build currently fails on latest upstream/main in src/logging/diagnostic-session-context.ts with TS18048, reproduced in a fresh upstream clone.
  • What you did not verify: Interactive/manual scenarios not captured in the staged issue bundle.

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

Compatibility / Migration

  • Backward compatible? (Yes/No): Yes
  • Config/env changes? (Yes/No): No
  • Migration needed? (Yes/No): No
  • If yes, exact upgrade steps: N/A

Risks and Mitigations

  • Risk: Regression in the touched behavior while closing the linked issue.
    • Mitigation: pnpm test src/gateway/control-ui.http.test.ts && pnpm test ui/src/ui/chat/grouped-render.test.ts && pnpm exec oxfmt --check --threads=1 src/gateway/control-ui.ts src/gateway/control-ui.http.test.ts ui/src/ui/chat/grouped-render.ts ui/src/ui/chat/grouped-render.test.ts docs/web/control-ui.md CHANGELOG.md reported passed; note: pnpm build currently fails on latest upstream/main in src/logging/diagnostic-session-context.ts with TS18048, reproduced in a fresh upstream clone and the changed-file scope stayed limited to the recorded diff.

Changed files

  • CHANGELOG.md (modified, +1/-0)
  • docs/web/control-ui.md (modified, +8/-0)
  • src/gateway/control-ui.http.test.ts (modified, +16/-9)
  • src/gateway/control-ui.ts (modified, +3/-35)
  • ui/src/ui/chat/grouped-render.test.ts (modified, +111/-49)
  • ui/src/ui/chat/grouped-render.ts (modified, +287/-63)

Code Example

function resolveAssistantMediaAuthToken(req: IncomingMessage): string | undefined {
  const bearer = getBearerToken(req);
  if (bearer) {
    return bearer;
  }
  const urlRaw = req.url;
  if (!urlRaw) {
    return undefined;
  }
  try {
    const url = new URL(urlRaw, "http://localhost");
    const token = url.searchParams.get("token")?.trim();
    return token || undefined;
  } catch {
    return undefined;
  }
}

---

export function resolveControlUiAuthToken(source: ControlUiAuthSource): string | null {
  return (
    sanitizeHeaderToken(normalizeOptionalString(source.hello?.auth?.deviceToken) ?? null) ??
    sanitizeHeaderToken(normalizeOptionalString(source.settings?.token) ?? null) ??
    sanitizeHeaderToken(normalizeOptionalString(source.password) ?? null) ??
    null
  );
}

function buildAssistantAttachmentUrl(
  source: string,
  basePath?: string,
  authToken?: string | null,
): string {
  if (!isLocalAssistantAttachmentSource(source)) {
    return source;
  }
  const normalizedBasePath =
    basePath && basePath !== "/" ? (basePath.endsWith("/") ? basePath.slice(0, -1) : basePath) : "";
  const params = new URLSearchParams({ source });
  const normalizedToken = authToken?.trim();
  if (normalizedToken) {
    params.set("token", normalizedToken);
  }
  return `${normalizedBasePath}/__openclaw__/assistant-media?${params.toString()}`;
}
RAW_BUFFERClick to expand / collapse

Severity Assessment

CVSS Assessment

Metricv3.1v4.0
Score7.5 / 10.08.7 / 10.0
SeverityHighHigh
VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:NCVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N
CalculatorCVSS v3.1 CalculatorCVSS v4.0 Calculator

Threat Model Alignment

Classification: security-specific

This finding crosses the gateway operator-auth boundary because the leaked value is an OpenClaw-owned shared gateway credential that can be replayed to authenticate to operator-protected routes. SECURITY.md explicitly treats exposed OpenClaw-owned credentials as in-scope report material and does not exclude credential-disclosure bugs of this kind. The issue is narrower than originally written, though: the bundled Control UI already sets Referrer-Policy: no-referrer and uses noreferrer link handling, so the proven disclosure path is the credential-bearing request URL itself, not downstream Referer propagation from this route.

Impact

The Control UI media route places live Control UI authentication material into the URL query string as ?token=<credential> on /__openclaw__/assistant-media. The client resolves this value from the active device token first, then falls back to the configured shared token or password, and the server accepts the same query value as either shared-secret auth or an operator device token on the media route.

That design exposes the credential anywhere the full request URL is recorded or surfaced, including top-level browser navigations to the media URL, copied/bookmarked links, and any gateway or reverse-proxy access logging that stores request targets.

Affected Component

Files:

  • src/gateway/control-ui.ts:232-248
  • src/gateway/control-ui.ts:264-352
  • src/gateway/control-ui.ts:441-487
  • ui/src/ui/control-ui-auth.ts:23-29
  • ui/src/ui/chat/grouped-render.ts:871-886
  • ui/src/ui/app-render.helpers.ts:49-53
function resolveAssistantMediaAuthToken(req: IncomingMessage): string | undefined {
  const bearer = getBearerToken(req);
  if (bearer) {
    return bearer;
  }
  const urlRaw = req.url;
  if (!urlRaw) {
    return undefined;
  }
  try {
    const url = new URL(urlRaw, "http://localhost");
    const token = url.searchParams.get("token")?.trim();
    return token || undefined;
  } catch {
    return undefined;
  }
}
export function resolveControlUiAuthToken(source: ControlUiAuthSource): string | null {
  return (
    sanitizeHeaderToken(normalizeOptionalString(source.hello?.auth?.deviceToken) ?? null) ??
    sanitizeHeaderToken(normalizeOptionalString(source.settings?.token) ?? null) ??
    sanitizeHeaderToken(normalizeOptionalString(source.password) ?? null) ??
    null
  );
}

function buildAssistantAttachmentUrl(
  source: string,
  basePath?: string,
  authToken?: string | null,
): string {
  if (!isLocalAssistantAttachmentSource(source)) {
    return source;
  }
  const normalizedBasePath =
    basePath && basePath !== "/" ? (basePath.endsWith("/") ? basePath.slice(0, -1) : basePath) : "";
  const params = new URLSearchParams({ source });
  const normalizedToken = authToken?.trim();
  if (normalizedToken) {
    params.set("token", normalizedToken);
  }
  return `${normalizedBasePath}/__openclaw__/assistant-media?${params.toString()}`;
}

The server enables allowQueryToken: true only for the assistant-media route, and the Control UI intentionally generates those URLs for local assistant attachment previews and downloads.

Technical Reproduction

  1. Run the current Control UI against a gateway that requires authentication.
  2. Authenticate normally so the UI has an active assistantAttachmentAuthToken; in current code this resolves from hello.auth.deviceToken, then settings.token, then password.
  3. Open a chat view that renders a local assistant attachment preview or download handled by __openclaw__/assistant-media.
  4. Observe the UI building URLs of the form: GET /__openclaw__/assistant-media?source=/path/to/file&token=<active-auth-token>
  5. Confirm the behavior in current tests:
    • ui/src/ui/chat/grouped-render.test.ts:811-868
    • ui/src/ui/chat/grouped-render.test.ts:1066-1169
    • src/gateway/control-ui.http.test.ts:320-487
  6. Any system that records that request target now sees the live bearer-equivalent credential in plaintext.

Demonstrated Impact

An attacker who obtains the leaked query token can replay it as a bearer-equivalent credential against Control UI read surfaces and, for shared-secret deployments, against other gateway routes that accept the same shared secret. The current route accepts either shared-secret credentials or paired operator device tokens from the token query parameter, so the URL exposes whichever active Control UI credential the browser is using.

The original report correctly identifies the credential-in-URL problem, but one claimed exposure path is overstated in the shipped UI. src/gateway/control-ui.ts:124-129 sets Referrer-Policy: no-referrer, document/video links use rel="noreferrer" in ui/src/ui/chat/grouped-render.ts:907-912 and ui/src/ui/chat/grouped-render.ts:928-933, and image preview opens use window.open(..., "noopener,noreferrer") in ui/src/ui/open-external-url.ts:62-75. Those mitigations do not fix the bug, but they do narrow it: the primary disclosure is the credential-bearing URL itself.

Environment

Re-verified against current upstream/main commit 616a4e978207649052b477894029a87727cb5fd9 after latest release v2026.5.2.

The Control UI binds to loopback by default, which narrows exposure in default deployments. The bug still exists in source and affects any deployment where the browser requests this route and the resulting URL is visible to browser or HTTP logging surfaces.

Remediation Advice

Do not remove the query-param fallback in isolation. The current Control UI depends on browser-managed <img>, <audio>, <video>, and <a href> loads, which cannot attach a custom Authorization header the way fetch() can.

A correct fix should replace URL-carried shared credentials with a browser-compatible auth mechanism, then remove query-token parsing from the route. Viable patterns include:

  • issuing a short-lived same-origin HttpOnly session/cookie for media requests after Control UI authentication, or
  • minting short-lived signed media URLs scoped to a single attachment/path rather than reusing the live device token or raw shared gateway secret.

After that replacement is in place, delete the ?token= fallback from resolveAssistantMediaAuthToken(), stop generating token query params in buildAssistantAttachmentUrl(), and update the Control UI and gateway tests that currently assert this behavior.

<!-- submission-marker:DE-lcf-token-url-exposure -->

extent analysis

TL;DR

Replace URL-carried shared credentials with a browser-compatible auth mechanism, such as issuing a short-lived same-origin HttpOnly session/cookie or minting short-lived signed media URLs.

Guidance

  1. Identify a suitable auth mechanism: Choose between issuing a short-lived same-origin HttpOnly session/cookie for media requests or minting short-lived signed media URLs scoped to a single attachment/path.
  2. Implement the new auth mechanism: Modify the Control UI to use the chosen auth mechanism after authentication, ensuring it is compatible with browser-managed loads.
  3. Remove query-token parsing: Update resolveAssistantMediaAuthToken() to no longer parse the token query parameter and modify buildAssistantAttachmentUrl() to stop generating token query params.
  4. Update tests: Reflect the changes in the Control UI and gateway tests that currently assert the old behavior.

Example

// Example of issuing a short-lived same-origin HttpOnly session/cookie
function setMediaAuthCookie(req: IncomingMessage, token: string) {
  const cookie = `media-auth=${token}; HttpOnly; Secure; SameSite=Strict; Max-Age=3600`;
  req.setHeader('Set-Cookie', cookie);
}

// Example of minting a short-lived signed media URL
function generateSignedMediaUrl(source: string, token: string) {
  const url = new URL(source, 'http://localhost');
  const signature = signUrl(url.toString(), token);
  return `${url.toString()}?signature=${signature}`;
}

Notes

The provided examples are simplified and may require additional implementation details, such as handling token expiration and revocation.

Recommendation

Apply a workaround by implementing a browser-compatible auth mechanism, such as issuing a short-lived same-origin HttpOnly session/cookie or minting short-lived signed media URLs, to replace the insecure query-token fallback.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - ✅(Solved) Fix [Bug]: Shared gateway credential exposed in URL query parameter for Control UI media endpoint [1 pull requests, 3 comments, 4 participants]