Skip to content

Build1 publisher2 min readPublished

Mastodon and Discourse write public-read S3 objects unless the deployer opts out

Stave flagged 47 security findings in the documented AWS defaults of Mastodon, Discourse and Chatwoot, according to a dev.to post. None of the three configures bucket encryption, access logging or Public Access Block, so whoever creates the bucket has to add them.

The Engineer · Build desk

Illustration accompanying Mastodon and Discourse write public-read S3 objects unless the deployer opts out

What happened

  • Discourse ships with s3_use_acls on and secure_uploads off, a combination that writes every non-secure upload to S3 with a public-read ACL.
  • Chatwoot hands storage to Active Storage, which creates private objects, so it logged 15 findings to 16 each for Mastodon and Discourse.
  • Stave rated the public-read default critical under control CTL.S3.PUBLIC.001, with an exposure score of 100 out of 100.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • exposure Every image URL a Mastodon instance serves carries its bucket name, so on the public default any object key someone can guess is downloadable by tools that enumerate bucket names, according to the author.
  • constraint Because each object's ACL is written at upload and a later bucket policy does not cancel it, media stored under the public default stays readable after an operator tightens the policy.
  • decision A Mastodon operator who sets S3_PERMISSION to an empty string drops the per-object grant remote servers use to fetch media, and has to decide how federated servers will reach those files instead.

Mastodon's public default is one line of Ruby. Line 57 of `config/initializers/paperclip.rb` reads `s3_permissions: ENV.fetch('S3_PERMISSION') { 'public-read' },` [7]. Leave the variable unset and every file the instance uploads, profile pictures and media attachments included, gets a public-read ACL [7]. The choice is deliberate. Remote servers in the federation have to fetch an instance's media [8].

The opt-out is careful work. Line 75 of the same file switches ACLs off entirely when the variable is an empty string: `Paperclip::Attachment.default_options[:s3_permissions] = ->(*) {} if ENV['S3_PERMISSION'] == ''` [9]. Unset means public-read. Set but empty means no ACLs at all [7][9]. An operator picks either behaviour from the environment without patching the initializer, and Mastodon's version of the fix comes down to two quote marks [9]. Discourse puts its equivalent in admin settings: enable `secure_uploads` or disable `s3_use_acls` [12].

The remaining 15 findings fire on all three projects [2]. The author describes them as infrastructure settings that Rails applications never touch and setup documentation never mentions [15]. Paperclip and Active Storage control what goes into the bucket. How the bucket itself is configured sits outside them [16]. Chatwoot's `config/storage.yml` shows the boundary. It reads an access key, a secret, a region and a bucket name from the environment and sets no ACL [13]. Encryption and access logging have nowhere to go in that file [13]. "These projects work exactly as documented. The problem is the documentation," the author wrote [20].

According to the author, the object-level ACL and the bucket policy are evaluated independently, and either one granting access is sufficient [17]. The guard above both is S3 Public Access Block. Without it, nothing at the account level stops this bucket, or any other bucket in the account, from being public, the post says [18].

The 47 is a count against a modeled deployment [2]. Each snapshot represents "what a deployer gets if they follow the docs without additional hardening" [5]. Stave, the open-source configuration checker the author used, evaluates those snapshots offline and checks what the bucket would look like given the application defaults [1][6]. For the count to transfer to a real instance, its bucket has to carry nothing beyond what the app's config implies [3]. A team that already provisions the bucket in its own infrastructure code, with encryption and logging set, starts from a different snapshot and would see fewer of the shared findings [3]. The post did not test live buckets, so settings applied when a bucket is created sit outside its count [6].

What to watch

  • Whether Mastodon, Discourse or Chatwoot add bucket encryption, access logging and Public Access Block steps to their AWS setup documentation.
  • Whether Discourse changes the shipped defaults for s3_use_acls and secure_uploads.
  • A rerun of the same Stave controls against live, separately provisioned buckets, to see how many of the 15 shared findings survive.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories