Dealing with user uploaded files

If you have a website that lets the user upload some sort of file, for example, profile pictures/avatar, attachments etc. you can use web browser standards such as Content-Security-Policy (CSP), Suborigins, Referrer-Policy, Content-Disposition and X-Content-Type-Options to reduce the attack vector greatly by limiting the permissions an evil crafted submitted file from a hacker have.

In my example I run a phpBB-forum where members can upload an avatar in the formats PNG and JPEG.

Because an attacker already has managed to upload files, I will not describe how you can mitigate that step.

Set the headers in the right place

When a user uploads an avatar, the file is stored on a path like:
/download/file.php?avatar=0_5.jpg. So the question is, where should the headers be delivered? The right answer is on file.php, but if you have paths before, you can, and should also deliver headers there - /download/ in this case.

So I did this in NginX:

location ~* /download/ {
more_set_headers "Content-Security-Policy form-action 'none'; default-src 'none'; frame-ancestors 'none'"; 
more_set_headers 'Referrer-Policy no-referrer';
add_header suborigin $rnd;

The CSP is designed such as embedded images only will be allowed from that path. If we directly visit for example /download/file.php?avatar=0_5.jpg the image will be shown just fine. But if there was any HTML-file instead of an image, it would be blocked (to a certain degree).

The Referrer-Policy should be set to no-referrer because otherwise we may expose secrets such as tokens or just information stored in the URI (private stuff). Actually, in this case the first number before "_" (?avatar=0_5.jpg = ID is 0) is the ID of the user which can be considered as sensitive information (used in pivoting attacks, spear phishing).

The $rnd variable is just to create a random string to be used as suborigin. The Perl-code I use is:
perl_set $rnd 'sub { my @chars = ("a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z"); my $string; $string .= $chars[rand @chars] for 1..40; return $string;}'; - just put this in your nginx.conf's http{}-department and use the variable like in the my example. Then use the random 40-char string in every suborigin value.

This is how it should look

curl -sI ""
HTTP/1.1 200 OK
Content-Type: image/jpeg
Cache-Control: public
Content-Disposition: inline; filename=0_5.jpg
X-XSS-Protection: 1; mode=block
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
X-Permitted-Cross-Domain-Policies: master-only
X-DNS-Prefetch-Control: off
Content-Security-Policy: form-action 'none'; default-src 'none'; frame-ancestors 'none' 
Referrer-Policy: no-referrer
suborigin: fxqmoxamvjmaifqipbntwxayangxswpbjaznxqhucat

Take note of the bolded. These headers are great to send out on paths that points to content such as images, text- and zip-files and so on.

Let's put it to the test!

Alright, let's test it out and see how our protection works! Let's say that a hacker is able to upload HTML-files instead of PNG/JPG-files! Oh no!!! The HTML-file contains this:

console.log("Your HTTP-cookies: " + document.cookie);
console.log("Your Referrer: " + document.referrer);
console.log("This suborigin: " + document.suborigin);
<a href="">Click</a>

With protection:

In this video the protection is there. As you can see, an attacker can't read any cookies due to Suborigin and the referrer is completely removed on outbound links. Note that no Content-Security-policy was in action which would remove any ability to run Javascript; because the attacker can upload files under det context of 'self' we should assume that CSP would be bypassed.

With our new headers set, document.cookie wont return any cookies (blocked by CSP ('none'), suborigin and HTTPOnly of course), document.referrer will be shown as only origin and document.suborigin will be a 40-char random character which will make every request use a separate origin!

So even in the worst case scenario where an attacker can upload files to your server, much of the uploaded content will be useless to target users and browsers due to the limited permissions.


  • Use Origin as your global referrer policy ¹
  • Use No-referrer on paths that serve static content ²
  • Use the strictest Content-Security-Policy on paths that is static content ³
  • Always send X-Content-Type-Options: nosniff to stop MIME-sniffing
  • Send a suborigin-header on paths that serve static content
  • The Content-Disposition header should be used correctly

1 To mitigate URL-info leaks within origin
2 To mitigate URL-info leaks from external origins
3 default-src 'none'; form-action 'none'; frame-ancestors 'none'