mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
fix(ui): resolve 3 critical security and UX issues
1. fix(security): prevent JSON DoS via size cap on auto-parse - Add MAX_JSON_AUTOPARSE_CHARS (20KB) to detectJson() - Prevents UI freeze from multi-MB JSON in assistant/tool messages - Addresses Aisle Security High severity CWE-400 2. fix(ux): prevent STT transcripts going to wrong session - Add cleanupChatModuleState() export in chat.ts - Call cleanup in applyTabSelection when leaving chat tab - Stops active recording to prevent voice input to unintended session - Addresses Greptile critical UX bug 3. fix(security): redact sensitive values in config diff panel - Add renderDiffValue() with stream-mode + sensitive-path checks - Use in diff panel rendering instead of raw truncateValue() - Prevents secrets from appearing during screen sharing - Addresses Aisle Security Medium severity CWE-200
This commit is contained in:
@@ -34,6 +34,7 @@ import {
|
||||
import { saveSettings, type UiSettings } from "./storage.ts";
|
||||
import { startThemeTransition, type ThemeTransitionContext } from "./theme-transition.ts";
|
||||
import { resolveTheme, type ResolvedTheme, type ThemeMode, type ThemeName } from "./theme.ts";
|
||||
import { cleanupChatModuleState } from "./views/chat.ts";
|
||||
|
||||
let systemThemeCleanup: (() => void) | null = null;
|
||||
import type { AgentsListResult, AttentionItem } from "./types.ts";
|
||||
@@ -365,9 +366,16 @@ function applyTabSelection(
|
||||
next: Tab,
|
||||
options: { refreshPolicy: "always" | "connected"; syncUrl?: boolean },
|
||||
) {
|
||||
const prev = host.tab;
|
||||
if (host.tab !== next) {
|
||||
host.tab = next;
|
||||
}
|
||||
|
||||
// Cleanup chat module state when navigating away from chat
|
||||
if (prev === "chat" && next !== "chat") {
|
||||
cleanupChatModuleState();
|
||||
}
|
||||
|
||||
if (next === "chat") {
|
||||
host.chatHasAutoScrolled = false;
|
||||
}
|
||||
|
||||
@@ -501,12 +501,25 @@ function renderCollapsedToolCards(
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Max characters for auto-detecting and pretty-printing JSON.
|
||||
* Prevents DoS from large JSON payloads in assistant/tool messages.
|
||||
*/
|
||||
const MAX_JSON_AUTOPARSE_CHARS = 20_000;
|
||||
|
||||
/**
|
||||
* Detect whether a trimmed string is a JSON object or array.
|
||||
* Must start with `{`/`[` and end with `}`/`]` and parse successfully.
|
||||
* Size-capped to prevent render-loop DoS from large JSON messages.
|
||||
*/
|
||||
function detectJson(text: string): { parsed: unknown; pretty: string } | null {
|
||||
const t = text.trim();
|
||||
|
||||
// Enforce size cap to prevent UI freeze from multi-MB JSON payloads
|
||||
if (t.length > MAX_JSON_AUTOPARSE_CHARS) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ((t.startsWith("{") && t.endsWith("}")) || (t.startsWith("[") && t.endsWith("]"))) {
|
||||
try {
|
||||
const parsed = JSON.parse(t);
|
||||
|
||||
@@ -147,6 +147,28 @@ let searchOpen = false;
|
||||
let searchQuery = "";
|
||||
let pinnedExpanded = false;
|
||||
|
||||
/**
|
||||
* Cleanup module-level state when navigating away from chat view.
|
||||
* Prevents STT recording from continuing after tab switch (which would
|
||||
* send transcripts to the wrong session) and resets ephemeral UI state.
|
||||
*/
|
||||
export function cleanupChatModuleState() {
|
||||
if (sttRecording) {
|
||||
stopStt();
|
||||
sttRecording = false;
|
||||
sttInterimText = "";
|
||||
}
|
||||
slashMenuOpen = false;
|
||||
slashMenuItems = [];
|
||||
slashMenuIndex = 0;
|
||||
slashMenuMode = "command";
|
||||
slashMenuCommand = null;
|
||||
slashMenuArgItems = [];
|
||||
searchOpen = false;
|
||||
searchQuery = "";
|
||||
pinnedExpanded = false;
|
||||
}
|
||||
|
||||
function adjustTextareaHeight(el: HTMLTextAreaElement) {
|
||||
el.style.height = "auto";
|
||||
el.style.height = `${Math.min(el.scrollHeight, 150)}px`;
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { ConfigUiHints } from "../types.ts";
|
||||
import {
|
||||
countSensitiveConfigValues,
|
||||
humanize,
|
||||
isSensitiveConfigPath,
|
||||
pathKey,
|
||||
REDACTED_PLACEHOLDER,
|
||||
schemaType,
|
||||
@@ -501,6 +502,26 @@ function truncateValue(value: unknown, maxLen = 40): string {
|
||||
return str.slice(0, maxLen - 3) + "...";
|
||||
}
|
||||
|
||||
/**
|
||||
* Render diff value with redaction when in stream mode or path is sensitive.
|
||||
* Prevents secrets from appearing in diff panel during screen sharing.
|
||||
*/
|
||||
function renderDiffValue(
|
||||
path: string,
|
||||
value: unknown,
|
||||
streamMode: boolean,
|
||||
uiHints: ConfigUiHints,
|
||||
): string {
|
||||
const hint = uiHints[path];
|
||||
const sensitive = hint?.sensitive ?? isSensitiveConfigPath(path);
|
||||
|
||||
if (streamMode && sensitive) {
|
||||
return REDACTED_PLACEHOLDER;
|
||||
}
|
||||
|
||||
return truncateValue(value);
|
||||
}
|
||||
|
||||
type ThemeOption = { id: ThemeName; label: string; description: string; icon: TemplateResult };
|
||||
const THEME_OPTIONS: ThemeOption[] = [
|
||||
{ id: "claw", label: "Claw", description: "Chroma family", icon: icons.zap },
|
||||
@@ -909,11 +930,11 @@ export function renderConfig(props: ConfigProps) {
|
||||
<div class="config-diff__path">${change.path}</div>
|
||||
<div class="config-diff__values">
|
||||
<span class="config-diff__from"
|
||||
>${truncateValue(change.from)}</span
|
||||
>${renderDiffValue(change.path, change.from, props.streamMode, props.uiHints)}</span
|
||||
>
|
||||
<span class="config-diff__arrow">→</span>
|
||||
<span class="config-diff__to"
|
||||
>${truncateValue(change.to)}</span
|
||||
>${renderDiffValue(change.path, change.to, props.streamMode, props.uiHints)}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user