Web security essentials
The most common web vulnerabilities and the standard defences every backend developer should apply.
13 min read
You will learn
- Name the main risks from the OWASP Top 10
- Prevent injection with parameterized queries
- Prevent script injection with output encoding and CSP
- Configure CORS and protect against cross-site request forgery
Security is part of the job
Every public backend is probed by automated scanners within hours of going online. You don't need to be a security specialist, but you must know the common failure modes and apply the standard defences by default. The OWASP Top 10 is the industry's list of the most critical web application risks; this lesson covers the ones backend developers influence most directly.
1. Broken access control
The most common serious vulnerability: users can reach data or actions they shouldn't.
// ❌ Returns ANY order — just change the id in the URL
app.MapGet("/orders/{id}", async (int id, ShopDb db) => await db.Orders.FindAsync(id));
// ✅ Checks ownership on the server
app.MapGet("/orders/{id}", async (int id, ShopDb db, ClaimsPrincipal user) =>
{
var order = await db.Orders.FindAsync(id);
if (order is null) return Results.NotFound();
if (order.CustomerId != user.GetUserId()) return Results.NotFound(); // don't reveal it exists
return Results.Ok(order);
}).RequireAuthorization();Rules: deny by default, check permissions on every request on the server, and check them for the specific resource, not just "is logged in".
2. Injection
Injection happens when untrusted input is mixed into something that gets interpreted — SQL, shell commands, LDAP queries. If a query is built by concatenating strings, crafted input can change the query's meaning.
Defence: never build queries by concatenating user input. Use parameters.
// ❌ Input becomes part of the SQL text
var sql = $"SELECT * FROM users WHERE email = '{email}'";
// ✅ Parameterized: the query shape is fixed; input is always just a value
using var cmd = new SqlCommand("SELECT * FROM users WHERE email = @email", connection);
cmd.Parameters.AddWithValue("@email", email);
// ✅ EF Core parameterizes LINQ automatically
var user = await db.Users.SingleOrDefaultAsync(u => u.Email == email);Be careful with APIs like FromSqlRaw — prefer FromSql($"... {email}"), which turns interpolated values into parameters.
3. Cross-site scripting (XSS)
XSS happens when user-supplied content is rendered into a page as markup rather than as text, so a browser may execute it as script in other users' sessions.
Defences:
- Encode output by default. Razor (
@comment.Text), Blazor and React all HTML-encode values automatically. Only bypass it (Html.Raw, raw HTML rendering) for content you fully control — or after sanitizing with a vetted library. - Set
HttpOnlyon session cookies so scripts can't read them. - Add a Content Security Policy header restricting where scripts may come from:
Content-Security-Policy: default-src 'self'.
4. Cross-site request forgery (CSRF)
With cookie-based sessions, the browser attaches cookies automatically — even to requests triggered by another site. A malicious page could cause your browser to submit a form to your bank.
Defences: SameSite=Lax (or Strict) cookies, anti-forgery tokens in forms (ASP.NET Core validates them automatically for Razor Pages and MVC forms), and never changing state with GET. APIs using Authorization: Bearer tokens instead of cookies aren't affected, because browsers don't attach those automatically.
5. CORS: who may read your API from a browser
Browsers enforce the same-origin policy: JavaScript on https://evil.example can't read responses from https://api.shop.example. CORS headers let you allow specific origins:
builder.Services.AddCors(o => o.AddPolicy("frontend", p => p
.WithOrigins("https://shop.example")
.AllowAnyHeader()
.AllowAnyMethod()));
app.UseCors("frontend");Avoid AllowAnyOrigin() combined with credentials. And remember: CORS is a browser protection, not authentication — curl and servers ignore it.
6. The rest of the essentials
| Risk | Defence |
|---|---|
| Cryptographic failures | HTTPS everywhere, HSTS, slow salted password hashes, no home-made crypto |
| Security misconfiguration | No detailed errors in production, no default credentials, minimal open ports |
| Vulnerable dependencies | Keep packages updated; dotnet list package --vulnerable |
| Secrets in source code | Use user secrets locally and a vault (Azure Key Vault) in production — never commit keys |
| Missing rate limiting | Throttle login and expensive endpoints (app.UseRateLimiter()) |
| Insufficient logging | Log authentication failures and access-control denials; monitor them |
| Server-side request forgery | Validate URLs your server fetches on behalf of users; block internal addresses |
Defence in depth
No single control is perfect. Combine them: validate input, parameterize queries, encode output, enforce authorization on the server, use secure cookie settings, keep dependencies patched, and log what happens.
Key takeaways
- Broken access control is the top risk: check permissions server-side for every specific resource.
- Prevent injection with parameterized queries — EF Core does this for you.
- Prevent XSS by encoding output (the default in Razor/Blazor/React),
HttpOnlycookies and CSP. - Use SameSite cookies and anti-forgery tokens against CSRF; configure CORS narrowly.
Quiz
4 questions · 3 correct to pass1.What's the correct defence against SQL injection?
2.How do modern frameworks like Razor and React mainly prevent cross-site scripting (XSS)?
3.What does CORS control?
4.What is "broken access control"?
To complete this lesson
- ○ Pass the quiz (3/4 correct)