mirror of
https://github.com/garrytan/gstack.git
synced 2026-06-17 23:30:09 +02:00
fix(security): close cross-model (Codex) adversarial findings
Codex adversarial review found a HIGH the Claude pass missed plus 3 mediums: - C1 (HIGH): gstack-decision-search --all returned every decide and IGNORED redact events, so a redacted secret still resurfaced via --all until compact ran. --all now excludes redacted (redact = expunge from every read path), still showing superseded history. - C-med: semantic (external gbrain) slug/snippet were printed raw — datamark them too so a gbrain hit can't spoof role markers / fences into agent context. - C4: semanticRecall fell back to an UNSCOPED gbrain search when no curated-memory source resolved, pulling code/doc corpora mislabeled as 'related decisions'. Now returns null (degrade) when there's no worktree-backed memory source. - C5: validateDecide scanned only decision/rationale/alternatives; branch and issue are stored + surfaced (raw via --json), so include them in the injection+secret scan. C2 (snapshot staleness) / C3 (compact TOCTOU residual): accepted for a single-user store — atomic appends never lose the event, rebuilds self-heal, and the compact size-recheck leaves only a sub-ms window; full append-locking would break the lock-free append design. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -82,10 +82,12 @@ export function semanticRecall(
|
||||
limit = 3,
|
||||
): SemanticHit[] | null {
|
||||
if (!query.trim()) return null;
|
||||
// Require the curated-memory source. If it's absent (gbrain down OR no worktree-backed
|
||||
// source), degrade to null rather than searching UNSCOPED — an unscoped search pulls
|
||||
// code/doc corpora that would be mislabeled as "related decisions" (Codex finding).
|
||||
const sourceId = resolveMemorySourceId(env);
|
||||
const args = ["search", query];
|
||||
if (sourceId) args.push("--source", sourceId);
|
||||
const r = spawnGbrain(args, { baseEnv: env, timeout: TIMEOUT_MS });
|
||||
if (!sourceId) return null;
|
||||
const r = spawnGbrain(["search", query, "--source", sourceId], { baseEnv: env, timeout: TIMEOUT_MS });
|
||||
if (r.status !== 0) return null; // gbrain down / not on PATH / errored → degrade
|
||||
return parseSearchHits(r.stdout || "", minScore, limit);
|
||||
}
|
||||
|
||||
@@ -119,7 +119,9 @@ export function validateDecide(input: Partial<DecisionEvent>): ValidateResult {
|
||||
}
|
||||
}
|
||||
|
||||
const freeText = [input.decision, input.rationale, input.alternatives_considered]
|
||||
// Scan ALL stored free-text — incl. branch/issue, which are surfaced (and emitted raw
|
||||
// via --json), so they must not carry secrets or injection either (Codex finding).
|
||||
const freeText = [input.decision, input.rationale, input.alternatives_considered, input.branch, input.issue]
|
||||
.filter((s): s is string => typeof s === "string")
|
||||
.join("\n");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user