mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-19 19:02:29 +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:
@@ -3,15 +3,34 @@
|
||||
* Localhost and private IPs are allowed (primary use case: QA testing local dev servers).
|
||||
*/
|
||||
|
||||
const BLOCKED_METADATA_HOSTS = new Set([
|
||||
'169.254.169.254', // AWS/GCP/Azure instance metadata (IPv4 link-local)
|
||||
export const BLOCKED_METADATA_HOSTS = new Set([
|
||||
'169.254.169.254', // AWS/GCP/Azure instance metadata
|
||||
'fe80::1', // IPv6 link-local — common metadata endpoint alias
|
||||
'fd00::', // IPv6 unique local (metadata in some cloud setups)
|
||||
'::ffff:169.254.169.254', // IPv4-mapped IPv6 form of the metadata IP
|
||||
'metadata.google.internal', // GCP metadata
|
||||
'metadata.azure.internal', // Azure IMDS
|
||||
]);
|
||||
|
||||
/**
|
||||
* IPv6 prefixes to block (CIDR-style). Any address starting with these
|
||||
* hex prefixes is rejected. Covers the full ULA range (fc00::/7 = fc00:: and fd00::).
|
||||
*/
|
||||
const BLOCKED_IPV6_PREFIXES = ['fc', 'fd'];
|
||||
|
||||
/**
|
||||
* Check if an IPv6 address falls within a blocked prefix range.
|
||||
* Handles the full ULA range (fc00::/7), not just the exact literal fd00::.
|
||||
* Only matches actual IPv6 addresses (must contain ':'), not hostnames
|
||||
* like fd.example.com or fcustomer.com.
|
||||
*/
|
||||
function isBlockedIpv6(addr: string): boolean {
|
||||
const normalized = addr.toLowerCase().replace(/^\[|\]$/g, '');
|
||||
// Must contain a colon to be an IPv6 address — avoids false positives on
|
||||
// hostnames like fd.example.com or fcustomer.com
|
||||
if (!normalized.includes(':')) return false;
|
||||
return BLOCKED_IPV6_PREFIXES.some(prefix => normalized.startsWith(prefix));
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize hostname for blocklist comparison:
|
||||
* - Strip trailing dot (DNS fully-qualified notation)
|
||||
@@ -37,7 +56,7 @@ function isMetadataIp(hostname: string): boolean {
|
||||
try {
|
||||
const probe = new URL(`http://${hostname}`);
|
||||
const normalized = probe.hostname;
|
||||
if (BLOCKED_METADATA_HOSTS.has(normalized)) return true;
|
||||
if (BLOCKED_METADATA_HOSTS.has(normalized) || isBlockedIpv6(normalized)) return true;
|
||||
// Also check after stripping trailing dot
|
||||
if (normalized.endsWith('.') && BLOCKED_METADATA_HOSTS.has(normalized.slice(0, -1))) return true;
|
||||
} catch {
|
||||
@@ -69,7 +88,7 @@ async function resolvesToBlockedIp(hostname: string): Promise<boolean> {
|
||||
const v6Check = resolve6(hostname).then(
|
||||
(addresses) => addresses.some(addr => {
|
||||
const normalized = addr.toLowerCase();
|
||||
return BLOCKED_METADATA_HOSTS.has(normalized) ||
|
||||
return BLOCKED_METADATA_HOSTS.has(normalized) || isBlockedIpv6(normalized) ||
|
||||
// fe80::/10 is link-local — always block (covers all fe80:: addresses)
|
||||
normalized.startsWith('fe80:');
|
||||
}),
|
||||
@@ -100,7 +119,7 @@ export async function validateNavigationUrl(url: string): Promise<void> {
|
||||
|
||||
const hostname = normalizeHostname(parsed.hostname.toLowerCase());
|
||||
|
||||
if (BLOCKED_METADATA_HOSTS.has(hostname) || isMetadataIp(hostname)) {
|
||||
if (BLOCKED_METADATA_HOSTS.has(hostname) || isMetadataIp(hostname) || isBlockedIpv6(hostname)) {
|
||||
throw new Error(
|
||||
`Blocked: ${parsed.hostname} is a cloud metadata endpoint. Access is denied for security.`
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user