Skip to content

Three SECURITY.md/README guarantees are stronger than the shipped code (write-path symlink escape, username-position token, fetch +refspec) #13

Description

@dutiona

Three claims in this package's own README/SECURITY.md are stronger than the shipped code. All three are read
from lib/ (0.2.17 / 82cd4c0a), not inferred; the first two were reproduced.

1. The mirror write path follows symlinks (source and delete walks refuse them)

README/SECURITY.md: "symlinks refused and every joined path containment-checked (PATH_UNSAFE fails loud)".

What the code does: the source walk (lib/mirror.mjs walk) and the delete walk skip symlinks, and the
guards are lexical on strings (lib/paths.mjs assertRelPath/assertNestingSafe, lib/sanitize.mjs
relativeWithin). The write path does not re-check after the walk:

async function writeIfDifferent(target, content) {
  ...
  await fs.mkdir(path.dirname(target), { recursive: true })
  await fs.writeFile(target, content)
}

Reproduced in a scratch directory: with a symlink at <repoDir>/sessions pointing elsewhere, the next push's
write landed OUTSIDE repoDir. The link can be planted locally, or materialized by this plugin's own
git merge --no-commit / git checkout --theirs from a remote tree.

Suggested fix: lstat the final path component (or open with O_NOFOLLOW) before writing, and assert the
resolved parent chain stays inside repoDir — the same containment the read direction already enforces.

2. sanitizeRemote misses a token in the username position

SECURITY.md: "Remote-URL credentials, tokens, and key=value secrets are redacted before reaching the model
or the log"
.

sanitizeRemote (lib/sanitize.mjs) redacts only url.password when BOTH username and password are non-empty,
plus known credential query keys. With the common PAT spelling there is no password:

https://ghp_ABCDEFGHIJKLMNOPQRSTUVWX@github.com/o/r.git   ->   unchanged

That renders through /sync status (lib/status.mjs -> lib/render.mjs), and /sync status is a read-only
surface that never asks for confirmation — so the token reaches the model conversation and the session log this
plugin then mirrors and pushes.

Suggested fix: redact the whole userinfo component (or the username when it matches a token shape), not only
user:password.

3. git fetch force-updates the tracking ref

lib/git.mjs guards startsWith('+') only for verb === 'push', but fetch() passes
+<branch>:refs/remotes/origin/<branch> positionally. A remote rewrite therefore force-updates the tracking ref,
and since every fetch is followed by merge --no-commit, the rewritten tree is merged rather than refused.
"Never force-pushes" stays literally true; the comment's +refspec invariant does not hold for fetch.


Filed by a user installing this plugin as their session-history transport. No CVE claimed, no exploit beyond the
local-repository scenario above; reporting here because these are the guarantees the README offers and two of
them are what made the install decision on their side.

Activity

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

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions