I think the only way to answer this is through a collaborative commenting effort, so here goes a community wiki:
- Of course only registered users can upload, but I know that doesn't mean anything.
Agreed.
- The site is exposed via nginx.
Which means you have the actual web application running behind that – make sure that your adapter (WSGI? fCGI?) is securely set up.
- The machine on which the webserver is hosted only accepts incoming connections from the nginx machine.
clever move!
- The image upload link (nginx directive) is rate limited (in a way that generously accounts for a legitimate user spike).
Make sure this doesn't make it easier to DoS-attack your site.
- The webserver accepts only jpg/png/tiff/webp image formats (it derives the mime type using the magic library).
magemagick is a very well-maintained library, but it's also a very large attack surface, and there's been a lot of critical breaches happening through it. Using a much more restricted library for validation might be a good choice.
ensure that uploaded file extension always matches the detected format, and the file can be properly decoded as such. You can also consider stripping various metadata, like EXIF tags - basically, add a processing step, that takes only what's necessary (only image data) and save as a new file, with this step running as much isolated as possible (so RCEs in image decoding libraries won't get attacker any much access). After this, ensure that nginx always responds with the correct Content-Type and all the safety headers, X-Content-Type-Options etc.
- Both nginx and the webserver have a limit on the accepted file size.
good.
- All uploaded files are saved using a filename which is a hash of the contents.
Make sure to salt that hash; hashes are crackable (within limits), and you don't want an attacker to control the file name of their uploads. Don't just use MD5, but a less broken hash.
- By default, they are saved with only rw permissions, not rwx.
Save them with read permissions. Once they are saved, they never need to be modified again.
Also, use SELinux (or your server OS'es equivalent) to make sure your web application can only write and read that directory, never execute files in it.
- The images are stored on the same system as the webserver.
Might very quickly become a scalability issue: Server software would these days preferably run on fast reliable storage media (SSDs with hot fallover), whereas your user pictures would sufficiently be stored on cheap storage media with slow backups (i.e. if two hard drives fail at the same moment, OK, you'll need 15 minutes to restore from your redundancy and might lose a couple minutes of new user images).
They are saved directly to the final location (there's no intermediate staging location) but the images are not available for viewing until a specific database column is updated (see next point).
A periodic virus scan is completed on the newly uploaded files (and any malicious files are removed to a different directory to which the servers have no access) and then the aforementioned database column is updated to make the files available for viewing.
There's no computational advantage to doing that periodically instead of instantaneously after uploading the file. So, do it instantaneously, and keep your storage load constant as possible. Running the antivirus as a daemon avoids having to load up the virus definitions into memory for each scan.
- Lastly, a tiny amount of random noise is added to each image by doing something like
magick image.jpg -evaluate Add 0.1 image.jpg
That is a very compute intense step, which uncompresses images, adds noise, and that's the worst part, which in case of JPEG leads to maximum quality loss if you want to keep the original file size. (noise is very hard to compress, unless it's so weak that it actually gets compressed away and the resulting image is actually as if you never added noise.)
I don't see any security advantage of this step.
Further points
- Containerize your web application.
- Use a separate logging server
- Monitor for sudden increases in storage consumption – in case someone abuses your site automatedly e.g. to host pictures for their own gain