Review insecure ASP.NET Core CORS policies and unguarded developer exception page

Rule ND3116 Security

Matches startup methods where: - a CORS policy combines AllowAnyOrigin() (or a permissive SetIsOriginAllowed()) with AllowCredentials(): any website can then make authenticated cross-origin requests with the user's cookies. ASP.NET Core rejects the literal AllowAnyOrigin + AllowCredentials pair at runtime, which is why SetIsOriginAllowed(_ => true) is the common workaround, hence its presence in this rule; - UseDeveloperExceptionPage() is called without an IsDevelopment() (or IsEnvironment()) check in the same method or its caller: stack traces, source snippets, headers and cookies would be shown to anyone triggering an exception in production.

These are CWE-942 Permissive Cross-domain Policy with Untrusted Domains and CWE-209 Generation of Error Message Containing Sensitive Information.

Flagged Code
builder.Services.AddCors(o => o.AddPolicy("open", p =>
   p.SetIsOriginAllowed(_ => true)                       // flagged: any origin...
    .AllowCredentials()));                               // ...with the user's cookies
var app = builder.Build();
app.UseDeveloperExceptionPage();                         // flagged: no environment check
Fixed Code
builder.Services.AddCors(o => o.AddPolicy("partners", p =>
   p.WithOrigins("https://partner.example.com")
    .AllowCredentials()));
var app = builder.Build();
if (app.Environment.IsDevelopment())
   app.UseDeveloperExceptionPage();
else
   app.UseExceptionHandler("/error");

How to fix

List the allowed origins explicitly with WithOrigins(...) when credentials are needed. Wrap the developer exception page in if (app.Environment.IsDevelopment()) and use UseExceptionHandler() elsewhere. Suppress the issue when the origin predicate is a real whitelist check.

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 insecure ASP.NET Core CORS policies and unguarded developer exception page</Name>
// <Id>ND3116:ReviewInsecureCorsPolicyAndUnguardedDeveloperExceptionPage</Id>
warnif count > 0

// Third-party members are part of the code model only when the application uses them.
let corsMembers = ThirdParty.Types.WithFullNameIn(
  "Microsoft.AspNetCore.Cors.Infrastructure.CorsPolicyBuilder",
  "Microsoft.AspNetCore.Cors.Infrastructure.CorsPolicy").ChildMethods().ToArray()
let allowAnyOrigin   = corsMembers.Where(m => m.SimpleNameEqualsAny("AllowAnyOrigin", "set_AllowAnyOrigin", "SetIsOriginAllowed", "set_IsOriginAllowed")).ToHashSetEx()
let allowCredentials = corsMembers.Where(m => m.SimpleNameEqualsAny("AllowCredentials", "set_SupportsCredentials")).ToHashSetEx()

let devExceptionPage = ThirdParty.Types.WithFullName("Microsoft.AspNetCore.Builder.DeveloperExceptionPageExtensions")
  .ChildMethods().Where(m => m.SimpleName == "UseDeveloperExceptionPage").ToHashSetEx()
let isDevelopment = ThirdParty.Types.WithFullNameIn(
  "Microsoft.Extensions.Hosting.HostEnvironmentEnvExtensions",
  "Microsoft.AspNetCore.Hosting.HostingEnvironmentExtensions")
  .ChildMethods().Where(m => m.SimpleNameEqualsAny("IsDevelopment", "IsEnvironment", "IsProduction", "IsStaging")).ToHashSetEx()

where (allowAnyOrigin.Any() && allowCredentials.Any()) || devExceptionPage.Any()

from m in Application.Methods.UsingAny(allowAnyOrigin.Concat(devExceptionPage))
let anyOriginWithCredentials = m.MethodsCalled.Any(x => allowAnyOrigin.Contains(x)) && m.MethodsCalled.Any(x => allowCredentials.Contains(x))
let unguardedDevPage = m.MethodsCalled.Any(x => devExceptionPage.Contains(x)) &&
                      !m.MethodsCalled.Any(x => isDevelopment.Contains(x)) &&
                      !m.MethodsCallingMe.Any(caller => caller.MethodsCalled.Any(x => isDevelopment.Contains(x)))
where anyOriginWithCredentials || unguardedDevPage
select new {
  m,
  problem = anyOriginWithCredentials ? "CORS policy allows credentials for any origin" : "UseDeveloperExceptionPage() not guarded by an environment check",
  Debt = 20.ToMinutes().ToDebt(),
  Severity = anyOriginWithCredentials ? Severity.High : Severity.Medium
}

//<Expl>
// The *{0}* configures the web host insecurely: {1}.
//</Expl>

//<Desc>
// Matches startup methods where:
// - a CORS policy combines *AllowAnyOrigin()* (or a permissive
//   *SetIsOriginAllowed()*) with *AllowCredentials()*: any website can then
//   make authenticated cross-origin requests with the user's cookies.
//   ASP.NET Core rejects the literal *AllowAnyOrigin + AllowCredentials*
//   pair at runtime, which is why *SetIsOriginAllowed(_ => true)* is the
//   common workaround, hence its presence in this rule;
// - *UseDeveloperExceptionPage()* is called without an *IsDevelopment()*
//   (or *IsEnvironment()*) check in the same method or its caller: stack
//   traces, source snippets, headers and cookies would be shown to anyone
//   triggering an exception in production.
//
// These are CWE-942 *Permissive Cross-domain Policy with Untrusted Domains*
// and CWE-209 *Generation of Error Message Containing Sensitive Information*.
//
// **Flagged Code**
// ```csharp
// builder.Services.AddCors(o => o.AddPolicy("open", p =>
//    p.SetIsOriginAllowed(_ => true)                       // flagged: any origin...
//     .AllowCredentials()));                               // ...with the user's cookies
// var app = builder.Build();
// app.UseDeveloperExceptionPage();                         // flagged: no environment check
// ```
//
// **Fixed Code**
// ```csharp
// builder.Services.AddCors(o => o.AddPolicy("partners", p =>
//    p.WithOrigins("https://partner.example.com")
//     .AllowCredentials()));
// var app = builder.Build();
// if (app.Environment.IsDevelopment())
//    app.UseDeveloperExceptionPage();
// else
//    app.UseExceptionHandler("/error");
// ```
//</Desc>

//<HowToFix>
// List the allowed origins explicitly with *WithOrigins(...)* when
// credentials are needed. Wrap the developer exception page in
// *if (app.Environment.IsDevelopment())* and use *UseExceptionHandler()*
// elsewhere. Suppress the issue when the origin predicate is a real
// whitelist check.
//</HowToFix>

Security: all 39 items