MS Stack Ch 20 — Security baseline
OWASP Top 10, secrets management, transport security, auth hardening, supply-chain integrity, input validation, output encoding, security headers. The non-negotiable security checklist for every web app.
Published Updated
Lesson preparation & details
Level: intermediate
By the end, you should be able to
- For GET /orders/42, Alice authenticates but Bob owns the row. Design tests beyond “Authorize is present.”
- Explain the version and execution boundaries before applying the examples
Bring with you
Sources checked: . A source check does not change the publication or update date.
Editorial review: · What review means
In this article · 26 sections
Review and execution boundary
OWASP Top 10 2021 categories are historical orientation; ASVS requirements drive scoped verification. ASP.NET Core 8 snippets, not a security certification.
The complete current body was reread on 9 October 2026; the original a72 full audit is explicitly reused, not a fresh whole-original read. Dated official snapshots support the corrected contracts below. The review is bounded editorial correction, not certification of every dependency, security property or cloud deployment. Historical setup commands and optional exercises were not executed. No cloud resources, third-party packages or external side effects were created. Old screenshots and unavailable private assets remain in the private recovery archive, not prerequisites for this lesson.
Chapter 20 of From Novice to Fluent on the Modern Microsoft Web Stack — a 22-chapter self-study plan.
Why this chapter
Security is not a feature you bolt on at the end. It is the cumulative effect of fifty small decisions across every chapter you have already read: how the runtime handles input, how the framework wires auth, how the deploy pipeline injects secrets, how the rollout gates on health. This chapter consolidates those decisions into a scoped review checklist for planning and testing controls; an afternoon can inventory gaps, not certify an arbitrary service.
The shipping-grade version of this skill is "I can explain OWASP Top 10 without notes and show where each item is mitigated in an ASP.NET Core + React + Azure app". The expert-tier version is "I run threat-modelling exercises on new features, can articulate the defence-in-depth layers, and know which third-party libraries are repeatedly the source of CVEs".
Representative failure modes include SQL injection in an admin tool, secrets in source control, plaintext first requests, missing JWT audience checks and unsafe deserialization. This is a threat catalogue, not a measured claim about the majority of breaches. The defence is to know the catalogue, walk the application against it on every release, and never assume that "the framework handles it" without verifying.
You finish this chapter when you can audit any one of your services against OWASP Top 10 in a one-page document, know where each mitigation lives in your code, and explain the trust boundary that each defence enforces.
OWASP Top 10 — Injection, broken auth, sensitive data exposure, broken access control, SSRF, deserialisation, logging gaps.
Secrets discipline — Never in source, never in logs, Key Vault references, rotation by automation.
Transport security — HTTPS-only, HSTS, modern TLS, certificate management.
Auth hardening — Tenant allow-list, audience validation, signature validation, clock skew.
Defence in depth — Layered controls, blast-radius limits, assume-breach posture.
Audit + response — Logging what matters, alerting on the right signals, post-incident review.
Concepts and depth
OWASP Top 10 at backend-dev depth
OWASP publishes the Top 10 web application risks every few years. This section uses the 2021 category labels for orientation, not as a complete current verification standard. Select applicable versioned ASVS requirements and document evidence and unknowns.
A03:2021 Injection. SQL injection is the canonical case but the family includes NoSQL injection (Mongo, Cosmos), command injection (shell exec with concatenated args), LDAP injection, and template injection. The mitigation is the same shape: never concatenate untrusted input into the structural part of a query or command. In .NET, use parameterised queries (SqlCommand with Parameters.AddWithValue) or, better, an ORM (Entity Framework Core) that parameterises by default. Cosmos DB's SDK parameterises if you use WithParameter on QueryDefinition. Avoid invoking a shell with untrusted input. When a reviewed executable is necessary, ProcessStartInfo.ArgumentList separates arguments, but does not stop option injection or a target program interpreting an argument as code; allowlist commands/options and use the program's end-of-options convention where supported. The gotcha: people sanitise instead of parameterising, then miss an edge case (UTF-16 encoded quote, MySQL-specific escape) and ship the vulnerability anyway. Parameterization binds values; allowlist structural identifiers and avoid shell invocation where possible. Validate business constraints separately.
A07 Identification and Authentication failures. Most modern web apps delegate auth to an identity provider (Entra ID, Auth0, Okta). The remaining responsibility is validating tokens correctly and managing session lifecycle. Failures: accepting an unsigned JWT (alg: none attack), not validating the audience, not validating the issuer, not validating the signature against the correct key set, accepting tokens outside their validity window; a jti claim alone does not prevent replay of reusable bearer tokens. ASP.NET Core's AddJwtBearer handles most of this if you set the options correctly; the gotcha is treating defaults as sufficient and skipping the ValidateIssuer, ValidateAudience, ValidateIssuerSigningKey, ValidateLifetime flags. Keep these validations enabled in this baseline, but the four Boolean flags are not a complete validator or authorization policy. Trusted issuer metadata/keys, signed-token and expiration requirements, intended token version/type, algorithms and endpoint permissions also matter.
A02 Cryptographic failures (formerly sensitive data exposure). Sensitive data in transit must be on TLS 1.2+; in storage must be encrypted at rest with a key you can rotate. The failures are: logging sensitive fields (passwords, full credit card numbers, PII), storing passwords with weak hashing (MD5, SHA-1, plain SHA-256), using ECB block-cipher mode, hardcoding crypto keys. Use managed storage encryption for stored datasets and established password-hashing libraries for passwords. ASP.NET Core Data Protection protects application payloads such as cookies; it is not a universal database-encryption API.
A01 Broken Access Control. A leading risk in the cited OWASP 2021 classification, not a universal measured breach ranking. The pattern: a user authenticates as Alice, then sends GET /api/orders/42 where order 42 belongs to Bob. The server returns Bob's order because the only check was "is the request authenticated" rather than "does the authenticated user own this resource". Mitigation: every resource-fetching endpoint must check ownership server-side. [Authorize] is necessary but not sufficient; you need authorisation handlers that take the resource and the user and answer "can this user access this resource?". Resource-based authorisation in ASP.NET Core (IAuthorizationService.AuthorizeAsync(user, resource, policy)) is the framework's answer. The gotcha is hiding the wrong-resource UI in the front-end and assuming the back-end is therefore safe — anyone can call the API directly.
A10 Server-Side Request Forgery (SSRF). Your service accepts a URL from user input and fetches it server-side (image upload by URL, webhook callbacks, link previews). An attacker targets link-local metadata or an internal service. Actual impact depends on reachable endpoints, required metadata headers, identities and response exposure; a bare URL is not proof that credentials are returned. Prefer an allowlist of required destinations and schemes plus restricted egress. Disable redirects or validate each hop; reject loopback, private, link-local, reserved and otherwise disallowed IPv4/IPv6 destinations, including alternative encodings. A one-time DNS check can race rebinding: enforce the decision at connection time and constrain the actual peer. DNS resolution alone is not SSRF protection; no live SSRF test is performed here.
A08 Software and Data Integrity / Insecure Deserialisation. Accepting a serialised object from untrusted input and deserialising it into arbitrary types is a remote code execution surface. .NET's BinaryFormatter is the historical culprit (and is now obsolete; refuse to use it). Newtonsoft.Json TypeNameHandling.Auto on untrusted input is unsafe without a strictly reviewed allowlisting serialization binder; arbitrary type selection creates a dangerous deserialization surface, not proof that every such configuration achieves remote code execution. System.Text.Json defaults to safer behaviour; never permit untrusted input to select arbitrary runtime types; constrained allowlisted polymorphism requires its own review.
A09 Security Logging and Monitoring failures. You cannot respond to what you cannot see. Auth failures, authorisation denials, input validation rejections, suspicious patterns — all must be logged with enough context to investigate. The gotcha: logging too much (PII, secret values) is also a failure. Strike the balance: log what an investigator needs (user ID, IP, action, outcome) without what an attacker would harvest (passwords, tokens, full payloads).
The Top 10 not deep-dived above (A04 Insecure Design, A05 Security Misconfiguration, A06 Vulnerable Components) are at least as important. A04 is "the architecture itself has a security flaw" — needs threat modelling. A05 is "the framework default is unsafe and you did not change it" — needs hardening checklists. A06 is "you depend on a library with a known CVE" — needs SBOM + dependency scanning from chapter 18.
Good enough to ship
• Parameterised queries everywhere; no string concat in SQL
• [Authorize] + resource-based handlers on every protected endpoint
• JWT validation with all four Validate* flags on
• No BinaryFormatter; no attacker-selected arbitrary CLR types; explicitly allowlisted polymorphism is a separate reviewed feature
Expert tier
• Threat modelling on every new feature • Annual penetration test against staging • Custom CodeQL queries for domain-specific anti-patterns • Centralised authorisation policy with auditable rules
Secret management
The cardinal rules: never in source, never in logs, never in error messages. Source control is forever; once a secret hits Git, treat it as compromised even if you immediately delete the commit (history is preserved, mirrors exist, indexers have already scraped it). Rotate it.
The mechanism is Azure Key Vault plus managed identity. Your app authenticates to KV using its managed identity (no client secret stored anywhere); KV returns the secret; the app uses it. For values referenced in App Service or Functions configuration, use Key Vault references in app settings: @Microsoft.KeyVault(SecretUri=https://kv.vault.azure.net/secrets/MySecret/). For a versionless reference, App Service caches the value and refreshes to a newer version within 24 hours under the documented service behavior; a configuration change triggers refetch, and a management refresh endpoint is available. A version-pinned URI stays on that version until updated. Permissions, networking and propagation can fail; verify resolution without printing the secret, then revoke the old value only after consumer uptake is confirmed.
Rotation is the often-skipped part. A secret that has not been rotated in two years is effectively a permanent credential. Build rotation pipelines for every secret with a non-trivial blast radius. Database connection strings, third-party API keys, signing keys — each should have an automated pipeline that creates a new value at the source, writes it to KV, and waits for app refresh. An automation failure still needs a rehearsed manual recovery path; do not revoke the old key until the new key works for all intended consumers.
Logging: pipelines, application logs, and exception messages are all places secrets can leak. Azure DevOps attempts to mask registered secret values but does not mask substrings; structured or transformed values and external sinks can escape masking. Never rely on it as a prevention boundary. [LoggerMessage] generates logging code; it is not an automatic redaction annotation. Allowlist fields and verify every configured provider, exception path and telemetry export with dummy markers. Audit your log output for the patterns of secret leakage; CodeQL has queries for this.
Good enough to ship
• All secrets in Key Vault; app uses managed identity • Key Vault references in app settings • Use a currently supported secret scanner and explicit failure policy; legacy CredScan is deprecated, and CI detection alone does not prevent commits • Quarterly access review of who can read each KV
Expert tier
• Automated rotation for every secret with a non-trivial blast radius • Soft-delete + purge protection on every KV • Logging pipelines audited for secret leakage • Just-in-time access for human readers; no standing access
Transport security
Every request and response is plaintext until you encrypt it. The defaults you must enforce: HTTPS only, HSTS, TLS 1.2 minimum (1.3 preferred), modern cipher suites.
App Service has httpsOnly: true (Bicep properties.httpsOnly) — set it; it forces an HTTP-to-HTTPS redirect at the platform level. In ASP.NET Core, app.UseHttpsRedirection() does the same in code; neither redirect makes an already-sent HTTP body secret. APIs may reject HTTP rather than redirect it, and reverse proxies require correctly configured trusted forwarding and scheme handling to avoid redirect loops.
HSTS (HTTP Strict Transport Security) is a response header that tells the browser "for the next N seconds, never request this domain over HTTP, even if the user typed http://". app.UseHsts() adds it on eligible HTTPS responses. Choose max-age and subdomain scope after domain review; do not enable preload by default. The preload service documentation accessed on 9 October 2026 recommends HSTS but discourages preloading in general; treat it as an exceptional, explicitly justified choice. Preload requires a qualifying header, domain-owner approval, submission and eventual inclusion in browser lists. Only browsers carrying the entry gain protection before their first visit; a preload header or submission alone is not inclusion.
TLS version is set at the platform layer. App Service's minTlsVersion: '1.2' (Bicep) rejects any handshake from clients that cannot negotiate 1.2+. Application Gateway and Front Door have similar controls. Audit these settings on every environment; the default may not be what you want.
Certificates: App Service has free managed certificates for custom domains; for anything else, use Key Vault-backed certificates with automated renewal. Monitor expiry and test renewal failures; no frequency ranking of outage causes is established here.
Good enough to ship
• httpsOnly + minTlsVersion 1.2 in Bicep • UseHttpsRedirection + UseHsts in Program.cs • Managed certs on custom domains, automated renewal
Expert tier
• TLS 1.3 enforced where supported • Domain-owner-approved HSTS preload only after hostname inventory and long-lived HTTPS readiness • Authorized TLS configuration checks; a scanner grade is not application-security assurance • Certificate Transparency monitoring for unauthorised certs
Auth hardening: token validation in depth
Every byte of a JWT must be validated. The four checks AddJwtBearer performs when configured correctly:
- Signature validation — verify the asymmetric signature using a trusted issuer public key obtained through authenticated metadata. If the alg is
none, reject; resolve signing keys only from trusted metadata (including controlled key refresh); if no acceptable key verifies the signature, reject; if the signature is invalid, reject. - Issuer validation — the
issclaim must match the expected issuer URL. Defends against tokens minted by a different IdP. - Audience validation — the
audclaim must include your API's identifier. Defends against tokens minted for a different application sharing the same IdP. - Lifetime validation —
nbf(not before) andexp(expiry) must bracket the current time, with a small clock-skew allowance (default 5 minutes).
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(opts =>
{
opts.Authority = "https://login.microsoftonline.com/<tenantId>/v2.0";
opts.MapInboundClaims = false; // retain tid/oid names used by the handler
opts.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = "https://login.microsoftonline.com/<tenantId>/v2.0",
ValidateAudience = true,
ValidAudience = "<api-application-client-id-guid>",
ValidateIssuerSigningKey = true,
ValidateLifetime = true,
ClockSkew = TimeSpan.FromMinutes(2)
};
});This example targets Entra v2 access tokens, whose audience is the API client-ID GUID; v1 tokens can use a GUID or resource URI. Replace the placeholders and verify the actual API token-version contract rather than accepting an ID token or copying the URI from another flow.
Tenant allow-list is a fifth check often needed in multi-tenant scenarios: the tid claim must be in your allowed-tenants list. This defends against the case where you accept Entra tokens but only customers from specific tenants should have access.
Clock skew trips people up. The default 5 minutes is generous and assumes nodes do not all have NTP synced perfectly. Choose an explicit allowance from measured issuer/server clock error and expiry requirements; two minutes here is an example, not a universally safe value. Monitor time synchronization before reducing it. Setting it too low produces flaky "token expired" failures during normal use.
The most important rule: never disable any of the four validations to fix a test. If your test breaks because of audience validation, the test was sending the wrong token. Disabling validation in production is a CVE waiting to be written.
Good enough to ship
• Four Validate* flags on; valid issuer + audience pinned
• ClockSkew explicit (not relying on default)
• [Authorize] on every endpoint; opt-out endpoints explicit
Expert tier
• Tenant allow-list enforced
• Custom token validators for domain claims (role, scope)
• A threat-specific replay strategy; jti presence alone does not stop replay and ordinary access tokens are reusable
• Periodic key-rotation drill
Defence in depth
The principle: no single control is sufficient. Stack them so that a bypass of one is caught by the next. Concrete layers for a typical web app:
- Network — Front Door / WAF in front; only HTTPS; rate limits; IP allow-list for admin endpoints.
- Platform — App Service
httpsOnly,minTlsVersion, managed identity, no public outbound for sensitive backends. - Framework — ASP.NET Core auth + authorisation, anti-forgery tokens, output encoding.
- Application — input validation, resource-based authz, parameterised queries.
- Data — encryption at rest, row-level security where applicable, key rotation.
- Audit — structured logs, alerting on anomalies, retention long enough for forensic review.
A SQL injection that slips through input validation should still fail because the DB query is parameterised. A leaked credential should still be limited because the principal has least-privilege RBAC. A successful exploit should still be contained because the service has no path to the data plane it does not need.
The mental model is "assume breach". Test the independence of the controls, not just their count: if an attacker reaches the API despite the WAF, the API must still enforce resource ownership; if a workload credential is stolen, its data permissions must still be limited. The three cards below compress the six-layer list into network, identity, and data boundaries rather than replacing it. Framework and application checks enforce those boundaries, while audit spans all three so a bypass leaves evidence.
name: Network layer; tagline: outer; bullets: WAF + rate limit — HTTPS-only ingress — Private endpoints for backends
name: Identity layer; tagline: middle; bullets: Managed identity avoids one stored workload credential — Least-privilege RBAC — Token validation plus authorization
name: Data layer; tagline: inner; bullets: Encryption at rest — Parameterised queries — Row-level filters
Good enough to ship
• Three+ independent layers between attacker and data • Least-privilege RBAC on every managed identity • Anomaly alerts on auth failures, authz denials
Expert tier
• Red-team exercises against staging • Blast-radius analysis per credential • Privileged access management (PAM) for break-glass paths
title: Validation flags are not the whole security contract
"The framework handles it" is rarely true by default. ValidateIssuerSigningKey=false does not itself permit unsigned tokens; RequireSignedTokens is separate and defaults to true. Disabling default audience or lifetime validation removes those checks unless another reviewed validator enforces them; do not infer that any particular token will be accepted without examining the full validator pipeline. The defaults are mostly safe but never assume — read the options class and pin every relevant flag.
Worked examples
Hardening Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
// Auth (chapter 14) with required validations
builder.Services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(opts =>
{
opts.Authority = builder.Configuration["Auth:Authority"];
opts.MapInboundClaims = false; // handler reads raw tid/oid names
opts.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuers = builder.Configuration.GetSection("Auth:ValidIssuers").Get<string[]>(),
ValidateAudience = true,
ValidAudiences = builder.Configuration.GetSection("Auth:ValidAudiences").Get<string[]>(),
ValidateIssuerSigningKey = true,
ValidateLifetime = true,
ClockSkew = TimeSpan.FromMinutes(2),
RequireExpirationTime = true,
RequireSignedTokens = true
};
opts.RequireHttpsMetadata = true;
});
builder.Services.AddAuthorization(opts =>
{
opts.FallbackPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
});
// HSTS + HTTPS redirect
builder.Services.AddHsts(opts =>
{
opts.Preload = false; // opt in only after domain/subdomain review
opts.IncludeSubDomains = false; // assess every subdomain before enabling
opts.MaxAge = TimeSpan.FromDays(365);
});
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();What to notice:
- The fallback policy applies when no explicit authorization policy metadata is present. Explicit
[Authorize]uses its named/default policy instead, and[AllowAnonymous]bypasses authorization. This does not protect static files or handlers served outside the authorization middleware; inspect endpoint ordering and every explicit policy. RequireHttpsMetadata = truemeans OIDC discovery must use HTTPS — defends against MITM on the metadata fetch.- Preload is deliberately false in this baseline; enabling it requires domain-owner approval and verification of every covered hostname.
- ClockSkew explicit at 2 minutes, not the 5-minute default.
Resource-based authorisation
public sealed class OrderOwnerRequirement : IAuthorizationRequirement { }
public sealed class OrderOwnerHandler : AuthorizationHandler<OrderOwnerRequirement, Order>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context, OrderOwnerRequirement requirement, Order resource)
{
// Assumes validated Entra claims and an Order with TenantId and OwnerId.
var tenant = context.User.FindFirst("tid")?.Value;
var owner = context.User.FindFirst("oid")?.Value;
if (context.User.Identity?.IsAuthenticated == true && tenant is not null &&
owner is not null && resource.TenantId == tenant && resource.OwnerId == owner)
context.Succeed(requirement);
return Task.CompletedTask;
}
}
// Program.cs, before builder.Build(); merge with the fallback-policy registration.
builder.Services.AddSingleton<IAuthorizationHandler, OrderOwnerHandler>();
builder.Services.AddAuthorization(opts =>
{
opts.AddPolicy("OrderOwner", policy => policy
.RequireAuthenticatedUser()
.AddRequirements(new OrderOwnerRequirement()));
});
// Controller fragment. _orders.FindInTenantAsync is an application repository
// contract: constrain both tenant and ID in the database query, not after loading.
[Authorize]
[HttpGet("orders/{id:guid}")]
public async Task<IActionResult> GetOrder(Guid id,
[FromServices] IAuthorizationService authz)
{
var tenant = User.FindFirst("tid")?.Value;
if (tenant is null) return Forbid();
var order = await _orders.FindInTenantAsync(tenant, id);
if (order is null) return NotFound();
var result = await authz.AuthorizeAsync(User, order, "OrderOwner");
if (!result.Succeeded) return NotFound(); // chosen existence-hiding policy
return Ok(order); // production response should use an allowlisted DTO
}The shown JWT options disable inbound claim-name mapping so the handler reads raw tid/oid; if mapping is enabled, use the corresponding mapped claim types consistently. Register the handler and a policy containing the requirement, authenticate before loading the resource, then call AuthorizeAsync(User, order, "OrderOwner") for every read/write. Scope the database lookup to the tenant as well. Choose 403 or an existence-hiding 404 consistently. The record type, repository implementation, controller imports and response DTO belong to the surrounding app. Registration and controller invocation are shown to preserve the original wiring lesson; these fragments are not a compiled standalone application. Test same-tenant wrong owner and same object-ID in another tenant.
Parameterised query against Cosmos DB
public async Task<List<Item>> SearchAsync(string nameFragment)
{
var query = new QueryDefinition(
"SELECT * FROM c WHERE CONTAINS(c.name, @fragment, true)")
.WithParameter("@fragment", nameFragment);
var iterator = _container.GetItemQueryIterator<Item>(query);
var items = new List<Item>();
while (iterator.HasMoreResults)
{
foreach (var item in await iterator.ReadNextAsync())
{
items.Add(item);
}
}
return items;
}What to notice:
@fragmentbinds a value separately from fixed query structure; do not describe this as a universal string-escaping algorithm.- This example does not concatenate the value into query text. It still lacks tenant/owner filtering, cancellation, maximum result count and a request-unit budget: add these in a real endpoint. A safely parameterized unbounded query can still leak data or exhaust resources.
- The same pattern (parameterised) applies to SQL, MongoDB, Cypher — any query language with an SDK that supports it.
Catching a leaky log entry
// BAD: serialises whole exception including inner secrets
catch (Exception ex)
{
_logger.LogError(ex, "Database call failed: {Details}", ex.ToString());
throw;
}
// GOOD: structured fields, no whole-object dump
catch (SqlException ex)
{
_logger.LogError("Database call failed. ErrorNumber={ErrorNumber}", ex.Number);
throw;
}What to notice:
ex.ToString()normally includes exception messages, inner exceptions and stack information; arbitraryException.Datais not guaranteed to be rendered by it. Messages, custom ToString implementations or provider-specific serialization can still leak secrets. Test the actual sink rather than asserting every Data field is always emitted.- Allowlisting fields helps, but passing ex still serializes exception data according to the provider. The corrected version omits the exception object and retains only a reviewed numeric code.
- Catch the specific exception type to avoid swallowing unrelated errors.
Hands-on exercises
-
Goal: Audit one of your services against OWASP Top 10. Steps: (1) Print the Top 10 list. (2) Walk each item, find the code or config that mitigates it, note any gap. (3) File issues for the gaps. You're done when you have a one-page checklist with every item either marked "mitigated by X" or "issue #Y filed".
-
Goal: Configure JWT validation with all flags on. Steps: (1) Find the
AddJwtBearercall. (2) VerifyValidateIssuer,ValidateAudience,ValidateIssuerSigningKey,ValidateLifetimeare all true. (3) Pin the issuer and audience. (4) Set ClockSkew explicitly. (5) Run integration tests; they should still pass. You're done when positive intended-token tests pass and wrong-issuer/audience, expired, unsigned and unauthorized-resource cases fail under the real middleware; matching source flags alone is insufficient. -
Goal: Replace a string-concat SQL with parameterised. Steps: (1) Search for
string.Formator$"SELECT ... {var}"patterns in any query. (2) Replace withSqlCommand+Parameters.AddWithValueor with EF Core. (3) Add a CodeQL query (or rule) to fail the build if the pattern returns. You're done when no concat-based SQL exists and the build will catch regressions. -
Goal: Add a resource-based authz handler. Steps: (1) Pick a resource type users own (orders, documents, etc.). (2) Write a requirement + handler. (3) Apply via
IAuthorizationService.AuthorizeAsyncin the controller. (4) Write tests proving Alice cannot fetch Bob's resource. You're done when cross-user fetch returns 403. -
Goal: Wire Key Vault references. Steps: (1) Create a Key Vault, put a secret in it. (2) Give the App Service's managed identity Key Vault Secrets User. (3) In app settings, reference
@Microsoft.KeyVault(SecretUri=...). (4) Restart the app; verify it reads the secret. (5) Rotate the secret in KV; verify the app picks up the new value (may need restart). You're done when the secret value never appears in code or pipeline. -
Goal: Evaluate HSTS preload safely. Steps: (1) Obtain domain-owner approval and inventory every covered hostname, including future/non-web uses; proceed only if long-lived HTTPS readiness is established. (2) Configure HSTS with
Preload = true,MaxAge >= 1 year,IncludeSubDomains = true. (3) In the authorized environment, inspect HTTPS response headers and redirect behavior. (4) Only after approval, submit the domain to https://hstspreload.org/. You're done when the header is set and the domain owners have explicitly approved preload after verifying every covered hostname; header presence alone is insufficient.
Self-check questions
- Name the versioned OWASP Top 10 2021 items in order, with a one-sentence example each; do not label that list the current edition.
- Why does parameterisation beat sanitisation for SQL?
- What are the four
Validate*flags onTokenValidationParameters, and what does each defend against? - Why is
ValidateIssuerSigningKey = falsedangerous? - What is broken access control, and how does resource-based authz prevent it?
- Describe SSRF and the defence against it.
- What is BinaryFormatter and why must you never use it on untrusted input?
- Why are Key Vault references in app settings safer than embedding secrets in app settings?
- Explain HSTS and the preload list. What does preload solve that the header alone does not?
- What is defence in depth? Give three layers you would stack for a public API.
- Why log auth failures, and what fields should each log entry include?
- Walk through the rotation pipeline for a third-party API key in Key Vault.
Worked assessment answers
- OWASP 2021 order. A01 Broken Access Control: Alice reads Bob's order. A02 Cryptographic Failures: plaintext sensitive storage. A03 Injection: concatenated SQL changes query structure. A04 Insecure Design: no abuse-case limits on a money transfer. A05 Security Misconfiguration: exposed debug endpoints. A06 Vulnerable and Outdated Components: an unpatched vulnerable library. A07 Identification and Authentication Failures: accepting the wrong issuer. A08 Software and Data Integrity Failures: trusting an unsigned update or arbitrary deserialized types. A09 Security Logging and Monitoring Failures: no actionable authorization-denial trail. A10 SSRF: user-controlled fetch reaches an internal endpoint. Use a separately selected versioned ASVS scope for testing.
- Parameterization. Bind data values separately from SQL syntax, so a quote-bearing value remains data. Allowlist identifiers/order clauses that cannot be parameters; validate business rules too. ORM raw-query and shell paths still need review.
- Four flags. ValidateIssuer checks issuer, ValidateAudience checks intended recipient, ValidateLifetime checks validity times, and ValidateIssuerSigningKey controls additional signing-key validation. Trusted signature verification, RequireSignedTokens, expiration requirements and endpoint authorization remain separate obligations.
- Signing-key nuance. False does not itself permit unsigned tokens; RequireSignedTokens is separate. Accepting attacker-controlled keys is dangerous even if they verify a signature. Review trusted metadata, algorithms, custom validators and key refresh rather than using one flag as an assurance test.
- Resource authorization. Authenticate, load within tenant scope and evaluate a server-side policy for the action and resource. Alice-to-Bob and cross-tenant same-ID requests must deny even though authentication succeeds; hiding a UI button is not enforcement.
- SSRF. A server fetch crosses a trust boundary for user-chosen input. Allowlist destinations/schemes, restrict actual egress, validate resolved addresses and connection peers including IPv6, and disable or revalidate redirects. A pre-fetch DNS-only check can lose a rebinding race.
- Deserialization. BinaryFormatter can instantiate unsafe object graphs and must not process untrusted input. Use a constrained data representation and reviewed types; safe JSON defaults do not authorize attacker-chosen CLR types or replace input validation.
- Key Vault references. Configuration carries a URI instead of a literal secret, while managed identity and scoped vault permissions control retrieval. The app still receives sensitive data and can leak it; network policy, logs and rotation require tests. Versionless refresh and version-pinned references differ.
- HSTS/preload. HSTS is learned over HTTPS and protects later requests for its lifetime. Preload can cover the first visit only once a browser carries the approved entry. A header is not submission or inclusion; subdomain coverage and slow removal require owner review.
- Defense in depth. Restrict ingress/egress, enforce token plus resource authorization, and constrain database permissions/queries. Test independent bypasses: bypassing a WAF must not bypass tenant authorization. Layer count alone proves nothing.
- Audit logging. Record safe correlation ID, action, outcome and minimally necessary pseudonymous actor context with retention/access policy. Avoid tokens, passwords and full payloads; IPs and IDs can themselves be personal data. Test log capture with dummy secret markers and rate-limit abusive-event floods.
- Rotation. Create the new credential at its issuer, publish a new vault version, refresh and verify each intended consumer, then revoke the old issuer credential and alert on continued use. A vault write alone does not rotate the external service. Keep a bounded overlap and recovery procedure for partial uptake.
Exercise 1 solution: Choose the historical Top 10 orientation plus a versioned ASVS scope. For each applicable control record asset, test, evidence, failure/unknown and owner; filing a gap is not mitigation, and absence of findings is not certification.
Exercise 2 solution: Configure the intended issuer/token version/audience and trusted keys, then run middleware integration tests with valid and deliberately wrong issuer/audience/signature/expiry cases. Test endpoint permissions independently. The local review here does not mint or validate real JWTs.
Exercise 3 solution: Replace interpolated query values with typed parameters and allowlist any dynamic identifiers. An input such as a quote plus OR remains a single value, not SQL syntax. Add regression tests and supported static rules; neither an ORM nor one scanner rule guarantees every query is safe.
Exercise 4 solution: Register the requirement/handler and invoke it on every read/write after tenant-scoped lookup. Owner succeeds; different owner, tenant, missing claims and unauthenticated principal deny. Assert consistent 403 or existence-hiding 404 and no response/log disclosure. The Boolean model below is not ASP.NET middleware acceptance.
Exercise 5 solution: In an independently authorized environment configure managed identity, vault RBAC and network access, then a versionless reference. Verify retrieval with a non-secret sentinel result, rotate at the source, verify refresh and only then revoke the old credential. Include denied permission, inaccessible vault and pinned-old-version cases; do not print values.
Exercise 6 solution: First inventory subdomains and obtain owner approval. Verify HTTPS readiness, required header and redirect semantics; submission and browser inclusion are later distinct steps. Stop if any covered hostname cannot comply. No preload submission or network/security scan was performed here.
High-signal resources
Official docs
- OWASP Top 10 — use the versioned 2021 categories discussed here for orientation, then a current versioned ASVS requirement set for verification.
- ASP.NET Core security overview — auth, authz, data protection, anti-forgery.
- Microsoft identity platform best practices.
- Azure Key Vault documentation.
- Mozilla SSL Configuration Generator — modern TLS settings.
- HSTS Preload List.
Books or courses
Optional original-list recommendations, not verified edition/chapter attributions or current API evidence. Check author/title/edition and contents before purchase or relying on a particular topic.
- The Web Application Hacker's Handbook — Stuttard & Pinto. Old but the structure still teaches.
- Threat Modeling: Designing for Security — Adam Shostack.
Practitioner posts
Optional discovery links; broad indexes do not establish a particular incident, lesson, current free tier or completeness. The original descriptions below are suggestions for exploration, not primary support for the security controls above.
- PortSwigger Web Security Academy — free interactive labs covering Top 10 and beyond.
- Google SRE on security incidents — how mature orgs handle breaches.
- Have I Been Pwned blog — Troy Hunt's writeups of real breach patterns.
- Krebs on Security — the journalist beat on cybercrime; awareness training in long form.
Weekly milestones
Illustrative pacing only; unknown controls remain unknown at the end of a week. No domain change, security scan, credential rotation or cloud operation is authorized by this reading schedule.
- Day 1: Read the versioned 2021 list used here, then compare any newer edition explicitly. Map each item to your stack. Answer self-check 1, 2, 5.
- Day 2: Audit JWT validation in your service; pin all four flags. Answer self-check 3, 4.
- Day 3: Add resource-based authz on one endpoint; write the cross-user test. Answer self-check 6, 7.
- Day 4-5: Move every secret to Key Vault references; select a supported secret scanner with a verified failure policy. Answer self-check 8, 12.
- Day 6-7: Evaluate preload only with domain-owner approval; perform separately authorized TLS checks without treating a score as assurance; document defence-in-depth layers for your service. Answer self-check 9, 10, 11.
How it shows up in the capstone
The proposed capstone must produce evidence against this baseline; no service is established as hardened by construction here. Program.cs pins all four JWT validation flags, enforces HTTPS, sets reviewed HSTS options (preload off unless separately approved), and uses default-deny authorisation via FallbackPolicy. Every endpoint that fetches a resource owned by a user goes through a resource-based authz handler. Every DB query is parameterised, every secret is a Key Vault reference, and every managed identity has least-privilege RBAC scoped to its purpose.
The proposed pipeline (chapter 18) uses supported code/secret/dependency/container checks and an SBOM with explicit findings policy; deprecated CredScan is not a current recommendation. A scan or SBOM alone does not establish supply-chain integrity: protect build identities and artifact provenance, pin dependencies, and verify the promoted digest. The infra (chapter 19) enforces httpsOnly, minTlsVersion: '1.2', no public outbound for sensitive backends, and managed identities throughout. When a future engineer adds a feature, the threat-model template lives in the repo to prompt the conversation before the code lands.
The chapter lands when the scoped verification record includes evidence, failures, unknowns and owners. A checklist with no reported gaps is not security assurance.
Previous chapter → Ch 19 — IaC + rollout orchestration (chapter reference; not linked while its review is pending)
Next chapter → Ch 21 — Testing strategy (chapter reference; not linked while its review is pending)
Corrected contracts and failure analysis
Browser-facing controls: encoding, CSRF and headers
Input validation is not output encoding. React text interpolation escapes text, but raw HTML sinks such as dangerouslySetInnerHTML, unsafe URL schemes and third-party rendering need separate review. Encode for the actual HTML/attribute/JavaScript/URL context; if rich HTML is required, use a maintained sanitizer with a reviewed allowlist. A Content-Security-Policy adds a mitigation layer, not a substitute for safe sinks. Consider X-Content-Type-Options: nosniff, a scoped referrer policy and CSP frame-ancestors; test the actual responses and application dependencies rather than copying a universal header set.
Cookie-authenticated unsafe requests need an antiforgery design because the browser attaches cookies automatically. A bearer token explicitly added to an Authorization header has a different CSRF exposure, but storing authentication in a cookie reintroduces that boundary. CORS controls browser response access; it is not authorization or a general CSRF defense. Test forged cross-origin state changes and legitimate form/API flows with the actual scheme.
The original afternoon-audit promise is withdrawn. A short session can inventory trust boundaries and unresolved risks; it cannot establish that an arbitrary service is secure. Define scope, assets, actors, data flows and abuse cases, then select versioned ASVS requirements and record positive and negative tests. Mark an untested requirement unknown, not mitigated because a middleware call exists.
ValidateIssuerSigningKey governs additional signing-key validation; setting it false does not by itself make unsigned JWTs valid. RequireSignedTokens is a separate setting whose documented default is true. Signature validation, trusted keys, issuer, audience, expiry and authorization remain separate obligations. Merely checking jti does not prevent replay of ordinary reusable bearer tokens.
Passing an exception object to LogError can emit the exception message and stack regardless of the structured fields beside it. Redact at the source, allowlist safe fields and test captured logs with a dummy secret marker. ASP.NET Core Data Protection is designed for protected application payloads such as cookies, not a universal replacement for storage encryption or password hashing. HSTS preload affects every covered hostname and has slow removal; inventory subdomains before opting in.
Boundary exercise with solution
For GET /orders/42, Alice authenticates but Bob owns the row. Design tests beyond “Authorize is present.”
Solution and reasoning
Test unauthenticated access, Alice-to-Alice success, Alice-to-Bob denial, cross-tenant same object ID denial, invalid audience, expired token, unsigned token and revoked/disabled account behavior appropriate to the identity contract. Decide whether denial is 403 or existence-hiding 404. Assert the response and logs contain no order body, token or secret marker.
Source-backed review notes
Fresh official snapshots checked on 9 October 2026 add these specific contracts; mutable docs and moniker views are not installed-package or service-execution evidence. The older dated quotations below remain historical support, not a new retrieval claim.
-
Entra access-token claims — v2 audience is the API client ID; do not substitute a v1 resource URI.
-
JWT inbound claim mapping — default mapping changes claim types; snippets opt out consistently with the handler.
-
FallbackPolicy contract — fallback is not a replacement for reviewing explicit endpoint policies.
-
MSDO supported analyzers and deprecation — CredScan is deprecated; detection and build failure policy are distinct.
-
OWASP SSRF prevention — allowlisting, redirects, DNS and egress boundaries need layered review.
-
OWASP XSS prevention and ASP.NET Core antiforgery — contextual encoding, safe sinks and browser credential behavior are separate mechanisms.
-
Exception.ToString — default messages/inner exceptions/stack text, not a promise to render arbitrary Data entries.
-
HSTS preload service — current discouragement of general preloading, opt-in and inclusion/removal boundaries.
-
OWASP Application Security Verification Standard (ASVS) | OWASP Foundation — accessed 2026-10-07. Exact supporting passage: “The OWASP Application Security Verification Standard (ASVS) Project provides a basis for testing web application technical security controls and also provides developers with a list of requirements for secure development.”
-
TokenValidationParameters.ValidateIssuerSigningKey Property (Microsoft.IdentityModel.Tokens) - Microsoft Authentication Library for .NET | Microsoft Learn — accessed 2026-10-07. Exact supporting passage: “It is possible for tokens to contain the public key needed to check the signature. For example, X509Data can be hydrated into an X509Certificate, which can be used to validate the signature. In these cases it is important to validate the SigningKey that was used to validate the signature. This boolean only applies to default signing key validation. If IssuerSigningKeyValidator is set, it will be called regardless of whether this property is true or false. The default is false.”
-
TokenValidationParameters.RequireSignedTokens Property (Microsoft.IdentityModel.Tokens) - Microsoft Authentication Library for .NET | Microsoft Learn — accessed 2026-10-07. Exact supporting passage: “Gets or sets a value indicating whether a SecurityToken can be considered valid if not signed. The default is true. public bool RequireSignedTokens { get; set; }”
-
Resource-based authorization in ASP.NET Core | Microsoft Learn — accessed 2026-10-07. Exact supporting passage: “Resource-based authorization requires special attention in ASP.NET Core apps. Attribute evaluation occurs before data binding and before execution of any method that loads a resource. Declarative authorization with an [Authorize] attribute doesn't suffice for resource-based authorization. Instead, the app must invoke a custom authorization method—an approach known as imperative authorization.”
-
Logging in C# - .NET | Microsoft Learn — accessed 2026-10-07. Exact supporting passage: “The logger methods have overloads that take an exception parameter:”
-
Use Key Vault References as App Settings - Azure App Service | Microsoft Learn — accessed 2026-10-07. Exact supporting passage: “If the secret version isn't specified in the reference, the app uses the latest version that exists in the key vault. When newer versions become available, such as with rotation, the app is automatically updated and begins using the latest version within 24 hours.”
Pause / Recall / Apply
Can you explain it without the page?
Close the example. Reconstruct the core idea, then change one assumption. Mark complete when you’re ready; you can always undo it.
Stored in this browser only. No account, no sync. Clearing browser data removes your record.