This episode changes the way you write secure code: input validation, output encoding, preventing SQL injection, XSS, and CSRF, protecting sensitive data, and security best practices applied from the first day of development.

Episode 12 secured transport and identity; episode 13 secures the code itself. Secure coding is the discipline of writing code that resists common attack patterns — not a feature bolted on at the end, but a habit embedded in every line.
You will learn input validation, output encoding, preventing injection, XSS, and CSRF, handling sensitive data, and production practices used by professional security teams. Much of this is already provided by the framework — your job is to not disable it.
The golden rule: never trust input. Validate at every system boundary — APIs, import files, and forms. Use the built-in DataAnnotations:
public class RegisterRequest
{
[Required, EmailAddress]
public string Email { get; set; }
[Required, MinLength(8)]
public string Password { get; set; }
}[EmailAddress] and [MinLength(8)] validate input during model binding. With [ApiController], an invalid model automatically produces a 400 response — you don't need to write manual checks.
Output that contains user input must be encoded so it cannot become markup. In Razor (Blazor and MVC), encoding happens automatically:
@{
var nama = HttpUtility.HtmlEncode(userInput);
}
<p>Halo, @nama</p>HttpUtility.HtmlEncode(userInput) turns dangerous characters into safe HTML entities. A simple rule: context determines the encoding — HTML in Razor, SQL in queries, URL in headers. Encode according to the context where the data is emitted.
SQL injection happens when input is concatenated directly into a SQL string. Always parameterize:
var hasil = await _db.Produk
.FromSqlRaw("SELECT * FROM Produk WHERE Nama = {0}", nama)
.ToListAsync();The {0} placeholder sends nama as a parameter, not as part of the query — the database treats it as pure data. Never concatenate variables directly into SQL strings, like the following dangerous example:
var sql = $"SELECT * FROM Produk WHERE Nama = '{nama}'";The pattern above is vulnerable to SQL injection because the user value is pasted directly into the query. Instead, always send values as parameters, as FromSqlRaw does with its placeholder. The same principle applies to raw ADO.NET: use SqlParameter.
XSS (Cross-Site Scripting) happens when scripts are injected into a page. The prevention is output encoding, which is automatic in Razor. CSRF (Cross-Site Request Forgery) forces users to send requests they did not intend. The framework provides anti-forgery tokens:
builder.Services.AddAntiforgery(options =>
{
options.HeaderName = "X-CSRF-TOKEN";
});AddAntiforgery provides a token that is verified for requests that change state. The token is sent via the X-CSRF-TOKEN header and validated by middleware — ensuring the request really comes from an approved user.
The most common leak is not hacking, but logs and responses displaying sensitive data. Watch these points:
public record UserDto(int Id, string Nama, string Email);
var dto = userList.Select(u => new UserDto(u.Id, u.Nama, u.Email));Mapping entities to DTOs ensures only the required fields are sent to clients — entities with internal properties never leak by accident.
The Data Protection API from episode 12 is also used to store secrets in a database:
var protectedToken = protector.Protect(accessToken);
await _db.ApiTokens.AddAsync(new ApiToken
{
UserId = userId,
TokenEnkripsi = protectedToken
});Store the result of Protect instead of the raw token. The encryption keys are managed by the runtime and can be persisted to a key ring. For passwords, use password-specific hashing algorithms such as PBKDF2 or bcrypt through ASP.NET Core Identity — do not use reversible encryption.
A few practices that must be checked before production:
ASPNETCORE_ENVIRONMENT=Production and disable the developer exception page.dotnet list package --vulnerable.dotnet list package --vulnerabledotnet list package --vulnerable checks NuGet packages against the known vulnerability database. Run it as a routine part of CI so problematic dependencies are caught before release.
Tip
Security is a process, not a product. Routine scanning, code review, and dependency updates are part of the lifecycle of every .NET application.
Key takeaways:
In the next episode 14 we will discuss asynchronous programming and concurrency — the task-based pattern with async/await, Task and ValueTask, cancellation tokens, parallel programming with Parallel.For and Channels, and thread safety and synchronization primitives.