Skip to content
highCVSS 7.5CWE-22A01:2021 – Broken Access Control

ASP.NET Core

Path.Combine discards everything before an absolute path. An application that carefully builds a safe base directory and then combines user input onto it has built nothing.

Path.Combine

C#the single most important fact on this pageVulnerable
// Path.Combine discards ALL preceding arguments as soon as one is rooted.
Path.Combine("/app/data", "reports/q1.pdf")   // "/app/data/reports/q1.pdf"
Path.Combine("/app/data", "/etc/passwd")      // "/etc/passwd"   <- base gone
Path.Combine(@"C:\app\data", @"C:\Windows\win.ini")  // "C:\Windows\win.ini"

// So this is not a traversal bug. It is a "the base directory evaporated"
// bug, and it needs no ../ at all — which means every filter looking for
// .. sees nothing wrong.
var path = Path.Combine(_root, Request.Query["file"]);
return PhysicalFile(path, "application/octet-stream");
//   ?file=/etc/passwd          -> reads /etc/passwd
//   ?file=..%2f..%2fsecret     -> also works, the ordinary way

// Python's os.path.join has exactly the same behaviour. See
// /guide/python-inclusion — these two are the only runtimes on the
// cheatsheet that do this.

The fix

C#Secure
// The parameter is a key. Nothing to combine, nothing to canonicalise.
private static readonly Dictionary<string, string> Reports = new()
{
    ["q1"] = "q1-2026.pdf",
    ["q2"] = "q2-2026.pdf",
};

[HttpGet("report")]
public IActionResult Get(string id)
{
    if (!Reports.TryGetValue(id ?? "", out var file))
        return NotFound();

    return PhysicalFile(Path.Combine(_root, file), "application/pdf");
}

The execute-shaped sinks

ASP.NET Core has no include(). The closest equivalents resolve a view name to a template, and the framework compiles and runs that template.

Returning a request-derived view name. return View(Request.Query["page"]) lets the caller choose which Razor view renders. The view engine searches its configured locations, so this cannot generally leave the views directory, but it can reach partials and layouts that were never meant to be rendered directly — occasionally including ones that expect a model they will not get, which turns into an information-disclosing exception.

Server.Execute() is classic ASP.NET (Framework), not Core. If you are testing a Framework application it is a genuine execute sink and worth looking for.

Razor runtime compilation. With AddRazorRuntimeCompilation() enabled, .cshtml files are compiled on demand from disk. Combine that with a write primitive anywhere under the views path and you have code execution. It is a development convenience that occasionally ships to production.

In practice, .NET inclusion findings are read primitives. Rate them as disclosure unless you can demonstrate one of the above.

Windows-specific notes

C#things that bite on Windows
// Both separators are accepted, so a filter checking only for '/' or only
// for '\' catches half the payloads.
//     ..\..\..\Windows\win.ini
//     ../../../Windows/win.ini

// Alternate Data Streams. A check on the extension sees ".txt"; the
// filesystem opens the stream.
//     file.txt:hidden
//     web.config::$DATA          <- returns the file contents

// Reserved device names still exist. Opening one can hang the request
// rather than error, which is an availability issue as much as a
// disclosure one.
//     CON  PRN  AUX  NUL  COM1..COM9  LPT1..LPT9

// Trailing dots and spaces are stripped by the Win32 layer, so
// "web.config." and "web.config " both open web.config — after passing a
// check that compared against "web.config" exactly.

// GetFullPath normalises all of the above, which is the argument for
// canonicalising before checking rather than pattern-matching the input.

What to grep for

Bash
# The footgun itself
grep -rn 'Path.Combine' --include='*.cs' . | grep -iE 'request|query|form|route|param'

# Read sinks
grep -rnE 'File\.(ReadAllText|ReadAllBytes|OpenRead|Open)\(|new (Stream|File)Reader\(' \
  --include='*.cs' . | grep -iE 'request|query|param'

# Returning a file by path
grep -rn 'PhysicalFile\|FileStreamResult\|return File(' --include='*.cs' .

# View names from request data
grep -rnE 'return View\(' --include='*.cs' . | grep -iE 'request|query|param'

# Framework-only execute sink
grep -rn 'Server.Execute\|Server.Transfer' --include='*.cs' .

# The correct calls, to confirm they are there
grep -rn 'GetFullPath\|PhysicalFileProvider' --include='*.cs' .