8 Essential Secure Coding Practices for Software Developers

Software development has become an essential component of modern technology, and with that comes the need for secure coding practices. As developers, it’s our responsibility to ensure that the code we write is secure, reliable, and free from vulnerabilities. This article will discuss some important secure coding practices software developers should know and provide references for further learning.

Input Validation

One of the most common ways attackers exploit software is through input validation vulnerabilities. It is imperative to validate all user input, ensuring that it conforms to the expected format and data type. This includes validating input from users, APIs, and databases.

Let’s say we want to create a method that accepts an integer as input and returns its square. We want to make sure that the input is a valid integer and not a string or any other data type. Here’s how we can validate the input:

Let’s say we want to create a method that accepts an integer as input and returns its square. We want to make sure that the input is a valid integer and not a string or any other data type. Here’s how we can validate the input:

public int Square(int number)
{
    if (!int.TryParse(number.ToString(), out int result))
    {
        throw new ArgumentException("Input must be a valid integer.");
    }
    return result * result;
}

In this example, we first convert the input number to a string using the ToString() method. We then use the int.TryParse() method to try to parse the string as an integer. If the parsing fails, the TryParse() method returns false, and we throw an ArgumentException with a message indicating that the input must be a valid integer. If the parsing succeeds, the TryParse() method returns true, and the parsed integer value is stored in the result variable, which we then use to calculate and return the square.

Authentication and Authorization

Authentication and authorization are critical components of secure coding. Developers must ensure that users are authenticated before allowing them to access any protected resource. Additionally, authorization must be implemented to ensure that users can only access resources they are authorized to access.

In this example of authentication, we are using OpenID Connect as our authentication protocol. We configure the authentication services to use cookies as the default authentication scheme and OpenID Connect as the default challenge scheme. We also configure the cookie options to specify the login and logout paths. For OpenID Connect, we specify the authority, client ID, client secret, response type, and scopes.

// In Startup.cs, configure authentication services
services.AddAuthentication(options =>
{
    options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(options =>
{
    options.LoginPath = "/Account/Login/";
    options.LogoutPath = "/Account/Logout/";
})
.AddOpenIdConnect(options =>
{
    options.Authority = Configuration["Authentication:Authority"];
    options.ClientId = Configuration["Authentication:ClientId"];
    options.ClientSecret = Configuration["Authentication:ClientSecret"];
    options.ResponseType = OpenIdConnectResponseType.Code;
    options.Scope.Add("openid");
    options.Scope.Add("profile");
    options.SaveTokens = true;
});

In this example, we create an authorization policy called “AdminOnly” that requires the user to have the “Admin” role. We then apply this policy to an action method using the [Authorize] attribute with the Policy parameter set to “AdminOnly”. This ensures that only users with the “Admin” role can access the AdminPage() action method.

// In Startup.cs, configure authorization services
services.AddAuthorization(options =>
{
    options.AddPolicy("AdminOnly", policy =>
    {
        policy.RequireRole("Admin");
    });
});

// In a controller or action method, apply the authorization policy
[Authorize(Policy = "AdminOnly")]
public IActionResult AdminPage()
{
    // Code for admin page
}

Error Handling and Logging

Proper error handling and logging are crucial for identifying and addressing security issues. Developers must ensure that all errors are logged in a secure location and that they don’t reveal sensitive information to the end user.

In this example, we configure exception handling middleware in the Configure method of Startup.cs. This middleware catches any unhandled exceptions that occur in the pipeline and returns an error response to the client. We also log the exception using an instance of the ILogger interface.

In a controller or action method, we can handle exceptions using a try-catch block. If an exception is caught, we log it using the _logger instance and return an error response to the client. If no exception is thrown, we return the normal response.

// In Startup.cs, configure exception handling middleware
app.UseExceptionHandler(errorApp =>
{
    errorApp.Run(async context =>
    {
        context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
        context.Response.ContentType = "text/html";

        var exception = context.Features.Get<IExceptionHandlerFeature>();
        if (exception != null)
        {
            var errorMessage = exception.Error.Message;
            await context.Response.WriteAsync($"<h1>Error: {errorMessage}</h1>").ConfigureAwait(false);
        }
    });
});

// In a controller or action method, handle exceptions
public IActionResult Index()
{
    try
    {
        // Code that may throw an exception
    }
    catch (Exception ex)
    {
        // Log the exception
        _logger.LogError(ex, "An error occurred in the Index action method.");

        // Return an error response
        return View("Error");
    }

    // Code that executes if no exception is thrown
    return View();
}

In this example, we configure logging services in the ConfigureServices method of Startup.cs. We add logging providers like Console and Debug, and configure them using options from the appsettings.json file.

In a controller or action method, we can log events using an instance of the ILogger interface. In this example, we log an information event using the _logger.LogInformation method. We can also log other types of events like warnings and errors using the corresponding methods of the ILogger interface. The logged events can be viewed in the configured logging providers like Console and Debug.

// In Startup.cs, configure logging services
services.AddLogging(loggingBuilder =>
{
    loggingBuilder.AddConfiguration(Configuration.GetSection("Logging"));
    loggingBuilder.AddConsole();
    loggingBuilder.AddDebug();
});

// In a controller or action method, log an event
public IActionResult Index()
{
    _logger.LogInformation("The Index action method was called.");

    // Code that executes after logging
    return View();
}

Secure Communication

It’s imperative to ensure that all communication between the application and external systems is protected. This includes using encryption, secure protocols, and secure authentication mechanisms.

Here’s an example of secure communication in C# using HTTPS. In this example, we configure HTTPS for our ASP.NET Core application. We add the AddHttpsRedirection method to the ConfigureServices method to configure HTTPS redirection. We specify the HTTPS port as 443, which is the default port for HTTPS.

We also enable the HTTPS redirection middleware in the Configure method by calling the UseHttpsRedirection method. This middleware redirects any HTTP requests to HTTPS, ensuring that all communication with the application is secure.

Note that in order to use HTTPS, you need to obtain a valid SSL certificate for your application domain. There are many ways to obtain an SSL certificate, such as through a certificate authority or using a tool like Let’s Encrypt. Once you have obtained a certificate, you can install it on your server and configure your application to use it.

// In Startup.cs, configure HTTPS
public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();
    services.AddHttpsRedirection(options =>
    {
        options.HttpsPort = 443;
    });
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    // Enable HTTPS redirection middleware
    app.UseHttpsRedirection();

    app.UseRouting();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}

Cryptography

Cryptography is vital to secure coding practices. Developers must understand the basics of cryptography and use secure algorithms for encrypting data, hashing passwords, and verifying digital signatures.

Here’s an example of cryptography in C# using the System.Security.Cryptography namespace. In this example, we use the AesCryptoServiceProvider class to generate a random symmetric key, convert it to a string, and save it to a file. We then load the key from the file and use it to encrypt a plaintext message and decrypt the resulting ciphertext.

To encrypt the message, we create an encryptor using the loaded key and an initialization vector (IV) generated by the AesCryptoServiceProvider. We then use a CryptoStream to write the plaintext to the encryptor, which produces the ciphertext. To decrypt the message, we create a decryptor using the loaded key and IV, and use another CryptoStream to read the ciphertext and produce the plaintext.

Note that this example uses symmetric key cryptography, where the same key is used for both encryption and decryption. For more advanced scenarios, such as public key cryptography or digital signatures, the System.Security.Cryptography namespace provides additional classes and algorithms.

// Generate a random symmetric key
SymmetricAlgorithm symmetricAlgorithm = new AesCryptoServiceProvider();
symmetricAlgorithm.GenerateKey();

// Convert the key to a string and save it
string keyString = Convert.ToBase64String(symmetricAlgorithm.Key);
File.WriteAllText("key.txt", keyString);

// Load the key from file
string loadedKeyString = File.ReadAllText("key.txt");
byte[] loadedKeyBytes = Convert.FromBase64String(loadedKeyString);

// Use the loaded key for encryption and decryption
byte[] plaintextBytes = Encoding.UTF8.GetBytes("This is a secret message.");
byte[] ciphertextBytes;

using (var encryptor = symmetricAlgorithm.CreateEncryptor(loadedKeyBytes, symmetricAlgorithm.IV))
using (var msEncrypt = new MemoryStream())
{
    using (var csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
    {
        csEncrypt.Write(plaintextBytes, 0, plaintextBytes.Length);
        csEncrypt.FlushFinalBlock();
        ciphertextBytes = msEncrypt.ToArray();
    }
}

using (var decryptor = symmetricAlgorithm.CreateDecryptor(loadedKeyBytes, symmetricAlgorithm.IV))
using (var msDecrypt = new MemoryStream(ciphertextBytes))
using (var csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read))
using (var srDecrypt = new StreamReader(csDecrypt))
{
    string plaintext = srDecrypt.ReadToEnd();
    Console.WriteLine($"Decrypted plaintext: {plaintext}");
}

Secure Storage

Developers must ensure that sensitive data is securely stored. This includes encrypting data at rest, using secure password hashing algorithms, and limiting access to sensitive data to authorized users only.

Here’s an example of secure storage in C# using the System.Security.Cryptography.ProtectedData class. In this example, we convert a plaintext message to a byte array using the Encoding.UTF8.GetBytes method. We then use the ProtectedData.Protect method to protect the data using the local machine’s protection scope. This method encrypts the data using a key derived from the current user’s credentials and the machine’s hardware and software configuration.

We save the protected data to a file using the File.WriteAllBytes method. To load the protected data from the file, we use the File.ReadAllBytes method.

We then use the ProtectedData.Unprotect method to unprotect the loaded data. This method decrypts the data using the same key that was used to encrypt it. We convert the unprotected data back to a string using the Encoding.UTF8.GetString method.

Note that the ProtectedData class provides a simple way to protect sensitive data, but it is not a substitute for a complete security strategy. For example, it does not protect against attacks that exploit vulnerabilities in the operating system or hardware. Additionally, it may not be suitable for protecting large amounts of data or data that needs to be shared across multiple machines or users. In such cases, more advanced encryption and key management techniques may be necessary.

// Convert the plaintext data to a byte array
byte[] plaintextBytes = Encoding.UTF8.GetBytes("This is a secret message.");

// Protect the data using the ProtectedData class
byte[] protectedBytes = ProtectedData.Protect(plaintextBytes, null, DataProtectionScope.LocalMachine);

// Save the protected data to a file
File.WriteAllBytes("protected.bin", protectedBytes);

// Load the protected data from the file
byte[] loadedBytes = File.ReadAllBytes("protected.bin");

// Unprotect the data using the ProtectedData class
byte[] unprotectedBytes = ProtectedData.Unprotect(loadedBytes, null, DataProtectionScope.LocalMachine);

// Convert the unprotected data back to a string
string plaintext = Encoding.UTF8.GetString(unprotectedBytes);

Console.WriteLine($"Unprotected plaintext: {plaintext}");

Other Considerations

  1. Code review and testing: Code review and testing are essential for identifying security vulnerabilities. Developers should conduct a thorough code review to identify any vulnerabilities and use automated testing tools to identify issues.
  2. Secure development life cycle: Developers must follow a secure development life cycle (SDLC) that includes secure coding practices. This includes identifying security requirements, designing secure systems, developing secure code, testing for vulnerabilities, and maintaining secure systems.

Secure coding practices are crucial for building secure and reliable software. As developers, we must prioritize security and implement the necessary measures to prevent vulnerabilities and protect users’ data. By following the secure coding practices outlined in this article, we can ensure that our software is secure and trusted. It is also important to note that security is a continuous process. Developers should continually educate themselves and stay updated with the latest security practices to secure their software. By following the references provided in this article, developers can continue to learn and improve their secure coding practices.

References

  1. “OWASP Top Ten Project,” OWASP Foundation, https://owasp.org/top10/
  2. “Microsoft Security Development Lifecycle (SDL),” Microsoft, https://www.microsoft.com/en-us/securityengineering/sdl
  3. “Secure Coding in C & C++,” CERT, https://www.securecoding.cert.org/
  4. “Secure Coding Guidelines for Java SE,” Oracle, https://www.oracle.com/java/technologies/javase/seccodeguide.html
  5. “Cryptography 101,” Stanford University, https://crypto.stanford.edu/~dabo/cryptobook/
  6. “The Basics of Web Application Security,” Mozilla, https://developer.mozilla.org/en-US/docs/Web/Security

Leave a Reply

I’m Peter

I’ve spent my career building software and leading engineering teams. I started as a developer and architect, grew into engineering leadership, and today I serve as a Chief Technology Officer.

Here, I share practical insights on technology, leadership, and building high-performing teams.

Connect with me on LinkedIn.

Discover more from Peter Mourfield

Subscribe now to keep reading and get access to the full archive.

Continue reading