Skip to content

fix(client): upload an attachment through the root that checked it - #198

Merged
willkg merged 3 commits into
mainfrom
upload-through-root
Sep 25, 2026
Merged

willkg merged 3 commits into
mainfrom
upload-through-root

Conversation

@willkg

@willkg willkg commented Sep 25, 2026

Copy link
Copy Markdown
Member

Closes #186.

The converter checked each image through the documentation root's os.Root, but the checksum and the upload then opened it by absolute path. That made three lookups, and only the first was bounded. A directory on the path replaced by a symlink between the check and the upload could make markfluence upload a file from outside the root, which S2 (no-read-outside-root) rules out. The plan is _plans/052_upload-through-the-root.md, including what the code review changed.

What changed

  • Opening through the root. LocalAttachment carries the root (json:"-", so check --show-html --json and the schema are unchanged) and an Open method, which the checksum and the upload both use. With a root, Open opens Source through it. After opening, it looks up the name again and refuses a symlink, or a name that is no longer the opened file. os.Root blocks escapes but follows a symlink that stays inside the root, and markfluence follows no symlinks, so without this a link swapped in at an image's name could publish an in-root .env under that name.
  • attachment-upload. It sets no root, because its files come from the command line rather than from a Markdown reference, and --name lets them live anywhere.
  • Regular files only. The open is non-blocking and the opened file must be a regular file, so a FIFO or directory swapped in is refused instead of hanging the upload.
  • Error wording. An escape is reported as "outside the documentation root", the converter's wording, not os.Root's bare error.
  • Checksums that match what was sent. The upload reads the whole file and writes the checksum of the bytes it actually sends into the attachment's comment. Before, the comment recorded the checksum from planning time, so a file changed in between got a comment that didn't describe its content.

Not changed

When a batch of uploads stops partway, the result still omits the uploads that landed before the failure. That predates this PR and is left alone.

Tests

  • Open refuses:
    • an escape through a symlinked directory created after the root was opened;
    • a relative symlink inside the root;
    • a FIFO and a directory.
  • Open without a root opens the file's path.
  • SyncAttachments fails on an escape and uploads nothing.
  • An upload of a file changed after planning carries the new bytes with their own checksum.
  • The converter sets the root on image attachments.
  • make check passes.

Answers #186: open an attachment through the os.Root that checked it,
and verify the upload sends the bytes whose checksum it records.
Closes #186. The converter checks an image through the documentation
root's os.Root, but the checksum and the upload opened it by absolute
path, so a directory on that path replaced by a symbolic link in between
could make markfluence upload a file from outside the root (S2).

LocalAttachment now carries the root (json:"-") and an Open method that
reads Source through it; the converter sets it. attachment-upload's files
have no root, since the person named them on the command line. Either
way the open is non-blocking and the handle must be a regular file.

The upload also hashes what it sends and refuses, sending nothing, when
the bytes no longer match the checksum taken while planning: the comment
would otherwise misdescribe the attachment's content.
- Open refuses a symbolic link inside the root, not only one out of it:
  os.Root follows an in-root link, and markfluence follows none, so a link
  swapped in at an image's name could publish an in-root .env under it.
  After opening it re-Lstats the name and requires os.SameFile with the
  handle.
- An escape reads as "outside the documentation root", the converter's
  wording, rather than os.Root's bare error, which read as a disk failure.
- A file changed between planning and upload no longer fails the
  publish: uploadAttachment reads it whole and stamps the comment with the
  checksum of the bytes it sends. This replaces the plan's D5.
- uploadAttachment drops its duplicate local names, and the CLAUDE.md
  sentence moves out of the parenthetical it landed in.
@willkg
willkg merged commit 03f0b7a into main Sep 25, 2026
1 check passed
@willkg
willkg deleted the upload-through-root branch September 25, 2026 13:09
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.

Attachment upload reads outside the os.Root that checked it

1 participant