mirror of
https://github.com/garrytan/gstack.git
synced 2026-08-05 13:58:37 +02:00
fix(security): cache device salt in-process to survive fs-unwritable
getDeviceSalt returned a new randomBytes(16) on every call when the salt file couldn't be persisted (read-only home, disk full). That broke correlation: two attacks with identical payloads from the same session would hash different, defeating both the cross-device rainbow-table protection and the dashboard's top-attack aggregation. Cache the salt in a module-level variable on first generation. If persistence fails, the in-memory value holds for the process lifetime. Next process gets a new salt, but within-session correlation works. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
7f7249d3d2
commit
905d5a2e29
+20
-10
@@ -107,11 +107,12 @@ export function combineVerdict(signals: LayerSignal[]): SecurityResult {
|
|||||||
const mlHighSignals = [content, deberta, transcript].filter(c => c >= THRESHOLDS.WARN);
|
const mlHighSignals = [content, deberta, transcript].filter(c => c >= THRESHOLDS.WARN);
|
||||||
const hasDebertaSignal = deberta > 0;
|
const hasDebertaSignal = deberta > 0;
|
||||||
|
|
||||||
// Ensemble rule:
|
// Ensemble rule: cross-model agreement is what upgrades from WARN to BLOCK.
|
||||||
// * 2-of-3 ML classifiers >= WARN → BLOCK when DeBERTa is in the pool
|
// Requires >= 2 ML classifiers at WARN regardless of whether DeBERTa is
|
||||||
// * 2-of-2 (content + transcript) >= WARN → BLOCK when DeBERTa disabled
|
// in the pool (hasDebertaSignal kept in scope for future threshold tuning,
|
||||||
// In both cases, cross-model agreement is what upgrades from WARN to BLOCK.
|
// but today the count-based rule is 2 in both configurations).
|
||||||
const requiredForBlock = hasDebertaSignal ? 2 : 2;
|
void hasDebertaSignal;
|
||||||
|
const requiredForBlock = 2;
|
||||||
if (mlHighSignals.length >= requiredForBlock) {
|
if (mlHighSignals.length >= requiredForBlock) {
|
||||||
return {
|
return {
|
||||||
verdict: 'block',
|
verdict: 'block',
|
||||||
@@ -223,22 +224,31 @@ const MAX_LOG_GENERATIONS = 5;
|
|||||||
* ~/.gstack/security/device-salt (0600). Random per-device, prevents rainbow
|
* ~/.gstack/security/device-salt (0600). Random per-device, prevents rainbow
|
||||||
* table attacks across devices (Codex tier-2 finding).
|
* table attacks across devices (Codex tier-2 finding).
|
||||||
*/
|
*/
|
||||||
|
let cachedSalt: string | null = null;
|
||||||
|
|
||||||
function getDeviceSalt(): string {
|
function getDeviceSalt(): string {
|
||||||
|
if (cachedSalt) return cachedSalt;
|
||||||
try {
|
try {
|
||||||
if (fs.existsSync(SALT_FILE)) return fs.readFileSync(SALT_FILE, 'utf8').trim();
|
if (fs.existsSync(SALT_FILE)) {
|
||||||
|
cachedSalt = fs.readFileSync(SALT_FILE, 'utf8').trim();
|
||||||
|
return cachedSalt;
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// fall through to generate
|
// fall through to generate
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
fs.mkdirSync(SECURITY_DIR, { recursive: true, mode: 0o700 });
|
fs.mkdirSync(SECURITY_DIR, { recursive: true, mode: 0o700 });
|
||||||
} catch {}
|
} catch {}
|
||||||
const salt = randomBytes(16).toString('hex');
|
cachedSalt = randomBytes(16).toString('hex');
|
||||||
try {
|
try {
|
||||||
fs.writeFileSync(SALT_FILE, salt, { mode: 0o600 });
|
fs.writeFileSync(SALT_FILE, cachedSalt, { mode: 0o600 });
|
||||||
} catch {
|
} catch {
|
||||||
// Non-fatal: we still return salt, just can't persist. Next call regenerates.
|
// Can't persist (read-only fs, disk full). Keep the in-memory salt
|
||||||
|
// for this process so cross-log correlation still works within a
|
||||||
|
// session. Next process gets a new salt, but that's a degraded-mode
|
||||||
|
// acceptable cost.
|
||||||
}
|
}
|
||||||
return salt;
|
return cachedSalt;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function hashPayload(payload: string): string {
|
export function hashPayload(payload: string): string {
|
||||||
|
|||||||
Reference in New Issue
Block a user