fix(gateway): require auth for control UI avatar route#69775
Conversation
🔒 Aisle Security AnalysisWe found 2 potential security issue(s) in this PR:
1. 🟠 Bearer token sent to arbitrary same-origin paths via unvalidated avatarUrl in refreshChatAvatar
DescriptionThe Control UI chat avatar refresh logic performs an authenticated secondary fetch to whatever
Vulnerable code: if (!authHeader || !isLocalControlUiAvatarUrl(avatarUrl)) {
setChatAvatarUrl(host, avatarUrl);
return;
}
const avatarRes = await fetch(avatarUrl, {
method: "GET",
headers: { Authorization: authHeader },
});
const blobUrl = URL.createObjectURL(await avatarRes.blob());
setChatAvatarUrl(host, blobUrl);RecommendationConstrain the authenticated follow-up fetch to a strict allowlist of expected avatar endpoints, instead of any path starting with Suggested approach:
Example: function isAllowedAvatarPath(basePath: string, avatarUrl: string): boolean {
const base = normalizeBasePath(basePath) || "";
const u = new URL(avatarUrl, window.location.origin);
if (u.origin !== window.location.origin) return false;
const allowedPrefix = `${base}/avatar/`;
return u.pathname.startsWith(allowedPrefix);
}
// ...
if (authHeader && isAllowedAvatarPath(host.basePath, avatarUrl)) {
const avatarRes = await fetch(avatarUrl, { headers: { Authorization: authHeader } });
// ...
} else {
setChatAvatarUrl(host, avatarUrl);
}Additionally, consider changing the meta response to return only an agent id or a fixed avatar route (not an arbitrary URL) when authentication is required, to avoid client-side token forwarding entirely. 2. 🟡 Control UI embeds auth token in assistant-media URLs (token in query string)
DescriptionThe Control UI now appends the gateway auth token as a This is a sensitive-data-in-URL issue:
Vulnerable code: const params = new URLSearchParams({ source });
const normalizedToken = authToken?.trim();
if (normalizedToken) {
params.set("token", normalizedToken);
}
return `${normalizedBasePath}/__openclaw__/assistant-media?${params.toString()}`;RecommendationAvoid putting authentication tokens in URLs. Preferred options:
Example sketch: const res = await fetch(metaOrMediaUrlWithoutToken, {
headers: { Authorization: `Bearer ${token}` },
credentials: "same-origin",
});
const blob = await res.blob();
const objectUrl = URL.createObjectURL(blob);
img.src = objectUrl;
Additionally:
Analyzed PR: #69775 at commit Last updated on: 2026-04-21T19:42:55Z |
Greptile SummaryThis PR closes a real security gap:
Confidence Score: 5/5Safe to merge — the security fix is correctly implemented and no P0/P1 issues were found. The core auth gate is correctly applied to both the meta and bytes code paths, server wiring mirrors the established assistant-media pattern, and client token propagation handles all URL schemes correctly. The one new inline finding is a brief broken-avatar flash during initial render (P2 UX, not a security concern). Previously surfaced style concerns (relative URL, helper duplication) are not repeated here. No files require special attention for correctness. Prompt To Fix All With AIThis is a comment left during a code review.
Path: ui/src/ui/views/chat.ts
Line: 741-750
Comment:
**Briefly broken avatar on initial render when auth is enabled**
`resolveAgentAvatarUrl` prefers `avatarUrl` over `avatar`. On the very first render (before `refreshChatAvatar` completes), `props.assistantAvatarUrl` is `null` so it falls back to `props.assistantAvatar` — the raw bootstrap-config URL without a token. With the avatar route now requiring auth, the browser's `<img>` request for `/avatar/<id>` receives a 401 until the first refresh cycle sets `chatAvatarUrl`. The result is a brief broken-avatar flash in the welcome state whenever auth is enabled.
A lightweight mitigation is to eagerly populate `host.chatAvatarUrl` with the token-appended raw avatar URL at the start of `refreshChatAvatar` (before the async fetch), using the already-available `resolveChatAvatarAuthToken` result, so the very first render already sends a credentialed URL.
How can I resolve this? If you propose a fix, please make it concise.Reviews (2): Last reviewed commit: "chore: add changelog for control UI avat..." | Re-trigger Greptile |
639c958 to
5951299
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 59512990c0
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
5951299 to
6905f2a
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6905f2a3fc
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
6905f2a to
9bc8365
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: cf3d97499b
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
cf3d974 to
792ba13
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 031412a032
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
* fix(gateway): require auth for control UI avatar route * chore: add changelog for control UI avatar auth * fix(control-ui): honor device auth for avatar urls * fix(control-ui): avoid query tokens for avatar auth * fix(control-ui): render authenticated avatar blob URLs in chat views * fix(control-ui): restore normalizeOptionalString import in render helpers
* fix(gateway): require auth for control UI avatar route * chore: add changelog for control UI avatar auth * fix(control-ui): honor device auth for avatar urls * fix(control-ui): avoid query tokens for avatar auth * fix(control-ui): render authenticated avatar blob URLs in chat views * fix(control-ui): restore normalizeOptionalString import in render helpers
* fix(gateway): require auth for control UI avatar route * chore: add changelog for control UI avatar auth * fix(control-ui): honor device auth for avatar urls * fix(control-ui): avoid query tokens for avatar auth * fix(control-ui): render authenticated avatar blob URLs in chat views * fix(control-ui): restore normalizeOptionalString import in render helpers
* fix(gateway): require auth for control UI avatar route * chore: add changelog for control UI avatar auth * fix(control-ui): honor device auth for avatar urls * fix(control-ui): avoid query tokens for avatar auth * fix(control-ui): render authenticated avatar blob URLs in chat views * fix(control-ui): restore normalizeOptionalString import in render helpers
* fix(gateway): require auth for control UI avatar route * chore: add changelog for control UI avatar auth * fix(control-ui): honor device auth for avatar urls * fix(control-ui): avoid query tokens for avatar auth * fix(control-ui): render authenticated avatar blob URLs in chat views * fix(control-ui): restore normalizeOptionalString import in render helpers
* fix(gateway): require auth for control UI avatar route * chore: add changelog for control UI avatar auth * fix(control-ui): honor device auth for avatar urls * fix(control-ui): avoid query tokens for avatar auth * fix(control-ui): render authenticated avatar blob URLs in chat views * fix(control-ui): restore normalizeOptionalString import in render helpers
* fix(gateway): require auth for control UI avatar route * chore: add changelog for control UI avatar auth * fix(control-ui): honor device auth for avatar urls * fix(control-ui): avoid query tokens for avatar auth * fix(control-ui): render authenticated avatar blob URLs in chat views * fix(control-ui): restore normalizeOptionalString import in render helpers
* fix(gateway): require auth for control UI avatar route * chore: add changelog for control UI avatar auth * fix(control-ui): honor device auth for avatar urls * fix(control-ui): avoid query tokens for avatar auth * fix(control-ui): render authenticated avatar blob URLs in chat views * fix(control-ui): restore normalizeOptionalString import in render helpers
fix(gateway): require auth for control UI avatar route
Summary
Describe the problem and fix in 2–5 bullets:
If this PR fixes a plugin beta-release blocker, title it
fix(<plugin-id>): beta blocker - <summary>and link the matchingBeta blocker: <plugin-name> - <summary>issue labeledbeta-blocker. Contributors cannot label PRs, so the title is the PR-side signal for maintainers and automation.?meta=1avatar metadata without running the gateway auth path when auth was enabled.Change Type (select all)
Scope (select all touched areas)
Linked Issue/PR
Root Cause (if applicable)
For bug fixes or regressions, explain why this happened, not just what changed. Otherwise write
N/A. If the cause is unclear, writeUnknown.handleControlUiAvatarRequest(...)applied security headers and served avatar data directly, but unlike the assistant-media route it never ran gateway auth or trusted-proxy scope checks./avatar/<agentId>, so the client also needed to add auth to those follow-up requests once the server enforced it.Regression Test Plan (if applicable)
For bug fixes or regressions, name the smallest reliable test coverage that should catch this. Otherwise write
N/A.src/gateway/control-ui.http.test.ts,ui/src/ui/app-chat.test.ts,ui/src/ui/chat/grouped-render.test.tsUser-visible / Behavior Changes
Diagram (if applicable)
Security Impact (required)
Yes/No) NoYes/No) YesYes/No) NoYes/No) NoYes/No) NoYes, explain risk + mitigation: The UI now appends the existing gateway auth token to same-origin avatar requests when auth is enabled. This matches the existing assistant-media pattern, keeps token use inside the authenticated dashboard flow, and closes the unauthenticated avatar disclosure by rejecting requests that do not present valid auth.Repro + Verification
Environment
Steps
node scripts/run-vitest.mjs run --config test/vitest/vitest.gateway.config.ts src/gateway/control-ui.http.test.tsnode scripts/run-vitest.mjs run --config test/vitest/vitest.unit-fast.config.ts ui/src/ui/app-chat.test.ts ui/src/ui/chat/grouped-render.test.tsExpected
Actual
Evidence
Attach at least one:
Human Verification (required)
What you personally verified (not just CI), and how:
operator.read, local avatar bytes, remote-avatar metadata, and existing symlink rejection behavior.Review Conversations
If a bot review conversation is addressed by this PR, resolve that conversation yourself. Do not leave bot review conversation cleanup for maintainers.
Compatibility / Migration
Yes/No) YesYes/No) NoYes/No) NoRisks and Mitigations
List only real risks for this PR. Add/remove entries as needed. If none, write
None.