Skip to content

Fix S3 uploads landing private on ACL-based providers (opt-in public-read) - #352

Open
clydesantiago wants to merge 1 commit into
sw33tLie:mainfrom
clydesantiago:fix/s3-public-read-acl
Open

Fix S3 uploads landing private on ACL-based providers (opt-in public-read)#352
clydesantiago wants to merge 1 commit into
sw33tLie:mainfrom
clydesantiago:fix/s3-public-read-acl

Conversation

@clydesantiago

Copy link
Copy Markdown

Problem

S3Uploader never sends an ACL header. The signed header list is fixed at four entries:

// S3Uploader.swift
let signedHeaderNames = ["content-type", "host", "x-amz-content-sha256", "x-amz-date"]

On any provider that honours object ACLs (AWS S3, DigitalOcean Spaces, MinIO, Backblaze B2), objects default to private. The upload succeeds with a 200, Test Connection reports "Connection successful!", and macshot copies a link to the clipboard that returns 403 AccessDenied when anyone opens it.

Reproduced against DigitalOcean Spaces:

$ curl -sI https://<space>.nyc3.cdn.digitaloceanspaces.com/files/Screenshot_2026-08-16_at_22-22-44.png
HTTP/1.1 403 Forbidden
<Error><Code>AccessDenied</Code>...

There is currently no way to fix this from inside macshot. The only workaround is a provider-side bucket policy.

This does not affect Cloudflare R2, which serves public objects through r2.dev or a custom domain and has no ACLs at all, which is likely why it went unnoticed.

Change

Adds a "Make uploads publicly readable" checkbox to the S3 settings section, backed by s3PublicRead, default off.

When enabled:

  • the PUT carries x-amz-acl: public-read
  • x-amz-acl is appended to the SigV4 signed header list and re-sorted, so the signature still validates

When disabled the request is byte-for-byte what it is today.

Off by default for two reasons: R2 rejects the header outright, and making uploads world-readable should be a deliberate choice rather than a silent default.

Files

File Change
macshot/Upload/S3Uploader.swift Config.publicRead, conditional header, conditional signing
macshot/UI/Windows/SettingsWindowController.swift checkbox + explanatory note + s3PublicReadChanged(_:)
macshot/en.lproj/Localizable.strings two new keys

Deployment notes

  • No migration needed. UserDefaults.bool(forKey:) returns false for an unset key, so existing installs keep exactly today's behaviour until the user ticks the box.
  • No new settings-export surface. SettingsPortability.secretSubstrings already contains "s3", so s3PublicRead is excluded from exports automatically by the existing fail-closed rule.
  • Files already uploaded stay private. The ACL is applied at PUT time. Existing objects need s3cmd setacl --acl-public --recursive or a bucket policy.
  • Localization: English keys added. The other 40 locales fall back to the English key until /translate-missing is run.
  • No new dependencies, no SwiftUI, no change to the minimum macOS 12.3 target.

Testing

  • swiftc -parse clean on both changed files; plutil -lint clean on the strings file.
  • Verified against DigitalOcean Spaces: unchecked reproduces the 403; checked returns a publicly readable object.
  • Verified the checkbox-off path leaves the R2 request unchanged.

I do not have Xcode set up for a full build of this project, so please give it a compile before merging.

S3Uploader never sent an ACL header, so on providers that honour object
ACLs (AWS S3, DigitalOcean Spaces, MinIO, Backblaze B2) every upload
landed private and the link macshot copies to the clipboard returned
403 AccessDenied. The upload itself succeeds, so nothing surfaces as an
error and Test Connection still reports success.

Add an "Make uploads publicly readable" checkbox to the S3 settings
section (s3PublicRead, default off). When enabled the PUT carries
x-amz-acl: public-read, and the header is added to the SigV4 signed
header list so the signature still validates.

Off by default because Cloudflare R2 has no object ACLs and rejects the
header, and because making uploads world-readable should be a
deliberate choice rather than a silent default.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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