Disclaimer: I do not accept responsibility for any issues arising from scripts being run without adequate understanding. It is the user's responsibility to review and assess any code before execution. More information

Building a Secure Web-Based File Manager for IIS Slideshow

In a previous post, I built an automated slideshow system that displays images from a folder on our web server. The slideshow works great, but there was a significant operational challenge: every time someone needed to add or remove images, they had to either RDP into the server or request IT assistance. This created unnecessary friction for content updates.

I needed a simple web interface that would allow authorized users to manage the slideshow images directly through their browser, while maintaining proper security controls and respecting the principle of least privilege.

Visuals of the Solution

This is the main interface of the application, it uses a file explorer view to be able to add and delete files without actual access to the filesystem:


The control bar at the top give you all the control, here you can see the upload and delete file option:

If you try to use the application are you have not been defined in the web.config file, you will see the access denied page:


Application overview

I built a lightweight ASP.NET Web Forms application using .NET Framework 4.0 that provides:

  • Windows Authentication for seamless integration with Active Directory
  • Authorization checking against a whitelist of approved users
  • File management interface for uploading and deleting images
  • Custom access denied page for unauthorized access attempts
  • Service account execution following least privilege principles

The entire solution consists of just two files: Default.aspx (with inline C# code) and Web.config.

Authentication and Authorization

The first challenge was controlling who could access the file manager. I configured the application to use Windows Authentication in IIS, which automatically passes the user's domain credentials to the application. This eliminated the need for a separate login system.

In the Web.config, I defined a list of authorized users in the DOMAIN\username format:

<appSettings>
  <add key="ImageDirectory" value="C:\inetpub\wwwroot\Slideshow\images" />
  <add key="AuthorizedUsers" value="bear\jsmith,bear\mjones,bear\tchen,bear\apatil,bear\lbrown,bear\rgarcia,bear\kwilson,bear\dlee,bear\smorgan" />
</appSettings>
<system.web>
  <authentication mode="Windows" />
  <authorization>
    <deny users="?" />
  </authorization>
</system.web>

The authorization check happens in the Page_Load event before any file operations occur:

protected void Page_Load(object sender, EventArgs e)
{
    imageDirectory = ConfigurationManager.AppSettings["ImageDirectory"];
    authorizedUsers = ConfigurationManager.AppSettings["AuthorizedUsers"]
        .Split(',')
        .Select(u => u.Trim().ToLower())
        .ToList();

    if (!IsUserAuthorized())
    {
        Response.StatusCode = 403;
        ShowAccessDeniedPage();
        return;
    }

    if (!IsPostBack)
    {
        DisplayUserInfo();
        LoadFiles();
    }
}

The authorization method performs a simple string comparison against the whitelist:

private bool IsUserAuthorized()
{
    try
    {
        WindowsIdentity identity = (WindowsIdentity)User.Identity;
        
        if (identity == null || !identity.IsAuthenticated)
            return false;

        string userName = identity.Name.ToLower();
        return authorizedUsers.Contains(userName);
    }
    catch
    {
        return false;
    }
}

Custom Access Denied Page

Rather than showing the generic IIS 403 error, I wanted unauthorized users to see a more informative and visually distinctive page. I created a custom access denied page that displays directly within the ASP.NET application.

The key was configuring IIS to pass through the HTTP response to ASP.NET instead of intercepting it:

<system.webServer>
  <httpErrors existingResponse="PassThrough" />
</system.webServer>

When an unauthorized user attempts to access the application, they see a dark-themed page with a pulsing red siren animation and clear instructions:

private void ShowAccessDeniedPage()
{
    string userName = "Unknown";
    try
    {
        WindowsIdentity identity = (WindowsIdentity)User.Identity;
        userName = identity != null ? identity.Name : "Not detected";
    }
    catch { }

    Response.Write(@"
<!DOCTYPE html>
<html>
<head>
    <title>Access Denied</title>
    <style>
        body {
            font-family: 'Segoe UI', sans-serif;
            background: #1a1a1a;
            color: #fff;
            display: flex;
            align-items: center;
            justify-content: center;
            min-height: 100vh;
        }
        .light {
            width: 120px;
            height: 120px;
            border-radius: 50%;
            background: #d32f2f;
            animation: pulse 1s ease-in-out infinite;
        }
        @keyframes pulse {
            0%, 100% {
                transform: scale(1);
                box-shadow: 0 0 40px rgba(211, 47, 47, 0.8);
            }
            50% {
                transform: scale(1.1);
                box-shadow: 0 0 80px rgba(211, 47, 47, 1);
            }
        }
    </style>
</head>
<body>
    <!-- Pulsing siren and error details -->
</body>
</html>");
    Response.End();
}

The CSS animation creates a smooth pulsing effect that immediately draws attention to the access denial without being overly dramatic or unprofessional.

File Manager Interface

For authorized users, the interface presents a clean grid layout showing all images in the slideshow folder. I used ASP.NET's Repeater control to dynamically generate the file cards:

private void LoadFiles()
{
    try
    {
        if (!Directory.Exists(imageDirectory))
        {
            Directory.CreateDirectory(imageDirectory);
        }

        DirectoryInfo dir = new DirectoryInfo(imageDirectory);
        FileInfo[] files = dir.GetFiles().OrderByDescending(f => f.LastWriteTime).ToArray();

        if (files.Length > 0)
        {
            rptFiles.DataSource = files;
            rptFiles.DataBind();
            pnlFiles.Visible = true;
            pnlEmpty.Visible = false;
        }
        else
        {
            pnlFiles.Visible = false;
            pnlEmpty.Visible = true;
        }
    }
    catch (Exception ex)
    {
        ShowMessage("Error loading files: " + ex.Message, "error");
    }
}

The Repeater template creates a card for each file with a thumbnail preview (for images) or a generic file icon (for other file types):

<asp:Repeater ID="rptFiles" runat="server">
    <ItemTemplate>
        <div class="file-item" onclick="toggleSelection(this, '<%# Eval("Name") %>')">
            <div class="file-icon">
                <asp:Image ID="imgPreview" runat="server" 
                    ImageUrl='<%# GetPreviewUrl(Eval("Name").ToString()) %>' 
                    AlternateText='<%# Eval("Name") %>' />
            </div>
            <div class="file-name"><%# Eval("Name") %></div>
            <div class="file-size"><%# FormatFileSize((long)Eval("Length")) %></div>
        </div>
    </ItemTemplate>
</asp:Repeater>

File Operations

Uploading Files

The upload functionality includes validation for file types and sizes:

protected void btnUpload_Click(object sender, EventArgs e)
{
    try
    {
        if (fileUpload.HasFile)
        {
            string fileName = Path.GetFileName(fileUpload.FileName);
            string filePath = Path.Combine(imageDirectory, fileName);

            string ext = Path.GetExtension(fileName).ToLower();
            string[] allowedExtensions = { ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".pdf", ".ppt", ".pptx" };
            
            if (!allowedExtensions.Contains(ext))
            {
                ShowMessage("Invalid file type. Allowed types: " + string.Join(", ", allowedExtensions), "error");
                return;
            }

            if (fileUpload.PostedFile.ContentLength > 104857600)
            {
                ShowMessage("File size exceeds 100MB limit.", "error");
                return;
            }

            if (File.Exists(filePath))
            {
                ShowMessage("A file with this name already exists.", "error");
                return;
            }

            fileUpload.SaveAs(filePath);
            ShowMessage("File uploaded successfully: " + fileName, "success");
            LoadFiles();
        }
    }
    catch (Exception ex)
    {
        ShowMessage("Error uploading file: " + ex.Message, "error");
    }
}

Deleting Files

Users can select multiple files using JavaScript and delete them in a batch operation:

var selectedFiles = [];

function toggleSelection(element, fileName) {
    var index = selectedFiles.indexOf(fileName);
    
    if (index > -1) {
        selectedFiles.splice(index, 1);
        element.classList.remove('selected');
    } else {
        selectedFiles.push(fileName);
        element.classList.add('selected');
    }
    
    document.getElementById('hdnSelectedFiles').value = selectedFiles.join('|');
    
    var deleteBtn = document.getElementById('btnDelete');
    deleteBtn.disabled = selectedFiles.length === 0;
}

The server-side deletion handler processes the selected files:

protected void btnDelete_Click(object sender, EventArgs e)
{
    try
    {
        string selectedFiles = hdnSelectedFiles.Value;
        
        if (string.IsNullOrEmpty(selectedFiles))
        {
            ShowMessage("No files selected for deletion.", "error");
            return;
        }

        string[] fileNames = selectedFiles.Split('|');
        int deletedCount = 0;

        foreach (string fileName in fileNames)
        {
            if (!string.IsNullOrWhiteSpace(fileName))
            {
                try
                {
                    string filePath = Path.Combine(imageDirectory, fileName);
                    if (File.Exists(filePath))
                    {
                        File.Delete(filePath);
                        deletedCount++;
                    }
                }
                catch { }
            }
        }

        if (deletedCount > 0)
        {
            ShowMessage(deletedCount + " file(s) deleted successfully.", "success");
        }

        hdnSelectedFiles.Value = string.Empty;
        LoadFiles();
    }
    catch (Exception ex)
    {
        ShowMessage("Error deleting files: " + ex.Message, "error");
    }
}

Service Account Configuration and Least Privilege

This is where the security model gets interesting. Rather than granting individual users write access to the images folder on the server, I configured the application pool to run under a dedicated service account.

Application Pool Setup

In IIS Manager, I configured the application pool identity:

  1. Created a service account in Active Directory: BEAR\svc_slideshow
  2. Granted this account only the "Log on as a batch job" right (required for application pools)
  3. Set the application pool to use this custom account identity
  4. Granted the service account Modify permissions on the images folder

This approach follows the principle of least privilege:

  • The service account has no interactive login rights
  • It cannot RDP to the server
  • It has write access only to the specific images folder
  • Individual users never need direct filesystem access
  • All operations are logged and auditable through IIS

Why This Matters?

When an authorized user uploads a file through the web interface, the file operation occurs under the service account's context, not the user's Windows identity. This means:

  • Users don't need NTFS permissions on the images folder
  • The security boundary is enforced at the application level (authorization check)
  • File operations are consistent regardless of which user performs them
  • The service account can be easily audited and monitored

The Web.config requires no special impersonation settings—file operations automatically use the application pool identity:

<system.web>
  <authentication mode="Windows" />
  <!-- No impersonation needed -->
</system.web>

Deployment

Deploying this solution is straightforward:

  1. Create the application folder structure:

    C:\inetpub\wwwroot\SlideShowManager\
    ├── Default.aspx
    ├── Web.config
    └── images\ (virtual directory to actual images folder)
    
  2. Configure IIS:

    • Create application pointing to the folder
    • Enable Windows Authentication
    • Disable Anonymous Authentication
    • Set application pool to use service account
  3. Create virtual directory for images:

    • Alias: images
    • Physical path: C:\inetpub\wwwroot\Slideshow\images
  4. Set NTFS permissions:

    • Service account: Modify on images folder
    • Application pool identity: Read on application folder

Scripts

The main script does not come with the CSS styles or file manager interface, but it does show you the placeholders where your code needs to go

Default.aspx

<%@ Page Language="C#" AutoEventWireup="true" %>
<%@ Import Namespace="System.IO" %>
<%@ Import Namespace="System.Linq" %>
<%@ Import Namespace="System.Collections.Generic" %>
<%@ Import Namespace="System.Configuration" %>
<%@ Import Namespace="System.Security.Principal" %>

<script runat="server">
    private string imageDirectory;
    private List<string> authorizedUsers;

    protected void Page_Load(object sender, EventArgs e)
    {
        imageDirectory = ConfigurationManager.AppSettings["ImageDirectory"];
        authorizedUsers = ConfigurationManager.AppSettings["AuthorizedUsers"]
            .Split(',')
            .Select(u => u.Trim().ToLower())
            .ToList();

        if (!IsUserAuthorized())
        {
            Response.StatusCode = 403;
            ShowAccessDeniedPage();
            return;
        }

        if (!IsPostBack)
        {
            DisplayUserInfo();
            LoadFiles();
        }
    }

    // [Additional methods: IsUserAuthorized, ShowAccessDeniedPage, LoadFiles, etc.]
</script>

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>Slideshow Manager</title>
    <!-- CSS styles -->
</head>
<body>
    <form id="form1" runat="server">
        <!-- File manager interface -->
    </form>
</body>
</html>

Web.config

<?xml version="1.0"?>
<configuration>
  <system.web>
    <compilation debug="true" targetFramework="4.0" />
    <httpRuntime targetFramework="4.0" maxRequestLength="102400" executionTimeout="300" />
    
    <authentication mode="Windows" />
    <authorization>
      <deny users="?" />
    </authorization>
    
    <sessionState mode="InProc" timeout="30" />
  </system.web>

  <system.webServer>
    <httpErrors existingResponse="PassThrough" />
    
    <security>
      <requestFiltering>
        <requestLimits maxAllowedContentLength="104857600" />
      </requestFiltering>
    </security>
  </system.webServer>

  <appSettings>
    <add key="ImageDirectory" value="C:\inetpub\wwwroot\Slideshow\images" />
    <add key="AuthorizedUsers" value="bear\jsmith,bear\mjones,bear\tchen,bear\apatil,bear\lbrown,bear\rgarcia,bear\kwilson,bear\dlee,bear\smorgan" />
  </appSettings>
</configuration>

Conclusion

This solution transformed slideshow content management from a tedious manual process requiring server access into a simple web-based workflow. Authorized users can now upload and remove images in seconds, while the service account model ensures proper security boundaries and maintains the principle of least privilege.

The entire implementation fits in two files with no external dependencies beyond .NET Framework 4.0, making it easy to deploy, maintain, and understand. The custom access denied page adds a professional touch while clearly communicating authorization failures to users.

Most importantly, the separation between user authentication (who you are), authorization (what you can access), and file operations (service account context) creates a clean security model that's both flexible and auditable.

Previous Post Next Post

نموذج الاتصال