Home>All Insights>Content-Disposition headers in .NET

Content-Disposition headers in .NET

When serving file downloads from ASP.NET one issue that often gets overlooked is correctly encoding the Content-Disposition header. You’ll frequently see the following code snippet on blogs and programming forums:

Response.Clear();
Response.ContentType = "application/pdf";
Response.AddHeader("Content-Disposition", "attachment; filename=" + filename);
Response.BinaryWrite(fileContents);
Response.End();

When serving file downloads from ASP.NET one issue that often gets overlooked is correctly encoding the Content-Disposition header. You’ll frequently see the following code snippet on blogs and programming forums:

The Content-Disposition header, as specified in RFC 2183, distinguishes between files served for download and files to be displayed by the browser. It’s also frequently used to specify the filename for a downloaded file and can include extra information such as the file’s date and time.

The Problem

The error, on the highlighted line, is that the filename should be escaped (RFC 2184) for inclusion in the header – if, for example, the filename has a space in it then either the whole filename should be quoted (and quotes in the string escaped)

Content-Disposition: attachment; filename="Q1 Report.pdf"

or the entire filename encoded using the scheme specified in RFC 2231, which uses %20 to represent a space:

Content-Disposition: attachment; filename*=UTF-8''Q1%20Report.pdf

This all said, the code above will usually work: the error is common enough that some browsers (such as Internet Explorer and Chrome) will still process a Content-Disposition header filename that contains spaces even if it is unquoted and not encoded – but others, notably Firefox, will not. In some applications the filenames here may be under a user’s control, e.g. serving back uploaded files with their original file names, and so may well contain spaces or other special characters.

A Partial Solution

Now .NET has a built-in class to handle this, System.Net.Mime.ContentDisposition, e.g.

1
2
var contentDispositionHeader = new ContentDisposition() { FileName = filename };
Response.AddHeader("Content-Disposition", contentDispositionHeader.ToString());

but unfortunately it only does half the job: it will always generate the first quoted form above and not the second, RFC 2231-encoded form. This second form is needed if the filename contains non-ASCII characters; if it does, then the AddHeader call will throw an exception.

This means than if you are using ASP.NET forms or the first release of ASP.NET MVC then you also need to detect non-ASCII filenames and generate the RFC 2231-encoded filename yourself. The simple code snippet above has become much more complicated!

A Full Solution – but only if you’re using ASP.NET MVC

Thankfully in ASP.NET MVC 2 or later there is a built-in code to handle this for you; the File method inherited from the base controller class

1
return File(data, contentType, filename);

will manage the whole download for you, including generating the Content-Disposition header correctly for non-ASCII character filenames.

Security

A final note about security – as with any situation where a user-supplied string is incorrectly encoded there’s a potential security issue here! For example, if you had free rein to inject text into HTTP headers you could name a file

Innocent_sounding_file<line break>
Content-Length: 26<line break>
<line break>
Malicious File Contents!<line break>
<line break>

effectively injecting fake, alternative file contents for any user that downloads this file, all through the Content-Disposition header. Fortunately this is not an issue in .NET: the whole header will be safely encoded according to RFC 2184 – you’ll end up with a file download called “Innocent_sounding_file%0D%0AContent-Length%2E” etc. Why encode this automatically but not the Content-Disposition header filename? It would need to accept a list of key-value pairs to assemble into a header; the current version of the AddHeader API instead accepts a single string value and assumes you have performed any necessary assembling yourself.