Three injection-family attacks account for an outsized share of real-world web breaches: SQL Injection, Cross-Site Scripting (XSS), and Cross-Site Request Forgery (CSRF). They sit behind several OWASP Top 10 categories (Injection, Broken Access Control). The unifying lesson: never trust input, and never mix untrusted data into a command/markup/request without the right escaping or token. This page covers how each works and the .NET defenses.

SQL Injection (SQLi)

The attacker smuggles SQL syntax through user input into a query that’s built by string concatenation, changing what the query means.

// VULNERABLE: input becomes part of the SQL text
var sql = $"SELECT * FROM Users WHERE Name = '{name}'";
// name = "x' OR '1'='1"  → returns every row
// name = "x'; DROP TABLE Users;--"  → destructive

Defense — parameterize. A parameter is sent separately from the SQL text, so the database treats it as data, never as code:

// Safe: ADO.NET parameter
cmd.CommandText = "SELECT * FROM Users WHERE Name = @name";
cmd.Parameters.AddWithValue("@name", name);
 
// Safe: EF Core parameterizes interpolated values automatically
var users = await db.Users.Where(u => u.Name == name).ToListAsync();
await db.Database.ExecuteSqlInterpolatedAsync($"DELETE FROM Logs WHERE Id = {id}");

Parameterization is the complete fix for SQLi; input validation and least-privilege DB accounts are defense-in-depth, not substitutes. The one place parameters can’t help is dynamic identifiers (table/column names) — allowlist those against a fixed set. NoSQL has the analogous NoSQL injection (e.g. passing an object where a string is expected), defended the same way: never interpolate untrusted input into a query.

Cross-Site Scripting (XSS)

The attacker gets their JavaScript to run in another user’s browser, in the victim site’s origin — so it can read cookies, the DOM, and act as the user. Three flavours:

  • Stored — the payload is saved server-side (a comment, a profile field) and served to every viewer. Worst, because it’s persistent and self-spreading.
  • Reflected — the payload is in the request (a query param) and echoed straight back into the response; the attacker must lure the victim to a crafted URL.
  • DOM-based — client-side JS writes untrusted data into the DOM (element.innerHTML = location.hash) with no server involved.

Defense — contextual output encoding. The root cause is putting untrusted data into HTML without encoding it for the context (HTML body vs attribute vs JS vs URL). Razor encodes by default; the danger is opting out:

@* Safe: Razor HTML-encodes automatically *@
<p>@Model.UserComment</p>
 
@* DANGEROUS: Html.Raw bypasses encoding — only for content you fully trust/sanitize *@
<p>@Html.Raw(Model.UserComment)</p>

Layered defenses:

  • Output-encode at every sink, in the right context (use the framework’s encoders; don’t hand-roll).
  • Sanitize rich HTML you must render (e.g. a WYSIWYG field) with an allowlist library like HtmlSanitizer — never a blocklist.
  • Content-Security-Policy (CSP) header — a strong backstop: disallow inline scripts so an injected <script> won’t execute even if encoding is missed.
  • Store JWTs/session tokens in HttpOnly cookies so XSS can’t read them (see JWT Bearer).

Cross-Site Request Forgery (CSRF)

CSRF abuses the fact that browsers auto-attach cookies to requests. A malicious page makes the victim’s browser send a state-changing request to a site where the victim is logged in; the cookie rides along and the request is honored as the user. The attacker can’t read the response (that’s what the same-origin policy and CORS prevent) — but a fire-and-forget POST /transfer still does damage.

Defenses (use both):

  • Anti-forgery token (synchronizer token pattern) — the server embeds an unpredictable token in the form that the attacker’s site can’t know or read. ASP.NET Core does this automatically for Razor form posts (@Html.AntiForgeryToken() + [ValidateAntiForgeryToken]):
[HttpPost]
[ValidateAntiForgeryToken]                  // rejects requests missing the token
public IActionResult Transfer(TransferDto dto) { /* ... */ }
  • SameSite cookiesSameSite=Lax (the modern browser default) or Strict stops the cookie from being sent on cross-site requests, neutralizing most CSRF at the browser level. Pair it with the token; don’t rely on it alone.

NOTE

Token-based APIs (JWT in an Authorization header) are largely CSRF-immune because the browser doesn’t auto-attach a header the way it does a cookie. CSRF is primarily a cookie-authentication problem — which is also why storing tokens in cookies (good for XSS) reintroduces CSRF and needs SameSite + anti-forgery.

Pitfalls

  • Blocklist filtering — trying to strip <script> or ' with regex. Attackers have endless encodings/variants; always use parameterization (SQLi) and contextual encoding/allowlist sanitization (XSS) instead.
  • Validation mistaken for output encoding — input validation reduces attack surface but is not the XSS/SQLi fix; the same data is safe in one context and dangerous in another. Encode/parameterize at the sink.
  • Html.Raw / dangerouslySetInnerHTML on user content — the classic stored-XSS hole.
  • Disabling anti-forgery “to make the SPA work” — fix it with SameSite + token-in-header, don’t switch the protection off.
  • Reflected XSS in error pages / search results — echoing the raw query string back is a common reflected-XSS sink.

Tradeoffs

VulnerabilityRoot causePrimary fixBackstop
SQL InjectionData concatenated into SQLParameterized queriesLeast-privilege DB user, input validation
XSSUntrusted data in HTML/JS unescapedContextual output encodingCSP, HtmlSanitizer, HttpOnly cookies
CSRFBrowser auto-sends cookiesAnti-forgery tokenSameSite cookies

Decision rule: parameterize every query, output-encode every untrusted value at the point of rendering, and protect every cookie-authenticated state-changing endpoint with an anti-forgery token plus SameSite. These three habits eliminate the most common and most damaging web vulnerabilities; treat CSP and least-privilege as defense-in-depth on top.

Questions

References