Skip to content

fix(build): refuse a symlink as an archive member - #91

Merged
rmyndharis merged 1 commit into
mainfrom
fix/packager-symlink-members
Aug 12, 2026
Merged

fix(build): refuse a symlink as an archive member#91
rmyndharis merged 1 commit into
mainfrom
fix/packager-symlink-members

Conversation

@rmyndharis

Copy link
Copy Markdown
Owner

Collecting archive members used statSync, which follows symlinks. A directory link inside a plugin therefore pulled whatever it pointed at into the zip: a ui link to a sibling directory put that directory's files into the archive, contents and all, and the build reported success.

The entry string itself is perfectly ordinary in that case — ui/index.html names a relative path inside the plugin — so validating the entry could never have caught it. The escape happens on the filesystem, not in the manifest.

Members are now collected with lstatSync, and a link is refused wherever it appears: as the entry itself, or nested inside a directory being packaged. The message names the offending path.

Refusing rather than resolving is both simpler and more correct here. The archive stores file contents, so a link would be flattened into a copy regardless of where it pointed, and an absolute link names a path that exists only on the machine that built the package.

Collection also moved next to the entry validation it belongs with, so both halves of "what goes into the package" are covered by the same tests rather than one being untestable inside the packaging script.

Verification

  • Reproduced first: a ui link to a sibling directory produced a 5-member archive whose contents included a file from outside the plugin entirely. After the change both that case and a link nested one level deeper are refused, with no zip left on disk.
  • 569 tests pass, typecheck clean, catalog up to date, all 10 plugins build and load.
  • Artifacts are unchanged: chat-flow, after-hours and gsheets-logger rebuild byte-identical to main.

Collecting archive members used stat, which follows links, so a directory link inside a plugin pulled
whatever it pointed at into the zip. A `ui` link to a sibling directory put that directory's files
into the archive, contents and all, and the build reported success — the entry string itself is
perfectly ordinary, so validating it could never have caught this.

Members are now collected with lstat and a link is refused wherever it appears, whether it is the
entry itself or nested inside a directory being packaged. Refusing rather than resolving is both
simpler and more correct: the archive stores file contents, so a link would be flattened into a copy
regardless, and an absolute one names a path that exists only on the machine that built it.

Collection moved next to the entry validation it belongs with, so both halves of "what goes into the
package" are covered by the same tests.
@rmyndharis
rmyndharis merged commit ccf947a into main Aug 12, 2026
1 check passed
@rmyndharis
rmyndharis deleted the fix/packager-symlink-members branch August 12, 2026 09:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant