Review web endpoints without [Authorize] nor [AllowAnonymous]
Matches ASP.NET Core controllers, SignalR hubs and legacy MVC / Web API controllers that are not authorized at class level (including inherited classes) and expose public actions carrying neither an authorization attribute ([Authorize], a derived attribute, or any attribute implementing IAuthorizeData) nor an explicit [AllowAnonymous]. The rule stays silent when the application sets a global fallback policy (AuthorizationOptions.FallbackPolicy) or registers a global AuthorizeFilter, since every endpoint is then protected by default.
An endpoint that is anonymous by omission rather than by decision is a classic broken access control (OWASP A01:2021, CWE-306 Missing Authentication for Critical Function).
Flagged Code
An endpoint that is anonymous by omission rather than by decision is a classic broken access control (OWASP A01:2021, CWE-306 Missing Authentication for Critical Function).
Flagged Code
public class OrdersController : ControllerBase // flagged: no [Authorize] on the class
{
[HttpGet("{id}")] // no [Authorize], no [AllowAnonymous]
public IActionResult Get(int id) => Ok(_orders.Find(id));
[HttpGet("status")]
public IActionResult Status() => Ok("up");
}
Fixed Code[Authorize] // every action requires authentication
public class OrdersController : ControllerBase
{
[HttpGet("{id}")]
public IActionResult Get(int id) => Ok(_orders.Find(id));
[AllowAnonymous] // explicit decision for the public probe
[HttpGet("status")]
public IActionResult Status() => Ok("up");
}How to fix
Tag the controller (or its base class) with [Authorize] and opt out with [AllowAnonymous] on the few public actions, or configure a fallback policy so that everything requires authentication by default: builder.Services.AddAuthorization(o => o.FallbackPolicy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build()). Public helper methods that are not actions should be non-public or tagged [NonAction].
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 without [Authorize] nor [AllowAnonymous]</Name>
// <Id>ND3118:ReviewWebEndpointsWithoutAuthorizeNorAllowAnonymous</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.SignalR.Hub",
"System.Web.Mvc.Controller",
"System.Web.Http.ApiController").ToArray()
where controllerBaseTypes.Any()
// A fallback policy or a global AuthorizeFilter protects every endpoint: nothing to report.
let appHasGlobalAuthorization = Application.Methods.Any(m =>
m.IsUsing("Microsoft.AspNetCore.Authorization.AuthorizationOptions.set_FallbackPolicy(AuthorizationPolicy)".AllowNoMatch()) ||
m.CreateA("Microsoft.AspNetCore.Mvc.Authorization.AuthorizeFilter".AllowNoMatch()) ||
m.IsUsing("System.Web.Mvc.AuthorizeAttribute..ctor()".AllowNoMatch()))
where !appHasGlobalAuthorization
// [Authorize], attributes deriving from it, and any attribute implementing IAuthorizeData (custom policies)
let authorizeAttrs = ThirdParty.Types.WithFullNameIn(
"Microsoft.AspNetCore.Authorization.AuthorizeAttribute",
"System.Web.Mvc.AuthorizeAttribute",
"System.Web.Http.AuthorizeAttribute").ToArray()
let authorizeDataInterfaces = ThirdParty.Types.WithFullName("Microsoft.AspNetCore.Authorization.IAuthorizeData").ToArray()
let authorizeAttrClasses = authorizeAttrs
.Concat(Types.ThatDeriveFromAny(authorizeAttrs))
.Concat(Types.ThatImplementAny(authorizeDataInterfaces))
.ToHashSetEx()
let anonymousAttrs = ThirdParty.Types.WithFullNameIn(
"Microsoft.AspNetCore.Authorization.AllowAnonymousAttribute",
"System.Web.Mvc.AllowAnonymousAttribute",
"System.Web.Http.AllowAnonymousAttribute").ToArray()
let allowAnonymousInterfaces = ThirdParty.Types.WithFullName("Microsoft.AspNetCore.Authorization.IAllowAnonymous").ToArray()
let anonymousAttrClasses = anonymousAttrs
.Concat(Types.ThatDeriveFromAny(anonymousAttrs))
.Concat(Types.ThatImplementAny(allowAnonymousInterfaces))
.ToHashSetEx()
let nonActionAttrs = ThirdParty.Types.WithFullNameIn("Microsoft.AspNetCore.Mvc.NonActionAttribute", "System.Web.Mvc.NonActionAttribute").ToHashSetEx()
let isAuthorized = new Func<IAttributeTarget, bool>(x => x.AttributeClassesThatTagMe.Any(a => authorizeAttrClasses.Contains(a)))
let isAnonymous = new Func<IAttributeTarget, bool>(x => x.AttributeClassesThatTagMe.Any(a => anonymousAttrClasses.Contains(a)))
from t in Application.Types.ThatDeriveFromAny(controllerBaseTypes)
where !t.IsAbstract && !t.IsGeneratedByCompiler
// An attribute on the class or on any base class covers every action.
let classChain = t.BaseClasses.Concat(new[] { t }).ToArray()
where !classChain.Any(c => isAuthorized(c)) && !classChain.Any(c => isAnonymous(c))
let unprotectedActions = t.Methods.Where(m =>
m.IsPublic && !m.IsStatic && !m.IsConstructor && !m.IsGeneratedByCompiler &&
!m.IsPropertyGetter && !m.IsPropertySetter && !m.IsEventAdder && !m.IsEventRemover &&
!m.OverriddensBase.Any(mb => mb.IsThirdParty) && // OnActionExecuting(), Dispose(), ToString()...
!m.AttributeClassesThatTagMe.Any(a => nonActionAttrs.Contains(a)) &&
!isAuthorized(m) && !isAnonymous(m)).ToArray()
where unprotectedActions.Length > 0
select new {
t,
unprotectedActions,
Debt = (5 * unprotectedActions.Length).ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The controller *{0}* has no *[Authorize]* on its class chain and
// exposes action(s) *{1}* tagged with neither *[Authorize]* nor *[AllowAnonymous]*:
// they answer any unauthenticated request.
//</Expl>
//<Desc>
// Matches ASP.NET Core controllers, SignalR hubs and legacy MVC / Web API
// controllers that are not authorized at class level (including inherited
// classes) and expose public actions carrying neither an authorization
// attribute (*[Authorize]*, a derived attribute, or any attribute
// implementing *IAuthorizeData*) nor an explicit *[AllowAnonymous]*.
// The rule stays silent when the application sets a global fallback
// policy (*AuthorizationOptions.FallbackPolicy*) or registers a global
// *AuthorizeFilter*, since every endpoint is then protected by default.
//
// An endpoint that is anonymous by omission rather than by decision is a
// classic broken access control (OWASP A01:2021, CWE-306 *Missing
// Authentication for Critical Function*).
//
// **Flagged Code**
// ```csharp
// public class OrdersController : ControllerBase // flagged: no [Authorize] on the class
// {
// [HttpGet("{id}")] // no [Authorize], no [AllowAnonymous]
// public IActionResult Get(int id) => Ok(_orders.Find(id));
//
// [HttpGet("status")]
// public IActionResult Status() => Ok("up");
// }
// ```
//
// **Fixed Code**
// ```csharp
// [Authorize] // every action requires authentication
// public class OrdersController : ControllerBase
// {
// [HttpGet("{id}")]
// public IActionResult Get(int id) => Ok(_orders.Find(id));
//
// [AllowAnonymous] // explicit decision for the public probe
// [HttpGet("status")]
// public IActionResult Status() => Ok("up");
// }
// ```
//</Desc>
//<HowToFix>
// Tag the controller (or its base class) with *[Authorize]* and opt out
// with *[AllowAnonymous]* on the few public actions, or configure a
// fallback policy so that everything requires authentication by default:
// *builder.Services.AddAuthorization(o => o.FallbackPolicy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build())*.
// Public helper methods that are not actions should be non-public or
// tagged *[NonAction]*.
//</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