There isn’t a week that goes by that there isn’t a new security breach in the news. It can be intimidating for developers to be expected to write clean code that is also resilient to any and all types of malicious attacks. The increased frequency of attacks makes writing automated security tests a necessity.
Testing for security is nowhere near as simple as writing unit tests. It requires out of the box thinking, and looking at an application from a completely different perspective to find its weaknesses. That’s hard to do for developers who’ve been working on the project day in and day out.
That being said, there are a surprisingly large number of security tests that can be automated with relative ease. Automated tests are always best because they can be easily repeated and run as part of a CI/CD pipeline, alerting immediately when a failure is found.
The examples I use below are from a sample project that I’ve been building out over the last couple of months. You can find the introductory post here, with more in-depth posts on the unit, acceptance, and load tests layers also available. The full source code for any of the code snippets found below can be found here.
Validating Input to an API
Input tests reduce the risk of bad data, or worse, SQL injection, from being introduced or executed within your cloud application. This is something developers should be doing from a unit testing perspective already. Writing the equivalent security test is similar but instead executes against a deployed version of the cloud application.
The Supermarket sample application has an API endpoint which uses a postal code parameter to calculate a cart’s cost. The method uses the postal code to do a lookup of address information via an API but could just as easily be searching through a database. In either case, invalid data should be rejected and an error code returned.
Here’s the security test that checks the behaviour when invalid data is supplied:
[Theory] [InlineData("94J3J92J")] [InlineData("11A 0A4")] public async Task GivenInvalidPostalCode_ThenBadRequest(string postalCode) { var token = AuthenticationHelper.GetToken(_clientApiUrl); var response = await HttpRequestFactory.Post(_clientApiUrl, _checkoutResourceUrlSegment, new { postalCode }); Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); }
The test gets a valid access token and then invokes the checkout API endpoint with the invalid data. It then ensures that the result of the operation is a Bad Request. There’s surely more imaginative test data that can be used but the test’s format would remain the same. Any failure of this test means that there’s something missing in terms of validations and should be investigated further.
Enforcing Authentication and Authorization
Almost every API that performs an action on behalf of a user will have some form authentication or authorization on its endpoints. A test that ensures authenticated endpoints return Unauthorized status codes should be written whether the authentication scheme is OAuth, Azure AD, or something completely custom. These tests are a no brainer and are some of the easiest to automate.
The Supermarket app’s Checkout endpoint requires a valid token to execute. A 401 Unauthorized response should be returned when the token is invalid, missing or expired. Here’s what the missing token test looks like:
[Fact] public async Task GivenMissingAuthorization_ThenUnauthorized() { var response = await HttpRequestFactory.PostAnonymous(_clientApiUrl, _checkoutResourceUrlSegment, new { postalCode = "K1A 0A4" }); Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); }
And here’s what an invalid token request would look like:
[Fact] public async Task GivenInvalidAuthorization_ThenUnauthorized() { var response = await HttpRequestFactory.Post(_clientApiUrl, _checkoutResourceUrlSegment, new { postalCode = "K1A 0A4" }, "totally-invalid-token"); Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); }
Testing for an expired token requires a bit more work and I haven’t implemented it here. Access tokens are generally valid for an hour or more so any test would take too long to execute. The easiest way to get around this limitation is to execute the test in a development or staging environment that has a shorter token lifespan. The logic would be identical to the two previous tests we just looked at.
Sending Large Requests To An Endpoint
Large requests can be leveraged to perform Denial Of Service attacks. Setting the maximum request size protects you from large requests that try to overload your application. The request size limit will be different based on what the application does. An API that has small request payloads can have a limit that is very low. An API that works with images, videos, or other large files needs a higher limit to handle those files.
It’s easy to configure the maximum request limit for an individual method or the application as a whole. The details can be found here. In the case of the Supermarket Checkout API, I’ve made the request size limit very small since all we’re passing to it is a postal code:
[Authorize] [HttpPost] [RequestSizeLimit(64)] public async Task Post([FromBody]CheckoutRequest request) { if (request == null || request.PostalCode == null) { return BadRequest(); } if (!ModelState.IsValid) { return BadRequest(); } var checkoutResult = await _checkoutService.Checkout(request.PostalCode); var response = new CheckoutResponse() { PreTax = checkoutResult.PreTax, PostTax = checkoutResult.PostTax }; return new OkObjectResult(response); }
The next step is to write a test that validates the request size limit is being respected. This protects us in the case of an accidental or unintentional change.
[Theory] [InlineData("94J3J92AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")] public async Task GivenLargeRequest_ThenBadRequest(string postalCode) { var response = await HttpRequestFactory.Post(_clientApiUrl, _checkoutResourceUrlSegment, new { postalCode }); Assert.Equal(HttpStatusCode.RequestEntityTooLarge, response.StatusCode); }
Where To Go From Here
That covers the basics of writing automated security tests. All the tests discussed should run once the application has been successfully deployed. The best way to ensure that happens is to run the tests as part of your CI/CD pipeline.
There are surely other automated test scenarios, and as I come across them I’ll come back here and update the article. This isn’t the end of the security story though. There is way, way more than I could possibly cover in a single blog post. To that end, here’s some resources that could help you think of what else might be needed from a security perspective:
- Troy Hunt’s Hack Yourself First course on Pluralsight
- Justin Boyer’s Green Machine Security blog
- Writing Secure Code
There’s tons of information on cloud application security on the web as well, so make sure to do your own research, or risk ending up in the news for all the wrong reasons.
2 comments