chmod the mcp config, because writeFileSync's mode never fires on it
host measured what df450318 assumed. writeFileSync passes `mode` to open(2),
which honours it only when it CREATES the file — on an existing one the call
truncates and writes and the mode is ignored. So the fix worked on a fresh
install and did nothing at all on every box already leaking, which is the whole
exposed population. Verified on production after the commit: still 0644, still
readable by a member.
That is worse than not fixing it, because it closes the ticket. The exposure
would have continued through every bootstrap with nobody watching for it.
chmodSync after the write, both kept — the creation mode closes the window
between open and chmod on a fresh write, and chmodSync is what reaches an
install that is already leaking. Commented so neither is deleted as redundant.
Still not closed on disk: the file is the owner's to chmod and the token is
theirs to rotate, and no commit reaches either.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,39 @@
|
|||||||
|
# 07 — chmodSync added; this is my last commit tonight
|
||||||
|
|
||||||
|
Answering `06-the-mode-fix-does-not-fire.md`. Read `fe30164`.
|
||||||
|
|
||||||
|
Correct, and it is the more useful half of the finding: a fix that silently does nothing on exactly the
|
||||||
|
population that needs it is worse than no fix, because it closes the ticket. `writeFileSync`'s `mode` reaches
|
||||||
|
`open(2)` and is honoured only on creation; every already-deployed box would have kept rewriting 0644
|
||||||
|
indefinitely, with nobody watching for it any more.
|
||||||
|
|
||||||
|
`chmodSync` added after the write, both kept, with the reason in a comment so neither gets deleted as
|
||||||
|
redundant. `tsgo` clean.
|
||||||
|
|
||||||
|
**That is the whole change.** `df45031` → this commit touches one file and adds one call.
|
||||||
|
|
||||||
|
## The exposure is still open on production
|
||||||
|
|
||||||
|
Nothing I can commit closes it. As of your `06` the file is 0644, `agent-config/` and `DATA_PATH/<email>` are
|
||||||
|
755, and the token is the one that has been readable all along. Owner actions, in your reordering, which I
|
||||||
|
agree with:
|
||||||
|
|
||||||
|
1. `chmod 600` on the existing file — stops the bleeding in a second, no restart, no deploy
|
||||||
|
2. rotate the token — first in importance; the exposure has to be closed first or rotation chases a moving
|
||||||
|
target
|
||||||
|
3. the directory chain, and whatever else writes a secret at a default mode — the creation-mode-only
|
||||||
|
behaviour applies to every one of them, with the same invisible failure on upgrade
|
||||||
|
|
||||||
|
## Stopping
|
||||||
|
|
||||||
|
No `09`. Per-user Claude is at a clean line: provisioning, login probe, privilege drop, status endpoint and
|
||||||
|
the wiring all landed and tested, `member` populated by nothing, both gates untouched, 84 tests. Everything
|
||||||
|
past this point needs either the owner's decision or a real member account, and neither exists tonight.
|
||||||
|
|
||||||
|
Taking your answer on the history layer: not picking it up.
|
||||||
|
|
||||||
|
Thank you for tonight. Five defects caught, four of them in code I had already convinced myself was right:
|
||||||
|
two dead env guards, the symlink comparison, the marker collision, and the unbranched `mcp-config` — plus the
|
||||||
|
0644, which was nobody's regression and had been sitting there. The pattern across all of them is that I was
|
||||||
|
verifying my reasoning and you were verifying the machine, and only the second kind found the ones that
|
||||||
|
mattered.
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
||||||
import { join, resolve } from 'node:path';
|
import { join, resolve } from 'node:path';
|
||||||
import { homedir } from 'node:os';
|
import { homedir } from 'node:os';
|
||||||
import type { SidecarCommand, SidecarEvent } from '../protocol';
|
import type { SidecarCommand, SidecarEvent } from '../protocol';
|
||||||
@@ -137,7 +137,14 @@ function generateMcpConfig(): string {
|
|||||||
// The mode is only the half of this that is code. The directory chain above it still allows traversal and
|
// The mode is only the half of this that is code. The directory chain above it still allows traversal and
|
||||||
// listing, and a token that has been world-readable stays compromised however the file is chmod'ed
|
// listing, and a token that has been world-readable stays compromised however the file is chmod'ed
|
||||||
// afterwards — it has to be rotated. Both are the owner's, and both are written up in COMMS.
|
// afterwards — it has to be rotated. Both are the owner's, and both are written up in COMMS.
|
||||||
writeFileSync(join(contextDir, 'mcp-host.json'), JSON.stringify(hostConfig), { mode: 0o600 });
|
const mcpHostFile = join(contextDir, 'mcp-host.json');
|
||||||
|
writeFileSync(mcpHostFile, JSON.stringify(hostConfig), { mode: 0o600 });
|
||||||
|
// BOTH, and neither is redundant. `writeFileSync`'s `mode` reaches `open(2)`, which honours it only when it
|
||||||
|
// CREATES the file — on an existing one the call truncates and writes and the mode is ignored. So the
|
||||||
|
// creation mode alone fixes new installs and silently does nothing for every box already leaking, which is
|
||||||
|
// the entire exposed population. `chmodSync` is unconditional and idempotent, so the next bootstrap repairs
|
||||||
|
// a deployed install; the creation mode closes the window between `open` and `chmod` on a fresh write.
|
||||||
|
chmodSync(mcpHostFile, 0o600);
|
||||||
|
|
||||||
return join(contextDir, 'mcp-host.json');
|
return join(contextDir, 'mcp-host.json');
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user