Upload files to a server folder and download them again in ASP.NET (C#)
Accepting files from users is one of the riskiest features a web application can have. Done carelessly, an upload form lets anyone place a script on your server and run it. Done properly, it is still short: limit the size, check the type, never trust the client's file name, store files where the web server will not execute them, and serve them back through your own code.

Configuration: size limits
Two settings cap the request size, and the smaller one wins:
<configuration>
<system.web>
<!-- kilobytes; default 4096 (4 MB) -->
<httpRuntime targetFramework="4.8" maxRequestLength="10240" />
</system.web>
<system.webServer>
<security>
<requestFiltering>
<!-- bytes; default 30000000 (about 28.6 MB) -->
<requestLimits maxAllowedContentLength="10485760" />
</requestFiltering>
</security>
</system.webServer>
</configuration>
Exceeding maxRequestLength raises "Maximum request length exceeded"; exceeding IIS's maxAllowedContentLength returns a 404.13 error before ASP.NET even runs. Keep both slightly above the largest file you accept, and check the size in code to give a friendly message.
The page
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Files.aspx.cs" Inherits="Demo.Files" %>
<!DOCTYPE html>
<html>
<head runat="server"><title>Files</title></head>
<body>
<form id="form1" runat="server">
<asp:FileUpload ID="fuFile" runat="server" accept=".pdf,.png,.jpg,.jpeg,.txt" />
<asp:Button ID="btnUpload" runat="server" Text="Upload" OnClick="btnUpload_Click" />
<asp:Label ID="lblMessage" runat="server" />
<asp:GridView ID="gvFiles" runat="server" AutoGenerateColumns="false" OnRowCommand="gvFiles_RowCommand"
EmptyDataText="No files uploaded yet.">
<Columns>
<asp:BoundField DataField="OriginalName" HeaderText="File" />
<asp:BoundField DataField="SizeKb" HeaderText="Size (KB)" DataFormatString="{0:N0}" />
<asp:BoundField DataField="Uploaded" HeaderText="Uploaded (UTC)" DataFormatString="{0:yyyy-MM-dd HH:mm}" />
<asp:ButtonField CommandName="Download" Text="Download" />
</Columns>
</asp:GridView>
</form>
</body>
</html>
The accept attribute filters the browser's file picker. It is a convenience: a user can still pick any file, and a script can post anything. The server makes the real decision.
The code-behind
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
namespace Demo
{
public partial class Files : Page
{
private const int MaxBytes = 10 * 1024 * 1024;
private static readonly HashSet<string> Allowed =
new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".pdf", ".png", ".jpg", ".jpeg", ".txt" };
// App_Data is never served directly by ASP.NET
private string StoreDir => Server.MapPath("~/App_Data/uploads");
protected void Page_Load(object sender, EventArgs e)
{
Directory.CreateDirectory(StoreDir);
if (!IsPostBack) BindFiles();
}
protected void btnUpload_Click(object sender, EventArgs e)
{
if (!fuFile.HasFile)
{
lblMessage.Text = "Choose a file first.";
return;
}
HttpPostedFile posted = fuFile.PostedFile;
string originalName = Path.GetFileName(posted.FileName); // older IE sent the full client path
string ext = Path.GetExtension(originalName);
if (!Allowed.Contains(ext))
{
lblMessage.Text = "Only PDF, PNG, JPEG and text files are accepted.";
return;
}
if (posted.ContentLength == 0 || posted.ContentLength > MaxBytes)
{
lblMessage.Text = "Files must be between 1 byte and 10 MB.";
return;
}
// Never use the client's name on disk: generate one, keep the original as metadata.
string storedName = Guid.NewGuid().ToString("N") + ext.ToLowerInvariant();
posted.SaveAs(Path.Combine(StoreDir, storedName));
File.WriteAllText(Path.Combine(StoreDir, storedName + ".name"), originalName);
lblMessage.Text = "Uploaded " + Server.HtmlEncode(originalName) + ".";
BindFiles();
}
private void BindFiles()
{
var files = new DirectoryInfo(StoreDir).GetFiles()
.Where(f => Allowed.Contains(f.Extension))
.OrderByDescending(f => f.CreationTimeUtc)
.Select(f => new
{
Stored = f.Name,
OriginalName = ReadOriginalName(f),
SizeKb = Math.Max(1, f.Length / 1024),
Uploaded = f.CreationTimeUtc
})
.ToList();
gvFiles.DataKeyNames = new[] { "Stored" };
gvFiles.DataSource = files;
gvFiles.DataBind();
}
private static string ReadOriginalName(FileInfo f)
{
string meta = f.FullName + ".name";
return File.Exists(meta) ? File.ReadAllText(meta) : f.Name;
}
protected void gvFiles_RowCommand(object sender, GridViewCommandEventArgs e)
{
if (e.CommandName != "Download") return;
int index = Convert.ToInt32(e.CommandArgument);
string stored = (string)gvFiles.DataKeys[index].Value;
// The key comes back from view state, but validate it anyway.
if (stored.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0 || !Allowed.Contains(Path.GetExtension(stored)))
return;
string fullPath = Path.Combine(StoreDir, stored);
if (!File.Exists(fullPath)) return;
string downloadName = ReadOriginalName(new FileInfo(fullPath));
Response.Clear();
Response.ContentType = MimeMapping.GetMimeMapping(downloadName);
Response.AddHeader("Content-Disposition",
"attachment; filename*=UTF-8''" + Uri.EscapeDataString(downloadName));
Response.AddHeader("X-Content-Type-Options", "nosniff");
Response.TransmitFile(fullPath);
Response.Flush();
Response.SuppressContent = true;
Context.ApplicationInstance.CompleteRequest();
}
}
}
Why each precaution matters
- Extension allow-list. A deny-list (".exe", ".aspx"…) always misses something:
.ashx,.config,.svgwith script, double extensions. Accept only what you need. - Generated names. Client file names can contain
..\sequences, reserved names such asCON, or collide with other users' files. A GUID avoids all of that; the original name is kept separately for display. App_Dataor a folder outside the site. ASP.NET blocks direct requests toApp_Data, so even a file that slipped through cannot be requested and executed by URL. A folder outside the web root is better still for large volumes.- Serving through code. Downloads pass through your page, where you can check permissions before sending anything.
TransmitFilestreams from disk without loading the whole file into memory. - Headers.
Content-Disposition: attachmentmakes the browser download instead of rendering; thefilename*form carries non-ASCII names correctly;nosniffstops the browser from guessing a different type.
Checking the extension does not prove the content. If you accept images, you can additionally decode them (for example with System.Drawing.Image.FromStream inside a try/catch, or a maintained imaging library) and reject files that are not valid images. For files other people will download, consider antivirus scanning on the server.
Several files at once
Since .NET 4.5, FileUpload supports multiple selection:
<asp:FileUpload ID="fuFiles" runat="server" AllowMultiple="true" />
foreach (HttpPostedFile file in fuFiles.PostedFiles)
{
// apply the same checks to each file
}
Remember that maxRequestLength applies to the whole request, so the total of all files must fit.
UpdatePanel and uploads
A FileUpload does not work inside an asynchronous postback: the browser cannot send files through the partial-page request that UpdatePanel uses. Register the upload button as a PostBackTrigger so it performs a full postback.
Related
- Validate the file extension before uploading, on the client and the server.
- Preview an image before uploading.
- List the files in a folder recursively.
Official documentation
- Microsoft Learn: FileUpload class, HttpPostedFile, HttpResponse.TransmitFile
- Microsoft Learn: HttpRuntimeSection.MaxRequestLength, IIS requestLimits
- OWASP: File Upload Cheat Sheet
- IETF: RFC 6266, Content-Disposition in HTTP