
Learn how to prevent data breaches with a practical security checklist for software teams, including C# code examples. Secure your .NET apps today.
Every week brings another headline about millions of leaked records, and the root cause is rarely an elite hacker. Most incidents trace back to ordinary engineering mistakes: a hard-coded connection string, an endpoint that forgot to check authorization, a password stored with MD5. If you want to know how to prevent data breaches, the answer is not a single product but a discipline your software team applies on every feature. This guide is a practical security checklist for software teams, with runnable C# and .NET examples, explaining not just what to do but why each control matters.
Why Data Breaches Happen: The Developer's View
Industry reports such as the Verizon DBIR consistently show the same top causes: stolen or weak credentials, web application vulnerabilities, misconfiguration, and human error. Notice that three of those four are firmly inside the software team's control. Data breach prevention for developers therefore comes down to a handful of recurring themes:
- Secrets exposure — API keys and connection strings committed to source control.
- Broken access control — the #1 item on the OWASP Top 10, where users can read data that isn't theirs.
- Injection — SQL, command, and LDAP injection from unsanitized input.
- Weak cryptography — plaintext passwords, reversible encryption, homemade algorithms.
- Insufficient logging — breaches that go unnoticed for months because nobody was watching.
The checklist below maps directly to these causes. Treat it as a definition of done for any feature that touches user data.
How to Prevent Data Breaches: The 10-Point Security Checklist
1. Never Store Secrets in Source Code
Hard-coded secrets are the fastest route to a breach because repositories get cloned, forked, and leaked. In .NET, use the configuration system with User Secrets locally and a managed vault (Azure Key Vault, AWS Secrets Manager, HashiCorp Vault) in production.
// Program.cs — secrets come from environment/vault, never from appsettings.json in git
var builder = WebApplication.CreateBuilder(args);
if (builder.Environment.IsDevelopment())
{
builder.Configuration.AddUserSecrets<Program>();
}
else
{
var vaultUri = new Uri(builder.Configuration["KeyVault:Uri"]!);
builder.Configuration.AddAzureKeyVault(vaultUri, new DefaultAzureCredential());
}
var connectionString = builder.Configuration.GetConnectionString("Default")
?? throw new InvalidOperationException("Connection string not configured.");
Why: Vaults give you rotation, audit logs, and per-environment isolation. Add a pre-commit secret scanner (gitleaks, GitHub secret scanning) so a slip never reaches the remote.
2. Parameterize Every Database Query
SQL injection is decades old and still appears in breach post-mortems. String concatenation is the bug; parameters are the fix.
// VULNERABLE — never do this
var sql = $"SELECT * FROM Users WHERE Email = '{email}'";
// SAFE — parameterized with Dapper
var user = await connection.QuerySingleOrDefaultAsync<User>(
"SELECT * FROM Users WHERE Email = @Email",
new { Email = email });
// SAFE — Entity Framework Core LINQ is parameterized automatically
var user2 = await db.Users.SingleOrDefaultAsync(u => u.Email == email);
// If you must use raw SQL in EF Core, use the interpolated overload
var users = await db.Users
.FromSqlInterpolated($"SELECT * FROM Users WHERE Email = {email}")
.ToListAsync();
Pitfall: FromSqlRaw with string interpolation is not safe. FromSqlInterpolated converts the interpolated values into parameters; FromSqlRaw($"...") does not.
3. Hash Passwords with a Slow, Salted Algorithm
If your database leaks, properly hashed passwords buy your users time. Use ASP.NET Core Identity's PasswordHasher, or Argon2id / bcrypt via a vetted library. Never use MD5, SHA-1, or unsalted SHA-256.
using Microsoft.AspNetCore.Identity;
public sealed class PasswordService
{
private readonly PasswordHasher<string> _hasher = new();
public string Hash(string userId, string password)
=> _hasher.HashPassword(userId, password); // PBKDF2, random salt, versioned format
public bool Verify(string userId, string storedHash, string password)
{
var result = _hasher.VerifyHashedPassword(userId, storedHash, password);
return result is PasswordVerificationResult.Success
or PasswordVerificationResult.SuccessRehashNeeded;
}
}
Why: Slow hashes make brute-force attacks economically painful, and unique salts defeat rainbow tables. Pair this with multi-factor authentication for anything administrative.
4. Enforce Authorization on Every Endpoint (Not Just Authentication)
Authentication proves who the user is; authorization decides what they can touch. Insecure Direct Object Reference (IDOR) — changing /orders/123 to /orders/124 — is one of the most common ways customer data leaks.
[Authorize]
[ApiController]
[Route("api/orders")]
public class OrdersController : ControllerBase
{
private readonly AppDbContext _db;
public OrdersController(AppDbContext db) => _db = db;
[HttpGet("{id:int}")]
public async Task<IActionResult> Get(int id)
{
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
// Scope the query to the caller — the ownership check is part of the query itself
var order = await _db.Orders
.Where(o => o.Id == id && o.CustomerId == userId)
.SingleOrDefaultAsync();
return order is null ? NotFound() : Ok(order);
}
}
Make authorization the default globally so a forgotten attribute fails closed:
builder.Services.AddAuthorization(options =>
{
options.FallbackPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
});
Why: Returning NotFound instead of Forbid also avoids confirming that the record exists to an attacker enumerating IDs.
5. Validate and Constrain All Input
Every byte from the network is hostile until proven otherwise. Use data annotations or FluentValidation, and reject rather than sanitize where possible.
public sealed record CreateUserRequest(
[property: Required, EmailAddress, StringLength(254)] string Email,
[property: Required, StringLength(100, MinimumLength = 2)] string DisplayName,
[property: Range(13, 120)] int Age);
Also bind only the properties you intend to accept. Over-posting — where an attacker sends "IsAdmin": true and it maps straight onto your entity — is a classic privilege-escalation bug. Use dedicated request DTOs instead of binding directly to EF entities.
6. Encrypt Data in Transit and at Rest
Force HTTPS, enable HSTS, and encrypt sensitive columns so a stolen database backup is useless without the key.
// Program.cs
app.UseHsts();
app.UseHttpsRedirection();
// Field-level encryption with the Data Protection API
public sealed class SsnProtector
{
private readonly IDataProtector _protector;
public SsnProtector(IDataProtectionProvider provider)
=> _protector = provider.CreateProtector("Customer.SSN.v1");
public string Protect(string ssn) => _protector.Protect(ssn);
public string Unprotect(string cipher) => _protector.Unprotect(cipher);
}
Pitfall: In a multi-server deployment, persist Data Protection keys to shared storage (blob storage, Redis, SQL) and protect them with a vault key; otherwise every server generates its own keys and ciphertext becomes unreadable after a deploy.
7. Minimize the Data You Collect and Expose
You cannot leak what you never stored. Collect only what the feature needs, set retention periods, and never return full entities from an API. Project into response DTOs so password hashes, internal flags, and PII don't slip out by accident.
var profile = await _db.Users
.Where(u => u.Id == userId)
.Select(u => new ProfileResponse(u.DisplayName, u.AvatarUrl))
.SingleAsync();
8. Keep Dependencies Patched
Many of the largest breaches (Equifax being the canonical example) exploited a known vulnerability in a library that was months out of date. Automate this:
- Run
dotnet list package --vulnerable --include-transitivein CI and fail the build on high-severity findings. - Enable Dependabot or Renovate for automatic update pull requests.
- Pin the .NET SDK version in
global.jsonand stay on a supported runtime.
9. Log Security Events — Without Logging Secrets
The average breach takes months to detect. Structured logging of authentication failures, permission denials, and unusual data access is what turns months into minutes.
_logger.LogWarning(
"Authorization denied. UserId={UserId} Resource={Resource} Ip={Ip}",
userId, resourceId, HttpContext.Connection.RemoteIpAddress);
Pitfall: Never log request bodies, tokens, or passwords. Redact PII at the logging-pipeline level so a compromised log store doesn't become a second breach.
10. Harden Configuration and Headers
Disable detailed error pages in production, add security headers, and rate-limit authentication endpoints to blunt credential-stuffing attacks.
builder.Services.AddRateLimiter(options =>
{
options.AddFixedWindowLimiter("login", o =>
{
o.PermitLimit = 5;
o.Window = TimeSpan.FromMinutes(1);
});
});
app.UseRateLimiter();
app.Use(async (context, next) =>
{
context.Response.Headers["X-Content-Type-Options"] = "nosniff";
context.Response.Headers["X-Frame-Options"] = "DENY";
context.Response.Headers["Content-Security-Policy"] = "default-src 'self'";
context.Response.Headers["Referrer-Policy"] = "no-referrer";
await next();
});
app.MapPost("/login", LoginHandler).RequireRateLimiting("login");
Security Best Practices for the Whole Team
Code-level controls only stick if the process supports them. The teams that succeed at data breach prevention bake security into their workflow:
- Threat model new features. Spend fifteen minutes asking "what could go wrong?" before writing code. It's cheaper than an incident response.
- Automate security scanning in CI. Static analysis (Roslyn security analyzers, Semgrep, CodeQL), dependency scanning, and secret scanning should block merges.
- Require security-focused code review. Add a checklist item: "Does this endpoint check ownership? Does this query use parameters?"
- Apply least privilege everywhere. The application's database user should not be
sa. Cloud roles should be scoped to exactly the resources needed. - Practice incident response. Know who to call, how to rotate every credential, and how to notify users before you need to. Regulations such as GDPR, the UK Data Protection Act, Australia's Notifiable Data Breaches scheme, and US state laws impose tight notification deadlines.
- Train developers regularly. The OWASP Top 10 changes; so do attack techniques. A short quarterly session pays for itself.
Common Pitfalls That Undermine Data Breach Prevention
- Trusting the client. Hidden form fields, disabled buttons, and JavaScript validation are UX features, not security controls. Re-validate everything server-side.
- Rolling your own crypto. Use the framework's primitives. If you're calling
Aes.Create()directly, make sure you understand IVs, authenticated modes (AES-GCM), and key management — or use Data Protection instead. - Security by obscurity. Unlisted URLs and GUID IDs are not authorization. Attackers find them.
- Forgetting non-production environments. Staging databases seeded with real customer data and protected by
admin/adminare a favorite entry point. - Verbose error responses. Stack traces and SQL errors returned to the browser tell attackers exactly what to try next. Use
app.UseExceptionHandler()and generic problem-details responses in production.
Conclusion: Key Takeaways
Knowing how to prevent data breaches is less about buying tools and more about consistently applying a small set of proven controls. If your team does nothing else, do these:
- Keep secrets out of source control and in a managed vault.
- Parameterize every query and validate every input.
- Hash passwords with a slow, salted algorithm and require MFA for privileged accounts.
- Check authorization — ownership, not just login — on every endpoint, with a fail-closed default.
- Encrypt data in transit and at rest, and collect only the data you actually need.
- Patch dependencies automatically and fail CI on known vulnerabilities.
- Log security events without logging secrets, and rehearse your incident response plan.
Print this security checklist for software teams, pin it next to your definition of done, and revisit it every sprint. A data breach costs millions in fines, remediation, and lost trust; the controls above cost a few hours of engineering discipline. That is the best trade in software.
Your go-to resource for C#, .NET, and modern software development. Follow along for daily tutorials, tips, and real-world examples.
Comments
Post a Comment