Getting IIS to serve any file type

If you want your application to download files over your corporate internet, using file shares will do just fine. However, once the data needs to be transferred between networks you’ll usually find that there’s a firewall somewhere that blocks the SMB ports.
Of course there are other file transfer protocols, but they have their own challenges in requiring the firewall to reserve port ranges for the data channel (FTP, FTPS) or lacking IIS support (SFTP).
Obviously, if you’re a developer you could always write your own webservice that provides file transfer functionality, but to me it feels like you shouldn’t have to. And indeed, if you think about it, you don’t need a webservice for this – IIS is perfectly capable of serving up files on its own.

IIS can even be configured to serve up ASP.NET-related files (i.e. dll’s, .aspx and web.config files) which it would normally prevent from being downloaded – this takes some web.config magic, and that is exactly what I’m going to show here.

Disclaimer: let me just say that IIS by default blocks these files for a good reason – it is a security feature that prevents visitors from downloading sensitive parts of your application/website. So before you start undoing all these safety measures, be sure that this is what you want, and that you limit this configuration only to the part of your website that people need to be able to download.

Configuring a virtual directory

All right, let’s say that, under the Default Web Site in IIS, you’ve created a Virtual Directory called “Staging” that maps to C:inetpubwwwrootStaging. It contains a folder called “Public”, of which the contents should be made available for download.

IIS Virtual Directory

Because the IIS Manager tends to create web.configs all over the place, we’re going to configure our virtual directory by creating a web.config by hand.

Limiting access to only the Public folder

To start configuring our virtual directory, place a web.config file in C:inetpubwwwrootStaging with the following contents:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <system.webServer>
    <security>
      <authorization>
        <!-- Deny all users access to the root of the website, since it
             contains this web.config -->
        <remove users="*" roles="" verbs="" />
        <add accessType="Deny" users="*" />
      </authorization>
    </security>
  </system.webServer>

  <location path="Public" allowOverride="false">
    <system.webServer>
      <directoryBrowse enabled="true"
                       showFlags="Date, Time, Size, Extension" />
      <security>
        <authorization>
          <!-- Allow all users access to the Public folder -->
          <remove users="*" roles="" verbs="" />
          <add accessType="Allow" users="*" roles="" />
        </authorization>
      </security>
    </system.webServer>
  </location>
</configuration>

This limits the access to only the Public folder, and enables directory listing for it. Note the use of <location path="Public" allowOverride="false" /> to specify the settings that apply to the Public subdirectory. If this is new to you, you should definitely check out Jon Galloway’s excellent article 10 Things ASP.NET Developers Should Know About Web.config Inheritance and Overrides.

Remove ASP.NET file handlers

Next, we want all file extensions such as .aspx, .svc and .cshtml to lose their special meaning to IIS and just be served like any other static file. Obviously, you only need to do this if you have one of the ASP.NET features installed, otherwise these extensions don’t have any special meaning to IIS to begin with.

  <location path="Public" allowOverride="false">
    <system.webServer>
      ...
      <handlers>
        <clear />
        <add name="StaticFile" path="*" verb="*"
             modules="StaticFileModule,DefaultDocumentModule,DirectoryListingModule"
             resourceType="Either" requireAccess="Read" />
      </handlers>

Don’t serve any default documents

When requesting a file listing, by default IIS will look to see if that folder contains a default document such as index.html or default.htm, and if it exists, return that file rather than the file listing. While useful in regular situations, we don’t want that kind of special treatment here, so let’s clear this list of special names:

    <system.webServer>
      ...
      <defaultDocument>
        <files>
          <clear />
        </files>
      </defaultDocument>

Serve all file extensions

IIS associates each file extension to a MIME type, and by default, IIS blocks requests for all unknown MIME types. By replacing all existing MIME type mappings with a single mapping to application/octet-stream, all file extensions will be served…

      <staticContent>
        <clear />
        <mimeMap fileExtension="*" mimeType="application/octet-stream" />
      </staticContent>

Remove request filtering

…or so you would think. However, downloading files with special extensions such as .cs will still fail, but this time with a 404.7. And according to this list of HTTP 404 error substatus codes, this means that the file extension is denied because of security considerations.
If you want to know which extensions IIS blocks with a 404.7, open the applicationHost.config (located in C:WindowsSystem32inetsrvconfig) and navigate to the system.webServer/security/requestFiltering element for the list of extensions. Just next to the <fileExtensions> child element, you’ll also find a <hiddenSegments> element which blocks web.config and bin folders, among others. Both filtering lists should be cleared in our web.config if we want these files to be downloaded:

      <security>
        <requestFiltering>
          <hiddenSegments>
            <clear />
          </hiddenSegments>
          <fileExtensions>
            <clear />
          </fileExtensions>
        </requestFiltering>
      </security>

Prevent IIS from using web.configs in subdirectories

Even though IIS will now happily serve web.config files from the Public folder, there is one last problem to tackle: IIS still assigns special meaning to web.config files and interprets its contents. This means that if you place a web.config file in the Public folder which contains an <add accessType="Deny" users="*" /> fragment, IIS will still honor that and disallow everone access to the Public folder. Since we consider all files in the Public folder as content, we don’t want this behaviour.

This can be changed, but only from within the applicationHost.config (located in C:WindowsSystem32inetsrvconfig) – to my knowledge, this setting cannot be reached from the IIS Manager user interface. Open the applicationHost.config and navigate to the configuration/system.applicationHost/sites element. In here, you’ll find the site definition for the Default Web Site. As you can read from the <virtualDirectory> documentation, the trick is to add an allowSubDirConfig="false" attribute to the virtualDirectory element:

    <site name="Default Web Site" id="1">
        <application path="/">
            <virtualDirectory path="/"
                              physicalPath="%SystemDrive%inetpubwwwroot" />
            <virtualDirectory path="/Staging"
                              physicalPath="C:inetpubwwwrootStaging"
                              allowSubDirConfig="false" />
        </application>
        <!-- ... -->
    </site>

This tells IIS that it should only look for a web.config in the C:inetpubwwwrootStaging directory, and disregard any other web.configs it may find in any subdirectories.

Summary

Combined, the web.config file now looks like this:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>

  <system.webServer>
    <security>
      <authorization>
        <!-- Deny all users access to the root of the website, since it
             contains this web.config -->
        <remove users="*" roles="" verbs="" />
        <add accessType="Deny" users="*" />
      </authorization>
    </security>
  </system.webServer>

  <location path="Public" allowOverride="false">
    <system.webServer>
      <directoryBrowse enabled="true"
                       showFlags="Date, Time, Size, Extension" />
      <defaultDocument>
        <files>
          <!-- When requesting a file listing, don't serve up the default 
               index.html file if it exists. -->
          <clear />
        </files>
      </defaultDocument>

      <security>
        <authorization>
          <!-- Allow all users access to the Public folder -->
          <remove users="*" roles="" verbs="" />
          <add accessType="Allow" users="*" roles="" />
        </authorization>

        <!-- Unblock all sourcecode related extensions (.cs, .aspx, .mdf)
             and files/folders (web.config, bin) -->
        <requestFiltering>
          <hiddenSegments>
            <clear />
          </hiddenSegments>
          <fileExtensions>
            <clear />
          </fileExtensions>
        </requestFiltering>
      </security>

      <!-- Remove all ASP.NET file extension associations.
           Only include this if you have the ASP.NET feature installed, 
           otherwise this produces an Invalid configuration error. -->
      <handlers>
        <clear />
        <add name="StaticFile" path="*" verb="*"
             modules="StaticFileModule,DefaultDocumentModule,DirectoryListingModule"
             resourceType="Either" requireAccess="Read" />
      </handlers>

      <!-- Map all extensions to the same MIME type, so all files can be
           downloaded. -->
      <staticContent>
        <clear />
        <mimeMap fileExtension="*" mimeType="application/octet-stream" />
      </staticContent>
    </system.webServer>
  </location>

</configuration>

Obviously you can still protect access to these files using HTTPS and Windows Authentication or IIS Client Certificate Mappings, but this is how you get IIS to serve any arbitrary file.