CWE-285: Improper Authorization - C# / ASP.NET Core
Overview
In ASP.NET Core applications, improper authorization occurs when authentication is configured via UseAuthorization() but controllers or actions lack [Authorize] or [AllowAnonymous] attributes, allowing unauthenticated access to protected resources. Missing or misconfigured attributes bypass authorization checks even when authentication middleware is enabled.
Detection Context
This guidance applies when a security scanner detects:
UseAuthorization()configured inProgram.csorStartup.cs- Controllers or actions missing
[Authorize]or[AllowAnonymous]attributes - Applies to ASP.NET Core Web and Web API projects
Primary Defence: Apply [Authorize] at the controller class level to secure all actions by default, use method-level [Authorize(Roles = "...")] or [Authorize(Policy = "...")] for role-based or policy-based authorization on privileged operations, explicitly mark truly public endpoints with [AllowAnonymous], and enable both UseAuthentication() and UseAuthorization() middleware in the correct order to prevent unauthorized access.
Attribute Precedence Rules
Critical Understanding:
- When both
[Authorize]and[AllowAnonymous]are present at the same level (class or method),[AllowAnonymous]takes precedence - This can accidentally disable authorization checks
- Method-level attributes override class-level attributes
Remediation Strategy
Apply [Authorize] at Controller Level (Secure by Default)
// VULNERABLE - No authorization attribute
public class UserController : ControllerBase
{
[HttpGet("profile/{id}")]
public IActionResult GetUserProfile(int id)
{
// Anyone can access this, even unauthenticated!
var user = _userService.GetUser(id);
return Ok(user);
}
[HttpDelete("{id}")]
public IActionResult DeleteUser(int id)
{
// Admin function accessible to anyone!
_userService.DeleteUser(id);
return NoContent();
}
}
// SECURE - Authorize at class level, selective anonymous access
[Authorize] // All methods require authentication by default
[ApiController]
[Route("api/[controller]")]
public class UserController : ControllerBase
{
[HttpGet("profile/{id}")]
public IActionResult GetUserProfile(int id)
{
// Requires authentication
var user = _userService.GetUser(id);
return Ok(user);
}
[HttpDelete("{id}")]
[Authorize(Roles = "Admin")] // Additional role restriction
public IActionResult DeleteUser(int id)
{
// Requires Admin role
_userService.DeleteUser(id);
return NoContent();
}
[HttpGet("public-info")]
[AllowAnonymous] // Explicitly allow public access
public IActionResult GetPublicInfo()
{
// Public endpoint - no auth required
return Ok(new { AppVersion = "1.0" });
}
}
Role-Based Authorization
[Authorize]
[ApiController]
[Route("api/[controller]")]
public class OrderController : ControllerBase
{
// Any authenticated user can view orders
[HttpGet]
public IActionResult GetOrders()
{
var orders = _orderService.GetOrdersForUser(User.Identity.Name);
return Ok(orders);
}
// Only users with "Manager" role
[HttpGet("all")]
[Authorize(Roles = "Manager")]
public IActionResult GetAllOrders()
{
var orders = _orderService.GetAllOrders();
return Ok(orders);
}
// Multiple roles (OR logic)
[HttpGet("reports")]
[Authorize(Roles = "Manager,Admin")]
public IActionResult GetReports()
{
return Ok(_reportService.GetReports());
}
// Admin-only function
[HttpDelete("{id}")]
[Authorize(Roles = "Admin")]
public IActionResult DeleteOrder(int id)
{
_orderService.DeleteOrder(id);
return NoContent();
}
}
Policy-Based Authorization (Best Practice)
// Program.cs - Define policies
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("RequireAdminRole", policy =>
policy.RequireRole("Admin"));
options.AddPolicy("RequireManagerOrAdmin", policy =>
policy.RequireRole("Manager", "Admin"));
options.AddPolicy("AtLeast21", policy =>
policy.Requirements.Add(new MinimumAgeRequirement(21)));
options.AddPolicy("CanEditResource", policy =>
policy.Requirements.Add(new ResourceOwnerRequirement()));
});
// Register custom authorization handlers
builder.Services.AddSingleton<IAuthorizationHandler, MinimumAgeHandler>();
builder.Services.AddSingleton<IAuthorizationHandler, ProfileOwnerHandler>();
// Controller using policies
[Authorize]
[ApiController]
[Route("api/[controller]")]
public class AdminController : ControllerBase
{
[HttpGet("dashboard")]
[Authorize(Policy = "RequireAdminRole")]
public IActionResult GetDashboard()
{
return Ok(_dashboardService.GetAdminDashboard());
}
[HttpPost("users")]
[Authorize(Policy = "RequireManagerOrAdmin")]
public IActionResult CreateUser([FromBody] UserDto user)
{
var result = _userService.CreateUser(user);
return CreatedAtAction(nameof(GetUser), new { id = result.Id }, result);
}
}
// Custom authorization requirement
public class MinimumAgeRequirement : IAuthorizationRequirement
{
public int MinimumAge { get; }
public MinimumAgeRequirement(int minimumAge)
{
MinimumAge = minimumAge;
}
}
// Custom authorization handler
public class MinimumAgeHandler : AuthorizationHandler<MinimumAgeRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
MinimumAgeRequirement requirement)
{
var dateOfBirthClaim = context.User.FindFirst(
c => c.Type == ClaimTypes.DateOfBirth);
if (dateOfBirthClaim == null)
{
return Task.CompletedTask;
}
var dateOfBirth = Convert.ToDateTime(dateOfBirthClaim.Value);
var age = DateTime.Today.Year - dateOfBirth.Year;
if (dateOfBirth.Date > DateTime.Today.AddYears(-age))
{
age--;
}
if (age >= requirement.MinimumAge)
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
User-Specific Authorization
[Authorize]
[ApiController]
[Route("api/[controller]")]
public class ProfileController : ControllerBase
{
// Restrict to specific users
[HttpGet("admin-profile")]
[Authorize(Users = "alice@company.com,bob@company.com")]
public IActionResult GetAdminProfile()
{
return Ok(_profileService.GetAdminProfile());
}
// Better approach: Resource-based authorization
[HttpPut("{id}")]
public async Task<IActionResult> UpdateProfile(
int id,
[FromBody] ProfileDto profile,
[FromServices] IAuthorizationService authService)
{
var userProfile = await _profileService.GetProfileAsync(id);
// Check if user can edit this specific resource
var authResult = await authService.AuthorizeAsync(
User,
userProfile,
"CanEditProfile");
if (!authResult.Succeeded)
{
return Forbid();
}
await _profileService.UpdateProfileAsync(id, profile);
return NoContent();
}
}
// Resource-based authorization handler
public class ResourceOwnerRequirement : IAuthorizationRequirement
{
}
public class ProfileOwnerHandler :
AuthorizationHandler<ResourceOwnerRequirement, Profile>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
ResourceOwnerRequirement requirement,
Profile resource)
{
var userId = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (resource.UserId.ToString() == userId)
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
Common Scenarios and Recommendations
Public Endpoints (Appropriate for Anonymous Access)
[ApiController]
[Route("api/[controller]")]
public class PublicController : ControllerBase
{
// Login page - should be anonymous
[HttpPost("login")]
[AllowAnonymous]
public IActionResult Login([FromBody] LoginDto credentials)
{
var token = _authService.Authenticate(credentials);
return Ok(new { Token = token });
}
// Error page - should be anonymous
[HttpGet("error")]
[AllowAnonymous]
public IActionResult Error()
{
return Ok(new { Message = "An error occurred" });
}
// Contact form - typically anonymous
[HttpPost("contact")]
[AllowAnonymous]
public IActionResult SubmitContact([FromBody] ContactDto contact)
{
_contactService.ProcessContact(contact);
return Accepted();
}
// Public content
[HttpGet("about")]
[AllowAnonymous]
public IActionResult About()
{
return Ok(_contentService.GetAboutContent());
}
}
Razor Pages Authorization
// VULNERABLE - No authorization
public class UserProfileModel : PageModel
{
public void OnGet()
{
// Anyone can access
}
}
// SECURE - With [Authorize]
[Authorize]
public class UserProfileModel : PageModel
{
public void OnGet()
{
// Requires authentication
}
}
// Role-based for Razor Pages
[Authorize(Roles = "Admin")]
public class AdminDashboardModel : PageModel
{
public void OnGet()
{
// Admin only
}
}
MVC Controllers
[Authorize]
public class AccountController : Controller
{
[AllowAnonymous]
public IActionResult Login()
{
return View();
}
[AllowAnonymous]
public IActionResult Register()
{
return View();
}
// Requires authentication (from class-level [Authorize])
public IActionResult Profile()
{
return View();
}
[Authorize(Roles = "Admin")]
public IActionResult AdminPanel()
{
return View();
}
}
Dangerous Attribute Combinations
Issue 1: Conflicting Attributes (AllowAnonymous Wins)
// DANGEROUS - [AllowAnonymous] takes precedence!
[Authorize]
[AllowAnonymous] // This wins - NO auth check!
public class DangerousController : ControllerBase
{
// All methods are accessible without authentication
}
// CORRECT - Remove conflicting attribute
[Authorize]
public class SecureController : ControllerBase
{
// All methods require authentication
}
Issue 2: Method-Level Override
[AllowAnonymous] // Class level
public class MixedController : ControllerBase
{
[Authorize] // This is ignored! AllowAnonymous at class level applies
public IActionResult SecureMethod()
{
// Still accessible without auth!
return Ok();
}
}
// CORRECT - Authorize at class, AllowAnonymous at method
[Authorize] // Class level
public class ProperController : ControllerBase
{
[AllowAnonymous] // Method level - overrides class
public IActionResult PublicMethod()
{
// No auth required
return Ok();
}
public IActionResult SecureMethod()
{
// Auth required (from class attribute)
return Ok();
}
}
Issue 3: Missing Attributes on Sensitive Operations
// VULNERABLE
[Authorize]
public class OrderController : ControllerBase
{
[HttpGet]
public IActionResult GetOrders()
{
return Ok(_orders);
}
// Missing additional role check!
[HttpDelete("{id}")]
public IActionResult DeleteOrder(int id)
{
// Any authenticated user can delete!
_orderService.Delete(id);
return NoContent();
}
}
// SECURE
[Authorize]
public class OrderController : ControllerBase
{
[HttpGet]
public IActionResult GetOrders()
{
return Ok(_orders);
}
[HttpDelete("{id}")]
[Authorize(Roles = "Admin,Manager")] // Restrict to admins
public IActionResult DeleteOrder(int id)
{
_orderService.Delete(id);
return NoContent();
}
}
Alternative Authorization Middleware
Custom Middleware Authorization
// Program.cs
app.Use(async (context, next) =>
{
if (context.Request.Path.StartsWithSegments("/api/admin"))
{
if (!context.User.IsInRole("Admin"))
{
context.Response.StatusCode = 403;
await context.Response.WriteAsync("Forbidden");
return;
}
}
await next();
});
Endpoint-Level Authorization (Minimal APIs)
// Program.cs - Minimal API
app.MapGet("/api/public", () => "Public data")
.AllowAnonymous();
app.MapGet("/api/private", () => "Private data")
.RequireAuthorization();
app.MapGet("/api/admin", () => "Admin data")
.RequireAuthorization("RequireAdminRole");
app.MapPost("/api/orders", (Order order) =>
{
// Process order
return Results.Created($"/api/orders/{order.Id}", order);
})
.RequireAuthorization(policy =>
policy.RequireRole("Customer", "Admin"));
Configuration Requirements
Program.cs Setup
var builder = WebApplication.CreateBuilder(args);
// Add authentication services
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = builder.Configuration["Jwt:Issuer"],
ValidAudience = builder.Configuration["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"]))
};
});
// Add authorization services with policies
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("RequireAdminRole",
policy => policy.RequireRole("Admin"));
options.FallbackPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build(); // Advanced hardening: require auth by default
});
var app = builder.Build();
// CRITICAL: Enable authentication AND authorization middleware
app.UseAuthentication(); // Must come before UseAuthorization
app.UseAuthorization(); // Enables [Authorize] attributes
app.MapControllers();
app.Run();
Testing Authorization
[TestClass]
public class AuthorizationTests
{
[TestMethod]
public async Task UnauthorizedUser_CannotAccessProtectedEndpoint()
{
// Arrange
var client = _factory.CreateClient();
// Act - No auth token
var response = await client.GetAsync("/api/protected");
// Assert
Assert.AreEqual(HttpStatusCode.Unauthorized, response.StatusCode);
}
[TestMethod]
public async Task NonAdminUser_CannotAccessAdminEndpoint()
{
// Arrange
var client = _factory.CreateClient();
var token = GenerateTokenForRole("User");
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", token);
// Act
var response = await client.GetAsync("/api/admin");
// Assert
Assert.AreEqual(HttpStatusCode.Forbidden, response.StatusCode);
}
[TestMethod]
public async Task AdminUser_CanAccessAdminEndpoint()
{
// Arrange
var client = _factory.CreateClient();
var token = GenerateTokenForRole("Admin");
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", token);
// Act
var response = await client.GetAsync("/api/admin");
// Assert
Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
}
}
Summary of Best Practices
- Apply
[Authorize]at controller level - Secure by default - Use
[AllowAnonymous]selectively - Only for truly public endpoints (login, contact, error pages) - Never combine
[Authorize]and[AllowAnonymous]at the same level - AllowAnonymous always wins - Use role-based authorization for admin/privileged functions
- Prefer policy-based authorization for complex requirements
- Implement resource-based authorization for user-specific data access
- Always enable
UseAuthentication()andUseAuthorization()in correct order - Test authorization thoroughly - Verify both positive and negative cases
- Document public endpoints - Make intentional anonymous access explicit
- Use fallback policies - Consider requiring auth by default across the app
Common Vulnerable Patterns
Missing Authorization Attributes on Controllers
// VULNERABLE - No authorization attribute
[ApiController]
[Route("api/[controller]")]
public class UserController : ControllerBase
{
[HttpGet("{id}")]
public IActionResult GetUser(int id)
{
// Anyone can access this, even unauthenticated!
var user = _userService.GetUser(id);
return Ok(user);
}
[HttpDelete("{id}")]
public IActionResult DeleteUser(int id)
{
// Critical operation accessible to anyone!
_userService.DeleteUser(id);
return NoContent();
}
}
Conflicting Attributes (AllowAnonymous Wins)
// VULNERABLE - AllowAnonymous takes precedence over Authorize
[Authorize]
[AllowAnonymous] // This wins - NO auth required!
public class DangerousController : ControllerBase
{
[HttpGet("admin/delete")]
public IActionResult DeleteAll()
{
// Still accessible without authentication!
_service.DeleteAllData();
return NoContent();
}
}
Missing Role Checks on Privileged Operations
// VULNERABLE - Any authenticated user can access
[Authorize]
public class AdminController : ControllerBase
{
[HttpPost("users")]
public IActionResult CreateUser([FromBody] UserDto user)
{
// Missing role check - any authenticated user can create users!
_userService.CreateUser(user);
return Created();
}
}
Class-Level AllowAnonymous Overriding Method-Level Authorize
// VULNERABLE - Method-level Authorize is ignored
[AllowAnonymous] // Class-level
public class ProblemsController : ControllerBase
{
[Authorize] // This is ignored! AllowAnonymous at class level wins
public IActionResult SecureMethod()
{
// Still accessible without authentication!
return Ok(_sensitiveData);
}
}
Secure Patterns
Controller-Level Authorization with Selective Anonymous Access
// SECURE - Authorize at class level, selective anonymous
[Authorize] // All methods require authentication by default
[ApiController]
[Route("api/[controller]")]
public class UserController : ControllerBase
{
[HttpGet("{id}")]
public IActionResult GetUser(int id)
{
// Requires authentication
var user = _userService.GetUser(id);
return Ok(user);
}
[HttpDelete("{id}")]
[Authorize(Roles = "Admin")] // Additional role requirement
public IActionResult DeleteUser(int id)
{
// Requires Admin role
_userService.DeleteUser(id);
return NoContent();
}
[HttpGet("public")]
[AllowAnonymous] // Explicitly allow public access
public IActionResult GetPublicInfo()
{
return Ok(new { Version = "1.0" });
}
}
Why this works:
- Secure by default: Applying
[Authorize]at the controller class level establishes authentication as the default requirement for all actions within that controller, preventing accidental exposure of protected endpoints - Automatic inheritance: Any new methods added to the controller automatically inherit this protection without requiring explicit authorization attributes
- Layered authorization: Method-level
[Authorize(Roles = "Admin")]adds an additional layer of authorization on top of the class-level authentication requirement, ensuring that only users who are both authenticated AND have the Admin role can access theDeleteUserendpoint - Explicit exceptions: The
[AllowAnonymous]attribute onGetPublicInfo()creates a clear, documented exception to the authentication requirement, making it obvious to code reviewers that this endpoint is intentionally public - Reversed security model: This pattern is superior to applying
[Authorize]to individual methods because it reverses the security model - instead of remembering to protect each sensitive endpoint, developers must explicitly opt-out of protection for public endpoints, significantly reducing the risk of authorization bypasses from forgotten attributes
Role-Based Authorization (Secure Implementation)
// SECURE - Proper role checks for privileged operations
[Authorize]
[ApiController]
[Route("api/[controller]")]
public class AdminController : ControllerBase
{
[HttpGet("dashboard")]
[Authorize(Roles = "Admin")]
public IActionResult GetDashboard()
{
return Ok(_dashboardService.GetData());
}
[HttpPost("users")]
[Authorize(Roles = "Admin,Manager")] // Multiple roles (OR logic)
public IActionResult CreateUser([FromBody] UserDto user)
{
_userService.CreateUser(user);
return Created();
}
}
Why this works:
- Declarative access control: Role-based authorization using the
[Authorize(Roles = "...")]attribute provides a declarative way to restrict access to specific user roles, with ASP.NET Core automatically verifying that the authenticated user's role claims match the specified roles before allowing the action to execute - Single and multiple role support: The
GetDashboardmethod demonstrates single-role authorization where only users with the "Admin" role can access the endpoint, whileCreateUsershows multiple-role authorization with comma-separated values ("Admin,Manager") implementing OR logic - users with either role can access it - Maintainability advantage: This approach is more maintainable than imperative role checks scattered throughout the code because the authorization logic is visible in the method signature and processed by the authorization middleware before the action executes
- Layered authentication and authorization: The class-level
[Authorize]ensures all users are authenticated first, then method-level role attributes add additional restrictions for privileged operations - Automatic claim validation: Role claims are typically added to the user's
ClaimsIdentityduring authentication (via JWT tokens or cookie authentication), and the authorization middleware validates these claims automatically without requiring custom code in each controller action
Policy-Based Authorization
// SECURE - Using policies for complex authorization
// Program.cs
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("RequireAdminRole", policy =>
policy.RequireRole("Admin"));
options.AddPolicy("RequireManagerOrAdmin", policy =>
policy.RequireRole("Manager", "Admin"));
options.AddPolicy("AtLeast21", policy =>
policy.Requirements.Add(new MinimumAgeRequirement(21)));
});
// Controller
[Authorize]
[ApiController]
[Route("api/[controller]")]
public class AdminController : ControllerBase
{
[HttpGet("dashboard")]
[Authorize(Policy = "RequireAdminRole")]
public IActionResult GetDashboard()
{
return Ok(_dashboardService.GetAdminDashboard());
}
}
Why this works:
- Flexible authorization: Policy-based approach centralizes logic in
Program.csviaAddAuthorization(), enabling rule changes without controller modifications - Complex requirements: Policies support sophisticated logic beyond simple role checks (e.g.,
AtLeast21with customIAuthorizationRequirementandAuthorizationHandler<T>) - Clear intent:
[Authorize(Policy = "RequireAdminRole")]attribute references policy by name, making authorization intent clear while separating implementation - Reusability: Policies enable shared authorization logic across controllers/actions, reducing duplication and inconsistencies
- Contextual decisions: Custom handlers access full
AuthorizationHandlerContext(user claims, HTTP context) for sophisticated decisions impossible with simple role attributes
Resource-Based Authorization
// SECURE - User can only edit their own resources
[Authorize]
[ApiController]
[Route("api/[controller]")]
public class ProfileController : ControllerBase
{
[HttpPut("{id}")]
public async Task<IActionResult> UpdateProfile(
int id,
[FromBody] ProfileDto profile,
[FromServices] IAuthorizationService authService)
{
var userProfile = await _profileService.GetProfileAsync(id);
// Check if user can edit this specific resource
var authResult = await authService.AuthorizeAsync(
User,
userProfile,
"CanEditProfile");
if (!authResult.Succeeded)
{
return Forbid();
}
await _profileService.UpdateProfileAsync(id, profile);
return NoContent();
}
}
Why this works:
- Resource-specific decisions: Evaluates authorization against specific resource instances (e.g., user can edit own profile, not others')
- Imperative evaluation:
IAuthorizationService.AuthorizeAsync()called in action with currentUser, loadeduserProfileresource, and policy name - Handler access to properties: Authorization handlers examine resource properties -
ProfileOwnerHandlercomparesresource.UserIdto authenticated user's ID claim - Proper HTTP semantics: Returns
Forbid()(403) whenauthResult.Succeededis false - user authenticated but lacks permission for this specific resource - Centralized logic: Authorization in handlers (not controller code) maintains separation of concerns while enabling fine-grained access control for multi-tenant/user-specific data
Verification
After implementing the recommended secure patterns, verify the fix through multiple approaches:
- Manual testing: Submit malicious payloads relevant to this vulnerability and confirm they're handled safely without executing unintended operations
- Code review: Confirm all instances use the secure pattern (parameterized queries, safe APIs, proper encoding) with no string concatenation or unsafe operations
- Static analysis: Use security scanners to verify no new vulnerabilities exist and the original finding is resolved
- Regression testing: Ensure legitimate user inputs and application workflows continue to function correctly
- Edge case validation: Test with special characters, boundary conditions, and unusual inputs to verify proper handling
- Framework verification: If using a framework or library, confirm the recommended APIs are used correctly according to documentation
- Authentication/session testing: Verify security controls remain effective and cannot be bypassed (if applicable to the vulnerability type)
- Rescan: Run the security scanner again to confirm the finding is resolved and no new issues were introduced