mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-08 13:39:45 +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:
@@ -87,8 +87,8 @@ function setConnected(healthData) {
|
||||
chrome.action.setBadgeBackgroundColor({ color: '#F59E0B' });
|
||||
chrome.action.setBadgeText({ text: ' ' });
|
||||
|
||||
// Broadcast health to popup and side panel (include token for sidepanel auth)
|
||||
chrome.runtime.sendMessage({ type: 'health', data: { ...healthData, token: authToken } }).catch((err) => {
|
||||
// Broadcast health to popup and side panel (token excluded — use getToken message instead)
|
||||
chrome.runtime.sendMessage({ type: 'health', data: healthData }).catch((err) => {
|
||||
console.debug('[gstack bg] No listener for health broadcast:', err.message);
|
||||
});
|
||||
|
||||
@@ -285,7 +285,7 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
||||
}
|
||||
|
||||
const ALLOWED_TYPES = new Set([
|
||||
'getPort', 'setPort', 'getServerUrl', 'fetchRefs',
|
||||
'getPort', 'setPort', 'getServerUrl', 'getToken', 'fetchRefs',
|
||||
'openSidePanel', 'sidebarOpened', 'command', 'sidebar-command',
|
||||
// Inspector message types
|
||||
'startInspector', 'stopInspector', 'elementPicked', 'pickerCancelled',
|
||||
@@ -315,7 +315,18 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
|
||||
return true;
|
||||
}
|
||||
|
||||
// getToken handler removed — token distributed via health broadcast
|
||||
// Token delivered via targeted sendResponse, not broadcast — limits exposure.
|
||||
// Only respond to extension pages (sidepanel/popup) — content scripts have
|
||||
// sender.tab set, so reject those to prevent token access from injected contexts.
|
||||
if (msg.type === 'getToken') {
|
||||
if (sender.tab) {
|
||||
console.warn('[gstack] Rejected getToken from content script context');
|
||||
sendResponse({ token: null });
|
||||
} else {
|
||||
sendResponse({ token: authToken });
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (msg.type === 'fetchRefs') {
|
||||
fetchAndRelayRefs().then(() => sendResponse({ ok: true }));
|
||||
|
||||
@@ -355,6 +355,10 @@
|
||||
function applyStyle(selector, property, value) {
|
||||
// Validate property name: alphanumeric + hyphens only
|
||||
if (!/^[a-zA-Z-]+$/.test(property)) return { error: 'Invalid property name' };
|
||||
// Validate CSS value: block exfiltration vectors (url(), expression(), @import, javascript:, data:)
|
||||
if (/url\s*\(|expression\s*\(|@import|javascript:|data:/i.test(value)) {
|
||||
return { error: 'CSS value contains blocked pattern' };
|
||||
}
|
||||
|
||||
const el = findElement(selector);
|
||||
if (!el) return { error: 'Element not found' };
|
||||
@@ -373,6 +377,9 @@
|
||||
}
|
||||
|
||||
function toggleClass(selector, className, action) {
|
||||
if (!/^[a-zA-Z0-9_-]+$/.test(className)) {
|
||||
return { error: 'Invalid class name' };
|
||||
}
|
||||
const el = findElement(selector);
|
||||
if (!el) return { error: 'Element not found' };
|
||||
|
||||
@@ -387,6 +394,12 @@
|
||||
}
|
||||
|
||||
function injectCSS(id, css) {
|
||||
if (!/^[a-zA-Z0-9_-]+$/.test(id)) {
|
||||
return { error: 'Invalid CSS injection id' };
|
||||
}
|
||||
if (/url\s*\(|expression\s*\(|@import|javascript:|data:/i.test(css)) {
|
||||
return { error: 'CSS contains blocked pattern (url, expression, @import)' };
|
||||
}
|
||||
const styleId = `gstack-inject-${id}`;
|
||||
let styleEl = document.getElementById(styleId);
|
||||
if (!styleEl) {
|
||||
|
||||
@@ -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