Review request-influenced outbound HTTP calls (SSRF)
Matches public actions of MVC controllers, API controllers, Razor page models and SignalR hubs that take a string or a Uri parameter and either call an HTTP client directly (HttpClient, HttpRequestMessage, WebRequest, WebClient, RestSharp) or carry a name that says they fetch a remote resource (Url, Uri, Remote, Fetch, Download, Proxy, Webhook, Callback). The callDepthToHttpClient column gives the length of the shortest call path to an HTTP client (0 when the client sits behind an interface), and validatesUri tells whether the action itself inspects the host, scheme or IP address of a URI.
The rule does not prove that the received value flows into the outbound request: it lists the endpoints where that flow must be reviewed. When the value does reach the request address, an attacker steers the server toward hosts it can reach and the attacker cannot: internal admin services, cloud instance metadata (169.254.169.254), databases. This is server-side request forgery, CWE-918 (OWASP Top 10 A10).
Flagged Code
The rule does not prove that the received value flows into the outbound request: it lists the endpoints where that flow must be reviewed. When the value does reach the request address, an attacker steers the server toward hosts it can reach and the attacker cannot: internal admin services, cloud instance metadata (169.254.169.254), databases. This is server-side request forgery, CWE-918 (OWASP Top 10 A10).
Flagged Code
public class PreviewController : ControllerBase {
[HttpGet("preview")]
public async Task<IActionResult> Preview(string url) { // flagged: calls HttpClient
var html = await _httpClient.GetStringAsync(url); // url chosen by the caller
return Content(Summarize(html));
}
}
Fixed Codepublic class PreviewController : ControllerBase {
[HttpGet("preview")]
public async Task<IActionResult> Preview(Guid feedId) { // an identifier, not an address
var feed = await _feeds.FindAsync(feedId); // address from trusted storage
if (feed == null) return NotFound();
var html = await _httpClient.GetStringAsync(feed.Url);
return Content(Summarize(html));
}
}How to fix
Never build the outbound address from a value received in the request. Map the value to an identifier (a row, a configured entry) and read the address from trusted storage, or validate the received URI against an allow-list of hosts and schemes: parse it with Uri.TryCreate(), accept https only, compare Uri.Host with the allowed hosts, reject loopback, link-local and private IP ranges, and disable redirects on the HttpClientHandler so a permitted host cannot bounce the request.
Suppress the issue when the string parameter cannot influence the address, for instance when it only names a format or a culture.
Suppress the issue when the string parameter cannot influence the address, 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 request-influenced outbound HTTP calls (SSRF)</Name>
// <Id>ND3136:ReviewRequestInfluencedOutboundHttpCalls</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",
"Microsoft.AspNetCore.SignalR.Hub",
"System.Web.Mvc.Controller",
"System.Web.Http.ApiController").ToArray()
where controllerBaseTypes.Any()
// Members that open an outbound HTTP connection to an address given as a string or a Uri.
let httpClientMembers = ThirdParty.Methods.Where(m =>
(m.ParentType.FullName == "System.Net.Http.HttpClient" && m.IsPublic && !m.IsConstructor &&
m.SimpleName.EndsWith("Async") && m.Name.ContainsAny("String", "Uri")) ||
(m.ParentType.FullName == "System.Net.Http.HttpRequestMessage" && m.IsConstructor && m.Name.ContainsAny("String", "Uri")) ||
(m.ParentType.FullNameEqualsAny("System.Net.WebRequest", "System.Net.HttpWebRequest") && m.SimpleName.StartsWith("Create")) ||
(m.ParentType.FullName == "System.Net.WebClient" && m.SimpleName.StartsWithAny("Download", "Upload", "OpenRead", "OpenWrite")) ||
(m.ParentType.FullName.StartsWith("RestSharp.") && (m.SimpleNameEqualsAny("ExecuteAsync", "Execute") || (m.IsConstructor && m.ParentType.SimpleName == "RestRequest"))))
.ToHashSetEx()
where httpClientMembers.Any()
// Length of the shortest call path from each application method down to an HTTP client member:
// 1 = direct call, 2 = through one helper... The fill only touches methods that actually reach the client.
// A path broken by an interface (the client is behind an injected service) is not seen.
let depthToHttpClient = httpClientMembers.FillIterative(methods => methods.SelectMany(m => m.MethodsCallingMe))
// The action itself inspects the host, scheme or IP address: a hint that an allow-list is applied.
let uriValidationMembers = ThirdParty.Methods.Where(m =>
(m.ParentType.FullName == "System.Uri" && m.SimpleNameEqualsAny("get_Host", "get_IsLoopback", "get_Scheme", "get_DnsSafeHost", "get_IdnHost")) ||
(m.ParentType.FullName == "System.Net.IPAddress" && m.SimpleNameEqualsAny("TryParse", "Parse", "IsLoopback")))
.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 or a Uri from the caller...
m.Name.ContainsAny("String", "Uri") &&
// ...and either calls an HTTP client itself, or its name says it fetches a remote resource.
// The name test is case-sensitive on purpose: PascalCase words only ("Download" but not "loaded").
((depthToHttpClient[m] != null && depthToHttpClient[m] <= 1) ||
m.SimpleNameContainsAny("Url", "Uri", "Remote", "Fetch", "Download", "Proxy", "Webhook", "Callback"))
// 0 when the HTTP client is reached only through an interface
let callDepthToHttpClient = depthToHttpClient[m] != null ? (int)depthToHttpClient[m].Value : 0
let validatesUri = m.MethodsCalled.Intersect(uriValidationMembers).Any()
orderby m.FullName
select new {
m,
callDepthToHttpClient,
validatesUri,
Debt = 20.ToMinutes().ToDebt(),
Severity = validatesUri ? Severity.Medium : Severity.High
}
//<Expl>
// The web endpoint *{0}* receives a string or a URI and fetches a remote
// resource. If the destination derives from the value received, the
// server can be made to request internal services, cloud metadata
// endpoints or arbitrary hosts.
//</Expl>
// <Desc>
// Matches public actions of MVC controllers, API controllers, Razor
// page models and SignalR hubs that take a string or a *Uri* parameter
// and either call an HTTP client directly (*HttpClient*,
// *HttpRequestMessage*, *WebRequest*, *WebClient*, RestSharp) or carry a
// name that says they fetch a remote resource (*Url*, *Uri*, *Remote*,
// *Fetch*, *Download*, *Proxy*, *Webhook*, *Callback*). The
// *callDepthToHttpClient* column gives the length of the shortest call
// path to an HTTP client (0 when the client sits behind an interface),
// and *validatesUri* tells whether the action itself inspects the host,
// scheme or IP address of a URI.
//
// The rule does not prove that the received value flows into the
// outbound request: it lists the endpoints where that flow must be
// reviewed. When the value does reach the request address, an attacker
// steers the server toward hosts it can reach and the attacker cannot:
// internal admin services, cloud instance metadata (*169.254.169.254*),
// databases. This is **server-side request forgery**, CWE-918 (OWASP
// Top 10 A10).
//
// **Flagged Code**
// ```csharp
// public class PreviewController : ControllerBase {
// [HttpGet("preview")]
// public async Task<IActionResult> Preview(string url) { // flagged: calls HttpClient
// var html = await _httpClient.GetStringAsync(url); // url chosen by the caller
// return Content(Summarize(html));
// }
// }
// ```
//
// **Fixed Code**
// ```csharp
// public class PreviewController : ControllerBase {
// [HttpGet("preview")]
// public async Task<IActionResult> Preview(Guid feedId) { // an identifier, not an address
// var feed = await _feeds.FindAsync(feedId); // address from trusted storage
// if (feed == null) return NotFound();
// var html = await _httpClient.GetStringAsync(feed.Url);
// return Content(Summarize(html));
// }
// }
// ```
// </Desc>
// <HowToFix>
// Never build the outbound address from a value received in the request.
// Map the value to an identifier (a row, a configured entry) and read the
// address from trusted storage, or validate the received URI against an
// allow-list of hosts and schemes: parse it with *Uri.TryCreate()*,
// accept *https* only, compare *Uri.Host* with the allowed hosts, reject
// loopback, link-local and private IP ranges, and disable redirects on
// the *HttpClientHandler* so a permitted host cannot bounce the request.
//
// Suppress the issue when the string parameter cannot influence the
// address, 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