Validation and API Security
Never trust the request, and never keep secrets where git can see them
Guess first
Pick an answer. You will see if you were right at the end of the lesson.
Your API has a strict CORS policy. Can someone still call it with curl from any machine?
Real-world analogy
What is it?
API security is a set of layers. Validation rejects bad input early. Parameterised queries stop injection. HTTPS protects data in transit, rate limiting slows abuse, and CORS controls which browser origins may read your responses. Secrets and password storage cover what happens when someone gets a copy of your code or database. No single layer is enough, and each one covers the gap the others leave.
Real-world relevance
Most real breaches here are boring. A connection string with a password is committed to a public repo. A search box builds SQL with string concatenation and an attacker dumps the users table. A "wildcard" CORS policy is added to make an error go away and left in. A login endpoint has no rate limit, so a script tries a million passwords. These are cheap to prevent and expensive to explain afterwards.
Key points
- Validate at the edge — Check every input as it enters, with DataAnnotations or FluentValidation. Return 400 with field errors, so bad data is stopped before your logic runs.
- Over-posting and DTOs — If you bind JSON straight to an entity, a caller can add "isAdmin": true. A request DTO lists only the fields you allow.
- SQL injection — It happens when user text is glued into a SQL string. EF Core LINQ and parameterised queries send values separately, so text cannot become code.
- CORS is a browser rule — CORS tells browsers which origins may read your responses. Tools like curl ignore it, so it is not a firewall and does not replace authentication.
- HTTPS and HSTS — HTTPS encrypts traffic in transit. HSTS tells browsers to use only HTTPS for your domain after the first visit. That blocks attacks that downgrade later visits to plain HTTP.
- Rate limiting — The built-in limiter caps requests per time window. Partition it by user or IP, because one shared window lets an attacker block everyone. It slows password guessing and protects expensive endpoints.
- Keep secrets out of git — Use user-secrets locally, and environment variables or a vault such as Key Vault in production. A secret committed in appsettings.json stays in git history after you delete it. Treat it as leaked and rotate it.
- Hash passwords, never encrypt — Store a slow salted hash such as the one from ASP.NET Core Identity. Encrypted passwords can be decrypted by anyone with the key, and plain ones are readable.
- Protect sensitive data at rest — For data you must read back, use database or column encryption with keys kept in a vault. The ASP.NET Core Data Protection API is built for short-lived payloads such as cookies and tokens.
Quick check
Answer from memory — scrolling up first makes it stick less.
Which approach protects a search endpoint from SQL injection?
What is the right way to store user passwords?
Code example
public class RegisterRequest
{
[Required, EmailAddress, StringLength(254)]
public string Email { get; set; } = "";
[Required, StringLength(100, MinimumLength = 12)]
public string Password { get; set; } = "";
}
[ApiController]
[Route("register")]
[EnableRateLimiting("strict")]
public class RegisterController : ControllerBase
{
[HttpPost]
public IActionResult Post(RegisterRequest request) => Accepted(); // bad input never gets here: automatic 400
}
// Program.cs
builder.Services.AddCors(o => o.AddPolicy("web", p => p
.WithOrigins("https://app.example.com")
.WithMethods("GET", "POST")
.WithHeaders("Content-Type", "Authorization")));
builder.Services.AddRateLimiter(o =>
{
o.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
o.AddPolicy("strict", http => RateLimitPartition.GetFixedWindowLimiter(
http.Connection.RemoteIpAddress?.ToString() ?? "unknown", // one window per client IP
_ => new FixedWindowRateLimiterOptions
{ PermitLimit = 5, Window = TimeSpan.FromMinutes(1), QueueLimit = 0 }));
});
// app.UseCors("web"); app.UseRateLimiter();Line-by-line walkthrough
- 1. RegisterRequest uses attributes: Email must be a valid address, Password must be 12 to 100 characters.
- 2. The controller has [ApiController], so a request that breaks those rules gets a 400 with field errors before Post runs.
- 3. EnableRateLimiting("strict") applies the named limiter to this endpoint only.
- 4. The CORS policy lists one exact origin, the methods and the headers. It is a rule for browsers, not a firewall.
- 5. AddPolicy gives each client IP its own fixed window of 5 requests per minute. QueueLimit 0 means extra calls get 429 at once. Behind a proxy, configure forwarded headers so the IP is the real caller.
- 6. The last line is a comment showing that UseCors and UseRateLimiter must be added to the pipeline too.
Spot the bug
public async Task<List<User>> Search(string name)
{
var sql = "SELECT * FROM Users WHERE Name = '" + name + "'";
return await _db.Users.FromSqlRaw(sql).ToListAsync();
}Need a hint?
Show answer
Explain like I'm 5
Fun fact
Hands-on challenge
More resources
- Model validation in ASP.NET Core (Microsoft Learn)
- Enable Cross-Origin Requests (CORS) (Microsoft Learn)
- Rate limiting middleware in ASP.NET Core (Microsoft Learn)
Lock it in