Skip to content

The social credentials write follows a link at the credentials name #2116

Closed

Description

@HugoXK

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 before mkdirSync / writeFileSync runs: nothing is created, no zero-byte file is left behind, the target's bytes and mtime are 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 nlink still 2.
  • startSocial refuses 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 verify green, generate a no-op on the second run.

Activity

  1. HugoXK commented on Nov 2, 2026

    @HugoXK
    ContributorAuthor

    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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions