Repository navigation
Description
Implementation: FlyBot (ours) — PR follows within 24h. Do not pick up without a pickup-ok comment on this issue.
Written-by: FlyBot Lead
Claimed-by: FlyBot Lead
Claim-Id: 06ade141579b43148a61f0ad8ced186d
Branch: dev/2-social-credentials-leaf
Base: 1e8c64e
At: 2026-11-02T04:56:41Z
What happens
plugins/social/src/social.js:475 writes the Bot Social credentials file with mkdirSync + writeFileSync(path, …, { mode: 0o600 }) and then chmodSync(path, 0o600) inside a try. The write is not checked against the name it is about to replace, so when that name is a symbolic link or a hard link, the JSON — token, owner id, display name, server — lands in the file the link names. The chmodSync that follows then either fails (swallowed by the catch) or locks down a file that belongs to something else. Everything that calls this one helper inherits the hole: the onboarding result (social.js:4695), the server switch backup (social.js:4693), the server backfill (social.js:4682) and the settings save (social.js:659).
Reproduction
Point the credentials path at a link to a scratch file, then start the social plugin so it onboards. The scratch file ends up holding the credentials JSON, readCredentials() reads it back through the link, and the plugin starts on the token that was written there.
Expected
A write must not land through a name that is not a plain file of its own, and it must be refused before the first byte:
- the name does not exist yet → writable, the file is created
0600; - a regular file already there → rewritten as today;
- a symbolic link, a hard link (
nlink > 1), or a directory → refused beforemkdirSync/writeFileSyncruns: nothing is created, no zero-byte file is left behind, the target's bytes andmtimeare unchanged, and one warning names the path; - the refusal message names the path only — never the token, the owner id, or any value it would have written;
- a failed backup write during a server switch leaves the existing credentials file byte-for-byte as it was, and
readCredentials()still returns the old token.
Scope
plugins/social/src/social.js — writeCredentials and its guard, plus one test file in the package. The guard stays package-local (plugins/ sources may not import core/). No change to the credentials format, to readCredentials, or to the outbox / inbox writes at social.js:619 / social.js:2669 (those write content-addressed names, not this leaf). Not in scope, recorded as a residual: readCredentials still follows a link at the name, and the write is a plain write rather than temp + rename, so a crash mid-write can still truncate.
Duplicate check
On live main at 1e8c64e9, writeCredentials is the only write to the credentials leaf; the four call sites above all go through it. search/issues for credentials + leaf, writeCredentials, and credentials + symbolic all return 0. Same family as #2109 / #2110 / #2114 / #2115 (a write landing through a name someone else holds); this is the credentials leaf itself, which those did not cover.
Acceptance
- Red first: a symbolic link, a hard link, and a directory at the credentials name are each refused, with the linked file's bytes unchanged and
nlinkstill 2. startSocialrefuses loudly when the credentials path is a link, and the token already in that file still reads back.- Refusal path writes zero bytes and leaves the directory listing unchanged; the message contains the path and no value.
- Healthy path: absent name is created with mode
0600, a regular file is rewritten. npm run verifygreen,generatea no-op on the second run.
Activity
HugoXK commentedon Nov 2, 2026
Release
Written-by: FlyBot Lead
Released: 06ade141579b43148a61f0ad8ced186d
Reason: PR #2118 merged into main as 7fe3521 (2026-11-02T05:37:48Z)
At: 2026-11-02T05:38:43Z