Secure coding interviews for senior .NET engineers rarely ask candidates to define SQL injection; they ask candidates to spot the injection vector in a code review, explain why a mitigation is incomplete, or design a defense that survives a determined attacker rather than a casual one. That distinction matters because the OWASP Top 10 itself keeps moving: the 2025 edition, which supersedes the long-standing 2021 list, folds Server-Side Request Forgery into Broken Access Control and replaces Vulnerable and Outdated Components with the broader Software Supply Chain Failures — changes a candidate who memorized the 2021 mnemonic will simply not know about. For engineers with a decade or more of production experience, these questions probe whether security is designed in from the start, with threat modeling, secure defaults and defense in depth, or bolted on after a penetration test finds it. The ten questions below cover injection, cross-site scripting, CSRF, SSRF, insecure deserialization, broken access control, sensitive data in logs, security headers and STRIDE threat modeling, mapped to .NET code and current OWASP guidance throughout.
Q1 How do you prevent SQL injection in .NET, and is "we use an ORM" a sufficient answer?#
Short answer: Parameterized queries — which EF Core generates automatically from LINQ, and which you must write explicitly with Dapper or raw ADO.NET — keep user input out of the SQL text entirely, sent instead as typed parameters the database binds separately from the query plan; "we use an ORM" is not sufficient on its own because every ORM has raw-SQL escape hatches that reintroduce the exact same vulnerability the moment user input is concatenated into them.
// Vulnerable: string interpolation builds SQL text from user input.
var orders = await db.Orders
.FromSqlRaw($"SELECT * FROM Orders WHERE CustomerId = '{customerId}'")
.ToListAsync(ct);
// Safe: the FromSql interpolated-string overload parameterizes automatically.
var orders = await db.Orders
.FromSql($"SELECT * FROM Orders WHERE CustomerId = {customerId}")
.ToListAsync(ct);FromSqlRaw takes a plain string and any interpolation you perform before calling it is just string concatenation with extra steps; FromSql takes an interpolated string directly and EF Core turns each {} placeholder into a real DbParameter, which is the distinction interviewers expect you to know cold. The same rule applies verbatim to Dapper: @customerId bound through the param argument is safe, string-formatting the value into the SQL text is not. The case that trips up experienced engineers is dynamic sorting and filtering — an endpoint that lets a client choose which column to ORDER BY. Column and table identifiers can't be parameterized the way values can, so the only safe approach is an allowlist that maps a small set of accepted client values to known-safe column names, never passing the client's string anywhere near the query itself.
What interviewers look for: the FromSqlRaw/FromSql distinction specifically, and recognition that identifiers (columns, table names, sort direction) need allowlisting because they can't be parameterized like values.
Common mistakes: assuming an ORM makes injection impossible everywhere in the codebase, and missing second-order injection, where a value stored safely once is later concatenated unsafely into a different query or a stored procedure.
Q2 How does ASP.NET Core protect against XSS by default, and where do those defaults break down?#
Short answer: Razor HTML-encodes all output by default, and Blazor's normal component rendering does the same, so most injection points are closed without any effort from the developer; the defaults break down wherever code explicitly opts out of encoding — Html.Raw, MarkupString in Blazor, a JSON value interpolated directly into a <script> tag, or client-side JavaScript that writes API responses into the DOM with innerHTML instead of textContent.
@* Vulnerable: explicitly opts out of Razor's automatic encoding. *@
<div>@Html.Raw(userSuppliedBio)</div>
@* Safe: Razor encodes this automatically. *@
<div>@userSuppliedBio</div>OWASP's 2025 Top 10 keeps cross-site scripting inside the Injection category (A05:2025) rather than as its own entry, which reflects the underlying mechanism correctly: XSS is attacker-controlled data being interpreted as code by a downstream context, exactly like SQL injection is attacker-controlled data interpreted as a query. The practical defense beyond "don't opt out of encoding" is a Content-Security- Policy header restricting which script sources the browser will execute, which limits the damage even if an encoding gap slips through review. When an application genuinely needs to accept limited user-authored HTML — a rich-text comment field, for example — the correct approach is an allowlisting HTML sanitizer library run server-side before storage, never a handwritten blocklist of "dangerous" tags, which attackers reliably find ways around.
What interviewers look for: naming the specific opt-outs (Html.Raw, MarkupString, innerHTML) rather than a generic "Razor encodes by default," and CSP framed as defense in depth, not the primary control.
Q3 Explain CSRF, and why does a Backend-for-Frontend API still need protection even though it "looks like" a token-based API?#
Short answer: CSRF exploits the browser's automatic inclusion of cookies on cross-site requests — if a user is authenticated via a session cookie and visits an attacker's page that submits a form or a credentialed fetch to your site, the browser attaches the valid cookie regardless of which page triggered the request, so a pure bearer-token API is naturally immune, since an attacker's page has no way to read or attach a token it was never given, but a BFF's browser-facing endpoints authenticate the same way a classic cookie-based app does and need the same explicit protection.
builder.Services.AddAntiforgery(options =>
{
options.HeaderName = "X-CSRF-TOKEN";
options.Cookie.SameSite = SameSiteMode.Strict;
});This is a detail candidates who've only worked on cookie-free APIs frequently miss: the whole point of the Backend-for-Frontend pattern is that the browser talks to the BFF using an HttpOnly session cookie instead of a bearer token, which removes token-theft risk but reintroduces CSRF risk, because the browser will happily attach that cookie to a request an attacker's page triggered. The fix is the same one classic server-rendered apps use: an anti-forgery token issued to the legitimate page and required on state-changing requests, verified server-side against the session, plus SameSite=Strict or Lax on the cookie itself as a second layer that blocks most cross-site submission before the anti-forgery check even runs.
What interviewers look for: the "ambient cookie vs. explicit token" framing, and specifically catching that a BFF's session cookie reintroduces the CSRF risk that a plain bearer-token API doesn't have.
Q4 What is SSRF, and why did OWASP fold it into Broken Access Control instead of keeping it as its own category?#
Short answer: Server-side request forgery happens when a server-side component fetches a URL supplied or influenced by a user, and an attacker redirects that fetch to an internal address they couldn't reach directly — a cloud metadata endpoint, an internal admin API, a database on a private subnet; OWASP's 2025 Top 10 rolled SSRF into Broken Access Control (A01:2025) because it's fundamentally an access-control failure — the server acts as a confused deputy, reaching resources on the attacker's behalf that the attacker has no direct access to.
var handler = new SocketsHttpHandler
{
ConnectCallback = async (context, ct) =>
{
var entry = await Dns.GetHostEntryAsync(context.DnsEndPoint.Host, ct);
if (entry.AddressList.Any(IsPrivateOrLinkLocal))
{
throw new InvalidOperationException("Blocked outbound request to a private address.");
}
var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
await socket.ConnectAsync(context.DnsEndPoint, ct);
return new NetworkStream(socket, ownsSocket: true);
},
};The defenses follow directly from the confused-deputy framing: allowlist acceptable destination schemes and hosts rather than trying to blocklist "bad" ones, and validate the resolved IP address — not just the hostname — immediately before connecting, since a DNS-rebinding attacker can pass validation with a public IP and then resolve to a private one for the actual connection. A SocketsHttpHandler.ConnectCallback like the one above lets you check the IP at the point of connection rather than trusting an earlier DNS lookup. Also disable automatic redirect following, or re-validate the destination on every hop, and treat the cloud metadata address (169.254.169.254) as always blocked for any outbound call built from user input.
What interviewers look for: the confused-deputy framing, DNS-rebinding awareness (check-then-connect races), and knowing SSRF now lives under Broken Access Control in the current OWASP edition rather than as its own category.
Q5 What makes deserialization insecure, and which .NET APIs have historically caused problems?#
Short answer: Deserialization becomes insecure when untrusted bytes are turned back into live objects using a mechanism that can be tricked into instantiating arbitrary types or running code as a side effect of that process; the two well-known .NET offenders are BinaryFormatter, which is now obsolete and throws by default on current .NET, and Newtonsoft.Json's TypeNameHandling set to anything other than None combined with attacker-controlled JSON, while System.Text.Json is safe by default because it never resolves a concrete type from the payload itself.
// Dangerous: the payload itself can choose which .NET type gets instantiated.
var settings = new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.Auto };
var obj = JsonConvert.DeserializeObject(untrustedJson, settings);
// Safe: you specify the target type; nothing about it comes from the payload.
var order = JsonSerializer.Deserialize<Order>(untrustedJson);OWASP still groups this under Software or Data Integrity Failures (A08:2025), specifically citing CWE-502, Deserialization of Untrusted Data. The mechanism worth being able to explain, not just recite: a serialized payload with type metadata ($type in Newtonsoft's TypeNameHandling output, or the type name in a BinaryFormatter stream) tells the deserializer which class to construct, and if that class has a constructor, property setter or IDeserializationCallback that does something dangerous — writes a file, starts a process, loads an assembly — an attacker who controls the payload effectively controls what code runs, without ever needing to find a traditional code-injection bug.
What interviewers look for: naming BinaryFormatter and TypeNameHandling as the two specific .NET foot-guns, and explaining why System.Text.Json's default behavior is safe rather than just asserting that it is.
Q6 How do you defend against broken access control in a multi-tenant API beyond adding [Authorize]?#
Short answer: [Authorize] only proves the caller is authenticated, and optionally holds a role; it says nothing about whether this caller should reach this specific resource, so broken access control in practice is almost always a missing object-level check — an endpoint that trusts an ID from the route or body without confirming it belongs to the caller's own tenant, a pattern commonly called an insecure direct object reference.
var order = await db.Orders.FindAsync([orderId], ct);
if (order is null || order.TenantId != currentUser.TenantId)
{
return Results.NotFound();
}Two details separate a strong answer here from a shallow one. First, the tenant or owner identifier used in the check must come from the authenticated principal's claims, never from a client-supplied parameter — if the tenant ID is read from the request body, an attacker simply supplies a different one. Second, returning 404 Not Found instead of 403 Forbidden when the resource exists but belongs to someone else avoids confirming to an unauthorized caller that the resource exists at all, which matters for anything where existence itself is sensitive (a competitor's order ID, another user's account). For anything beyond simple ownership checks — role plus resource state plus organizational hierarchy — IAuthorizationService with resource-based authorization handlers keeps that logic centralized and testable instead of duplicated across every controller action.
What interviewers look for: recognizing IDOR as the concrete shape broken access control takes in real APIs, deriving the tenant/owner from claims rather than input, and the 404-vs-403 nuance.
Q7 What sensitive data commonly leaks into application logs, and how do you prevent it?#
Short answer: The usual leaks are tokens, API keys and full authorization headers captured by "log everything" request/response logging, complete exception objects logged in production whose messages or Data dictionaries include connection strings or personal data, and structured logging's own convenience — destructuring an entire request or entity with {@Request} — capturing fields nobody intended to persist; OWASP's current Top 10 calls this out explicitly as Security Logging and Alerting Failures (A09:2025), naming "inserting sensitive data into log files" as its own weakness.
logger.LogInformation("Processed payment for order {OrderId}, card ending {Last4}",
order.Id, cardNumber[^4..]);The fix is discipline about what gets logged, not a smarter log sink after the fact: log specific, named fields (as above) instead of destructuring whole domain objects or request payloads that will inevitably grow a sensitive field over time without anyone noticing the logging statement needs updating. Mask or truncate anything identifying by nature — card numbers, tokens, government IDs — rather than logging it and hoping the sink is access-controlled well enough. The Compliance libraries in Microsoft.Extensions exist for exactly this problem at scale: data classification annotations and telemetry redaction let a team enforce "this field is never written to a log sink in the clear" as a policy rather than a code-review hope.
What interviewers look for: concrete examples of what leaks (tokens, full exceptions, destructured objects) rather than a vague "don't log secrets," and awareness that this is now its own named OWASP category, not an afterthought under general logging.
Q8 Which HTTP security headers should a production ASP.NET Core app set, and what does each one actually protect against?#
Short answer: At minimum, HSTS to force HTTPS on every future visit, X-Content-Type-Options: nosniff to stop the browser from guessing a response's content type in a way that turns data into executable script, a Content-Security-Policy to restrict which script and resource origins the page will execute or load, Referrer-Policy to avoid leaking full URLs (including query strings) to third-party origins, and Permissions-Policy to disable browser features the app doesn't use; OWASP's 2025 edition groups missing or misconfigured headers under Security Misconfiguration (A02:2025).
app.Use(async (context, next) =>
{
var headers = context.Response.Headers;
headers.Append("X-Content-Type-Options", "nosniff");
headers.Append("Referrer-Policy", "strict-origin-when-cross-origin");
headers.Append("Permissions-Policy", "geolocation=(), microphone=(), camera=()");
headers.Append("Content-Security-Policy", "default-src 'self'; frame-ancestors 'none'");
await next();
});
app.UseHsts();Each header earns its place by closing a distinct attack: HSTS stops a downgrade to plain HTTP even if a user types the bare domain or clicks an old http:// link; nosniff stops a browser from executing a file as script because it "looks like" JavaScript despite an honest Content-Type; CSP is the strongest layer against XSS that does slip through encoding, since even injected markup can't load or run disallowed scripts; frame-ancestors 'none' inside CSP (the modern replacement for X-Frame-Options) stops the page from being embedded in an attacker's <iframe> for clickjacking. Setting these once in shared middleware, rather than per-endpoint, is what keeps them from silently regressing as new endpoints are added.
What interviewers look for: matching each header to the specific threat it mitigates, not just a memorized list, and setting them centrally rather than ad hoc.
Q9 Walk through a STRIDE threat model for a public webhook endpoint that receives payment-provider callbacks.#
Short answer: Spoofing is addressed by verifying the provider's HMAC signature over the raw request body so only the real provider's requests are trusted; tampering by computing that signature over the exact bytes received, not a re-serialized copy; repudiation by logging every callback with its verification result and a correlation ID; information disclosure by never returning internal error detail in the response; denial of service by validating the signature and capping payload size before any expensive processing runs; and elevation of privilege by giving the handler only the narrow permissions it needs, nothing resembling admin access.
STRIDE is most useful in an interview when you apply it to something concrete rather than reciting the acronym, so working through a webhook endpoint end to end is a good test of whether a candidate actually threat-models or just knows the mnemonic. The tampering case has a subtle, common implementation bug worth naming specifically: computing the HMAC over a model you deserialized and re-serialized, rather than over the literal bytes the provider sent, can silently accept a tampered payload whose re-serialized form happens to match, because whitespace, key ordering or numeric formatting differences never enter the comparison. The denial-of-service angle matters because a public, pre-authentication endpoint is an easy flood target — verifying the signature (a cheap operation) before touching the database (an expensive one) keeps a flood of invalid requests from becoming a flood of expensive ones.
What interviewers look for: mapping all six STRIDE categories to concrete, endpoint-specific controls rather than generic security advice, and the raw-bytes-versus-reserialized signature detail specifically.
Q10 How do you build security into the SDLC so issues are caught before code review, not after a penetration test?#
Short answer: Threat model at design time, before code is written, not as a retrospective exercise; run static analysis and dependency/vulnerability scanning in CI on every pull request rather than on a periodic schedule; treat secret scanning as a merge-blocking check, not an advisory one; and recognize that OWASP's 2025 rename of "Vulnerable and Outdated Components" to the broader Software Supply Chain Failures (A03:2025) reflects that the risk isn't just stale package versions but the whole build and publish pipeline — compromised CI runners, typosquatted packages, unsigned artifacts.
dotnet list package --vulnerable --include-transitiveShifting left means the design review, not the pull request, is where a threat model first gets drawn — by the time code exists, the cheapest opportunities to change an architecture (add an approval step, separate a privileged operation into its own service) are already gone. On the tooling side, modern .NET SDKs run a NuGet vulnerability audit during restore by default, and dotnet list package --vulnerable gives the same information on demand, including transitive dependencies, which is where the highest-risk, least-visible packages usually hide. Static analysis (CodeQL or an equivalent) catches the injection and deserialization patterns from earlier questions automatically, and secret scanning catches the credential that ends up in a commit despite every code-review best intention. None of this replaces a penetration test, but a pentest that only ever finds issues your pipeline should have caught earlier is a sign the pipeline, not the codebase, needs the next investment.
What interviewers look for: the shift-left framing applied concretely (design-time threat modeling, CI-blocking checks), naming real .NET tooling instead of "use a scanner," and connecting supply-chain risk to the current OWASP category rather than the outdated "watch for old NuGet versions" framing.
Quick-Fire Round#
| Question | Answer |
|---|---|
| Which EF Core method parameterizes an interpolated SQL string automatically? | FromSql, not FromSqlRaw. |
| Which 2025 OWASP category absorbed SSRF? | A01:2025 Broken Access Control. |
| Which .NET JSON setting historically enabled deserialization attacks? | TypeNameHandling (Newtonsoft.Json) set beyond None. |
| What should an ownership check compare against, claims or request input? | The authenticated principal's claims. |
| What status code avoids confirming a resource exists to an unauthorized caller? | 404, not 403. |
| Which header stops a browser from executing a mistyped-content-type file as script? | X-Content-Type-Options: nosniff. |
| What replaced "Vulnerable and Outdated Components" in OWASP's 2025 Top 10? | Software Supply Chain Failures (A03:2025). |
| Which STRIDE category does an HMAC signature on a webhook address? | Spoofing (and tampering, if it covers the payload). |
How to Prepare#
- Review OWASP's current Top 10 release directly rather than trusting a five-year-old mnemonic; know which categories moved, merged or were renamed and why.
- Take one real endpoint from a project you know well and run a full STRIDE pass on it out loud.
- Deliberately write a vulnerable
FromSqlRawcall and a safeFromSqlequivalent side by side so the distinction is automatic, not memorized. - Practice explaining IDOR with a concrete multi-tenant example, including why the tenant ID must come from claims, not from the request.
- Be ready to name specific .NET foot-guns (
BinaryFormatter,Html.Raw,TypeNameHandling) rather than only abstract vulnerability classes.