Review web endpoints building file paths from their parameters
Matches public actions of MVC controllers, API controllers and Razor page models that take a string parameter, build a file path with it (Path.Combine(), concatenation, interpolation) and then read or write the file system, without calling Path.GetFileName(), Path.GetFullPath() or Path.IsPathRooted() first.
A segment such as ..\..\appsettings.json or an absolute path then escapes the intended directory: path traversal. Note that Path.Combine() makes this worse, since it discards everything before an absolute second argument.
This is CWE-22 Improper Limitation of a Pathname to a Restricted Directory (OWASP Top 10 A01 Broken Access Control).
Flagged Code
A segment such as ..\..\appsettings.json or an absolute path then escapes the intended directory: path traversal. Note that Path.Combine() makes this worse, since it discards everything before an absolute second argument.
This is CWE-22 Improper Limitation of a Pathname to a Restricted Directory (OWASP Top 10 A01 Broken Access Control).
Flagged Code
public class DownloadController : ControllerBase {
[HttpGet("download")]
public IActionResult Download(string fileName) {
var path = Path.Combine(_root, fileName); // flagged: "..\..\web.config" escapes _root
return File(System.IO.File.ReadAllBytes(path), "application/octet-stream");
}
}
Fixed Codepublic class DownloadController : ControllerBase {
[HttpGet("download")]
public IActionResult Download(string fileName) {
var safeName = Path.GetFileName(fileName); // drops any directory segment
var path = Path.Combine(_root, safeName);
return File(System.IO.File.ReadAllBytes(path), "application/octet-stream");
}
}How to fix
Reduce the caller's segment to a file name with Path.GetFileName(), or resolve the combined path with Path.GetFullPath() and verify it still starts with the intended root directory before touching it. Better still, map the caller's value to an identifier (a Guid, a database row) and build the path from trusted data only.
Suppress the issue when the string parameter cannot reach the path, for instance when it only names a format or a culture.
Suppress the issue when the string parameter cannot reach the path, for instance when it only names a format or a culture.
CQLinq source
This rule is written in CQLinq, C# LINQ over the NDepend code model. It ships with every new NDepend project: you can read it, adapt it, or use it as a starting point for your own custom rules.
// <Name>Review web endpoints building file paths from their parameters</Name>
// <Id>ND3129:ReviewWebEndpointsBuildingFilePathsFromTheirParameters</Id>
warnif count > 0
// Third-party types are part of the code model only when the application uses them.
let controllerBaseTypes = ThirdParty.Types.WithFullNameIn(
"Microsoft.AspNetCore.Mvc.ControllerBase",
"Microsoft.AspNetCore.Mvc.RazorPages.PageModel",
"System.Web.Mvc.Controller",
"System.Web.Http.ApiController").ToArray()
where controllerBaseTypes.Any()
let fileSystemMembers = ThirdParty.Methods.Where(m =>
m.ParentType.FullName.StartsWithAny(
"System.IO.File", "System.IO.Directory", "System.IO.FileStream",
"System.IO.FileInfo", "System.IO.DirectoryInfo", "System.IO.StreamReader")).ToHashSetEx()
// The path is assembled rather than picked from a closed set of known paths.
let pathBuildingMembers = ThirdParty.Methods.Where(m =>
(m.ParentType.FullName == "System.IO.Path" && m.SimpleNameEqualsAny("Combine", "Join")) ||
(m.ParentType.FullName == "System.String" && m.SimpleNameEqualsAny("Concat", "Format")) ||
m.ParentType.FullName == "System.Runtime.CompilerServices.DefaultInterpolatedStringHandler").ToHashSetEx()
// Calling any of these means the segment is being reduced or resolved before use.
let pathSanitizingMembers = ThirdParty.Methods.Where(m =>
m.ParentType.FullName == "System.IO.Path" &&
m.SimpleNameEqualsAny("GetFileName", "GetFullPath", "IsPathRooted", "IsPathFullyQualified",
"GetInvalidFileNameChars", "GetInvalidPathChars")).ToHashSetEx()
from t in Application.Types.ThatDeriveFromAny(controllerBaseTypes)
where !t.IsGeneratedByCompiler
from m in t.Methods
where m.IsPublic && !m.IsConstructor && !m.IsGeneratedByCompiler &&
// The endpoint receives a string from the caller...
m.Name.Contains("String") &&
// ...assembles a path with it and reaches the file system...
m.MethodsCalled.Intersect(pathBuildingMembers).Any() &&
m.MethodsCalled.Intersect(fileSystemMembers).Any() &&
// ...without reducing the segment first.
!m.MethodsCalled.Intersect(pathSanitizingMembers).Any()
select new {
m,
fileSystemMembersUsed = m.MethodsCalled.Intersect(fileSystemMembers).ToArray(),
Debt = 20.ToMinutes().ToDebt(),
Severity = Severity.High
}
// <Desc>
// Matches public actions of MVC controllers, API controllers and Razor
// page models that take a string parameter, build a file path with it
// (*Path.Combine()*, concatenation, interpolation) and then read or write
// the file system, without calling *Path.GetFileName()*,
// *Path.GetFullPath()* or *Path.IsPathRooted()* first.
//
// A segment such as *..\..\appsettings.json* or an absolute path then
// escapes the intended directory: **path traversal**. Note that
// *Path.Combine()* makes this worse, since it discards everything before
// an absolute second argument.
//
// This is CWE-22 *Improper Limitation of a Pathname to a Restricted
// Directory* (OWASP Top 10 A01 Broken Access Control).
//
// **Flagged Code**
// ```csharp
// public class DownloadController : ControllerBase {
// [HttpGet("download")]
// public IActionResult Download(string fileName) {
// var path = Path.Combine(_root, fileName); // flagged: "..\..\web.config" escapes _root
// return File(System.IO.File.ReadAllBytes(path), "application/octet-stream");
// }
// }
// ```
//
// **Fixed Code**
// ```csharp
// public class DownloadController : ControllerBase {
// [HttpGet("download")]
// public IActionResult Download(string fileName) {
// var safeName = Path.GetFileName(fileName); // drops any directory segment
// var path = Path.Combine(_root, safeName);
// return File(System.IO.File.ReadAllBytes(path), "application/octet-stream");
// }
// }
// ```
// </Desc>
//<Expl>
// The web endpoint *{0}* builds a file path from a string it receives and
// calls *{1}*, without reducing the segment first. A caller sending
// *..\..\* or an absolute path can reach files outside the intended directory.
//</Expl>
// <HowToFix>
// Reduce the caller's segment to a file name with *Path.GetFileName()*, or
// resolve the combined path with *Path.GetFullPath()* and verify it still
// starts with the intended root directory before touching it. Better
// still, map the caller's value to an identifier (a Guid, a database row)
// and build the path from trusted data only.
//
// Suppress the issue when the string parameter cannot reach the path, for
// instance when it only names a format or a culture.
// </HowToFix>
Open in the interactive Rules Explorer · All default rules
Security: all 39 items
- Software Composition Analysis (SCA) ND3120
- Transitive Software Composition Analysis (SCA) ND3121
- Don't hard-code secrets in constants ND3114
- Review credentials built from hard-coded values ND3115
- Don't use insecure deserializers ND3110
- Don't use weak hash algorithms (MD5, SHA1) ND3109
- Don't use DES/3DES/RC2 weak cipher algorithms ND3102
- Don't use weak password-based key derivation ND3123
- Don't use the ECB encryption mode ND3132
- Don't disable certificate validation ND3103
- Don't hard-code TLS/SSL protocol versions ND3122
- Review commands vulnerable to SQL injection ND3107
- Review data adapters vulnerable to SQL injection ND3108
- Review raw SQL built from composed strings ND3128
- Review LDAP filters composed from strings ND3137
- Don't enable external XML entity resolution (XXE) ND3111
- Review processes started with a composed arguments string ND3124
- Review web endpoints building file paths from their parameters ND3129
- Review custom archive extraction (Zip Slip) ND3135
- Review assemblies and types loaded from runtime-provided names ND3125
- Review web endpoints without [Authorize] nor [AllowAnonymous] ND3118
- Review state-changing endpoints opened to anonymous callers ND3130
- Review state-changing MVC actions without anti-forgery validation ND3119
- Review HTML-encoding bypasses ND3133
- Review explicitly disabled request validation ND3134
- Review redirects to a URL that is not validated as local ND3126
- Review request-influenced outbound HTTP calls (SSRF) ND3136
- Review insecure ASP.NET Core CORS policies and unguarded developer exception page ND3116
- Review cookies created without HttpOnly and Secure flags ND3117
- Review regular expressions without a match timeout (ReDoS) ND3112
- Review secrets compared with non-constant-time equality ND3113
- Review secrets passed to logging methods ND3127
- Review security operations that log nothing ND3131
- Don't use System.Random for security purposes ND3101
- Don't use CoSetProxyBlanket and CoInitializeSecurity ND3100
- Review publicly visible event handlers ND3104
- Pointers should not be publicly visible ND3105
- Seal methods that satisfy non-public interfaces ND3106
- Restrict sensitive APIs to reviewed components ND3138