Skip to content

fix(media): store uploads world-readable so the media server can serve them - #133

Merged
ssavutu merged 1 commit into
mainfrom
fix/media-upload-file-mode
Aug 1, 2026
Merged

fix(media): store uploads world-readable so the media server can serve them#133
ssavutu merged 1 commit into
mainfrom
fix/media-upload-file-mode

Conversation

@ssavutu

@ssavutu ssavutu commented Aug 1, 2026

Copy link
Copy Markdown
Member

Follow-up to #132, which merged while this was being written.

Symptom

An uploaded image lands on disk at the correct path, and GET https://delta.thetriangle.org/wp-content/uploads/2026/08/image.png returns 403 Forbidden from nginx. 403 rather than 404 is the tell: nginx found the file and could not open it.

Cause

storeUpload writes to a temp file and renames it into place:

  • os.CreateTemp hardcodes mode 0600
  • reserveAndRename claims the destination name with O_CREATE|O_EXCL, 0644
  • os.Rename then moves the temp file's inode onto that name

The rename discards the reserved 0644 file entirely, so the stored asset keeps the temp file's 0600. It is readable only by the CMS's own uid, while the nginx worker serving location /wp-content/ runs as another user and gets EACCES.

Fix

Chmod(0o644) on the temp file before the rename. Chmod is not masked by the umask, so the mode is exact.

Also best-effort widens a freshly created YYYY/MM directory. MkdirAll's mode is umask-masked, so under a strict umask the first upload of a new month can leave the directory without the world-execute bit nginx needs to traverse it — producing a 403 indistinguishable from the file-mode one. Failures are ignored deliberately: the only way chmod fails there is that the ETL's rsync owns the directory, in which case its modes are already correct.

Not a regression from #132

This predates the editor work. It could not surface while uploading was admin-only and no uploaded image had been fetched back through nginx — #132 is simply what made anyone actually request one.

Testing

TestStoreUpload_StoresWorldReadableFile pins the mode. Verified it fails without the fix:

--- FAIL: TestStoreUpload_StoresWorldReadableFile
    media_test.go:422: stored file mode = 0600, want 0644 (0600 means Nginx will 403 it)

Full suite green with it. TestEnsureTraversable_WidensNarrowDirectory covers the directory case.

Note on already-uploaded files

Anything uploaded before this ships is still 0600 on disk and will keep 403ing. To repair in place on the media host:

find /mnt/cephfs/media/wp-content/uploads -type f -perm 600 -exec chmod 644 {} +

🤖 Generated with Claude Code

…e them

An uploaded image landed on disk at the right path but every request for
it got 403 from Nginx.

storeUpload writes to a temp file and renames it into place. os.CreateTemp
hardcodes mode 0600, and os.Rename moves the temp file's inode onto the
destination -- so the 0644 that reserveAndRename uses to claim the name is
discarded along with the empty file it created. Every uploaded asset ended
up readable only by the CMS's own uid, while the Nginx worker serving
/wp-content/ runs as another user and got EACCES.

Chmod the temp file before the rename, and best-effort widen a freshly
created YYYY/MM directory, whose mode is masked by the process umask and
can produce an identical 403 on the first upload of a month.

This predates the editor work -- it just could not surface while uploading
was admin-only and no uploaded image had been fetched back through Nginx.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ssavutu
ssavutu merged commit 6aaf2c7 into main Aug 1, 2026
6 checks passed
@ssavutu
ssavutu deleted the fix/media-upload-file-mode branch August 1, 2026 04:14
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