Static Files (Security Foot-guns)
On this page
Static Files Can Be a Data Leak
Serving static files sounds harmless until you ship: - config backups - .env files - source maps with secrets - internal admin UIs - user-uploaded files from the same directory as app assets Static file hosting is an attack surface. Treat it as one.Real Production Incident
Symptoms: - Security team reports that production secrets were exposed publicly. - Attackers downloaded a file that should never be public. - Incident response discovers that backups were accessible under /static/. Root cause: - Static files middleware was configured to serve from a directory that also contained deployment artifacts. - A “temporary” debug dump file was left on disk. - No denylist for sensitive extensions. - No CDN boundary; app served static directly. This is not “oops”. This is preventable design failure.Symptom → Cause → Diagnosis → Fix
Symptom: - Unexpected files accessible via HTTP (e.g., /appsettings.json, /.env, /backup.zip) - Strange 200 responses for paths that should 404 - Security findings: exposed internal assets Cause: - Static file root includes sensitive directories - Misconfigured request path mappings - Serving user uploads via static middleware - Missing security headers and content-type controls Diagnosis: - Enumerate accessible static paths with automated scanning. - Check app container filesystem layout. - Verify StaticFileOptions and file providers. - Inspect response headers (cache, content-type, nosniff). Fix: - Strictly separate app assets from everything else. - Never serve secrets or build artifacts from the same root. - Lock down static file mappings and headers. - Consider serving static via CDN/object storage.Anti-Pattern: Serving the Wrong Directory
This is how you leak files:
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider("/app"),
RequestPath = ""
});
If /app contains:
- configs
- logs
- dumps
- backups
You just exposed them.
Production rule:
Static file root must contain only public assets.
Correct Pattern: Dedicated Static Root
Keep public assets in a dedicated directory (wwwroot) and nothing else. Basic safe setup:app.UseStaticFiles();If you must serve a custom directory, do it explicitly and narrowly:
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider("/app/public"),
RequestPath = "/static"
});
And ensure /app/public contains only safe, intended files.
User Uploads Are Not Static Assets
Never serve user uploads from the same middleware/root as your app assets. Reasons: - content-type spoofing - stored XSS via SVG/HTML - cache poisoning - access control requirements Better patterns: - store uploads in object storage (S3, Blob Storage) - use signed URLs - scan files (AV) - enforce content-type and disposition If you must serve uploads yourself: - isolate directory - enforce strict headers - deny dangerous extensionsContent-Type Sniffing and XSS Risk
Browsers may sniff content type. If you serve untrusted content without protections, you can enable XSS. Set: - X-Content-Type-Options: nosniff Example header middleware (conceptual):
app.Use(async (ctx, next) =>
{
ctx.Response.Headers["X-Content-Type-Options"] = "nosniff";
await next();
});
Production rule:
Static responses should include nosniff. Especially if any user content is involved.
Cache-Control and Versioning
Static assets should be cached aggressively if they are versioned (hash in filename). If you do not version assets: - you will ship broken UIs due to stale caches - rollback becomes messy Recommended: - /app.js?v=hash or app.hash.js - Cache-Control: public, max-age=31536000, immutable (for versioned files) - Short cache for non-versioned assets But do not blindly cache sensitive content.Directory Browsing and File Enumeration
Do not enable directory browsing. Even if it seems harmless, it increases information disclosure: - file names - build artifacts - internal structures Production rule: If users can list directories, you have already lost control.Path Traversal Considerations
ASP.NET Core static file middleware does not simply “join strings”, but mistakes happen when people build custom file endpoints. Anti-pattern custom endpoint:
app.MapGet("/files/{name}", (string name) =>
{
var path = "/uploads/" + name;
return Results.File(path);
});
This invites traversal attempts like ../../etc/passwd (platform dependent).
If you must do dynamic file serving:
- normalize paths
- enforce allowlist
- ensure resolved path is under the intended root