mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-21 20:28:24 +08:00
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:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user