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:
Val Alexander
2026-03-06 00:47:59 -06:00
parent 39020f8d62
commit f4f8eac3a3
4 changed files with 66 additions and 2 deletions

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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`;

View File

@@ -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>