fix(media): store uploads world-readable so the media server can serve them - #133
Merged
Conversation
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.pngreturns 403 Forbidden from nginx. 403 rather than 404 is the tell: nginx found the file and could not open it.Cause
storeUploadwrites to a temp file and renames it into place:os.CreateTemphardcodes mode 0600reserveAndRenameclaims the destination name withO_CREATE|O_EXCL, 0644os.Renamethen moves the temp file's inode onto that nameThe 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 servinglocation /wp-content/runs as another user and getsEACCES.Fix
Chmod(0o644)on the temp file before the rename.Chmodis not masked by the umask, so the mode is exact.Also best-effort widens a freshly created
YYYY/MMdirectory.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 waychmodfails 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_StoresWorldReadableFilepins the mode. Verified it fails without the fix:Full suite green with it.
TestEnsureTraversable_WidensNarrowDirectorycovers 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:
🤖 Generated with Claude Code