API Security Hardening — OWASP Top 10 for Web APIs

The OWASP API Security Top 10 represents the most common and impactful vulnerabilities in real-world APIs. Unlike the classic OWASP Top 10 (web applications), the API-specific list focuses on issues unique to APIs: broken object-level authorization (BOLA), mass assignment, and excessive data exposure are API-specific vulnerabilities that many developers unknowingly introduce. A systematic security review against these categories before deploying Part 5 (Angular integration) ensures the API surface is hardened before clients connect.

OWASP API Security Top 10 Mitigations

// ── API1: Broken Object Level Authorization (BOLA) ────────────────────────
// Most common API vulnerability — checking resource exists but not ownership

// ❌ VULNERABLE: returns any post for any authenticated user
[HttpPut("{id:int}")]
[Authorize]
public async Task<IActionResult> Update(int id, UpdatePostRequest request)
{
    var post = await _repo.GetByIdAsync(id, ct);
    if (post is null) return NotFound();
    // Bug: never checked if this user OWNS the post!
    await _repo.UpdateAsync(id, request, ct);
    return Ok();
}

// ✅ SECURE: always verify resource ownership
[HttpPut("{id:int}")]
[Authorize]
public async Task<IActionResult> Update(int id, UpdatePostRequest request)
{
    var userId = User.GetUserId();
    var post   = await _repo.GetByIdAsync(id, ct);

    if (post is null) return NotFound();

    // Authorisation check: user must own the post OR be an Admin
    if (post.AuthorId != userId && !User.IsInRole("Admin"))
        return Forbid();

    await _repo.UpdateAsync(id, request, ct);
    return Ok();
}

// ── API3: Broken Object Property Level Authorization (mass assignment) ────
// ❌ VULNERABLE: binding entity directly (client can set any property)
[HttpPost]
public async Task<IActionResult> Create([FromBody] Post post)   // NEVER bind entity!
{
    // Client can set: post.IsAdmin = true, post.AuthorId = "admin-id"
    await _db.Posts.AddAsync(post);
}

// ✅ SECURE: use dedicated request DTO with only client-settable fields
[HttpPost]
public async Task<IActionResult> Create([FromBody] CreatePostRequest request)
{
    var authorId = User.GetUserId();   // always from JWT, never from request body
    var post = Post.Create(request.Title, request.Slug, request.Body, authorId);
    await _repo.AddAsync(post, ct);
}

// ── API4: Unrestricted Resource Consumption ───────────────────────────────
// Validate collection limits at every entry point
[HttpGet]
public async Task<IActionResult> GetAll(
    [FromQuery, Range(1, 100)] int size = 10,   // never allow unlimited page size
    [FromQuery, Range(1, int.MaxValue)] int page = 1)
{ ... }

// ── API8: Security Misconfiguration ───────────────────────────────────────
// Validate all critical settings at startup
builder.Services.AddOptions<JwtSettings>()
    .ValidateDataAnnotations()
    .ValidateOnStart();   // fail fast if signing key is missing/weak
Note: BOLA (Broken Object Level Authorization) — also called IDOR (Insecure Direct Object Reference) — is the most frequently exploited API vulnerability. The pattern: the API checks that the user is authenticated but not that the user has permission for the specific resource. In the BlogApp API, every PUT/DELETE/PATCH endpoint must verify that the requesting user’s ID matches the resource’s owner ID (or that the user has an elevated role). Fixing BOLA requires an explicit ownership check on every resource-modifying endpoint — there is no framework feature that does this automatically.
Tip: Create a ResourceAuthorizationService that centralises ownership checks: await _authzService.RequireOwnerOrAdminAsync(User, post.AuthorId, ct). This service throws ForbiddenException (mapped to 403 by the global exception handler) if the check fails, keeping controller actions clean. Centralising the check also ensures consistent behaviour — rather than each developer writing the ownership check differently, there is one place where the rule is defined and one place to update if it changes.
Warning: Excessive data exposure (OWASP API6) occurs when endpoints return more data than clients need — entire entity objects including sensitive fields. An /api/users endpoint that returns the full ApplicationUser entity may include password hashes, internal flags, and PII that the Angular list view never displays. Response DTOs with explicitly selected fields are the mitigation — they make it impossible to accidentally expose a new entity field added to the model. Never return entities directly; always map to response DTOs.

Security Checklist — Pre-Integration Review

// ── Security checklist before Angular integration ─────────────────────────
// ✅ Authentication
// [ ] JWT signing key in environment variable / Key Vault (not appsettings)
// [ ] Token expiry configured (access: 60min, refresh: 30days)
// [ ] ClockSkew = TimeSpan.Zero
// [ ] Refresh token rotation implemented
// [ ] JTI-based revocation on logout

// ✅ Authorization
// [ ] Global [Authorize] filter applied (secure by default)
// [ ] [AllowAnonymous] on public endpoints only
// [ ] BOLA check on every resource-modifying endpoint
// [ ] Role checks consistent across all admin/elevated endpoints

// ✅ Input Validation
// [ ] All request DTOs have validation (FluentValidation or DataAnnotations)
// [ ] File uploads validate magic bytes, not just MIME type
// [ ] Pagination limits enforced (max page size = 100)
// [ ] No entity binding (always dedicated DTOs)

// ✅ Output
// [ ] Response DTOs used everywhere (no direct entity returns)
// [ ] Sensitive fields excluded from all response DTOs
// [ ] Error responses use ProblemDetails (no stack traces)

// ✅ Infrastructure
// [ ] HTTPS only in production (HSTS configured)
// [ ] Security headers middleware applied
// [ ] Rate limiting configured for auth endpoints
// [ ] CORS explicitly lists allowed origins (no wildcard)

Common Mistakes

Mistake 1 — Checking resource existence but not ownership (BOLA vulnerability)

❌ Wrong — if (post is null) return NotFound(); followed by update — any authenticated user can edit any post.

✅ Correct — after existence check, always verify post.AuthorId == userId || User.IsInRole("Admin").

Mistake 2 — Returning full entity objects from endpoints (excessive data exposure)

❌ Wrong — return Ok(post) where post is the EF Core entity with all navigation properties and sensitive fields.

✅ Correct — always map to a dedicated response DTO: return Ok(post.ToDto()).

🧠 Test Yourself

User A sends DELETE /api/posts/42 where post 42 belongs to User B. The endpoint checks if (post is null) return NotFound() but has no ownership check. What vulnerability is this?