Building a File Manager in Classic ASP

A browsable folder UI, and the path-traversal checks that stop it exposing the whole disk.

A file manager is an upload form with three extra verbs — list, download, delete — and each one adds a way to get it badly wrong. The code is not hard; the authorisation and path handling are where the care goes.

Listing a folder

<%
Dim fso, folder, f, root
root = Server.MapPath("userfiles")

Set fso    = Server.CreateObject("Scripting.FileSystemObject")
Set folder = fso.GetFolder(root)
%>
<table>
<tr><th>Name</th><th>Size</th><th>Modified</th><th></th></tr>
<%
For Each f In folder.Files
%>
    <tr>
        <td><%= Server.HTMLEncode(f.Name) %></td>
        <td><%= FormatSize(f.Size) %></td>
        <td><%= f.DateLastModified %></td>
        <td>
            <a href="download.asp?f=<%= Server.URLEncode(f.Name) %>">Download</a>
            <a href="delete.asp?f=<%= Server.URLEncode(f.Name) %>">Delete</a>
        </td>
    </tr>
<%
Next
%>
</table>

Server.HTMLEncode on the name, Server.URLEncode in the link. A file named <script>alert(1)</script>.txt is legal on NTFS, and a file manager that renders names raw is a stored XSS waiting for someone to notice.

The one bug every file manager has

Both download.asp and delete.asp take a file name from the query string. Passed straight to Server.MapPath, that is a path traversal vulnerability:

delete.asp?f=..%5C..%5Cweb.config
delete.asp?f=..%5C..%5Cglobal.asa

Validating the string is a game of guessing every encoding. Resolving the path and checking where it actually landed is not:

<%
Function ResolveInside(root, userName)
    Dim fso, full
    ResolveInside = ""

    If InStr(userName, Chr(0)) > 0 Then Exit Function        ' null byte
    If InStr(userName, "/") > 0 Or InStr(userName, "\") > 0 Then Exit Function
    If InStr(userName, "..") > 0 Then Exit Function

    Set fso = Server.CreateObject("Scripting.FileSystemObject")
    full    = fso.BuildPath(root, userName)

    ' the decisive check: after resolution, is it still under root?
    If LCase(Left(full, Len(root))) <> LCase(root) Then Exit Function
    If Not fso.FileExists(full) Then Exit Function

    ResolveInside = full
End Function
%>

Reject anything that resolves outside the root, rather than trying to strip dangerous sequences out of the input. Stripping is a filter you have to get right every time; resolving-and-comparing is a property you can actually verify.

Downloading safely

<%
Dim root, path, stm, q
q    = Chr(34)
root = Server.MapPath("userfiles")

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

path = ResolveInside(root, Request.QueryString("f") & "")

If path = "" Then
    Response.Status = "404 Not Found"
    Response.End
End If

Set stm = Server.CreateObject("ADODB.Stream")
stm.Type = 1
stm.Open
stm.LoadFromFile path

Response.Clear
Response.ContentType = "application/octet-stream"
Response.AddHeader "Content-Disposition", "attachment; filename=" & q & fso.GetFileName(path) & q
Response.AddHeader "X-Content-Type-Options", "nosniff"
Response.BinaryWrite stm.Read
stm.Close
Response.End
%>

Serving through a script rather than linking directly is the point: it lets you check the session before a single byte goes out, and it means files can live outside the webroot with no URL that reaches them.

Deleting

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

' deletes must not be reachable by GET - a crawler or a prefetch will find them
If UCase(Request.ServerVariables("REQUEST_METHOD")) <> "POST" Then
    Response.Status = "405 Method Not Allowed"
    Response.End
End If

path = ResolveInside(Server.MapPath("userfiles"), Request.Form("f") & "")

If path <> "" Then
    fso.DeleteFile path, True
    LogAction Session("UserID"), "delete", path
End If
%>

A destructive action behind a GET link will eventually be triggered by a crawler, a link prefetcher or a browser extension. Make deletes POST, and add a CSRF token if the manager is behind a login.

Adding upload

This is the easy part. Files land in the same folder the listing reads, so the manager refreshes and the new file is simply there:

<%@ Language="VBScript" %>
<!-- #include file="aspuploader/include_aspuploader.asp" -->

<%
Dim uploader
Set uploader = new AspUploader
uploader.Name = "myuploader"
uploader.MultipleFilesUpload = true
uploader.MaxSizeKB = 51200
uploader.AllowedFileExtensions = "*.pdf,*.doc,*.docx,*.xls,*.xlsx,*.jpg,*.png,*.zip"
uploader.SaveDirectory = "userfiles"        ' written as each file completes
uploader.Render()
%>

There is a working AJAX file manager demo that lists, uploads and deletes without reloading the page.

Per-user folders

The moment more than one person uses the manager, a shared folder becomes a shared filesystem. Give each user a root and derive it on the server — never from a parameter:

<%
Dim userRoot
' from the session, NOT from Request - a user-supplied folder id is a traversal bug
userRoot = Server.MapPath("userfiles/" & CLng(Session("UserID")))

If Not fso.FolderExists(userRoot) Then fso.CreateFolder userRoot
%>

File manager checklist

  1. Every action checks the session — list, download, upload and delete alike.
  2. Paths are resolved and verified to be under the root, never string-filtered.
  3. The user's folder comes from the session, not from a request parameter.
  4. Names are HTML-encoded on output and URL-encoded in links.
  5. Deletes are POST, ideally with a CSRF token.
  6. The storage folder has execute permission removed, or lives outside the webroot.
  7. Uploads have an extension allow-list and a size cap.
  8. Every destructive action is logged with user, file and timestamp.

The upload security checklist covers the storage side in more depth.

Frequently asked questions

How do I list the files in a folder in Classic ASP?

Use Scripting.FileSystemObject: GetFolder(Server.MapPath("...")) and iterate its Files collection. HTML-encode every name you render.

How do I stop path traversal in a file manager?

Resolve the requested name to a full path and confirm it still starts with your root folder. Reject it otherwise. Do not try to strip .. out of the input.

Should uploaded files be inside the website folder?

Preferably not. Storing them outside the webroot and serving through a script means no URL maps to a user file, and authorisation is checked on every request.

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, incremental large-file handling, 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