fix: community security wave — 8 PRs, 4 contributors (v0.15.13.0) (#847)

* fix(bin): pass search params via env vars (RCE fix) (#819)

Replace shell string interpolation with process.env in gstack-learnings-search
to prevent arbitrary code execution via crafted learnings entries. Also fixes
the CROSS_PROJECT interpolation that the original PR missed.

Adds 3 regression tests verifying no shell interpolation remains in the bun -e block.

Co-authored-by: garagon <garagon@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(browse): add path validation to upload command (#821)

Add isPathWithin() and path traversal checks to the upload command,
blocking file exfiltration via crafted upload paths. Uses existing
SAFE_DIRECTORIES constant instead of a local copy. Adds 3 regression tests.

Co-authored-by: garagon <garagon@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(browse): symlink resolution in meta-commands validateOutputPath (#820)

Add realpathSync to validateOutputPath in meta-commands.ts to catch
symlink-based directory escapes in screenshot, pdf, and responsive
commands. Resolves SAFE_DIRECTORIES through realpathSync to handle
macOS /tmp -> /private/tmp symlinks. Existing path validation tests
pass with the hardened implementation.

Co-authored-by: garagon <garagon@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: add uninstall instructions to README (#812)

Community PR #812 by @0531Kim. Adds two uninstall paths: the gstack-uninstall
script (handles everything) and manual removal steps for when the repo isn't
cloned. Includes CLAUDE.md cleanup note and Playwright cache guidance.

Co-Authored-By: 0531Kim <0531Kim@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(browse): Windows launcher extraEnv + headed-mode token (#822)

Community PR #822 by @pieterklue. Three fixes:
1. Windows launcher now merges extraEnv into spawned server env (was
   only passing BROWSE_STATE_FILE, dropping all other env vars)
2. Welcome page fallback serves inline HTML instead of about:blank
   redirect (avoids ERR_UNSAFE_REDIRECT on Windows)
3. /health returns auth token in headed mode even without Origin header
   (fixes Playwright Chromium extensions that don't send it)

Also adds HOME/USERPROFILE fallback for cross-platform compatibility.

Co-Authored-By: pieterklue <pieterklue@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(browse): terminate orphan server when parent process exits (#808)

Community PR #808 by @mmporong. Passes BROWSE_PARENT_PID to the spawned
server process. The server polls every 15s with signal 0 and calls
shutdown() if the parent is gone. Prevents orphaned chrome-headless-shell
processes when Claude Code sessions exit abnormally.

Co-Authored-By: mmporong <mmporong@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(security): IPv6 ULA blocking, cookie redaction, per-tab cancel, targeted token (#664)

Community PR #664 by @mr-k-man (security audit round 1, new parts only).

- IPv6 ULA prefix blocking (fc00::/7) in url-validation.ts with false-positive
  guard for hostnames like fd.example.com
- Cookie value redaction for tokens, API keys, JWTs in browse cookies command
- Per-tab cancel files in killAgent() replacing broken global kill-signal
- design/serve.ts: realpathSync upgrade prevents symlink bypass in /api/reload
- extension: targeted getToken handler replaces token-in-health-broadcast
- Supabase migration 003: column-level GRANT restricts anon UPDATE scope
- Telemetry sync: upsert error logging
- 10 new tests for IPv6, cookie redaction, DNS rebinding, path traversal

Co-Authored-By: mr-k-man <mr-k-man@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(security): CSS injection guard, timeout clamping, session validation, tests (#806)

Community PR #806 by @mr-k-man (security audit round 2, new parts only).

- CSS value validation (DANGEROUS_CSS) in cdp-inspector, write-commands, extension inspector
- Queue file permissions (0o700/0o600) in cli, server, sidebar-agent
- escapeRegExp for frame --url ReDoS fix
- Responsive screenshot path validation with validateOutputPath
- State load cookie filtering (reject localhost/.internal/metadata cookies)
- Session ID format validation in loadSession
- /health endpoint: remove currentUrl and currentMessage fields
- QueueEntry interface + isValidQueueEntry validator for sidebar-agent
- SIGTERM->SIGKILL escalation in timeout handler
- Viewport dimension clamping (1-16384), wait timeout clamping (1s-300s)
- Cookie domain validation in cookie-import and cookie-import-browser
- DocumentFragment-based tab switching (XSS fix in sidepanel)
- pollInProgress reentrancy guard for pollChat
- toggleClass/injectCSS input validation in extension inspector
- Snapshot annotated path validation with realpathSync
- 714-line security-audit-r2.test.ts + 33-line learnings-injection.test.ts

Co-Authored-By: mr-k-man <mr-k-man@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: bump version and changelog (v0.15.13.0)

Community security wave: 8 PRs from 4 contributors (@garagon, @mr-k-man,
@mmporong, @0531Kim, @pieterklue). IPv6 ULA blocking, cookie redaction,
per-tab cancel signaling, CSS injection guards, timeout clamping, session
validation, DocumentFragment XSS fix, parent process watchdog, uninstall
docs, Windows fixes, and 750+ lines of security regression tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: garagon <garagon@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: 0531Kim <0531Kim@users.noreply.github.com>
Co-authored-by: pieterklue <pieterklue@users.noreply.github.com>
Co-authored-by: mmporong <mmporong@users.noreply.github.com>
Co-authored-by: mr-k-man <mr-k-man@users.noreply.github.com>
This commit is contained in:
Garry Tan
2026-04-06 00:47:04 -07:00
committed by GitHub
parent b3d064aabb
commit 03973c2fab
30 changed files with 4066 additions and 108 deletions

View File

@@ -20,7 +20,8 @@ let connState = 'disconnected'; // disconnected | connected | reconnecting | dea
let lastOptimisticMsg = null; // track optimistically rendered user msg to avoid dupes
let sidebarActiveTabId = null; // which browser tab's chat we're showing
const chatLineCountByTab = {}; // tabId -> last seen chatLineCount
const chatDomByTab = {}; // tabId -> saved innerHTML
const chatDomByTab = {}; // tabId -> saved DocumentFragment (never serialized HTML)
let pollInProgress = false; // reentrancy guard — prevents concurrent/recursive pollChat calls
let reconnectAttempts = 0;
let reconnectTimer = null;
const MAX_RECONNECT_ATTEMPTS = 30; // 30 * 2s = 60s before showing "dead"
@@ -390,7 +391,9 @@ document.getElementById('stop-agent-btn').addEventListener('click', stopAgent);
let initialLoadDone = false;
async function pollChat() {
if (!serverUrl || !serverToken) return;
if (pollInProgress) return;
pollInProgress = true;
if (!serverUrl || !serverToken) { pollInProgress = false; return; }
try {
// Request chat for the currently displayed tab
const tabParam = sidebarActiveTabId !== null ? `&tabId=${sidebarActiveTabId}` : '';
@@ -449,6 +452,8 @@ async function pollChat() {
updateStopButton(data.agentStatus === 'processing');
} catch (err) {
console.error('[gstack sidebar] Chat poll error:', err.message);
} finally {
pollInProgress = false;
}
}
@@ -458,7 +463,11 @@ function switchChatTab(newTabId) {
// Save current tab's chat DOM + scroll position
if (sidebarActiveTabId !== null) {
chatDomByTab[sidebarActiveTabId] = chatMessages.innerHTML;
const frag = document.createDocumentFragment();
while (chatMessages.firstChild) {
frag.appendChild(chatMessages.firstChild);
}
chatDomByTab[sidebarActiveTabId] = frag;
chatLineCountByTab[sidebarActiveTabId] = chatLineCount;
}
@@ -468,7 +477,8 @@ function switchChatTab(newTabId) {
// mid-message (the server may have switched tabs because the user's
// Chrome tab changed, but we still want to show the optimistic UI).
if (chatDomByTab[newTabId]) {
chatMessages.innerHTML = chatDomByTab[newTabId];
while (chatMessages.firstChild) chatMessages.removeChild(chatMessages.firstChild);
chatMessages.appendChild(chatDomByTab[newTabId]);
chatLineCount = chatLineCountByTab[newTabId] || 0;
// Reset agent state for restored tab
agentContainer = null;
@@ -480,12 +490,22 @@ function switchChatTab(newTabId) {
chatLineCount = 0;
// agentContainer/agentTextEl are already set from sendMessage()
} else {
chatMessages.innerHTML = `
<div class="chat-welcome" id="chat-welcome">
<div class="chat-welcome-icon">G</div>
<p>Send a message about this page.</p>
<p class="muted">Each tab has its own conversation.</p>
</div>`;
while (chatMessages.firstChild) chatMessages.removeChild(chatMessages.firstChild);
const welcomeDiv = document.createElement('div');
welcomeDiv.className = 'chat-welcome';
welcomeDiv.id = 'chat-welcome';
const iconDiv = document.createElement('div');
iconDiv.className = 'chat-welcome-icon';
iconDiv.textContent = 'G';
welcomeDiv.appendChild(iconDiv);
const p1 = document.createElement('p');
p1.textContent = 'Send a message about this page.';
welcomeDiv.appendChild(p1);
const p2 = document.createElement('p');
p2.className = 'muted';
p2.textContent = 'Each tab has its own conversation.';
welcomeDiv.appendChild(p2);
chatMessages.appendChild(welcomeDiv);
chatLineCount = 0;
// Reset agent state for fresh tab
agentContainer = null;
@@ -494,7 +514,7 @@ function switchChatTab(newTabId) {
}
// Immediately poll the new tab's chat
pollChat();
setTimeout(pollChat, 0);
}
function updateStopButton(agentRunning) {
@@ -1570,7 +1590,10 @@ chrome.runtime.onMessage.addListener((msg) => {
if (msg.type === 'health') {
if (msg.data) {
const url = `http://127.0.0.1:${msg.data.port || 34567}`;
updateConnection(url, msg.data.token);
// Request token via targeted sendResponse (not broadcast) to limit exposure
chrome.runtime.sendMessage({ type: 'getToken' }, (resp) => {
updateConnection(url, resp?.token || null);
});
applyChatEnabled(!!msg.data.chatEnabled);
} else {
updateConnection(null);