mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-02 11:45:20 +02:00
7450b5160b
* fix: remove auth token from /health, secure extension bootstrap (CRITICAL-02 + HIGH-03) - Remove token from /health response (was leaked to any localhost process) - Write .auth.json to extension dir for Manifest V3 bootstrap - sidebar-agent reads token from state file via BROWSE_STATE_FILE env var - Remove getToken handler from extension (token via health broadcast) - Extension loads token before first health poll to prevent race condition * fix: require auth on cookie-picker data routes (CRITICAL-01) - Add Bearer token auth gate on all /cookie-picker/* data/action routes - GET /cookie-picker HTML page stays unauthenticated (UI shell) - Token embedded in served HTML for picker's fetch calls - CORS preflight now allows Authorization header * fix: add state file TTL and plaintext cookie warning (HIGH-02) - Add savedAt timestamp to state save output - Warn on load if state file older than 7 days - Auto-delete stale state files (>7 days) on server startup - Warning about plaintext cookie storage in save message * fix: innerHTML XSS in extension content script and sidepanel (MEDIUM-01) - content.js: replace innerHTML with createElement/textContent for ref panel - sidepanel.js: escape entry.command with escapeHtml() in activity feed - Both found by security audit + Codex adversarial red team * fix: symlink bypass in validateReadPath (MEDIUM-02) - Always resolve to absolute path first (fixes relative path bypass) - Use realpathSync to follow symlinks before boundary check - Throw on non-ENOENT realpathSync failures (explicit over silent) - Resolve SAFE_DIRECTORIES through realpathSync (macOS /tmp → /private/tmp) - Resolve directory part for non-existent files (ENOENT with symlinked parent) * fix: freeze hook symlink bypass and prefix collision (MEDIUM-03) - Add POSIX-portable path resolution (cd + pwd -P, works on macOS) - Fix prefix collision: /project-evil no longer matches /project freeze dir - Use trailing slash in boundary check to require directory boundary * fix: shell script injection in gstack-config and telemetry (MEDIUM-04) - gstack-config: validate keys (alphanumeric+underscore only) - gstack-config: use grep -F (fixed string) instead of -E (regex) - gstack-config: escape sed special chars in values, drop newlines - gstack-telemetry-log: sanitize REPO_SLUG and BRANCH via json_safe() * test: 20 security tests for audit remediation - server-auth: verify token removed from /health, auth on /refs, /activity/* - cookie-picker: auth required on data routes, HTML page unauthenticated - path-validation: symlink bypass blocked, realpathSync failure throws - gstack-config: regex key rejected, sed special chars preserved - state-ttl: savedAt timestamp, 7-day TTL warning - telemetry: branch/repo with quotes don't corrupt JSON - adversarial: sidepanel escapes entry.command, freeze prefix collision * chore: bump version and changelog (v0.13.1.0) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: tone down changelog — defense in depth, not catastrophic bugs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
160 lines
4.7 KiB
JavaScript
160 lines
4.7 KiB
JavaScript
/**
|
|
* gstack browse — content script
|
|
*
|
|
* Receives ref data from background worker via chrome.runtime.onMessage.
|
|
* Renders @ref overlay badges on the page (CDP mode only — positions are accurate).
|
|
* In headless mode, shows a floating ref panel instead (positions unknown).
|
|
*/
|
|
|
|
let overlayContainer = null;
|
|
let statusPill = null;
|
|
let pillFadeTimer = null;
|
|
let refCount = 0;
|
|
|
|
// ─── Connection Status Pill ──────────────────────────────────
|
|
|
|
function showStatusPill(connected, refs) {
|
|
refCount = refs || 0;
|
|
|
|
if (!statusPill) {
|
|
statusPill = document.createElement('div');
|
|
statusPill.id = 'gstack-status-pill';
|
|
statusPill.style.cursor = 'pointer';
|
|
statusPill.addEventListener('click', () => {
|
|
// Ask background to open the side panel
|
|
chrome.runtime.sendMessage({ type: 'openSidePanel' });
|
|
});
|
|
document.body.appendChild(statusPill);
|
|
}
|
|
|
|
if (!connected) {
|
|
statusPill.style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
const refText = refCount > 0 ? ` · ${refCount} refs` : '';
|
|
statusPill.innerHTML = `<span class="gstack-pill-dot"></span> gstack${refText}`;
|
|
statusPill.style.display = 'flex';
|
|
statusPill.style.opacity = '1';
|
|
|
|
// Fade to subtle after 3s
|
|
clearTimeout(pillFadeTimer);
|
|
pillFadeTimer = setTimeout(() => {
|
|
statusPill.style.opacity = '0.3';
|
|
}, 3000);
|
|
}
|
|
|
|
function hideStatusPill() {
|
|
if (statusPill) {
|
|
statusPill.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
function ensureContainer() {
|
|
if (overlayContainer) return overlayContainer;
|
|
overlayContainer = document.createElement('div');
|
|
overlayContainer.id = 'gstack-ref-overlays';
|
|
overlayContainer.style.cssText = 'position: fixed; top: 0; left: 0; width: 0; height: 0; z-index: 2147483647; pointer-events: none;';
|
|
document.body.appendChild(overlayContainer);
|
|
return overlayContainer;
|
|
}
|
|
|
|
function clearOverlays() {
|
|
if (overlayContainer) {
|
|
overlayContainer.innerHTML = '';
|
|
}
|
|
}
|
|
|
|
function renderRefBadges(refs) {
|
|
clearOverlays();
|
|
if (!refs || refs.length === 0) return;
|
|
|
|
const container = ensureContainer();
|
|
|
|
for (const ref of refs) {
|
|
// Try to find the element using accessible name/role for positioning
|
|
// In CDP mode, we could use bounding boxes from the server
|
|
// For now, use a floating panel approach
|
|
const badge = document.createElement('div');
|
|
badge.className = 'gstack-ref-badge';
|
|
badge.textContent = ref.ref;
|
|
badge.title = `${ref.role}: "${ref.name}"`;
|
|
container.appendChild(badge);
|
|
}
|
|
}
|
|
|
|
function renderRefPanel(refs) {
|
|
clearOverlays();
|
|
if (!refs || refs.length === 0) return;
|
|
|
|
const container = ensureContainer();
|
|
|
|
const panel = document.createElement('div');
|
|
panel.className = 'gstack-ref-panel';
|
|
|
|
const header = document.createElement('div');
|
|
header.className = 'gstack-ref-panel-header';
|
|
header.textContent = `gstack refs (${refs.length})`;
|
|
header.style.cssText = 'pointer-events: auto; cursor: move;';
|
|
panel.appendChild(header);
|
|
|
|
const list = document.createElement('div');
|
|
list.className = 'gstack-ref-panel-list';
|
|
for (const ref of refs.slice(0, 30)) { // Show max 30 in panel
|
|
const row = document.createElement('div');
|
|
row.className = 'gstack-ref-panel-row';
|
|
const idSpan = document.createElement('span');
|
|
idSpan.className = 'gstack-ref-panel-id';
|
|
idSpan.textContent = ref.ref;
|
|
const roleSpan = document.createElement('span');
|
|
roleSpan.className = 'gstack-ref-panel-role';
|
|
roleSpan.textContent = ref.role;
|
|
const nameSpan = document.createElement('span');
|
|
nameSpan.className = 'gstack-ref-panel-name';
|
|
nameSpan.textContent = '"' + ref.name + '"';
|
|
row.append(idSpan, document.createTextNode(' '), roleSpan, document.createTextNode(' '), nameSpan);
|
|
list.appendChild(row);
|
|
}
|
|
if (refs.length > 30) {
|
|
const more = document.createElement('div');
|
|
more.className = 'gstack-ref-panel-more';
|
|
more.textContent = `+${refs.length - 30} more`;
|
|
list.appendChild(more);
|
|
}
|
|
panel.appendChild(list);
|
|
container.appendChild(panel);
|
|
}
|
|
|
|
// Listen for messages from background worker
|
|
chrome.runtime.onMessage.addListener((msg) => {
|
|
if (msg.type === 'refs' && msg.data) {
|
|
const refs = msg.data.refs || [];
|
|
const mode = msg.data.mode;
|
|
|
|
if (refs.length === 0) {
|
|
clearOverlays();
|
|
showStatusPill(true, 0);
|
|
return;
|
|
}
|
|
|
|
// CDP mode: could use bounding boxes (future)
|
|
// For now: floating panel for all modes
|
|
renderRefPanel(refs);
|
|
showStatusPill(true, refs.length);
|
|
}
|
|
|
|
if (msg.type === 'clearRefs') {
|
|
clearOverlays();
|
|
showStatusPill(true, 0);
|
|
}
|
|
|
|
if (msg.type === 'connected') {
|
|
showStatusPill(true, refCount);
|
|
}
|
|
|
|
if (msg.type === 'disconnected') {
|
|
hideStatusPill();
|
|
clearOverlays();
|
|
}
|
|
});
|