Comprehensive API Security 2026 — OWASP Top 10, JWT Hardening, and Defense in Depth
Posted on: 4/17/2026 7:13:31 PM
Table of contents
- Contents
- 1. The API security landscape in 2026
- 2. OWASP API Security Top 10 — A detailed walkthrough
- 3. JWT Hardening per RFC 8725
- 4. CORS, CSP, and Security Headers
- 5. Input validation and output encoding
- 6. A Defense-in-Depth architecture for APIs
- 7. Implementation on .NET 10 and Vue.js
- 8. Conclusion
1. The API security landscape in 2026
APIs are the backbone of every modern application. From mobile apps to SPAs, from microservices to IoT, everything talks over APIs. But that ubiquity makes APIs the top target for attackers. Security reports consistently show that more than 90% of web applications contain at least one API-related vulnerability, and the damage from API-driven breaches keeps growing.
Why is API security different from traditional web security?
APIs have no UI to "hide" backend logic — attackers interact directly with business logic. An authorization bug in a web form might be limited by the UI flow; the same bug in an API endpoint lets an attacker exploit it at scale via automated scripts. That's why OWASP separated the API Security Top 10 from the Web Application Top 10.
2. OWASP API Security Top 10 — A detailed walkthrough
OWASP API Security Top 10 (2023 edition) is the list of 10 most critical API security risks, updated from the 2019 version with many important changes reflecting new attack realities.
graph TD
A["OWASP API
Security Top 10
(2023)"] --> B["API1
Broken Object Level
Authorization"]
A --> C["API2
Broken
Authentication"]
A --> D["API3
Broken Object Property
Level Authorization"]
A --> E["API4
Unrestricted Resource
Consumption"]
A --> F["API5
Broken Function Level
Authorization"]
A --> G["API6
Unrestricted Access to
Sensitive Business Flows"]
A --> H["API7
Server Side
Request Forgery"]
A --> I["API8
Security
Misconfiguration"]
A --> J["API9
Improper Inventory
Management"]
A --> K["API10
Unsafe Consumption
of APIs"]
style A fill:#e94560,stroke:#fff,color:#fff
style B fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style C fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style D fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style E fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style F fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style G fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style H fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style I fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style J fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style K fill:#f8f9fa,stroke:#e94560,color:#2c3e50
The 10 API security risks per OWASP 2023
API1:2023 — Broken Object Level Authorization (BOLA)
Broken Object Level Authorization
CRITICALThis is the most common and most dangerous vulnerability. It happens when an API doesn't properly check object ownership for the user making the request. An attacker only has to change the object ID in the request to access someone else's data.
Example attack:
// Valid request — user looks up their own order
GET /api/orders/1001
Authorization: Bearer eyJhbGciOi...
// Attacker swaps the ID → reads someone else's order
GET /api/orders/1002
Authorization: Bearer eyJhbGciOi... (same token!)
Prevention in .NET 10:
// Authorization handler that checks ownership
public class OrderOwnerHandler : AuthorizationHandler<OrderOwnerRequirement, Order>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
OrderOwnerRequirement requirement,
Order order)
{
var userId = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (order.UserId == userId)
context.Succeed(requirement);
return Task.CompletedTask;
}
}
// Use in the controller
[HttpGet("{id}")]
public async Task<IActionResult> GetOrder(int id)
{
var order = await _repo.GetByIdAsync(id);
var authResult = await _authService
.AuthorizeAsync(User, order, "OrderOwner");
if (!authResult.Succeeded) return Forbid();
return Ok(order);
}
API2:2023 — Broken Authentication
Broken Authentication
CRITICALWeak or missing API authentication lets attackers impersonate others. Common flaws: not validating JWT signatures, accepting expired tokens, unblocked brute force, credential stuffing.
Common mistakes when implementing authentication:
| Mistake | Consequence | Fix |
|---|---|---|
Not validating the JWT alg header |
Attacker switches to "alg":"none" to bypass the signature |
Whitelist algorithms; reject none |
| Weak or hardcoded secret key | Brute-force the secret → forge tokens | Use RSA/ECDSA; rotate keys regularly |
Not checking the exp claim |
Old tokens remain valid forever | Validate expiration; use short-lived tokens |
| No rate limit on the login endpoint | Credential stuffing, password brute-force | Rate limiting + account lockout + CAPTCHA |
| Token in URL query string | Token gets logged in server logs, browser history, Referer header | Only pass tokens via the Authorization header |
API3:2023 — Broken Object Property Level Authorization
Broken Object Property Level Authorization
HIGHMerged from two older issues: Excessive Data Exposure (API returns too many fields) and Mass Assignment (API accepts unintended fields). This is the result of binding request/response directly to database entities.
// WRONG — returns the whole entity including sensitive fields
[HttpGet("{id}")]
public async Task<User> GetUser(int id)
=> await _db.Users.FindAsync(id);
// Response leaks: PasswordHash, SSN, InternalNotes...
// RIGHT — use a DTO containing only the needed fields
[HttpGet("{id}")]
public async Task<UserDto> GetUser(int id)
{
var user = await _db.Users.FindAsync(id);
return new UserDto(user.Id, user.Name, user.Email);
}
// WRONG — Mass Assignment: binding straight to the request body
[HttpPut("{id}")]
public async Task UpdateUser(int id, [FromBody] User user) { ... }
// Attacker sends: {"role": "admin", "isVerified": true}
// RIGHT — use a DTO with allowed fields
[HttpPut("{id}")]
public async Task UpdateUser(int id, [FromBody] UpdateUserDto dto) { ... }
// UpdateUserDto only has: Name, Email, Avatar
API4:2023 — Unrestricted Resource Consumption
APIs without resource limits are fair game for DoS, bill shock (on the cloud), or simply bringing down the system. You need to control: request count, body size, record count returned, processing time, upload size.
// .NET 10 — built-in Rate Limiting middleware
builder.Services.AddRateLimiter(options =>
{
options.AddFixedWindowLimiter("api", opt =>
{
opt.PermitLimit = 100;
opt.Window = TimeSpan.FromMinutes(1);
opt.QueueLimit = 0;
});
options.AddTokenBucketLimiter("upload", opt =>
{
opt.TokenLimit = 10;
opt.ReplenishmentPeriod = TimeSpan.FromSeconds(10);
opt.TokensPerPeriod = 2;
});
options.RejectionStatusCode = 429;
});
// Cap request body size
builder.WebHost.ConfigureKestrel(options =>
{
options.Limits.MaxRequestBodySize = 10 * 1024 * 1024; // 10MB
});
API5-API10 — Quick summary
| Risk | Description | Main defense |
|---|---|---|
| API5: Broken Function Level Authorization | Regular users can reach admin endpoints | RBAC/ABAC; role checks on every endpoint |
| API6: Unrestricted Access to Sensitive Business Flows | Buyer bots, signup spam, scraping | CAPTCHA, device fingerprinting, business-logic rate limits |
| API7: Server Side Request Forgery (SSRF) | API fetches a URL from user input → reaches internal services | Domain whitelists, block internal IPs, use allowlists |
| API8: Security Misconfiguration | CORS *, debug mode in prod, default credentials | Hardening checklist, automated scanning, IaC |
| API9: Improper Inventory Management | Old APIs, shadow APIs, undocumented endpoints | API gateway, OpenAPI spec, deprecation policy |
| API10: Unsafe Consumption of APIs | Trusting data from third-party APIs blindly | Validate external API responses; timeouts; circuit breakers |
3. JWT Hardening per RFC 8725
JSON Web Token is the most widely-used API authentication method. But JWTs have many ways to be exploited if implemented wrong. RFC 8725 (JSON Web Token Best Current Practices) lays down the key guidelines.
sequenceDiagram
participant C as Client (Vue.js)
participant A as Auth Server
participant API as API Server (.NET)
participant DB as Database
C->>A: POST /token (credentials)
A->>A: Validate credentials
A->>A: Generate JWT (RS256, short exp)
A-->>C: Access Token + Refresh Token
C->>API: GET /api/data
Authorization: Bearer {JWT}
API->>API: Validate signature (public key)
API->>API: Check exp, iss, aud, nbf
API->>API: Extract claims, check permissions
API->>DB: Query with user context
DB-->>API: Filtered data
API-->>C: 200 OK + data
Note over C,API: Token expires after 15 minutes
C->>A: POST /token/refresh
A->>A: Validate refresh token
A->>A: Issue new access token
A-->>C: New Access Token
A standard JWT auth flow with short-lived access tokens and refresh tokens
JWT hardening checklist
JWT security checklist
- Algorithm: use RS256 or ES256 (asymmetric). Never use HS256 with a weak secret. Reject
"alg":"none". - Expiration: access tokens: 15-30 minutes. Refresh tokens: 7-30 days. Always validate the
expclaim. - Audience (
aud): every API must validate that theaudclaim matches itself — preventing a service-A token from being used against service B. - Issuer (
iss): validate the issuer so the token must come from a trusted auth server. - Not Before (
nbf): setnbfto the issue time to keep tokens from being used before creation. - JTI (JWT ID): unique ID per token; add to a blacklist on revocation.
- Claims: include only necessary information in the payload. NEVER put passwords, sensitive PII, or internal IDs in it.
- Key rotation: rotate signing keys regularly. Publish public keys via a JWKS endpoint with the
kidheader.
JWT authentication configuration in .NET 10:
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = "https://auth.example.com";
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = "https://auth.example.com",
ValidateAudience = true,
ValidAudience = "https://api.example.com",
ValidateLifetime = true,
ClockSkew = TimeSpan.FromSeconds(30),
ValidateIssuerSigningKey = true,
ValidAlgorithms = new[] { "RS256", "ES256" },
RequireExpirationTime = true,
RequireSignedTokens = true,
};
options.Events = new JwtBearerEvents
{
OnAuthenticationFailed = context =>
{
if (context.Exception is SecurityTokenExpiredException)
context.Response.Headers["X-Token-Expired"] = "true";
return Task.CompletedTask;
}
};
});
// Fallback policy — every endpoint requires authentication
builder.Services.AddAuthorizationBuilder()
.SetFallbackPolicy(new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build());
Warning: JWT is not a session
JWT is stateless — once issued, the server can't revoke it until it expires. If you need to revoke immediately (user banned, password changed), you must also use a token blacklist or short-lived tokens + refresh rotation. Don't set exp too long (24h+) — it widens the "zombie token" window.
4. CORS, CSP, and Security Headers
CORS — Cross-Origin Resource Sharing
CORS is the browser's mechanism to control cross-origin requests. Misconfigured CORS is one of the most common Security Misconfiguration (API8) bugs.
sequenceDiagram
participant B as Browser
participant API as API Server
Note over B,API: Preflight Request (PUT/DELETE/custom headers)
B->>API: OPTIONS /api/data
Origin: https://app.example.com
Access-Control-Request-Method: PUT
API->>API: Is the origin in the allowlist?
API-->>B: 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Max-Age: 3600
Note over B,API: Actual Request
B->>API: PUT /api/data
Origin: https://app.example.com
API-->>B: 200 OK
Access-Control-Allow-Origin: https://app.example.com
The CORS preflight flow and the actual request
| Config | Risk | Recommendation |
|---|---|---|
Access-Control-Allow-Origin: * |
Any domain can call the API | Use only for public, unauthenticated APIs |
| Reflecting the Origin header as-is | Completely bypasses CORS — equivalent to * |
Whitelist specific origins |
Allow-Credentials: true + Origin: * |
Browsers block it (invalid), but devs sometimes bypass by reflecting | Credentials only with specific origins |
No Max-Age set |
Browser sends a preflight on every request → higher latency | Set Max-Age: 3600 (1 hour) |
Correct CORS configuration in .NET 10:
builder.Services.AddCors(options =>
{
options.AddPolicy("Production", policy =>
{
policy
.WithOrigins(
"https://app.example.com",
"https://admin.example.com"
)
.WithMethods("GET", "POST", "PUT", "DELETE")
.WithHeaders("Authorization", "Content-Type", "X-Request-Id")
.SetPreflightMaxAge(TimeSpan.FromHours(1))
.AllowCredentials();
});
});
Essential security headers
Beyond CORS, a whole set of HTTP headers help protect the API and its clients:
// Security headers middleware for .NET 10
app.Use(async (context, next) =>
{
var headers = context.Response.Headers;
// Clickjacking protection
headers["X-Frame-Options"] = "DENY";
// MIME-type sniffing protection
headers["X-Content-Type-Options"] = "nosniff";
// Content Security Policy
headers["Content-Security-Policy"] =
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'";
// Strict Transport Security (HTTPS only)
headers["Strict-Transport-Security"] =
"max-age=31536000; includeSubDomains; preload";
// Prevent sensitive info in Referer headers
headers["Referrer-Policy"] = "strict-origin-when-cross-origin";
// Turn off unnecessary browser APIs
headers["Permissions-Policy"] =
"camera=(), microphone=(), geolocation=()";
await next();
});
5. Input validation and output encoding
Input validation is the first line of defense against injection attacks (SQL Injection, NoSQL Injection, Command Injection, XSS). The core rule: never trust user input — validate everything entering the system from outside.
graph LR
A["Client Input"] --> B["Schema
Validation"]
B --> C["Type
Checking"]
C --> D["Business
Rules"]
D --> E["Sanitization"]
E --> F["Safe Data"]
B -->|Invalid| G["400
Bad Request"]
C -->|Invalid| G
D -->|Invalid| G
style A fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style B fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style C fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style D fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style E fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style F fill:#4CAF50,stroke:#fff,color:#fff
style G fill:#e94560,stroke:#fff,color:#fff
Input-validation pipeline
Validation on .NET 10 — FluentValidation + Minimal API
// DTO with validation attributes
public record CreateOrderRequest(
[Required, StringLength(200)] string ProductName,
[Range(1, 1000)] int Quantity,
[Required, EmailAddress] string CustomerEmail,
[RegularExpression(@"^[a-zA-Z0-9\-]+$")] string? CouponCode
);
// FluentValidation for complex business rules
public class CreateOrderValidator : AbstractValidator<CreateOrderRequest>
{
public CreateOrderValidator()
{
RuleFor(x => x.ProductName)
.NotEmpty()
.MaximumLength(200)
.Must(name => !name.Contains("<script>"))
.WithMessage("Product name contains invalid characters");
RuleFor(x => x.Quantity)
.InclusiveBetween(1, 1000);
RuleFor(x => x.CustomerEmail)
.EmailAddress(EmailValidationMode.AspNetCoreCompatible);
RuleFor(x => x.CouponCode)
.Matches(@"^[A-Z0-9\-]{4,20}$")
.When(x => x.CouponCode != null);
}
}
// Minimal API endpoint with validation
app.MapPost("/api/orders", async (
CreateOrderRequest request,
IValidator<CreateOrderRequest> validator,
OrderService service) =>
{
var result = await validator.ValidateAsync(request);
if (!result.IsValid)
return Results.ValidationProblem(result.ToDictionary());
var order = await service.CreateAsync(request);
return Results.Created($"/api/orders/{order.Id}", order);
});
Stopping SQL injection — parameterized queries
SQL injection is still threat #1
Even in 2026, SQL injection remains in the top common vulnerabilities. The main cause: developers still concatenate SQL strings instead of using parameterized queries. ORMs like Entity Framework Core prevent most of these, but raw SQL still demands care.
// WRONG — SQL injection vulnerability
var sql = $"SELECT * FROM Users WHERE Name = '{name}'";
// Input: ' OR '1'='1' -- → returns every user
// RIGHT — parameterized query with EF Core
var users = await _db.Users
.Where(u => u.Name == name)
.ToListAsync();
// RIGHT — raw SQL with a parameter
var users = await _db.Users
.FromSqlInterpolated($"SELECT * FROM Users WHERE Name = {name}")
.ToListAsync();
Stopping XSS in Vue.js
<!-- WRONG — v-html renders HTML without sanitizing -->
<div v-html="userComment"></div>
<!-- Input: <img src=x onerror=alert(document.cookie)> → XSS! -->
<!-- RIGHT — Vue templates escape by default -->
<div>{{ userComment }}</div>
<!-- Output: <img src=x onerror=alert(...)> → safe -->
<!-- If you MUST render HTML (rich text editor): use DOMPurify -->
<script setup>
import DOMPurify from 'dompurify';
const safeHtml = computed(() =>
DOMPurify.sanitize(userComment.value, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
ALLOWED_ATTR: ['href', 'target', 'rel']
})
);
</script>
<div v-html="safeHtml"></div>
6. A Defense-in-Depth architecture for APIs
No single security measure is enough. Defense in Depth layers multiple overlapping protections — when one layer is bypassed, the next still stops the attacker.
graph TB
subgraph L1["Layer 1: Network"]
WAF["WAF
(Cloudflare/AWS WAF)"]
DDoS["DDoS
Protection"]
TLS["TLS 1.3
Termination"]
end
subgraph L2["Layer 2: API Gateway"]
RL["Rate
Limiting"]
AUTH["Authentication
(JWT/OAuth)"]
CORS2["CORS
Enforcement"]
end
subgraph L3["Layer 3: Application"]
AUTHZ["Authorization
(RBAC/ABAC)"]
VAL["Input
Validation"]
BIZ["Business Logic
Guards"]
end
subgraph L4["Layer 4: Data"]
ENC["Encryption
at Rest"]
MASK["Data
Masking"]
AUDIT["Audit
Logging"]
end
L1 --> L2 --> L3 --> L4
style L1 fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style L2 fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style L3 fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style L4 fill:#f8f9fa,stroke:#e94560,color:#2c3e50
style WAF fill:#e94560,stroke:#fff,color:#fff
style DDoS fill:#e94560,stroke:#fff,color:#fff
style TLS fill:#e94560,stroke:#fff,color:#fff
style RL fill:#2c3e50,stroke:#fff,color:#fff
style AUTH fill:#2c3e50,stroke:#fff,color:#fff
style CORS2 fill:#2c3e50,stroke:#fff,color:#fff
style AUTHZ fill:#4CAF50,stroke:#fff,color:#fff
style VAL fill:#4CAF50,stroke:#fff,color:#fff
style BIZ fill:#4CAF50,stroke:#fff,color:#fff
style ENC fill:#16213e,stroke:#fff,color:#fff
style MASK fill:#16213e,stroke:#fff,color:#fff
style AUDIT fill:#16213e,stroke:#fff,color:#fff
A 4-layer Defense-in-Depth architecture for production APIs
Layer-by-layer details
Layer 1 — Network: a WAF (Web Application Firewall) like Cloudflare WAF or AWS WAF in front of the API blocks known attack patterns (SQL injection, XSS, path traversal) before they reach the application. DDoS protection (Cloudflare includes it free on every plan) absorbs abnormal traffic. TLS 1.3 encrypts all traffic.
Layer 2 — API Gateway: centralizes authentication, rate limiting, and CORS enforcement. The API Gateway is the single entry point, helping apply consistent policies across every endpoint. On .NET 10 you can use YARP (Yet Another Reverse Proxy) or Ocelot.
Layer 3 — Application: fine-grained authorization (who can do what on which resource), input validation, and business-logic guards (e.g. a user can't increase their own balance; order quantity can't be negative).
Layer 4 — Data: encrypt sensitive data at rest (SQL Server Always Encrypted, Azure Key Vault), mask data for staging/dev environments, and audit-log every important operation for forensics when needed.
7. Implementation on .NET 10 and Vue.js
Security pipeline on .NET 10
The middleware order in ASP.NET Core matters — security middleware must sit in the right spot:
var app = builder.Build();
// 1. Exception handler (first — catches all errors)
app.UseExceptionHandler("/error");
// 2. HSTS (redirect HTTP → HTTPS)
app.UseHsts();
app.UseHttpsRedirection();
// 3. Security headers (custom middleware)
app.UseSecurityHeaders();
// 4. CORS (before authentication)
app.UseCors("Production");
// 5. Rate limiting
app.UseRateLimiter();
// 6. Authentication (identity verification)
app.UseAuthentication();
// 7. Authorization (permission check)
app.UseAuthorization();
// 8. Endpoints
app.MapControllers();
Tip: use Problem Details for security errors
.NET 10 supports RFC 9457 (Problem Details) for error responses. When returning a security error, NEVER leak internal information: no stack trace, table names, or query details. Return only what the client needs to handle.
// Global exception handler — no internal detail leaks
app.MapGet("/error", (HttpContext context) =>
{
var error = context.Features.Get<IExceptionHandlerFeature>()?.Error;
return Results.Problem(
title: "An error occurred",
statusCode: 500,
detail: "Please contact support with the trace ID.",
extensions: new Dictionary<string, object?>
{
["traceId"] = Activity.Current?.Id ?? context.TraceIdentifier
}
);
});
Security patterns in Vue.js
// composables/useAuth.ts — Secure token management
import { ref, computed } from 'vue';
const accessToken = ref<string | null>(null);
const refreshToken = ref<string | null>(null);
export function useAuth() {
// Do NOT store tokens in localStorage (XSS-accessible)
// Use in-memory + httpOnly cookie for the refresh token
const isAuthenticated = computed(() => !!accessToken.value);
async function login(email: string, password: string) {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include', // sends the httpOnly cookie
body: JSON.stringify({ email, password })
});
if (!response.ok) throw new Error('Login failed');
const data = await response.json();
accessToken.value = data.accessToken;
// refreshToken lives in an httpOnly cookie — JS can't read it
}
async function fetchWithAuth(url: string, options: RequestInit = {}) {
const headers = new Headers(options.headers);
if (accessToken.value) {
headers.set('Authorization', `Bearer ${accessToken.value}`);
}
let response = await fetch(url, {
...options,
headers,
credentials: 'include'
});
// Auto-refresh on 401
if (response.status === 401) {
await refreshAccessToken();
headers.set('Authorization', `Bearer ${accessToken.value}`);
response = await fetch(url, {
...options,
headers,
credentials: 'include'
});
}
return response;
}
return { isAuthenticated, login, fetchWithAuth };
}
Pre-production API security checklist
| Item | Check | Tools |
|---|---|---|
| Authentication | JWT validated properly (alg, exp, iss, aud); unsigned tokens rejected | jwt.io debugger, Burp Suite |
| Authorization | Every endpoint checks permissions; test BOLA via ID enumeration | OWASP ZAP, Postman scripts |
| Input validation | Reject inputs outside the schema; test SQL injection/XSS payloads | SQLMap, XSStrike, Burp Scanner |
| CORS | No * wildcard for authed APIs; test with unknown origins |
curl manual tests, CORS tester |
| Rate limiting | Block >100 requests/min/IP; test burst traffic | ab (Apache Bench), k6 |
| Security headers | HSTS, X-Content-Type-Options, CSP, X-Frame-Options | securityheaders.com, Mozilla Observatory |
| Error handling | No stack trace, DB schema, or internal paths leaked | Manual testing, Sentry config |
| Logging | Log auth failures and authz denials; NEVER log tokens/passwords | Structured logging, SIEM |
| Dependencies | No known CVEs in NuGet/npm packages | dotnet audit, npm audit, Snyk |
| TLS | TLS 1.2+ only, strong cipher suites, valid certificates | SSL Labs, testssl.sh |
8. Conclusion
API security isn't something you "add later" — it has to be designed in from day one, embedded into every layer of the system. The OWASP API Security Top 10 is a great framework for systematically assessing and improving API security. Combined with JWT hardening per RFC 8725, correct CORS/CSP, strict input validation, and a Defense-in-Depth architecture, you can build production-safe APIs without sacrificing developer experience.
Remember: security is a continuous process, not a state to be reached. Regular audits, updated dependencies, and tracking OWASP plus security advisories are work that never really ends.
Quick recap
- OWASP API Security Top 10 (2023) is the compass — BOLA (#1) and Broken Authentication (#2) are the most common
- JWT: use RS256/ES256, short-lived tokens, validate enough claims (exp, iss, aud, nbf)
- CORS: whitelist specific origins, no wildcards for authed APIs
- Input validation: validate at the boundary, use parameterized queries for SQL, DOMPurify for HTML
- Defense in Depth: WAF → API Gateway → Application → Data — each layer blocks a different class of attack
References
- OWASP API Security Top 10 — 2023
- RFC 8725 — JSON Web Token Best Current Practices
- Authentication and authorization in Minimal APIs — Microsoft Learn
- .NET 10: What's New for Authentication and Authorization — Auth0
- Content Security Policy (CSP) — MDN Web Docs
- Master API Security: Essential Best Practices for 2026
Vite+ 2026 — One Toolchain to Replace Webpack, ESLint, and Prettier
Cloudflare AI Platform 2026 — Edge Infrastructure for Serverless AI Agents
Disclaimer: The opinions expressed in this blog are solely my own and do not reflect the views or opinions of my employer or any affiliated organizations. The content provided is for informational and educational purposes only and should not be taken as professional advice. While I strive to provide accurate and up-to-date information, I make no warranties or guarantees about the completeness, reliability, or accuracy of the content. Readers are encouraged to verify the information and seek independent advice as needed. I disclaim any liability for decisions or actions taken based on the content of this blog.