Securing File Uploads in Classic ASP: A Practical Checklist

The twelve checks that separate a safe upload form from a remote-code-execution hole.

An upload form is the one place where you let anonymous strangers write files onto your server. On a Classic ASP site running on IIS, one careless folder setting turns that into remote code execution. This is the checklist, ordered by how badly each item bites.

1. Remove execute permission from the upload folder

This is the whole ball game. If an attacker can save shell.asp into a folder that IIS will execute, they own the application — every other control on this page is a delay tactic.

In IIS Manager, select the upload folder → Handler MappingsEdit Feature Permissions → untick Script and Execute. Or drop a web.config into the folder itself:

<!-- /uploads/web.config -->
<configuration>
  <system.webServer>
    <handlers accessPolicy="Read" />
    <staticContent>
      <!-- serve everything as a download, never as script -->
      <clear />
      <mimeMap fileExtension=".*" mimeType="application/octet-stream" />
    </staticContent>
  </system.webServer>
</configuration>

Better still, store uploads outside the webroot entirely and serve them through a script that checks authorisation and streams the bytes. Then there is no URL that maps to the file at all.

2. Use an allow-list of extensions, never a block-list

A block-list is a promise that you thought of every dangerous extension. You did not: .asp, .asa, .cer, .cdx, .aspx, .ashx, .config, .htaccess, .svg (scriptable), .html (stored XSS)…

Name the handful you actually want and reject everything else:

uploader.AllowedFileExtensions = "*.jpg,*.jpeg,*.png,*.gif,*.pdf"

Check the extension on the server too. The client-side check is a courtesy; anyone can post directly to your handler.

3. Never trust the supplied file name

The browser sends whatever the file was called, and an attacker sends whatever they like: ..\..\..\inetpub\wwwroot\default.asp, or a name with a null byte, or a 4,000-character name that overflows your database column.

The safest pattern is to not use the supplied name as a path at all — generate your own and keep theirs as display text:

<%
Function SafeExtension(fileName, allowed)
    Dim ext
    SafeExtension = ""
    If InStrRev(fileName, ".") > 0 Then
        ext = LCase(Mid(fileName, InStrRev(fileName, ".") + 1))
        If InStr(1, "," & allowed & ",", "," & ext & ",") > 0 Then
            SafeExtension = "." & ext
        End If
    End If
End Function

Dim ext, storedName
ext = SafeExtension(mvcfile.FileName, "jpg,jpeg,png,gif,pdf")

If ext = "" Then
    Response.Write "File type not allowed."
Else
    storedName = list(i) & ext              ' GUID + validated extension
    mvcfile.MoveTo Server.MapPath("uploads/" & storedName)
    ' store mvcfile.FileName separately as the display name
End If
%>

Watch for double extensions (photo.jpg.asp) and trailing characters (shell.asp., shell.asp::$DATA). Taking only the last dot-segment and requiring it to match your allow-list defeats all of them.

4. Validate the content, not just the name

Renaming shell.asp to shell.jpg is trivial. For image uploads, confirm the bytes really are an image by checking the magic number:

TypeFirst bytes (hex)
JPEGFF D8 FF
PNG89 50 4E 47 0D 0A 1A 0A
GIF47 49 46 38 ("GIF8")
PDF25 50 44 46 ("%PDF")
ZIP / Office50 4B 03 04 ("PK")

Do not trust the Content-Type header for this — it is supplied by the client and means nothing.

A file can pass a magic-number check and still be hostile: a valid JPEG with ASP source appended executes if it ever gets served as script. Magic numbers are a second layer, not a replacement for removing execute permission.

5. Enforce a size limit on the server

Without one, a single request can fill the disk and take the whole site down. Set the limit in the uploader and in web.config, so a direct post to your handler is capped too:

uploader.MaxSizeKB = 10240
uploader.FileTooLargeMsg = "That file is larger than the 10 MB limit."

Then keep an eye on total disk usage, not just per-file size. Ten thousand legitimate 10 MB uploads is still 100 GB.

6. Require authentication and authorisation

Check the session on the handler itself, not only on the page that renders the form. The handler is a URL, and attackers post to URLs directly:

<%
If Session("UserID") = "" Then
    Response.Status = "403 Forbidden"
    Response.End
End If
%>

If uploads are public by necessity, rate-limit by IP and require a CAPTCHA. An anonymous upload endpoint with no throttle becomes someone's free file host within days.

7. Serve uploaded files safely

  • Send Content-Disposition: attachment so browsers download rather than render.
  • Send X-Content-Type-Options: nosniff so Internet Explorer and friends do not guess a type and render HTML from a .jpg.
  • Serve user content from a separate hostname where practical, so a stored XSS cannot read cookies for your main domain.
  • Never echo the raw file name into a page without HTML-encoding it — a file called <img src=x onerror=alert(1)>.jpg is a stored XSS payload.
Dim q
q = Chr(34)
Response.AddHeader "Content-Disposition", "attachment; filename=" & q & safeName & q
Response.AddHeader "X-Content-Type-Options", "nosniff"

8–12. The rest of the list

  1. Scan for malware if uploads are shared between users. A clean file for you may be a payload for the next person who downloads it.
  2. Give the app pool identity Modify, not Full Control, and only on the upload and temp folders. It should not be able to change permissions.
  3. Clean up temp files. Abandoned uploads accumulate. ASP Uploader expires its own temp files, but check anything you write yourself.
  4. Log every upload — who, what, when, size, source IP. You will want this exactly once, and you will want it badly.
  5. Turn off detailed errors in production. A stack trace that reveals Server.MapPath output hands out your directory layout for free.

The five-minute audit

  1. Upload a file called test.asp containing <% Response.Write "x" %>. Request it. If you see x, stop and fix execute permissions now.
  2. Rename a text file to test.jpg and upload it. Is it accepted? Then you have no content check.
  3. Post directly to your handler URL with no session. Rejected? Good.
  4. Upload a file named ..\..\web.config. Where did it land?
  5. Upload something over your size limit using a tool rather than the browser. Still capped?

Frequently asked questions

What is the single most important file upload security control?

Removing execute permission from the folder uploads land in. Everything else is defence in depth.

Is checking the Content-Type header enough to validate a file?

No. The client sends that header and can set it to anything. Check the extension against an allow-list and verify the file's magic number.

Should uploaded files be stored in the database instead?

It removes the execute-permission risk entirely, at a cost in memory and throughput. See saving uploads to a folder or SQL Server.

Skip the plumbing

ASP Uploader does everything on this page out of the box: multi-file selection, a real progress bar, client and server validation, chunked large-file transfer, and no COM component to register. Drop the folder on your server and add one include line.

Download the free trial Try the live demo Pricing