jaffar.dev

Secure Coding in .NET

April 30, 2025 | by Jaffar Ali Mohamedkasim

gdfae072219f7d8c26d8d99c2f342b41889841a3b937efab3bc1713a7ea3e89a527a08e38c7285fb54d47ba94704b1270a74a3f7897a22133783e7ef3585eda4f_1280-2347442

Secure Coding in .NET (C#): Best Practices, Checklist, and Automation

Writing secure code is no longer optional — it’s essential. Whether you’re building APIs, web apps, or enterprise tools in C#, following security best practices helps protect against common vulnerabilities and maintain trust with users.

In this post, you’ll learn:

  • Key standards and guidelines to follow
  • A real-world secure C# class example
  • A ready-made secure code review checklist
  • A GitHub Action to automatically scan your .NET code for security issues

1. Follow Industry Standards

a. Microsoft Secure Coding Guidelines

Microsoft provides official guidance for secure development:

Key recommendations:

  • Validate all inputs
  • Encode all outputs
  • Apply the principle of least privilege
  • Avoid hard-coded credentials
  • Use secure frameworks
  • Handle errors securely

b. OWASP Best Practices

The OWASP Top 10 is a must-follow industry standard for web application security.

Highlights include:

  • A01: Broken Access Control
  • A03: Injection (SQL, Command)
  • A05: Security Misconfiguration

Also refer to the OWASP Cheat Sheet Series for deep dives into each topic.


2. Secure C# Class Example

Here is a practical example of secure coding using ASP.NET Core Web API:

[ApiController]
[Route(“api/[controller]”)]
public class AccountController : ControllerBase
{
private readonly ILogger _logger;
private readonly string _connectionString;

public AccountController(ILogger<AccountController> logger)
{
    _logger = logger;
    _connectionString = Environment.GetEnvironmentVariable("DB_CONNECTION_STRING")
                        ?? throw new InvalidOperationException("Connection string not found.");
}

[HttpPost("register")]
[AllowAnonymous]
public IActionResult Register([FromBody] RegisterRequest model)
{
    if (!ModelState.IsValid)
        return BadRequest("Invalid input.");

    try
    {
        using var connection = new SqlConnection(_connectionString);
        connection.Open();

        using var cmd = new SqlCommand("INSERT INTO Users (Username, PasswordHash) VALUES (@Username, @PasswordHash)", connection);
        cmd.Parameters.AddWithValue("@Username", model.Username);
        cmd.Parameters.AddWithValue("@PasswordHash", HashPassword(model.Password));
        cmd.ExecuteNonQuery();

        return Ok(new { message = "User registered successfully." });
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "Error while registering user.");
        return StatusCode(500, "An error occurred.");
    }
}

private string HashPassword(string password)
{
    using var sha256 = SHA256.Create();
    var hashed = sha256.ComputeHash(Encoding.UTF8.GetBytes(password));
    return Convert.ToBase64String(hashed);
}

}

public class RegisterRequest
{
[Required, StringLength(50, MinimumLength = 3)]
public string Username { get; set; }

[Required, StringLength(100, MinimumLength = 8)]
public string Password { get; set; }

}

Secure Practices Demonstrated:

  • Parameterized SQL queries (avoiding SQL Injection)
  • Input validation with data annotations
  • Password hashing
  • Environment variable for secrets
  • Generic error response to users, detailed logs internally

3. Secure Code Review Checklist (Before Deployment)

Use this checklist to review your code before releasing it to production:


4. GitHub Action to Scan .NET Code Automatically

Add the following to .github/workflows/security-scan.yml:

```yaml
name: .NET Security Scan

on:
  pull_request:
    branches: [main]
  push:
    branches: [main]

jobs:
  security-scan:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup .NET
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '8.0.x'

      - name: Restore dependencies
        run: dotnet restore

      - name: Build project
        run: dotnet build --no-restore

      - name: Static code analysis
        run: dotnet format analyze --severity error

      - name: Scan for vulnerabilities
        uses: dependency-check/Dependency-Check_Action@main
        with:
          project: "MyProject"
          path: "."
          format: "HTML"
          out: "reports"

      - name: Upload vulnerability report
        uses: actions/upload-artifact@v4
        with:
          name: dependency-check-report
          path: reports

      - name: .NET built-in vulnerability check
        run: dotnet list package --vulnerable

      - name: Upload SARIF to GitHub Security tab
        uses: github/codeql-action/upload-sarif@v2
        with:
          sarif_file: reports/dependency-check-report.sarif
```

This action:

  • Checks out your code
  • Restores and builds the project
  • Runs static code analysis
  • Scans for known dependency vulnerabilities
  • Uploads a security report to GitHub

Final Words

Security is a continuous process — not a one-time fix. By following best practices, reviewing code thoroughly, and automating checks, you can protect your .NET applications and build trust with your users.

Ready to level up your security? Add these practices to your next sprint or CI/CD pipeline today!

RELATED POSTS

View all

view all