Skip to main content

3. Testing the Service

It is time to run some tests. But, before we begin:

  1. You will want the terminal where you started the demo web service at hand so we can see precisely how the web service experienced the test runs.
  2. You should have the source code of the web service open in your preferred IDE or a text editor so we can fix the issues found during the test runs.

We will go through testing and fixing the createUser operation together. The testing and fixing of the remaining two operations will be left to you as an exercise.

Start Testing

To start testing the createUser operation:

  1. On the Test Assets page, locate the createUser project and click the Start Test button (blue play icon) of the Project.
  2. Quickly move to the Tests page to catch the test showing up on the screen.

The first test run will be swift, and the test will disappear once the test run is completed. Do not worry if you missed it; you will have the opportunity to have a good look at the real-time test information later.

Viewing the Report

To view the Report produced by the test:

  1. Go to the Test Assets page and locate the createUser project.
  2. Click the Project's View Report button to see the list of available reports.
  3. Select the latest report on the top of the list by ticking the checkbox.
  4. Click the View Report icon in the top-right corner of the screen to view the report.

You land on the Summary screen of the test run report. Feel free to take a look at the page. This tutorial will not discuss the different information presented on the Summary screen. Instead, we will jump straight to the findings section of the report.

Report Summary

The Findings tab summarises the issues uncovered during the test run using a table. The table has 5 columns, these are:

  • Date: The date and the the unexpected behaviour was observed.
  • Messages: The list of Message Templates that were involved in the test run. A Message Template represents each operation, and the name of the Message Template was derived from the operationId property of the operation defined in the OpenAPI document. Therefore, this column lists the operations tested.
  • Fields: The list of Fields with their full path within the Message Template tested at the time GUARDARA observed the unexpected behaviour. The generated value of one or more of these fields was the likely trigger of the observed behaviour.
  • Observer: The name of the issue detection mechanism that picked up the suspicious behaviour.
  • Reproducible: When GUARDARA encounters any unexpected behaviour, it performs additional analysis to see if it can reproduce the issue. If GUARDARA could not reproduce the problem or no analysis was performed, the column will contain "No". If an analysis was performed during which GUARDARA could reproduce the problem, the column would say "Yes".

Findings List

You can learn more about each finding by clicking on the rows representing the findings. Let's take a quick look at the first finding.

Findings

Broken Authentication

The Observation tab tells you about the issue detected. In this case, GUARDARA noticed that even though the OpenAPI documentation says only authenticated clients should be able to call the operation, it was possible to call it without authentication, suggesting the authentication mechanism was broken or was not even implemented.

Missing Authentication

Triage

Let's quickly double check the definition of the operation in the OpenAPI document.

paths:
/user:
post:
operationId: createUser
summary: Create user
description: Create a new user in the system.
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/User'
application/x-www-form-urlencoded:
schema:
$ref: '#/components/schemas/User'
responses:
default:
description: The default response
content:
application/json:
schema:
$ref: '#/components/schemas/User'
security:
- api_key: []

Indeed, the last two lines suggest that the client (in this case, GUARDARA) should present an API Key to be able to interact with the operation.

Let's move to the Test Cases tab of the Finding where we will see proof that the authentication mechanism was broken.

Authentication Test Cases

What you see on the screen is a list of messages sent and received. Click on the View button of the first request to see how the HTTP request looked like.

As can be seen, even though the api_key header was included in the request, no API Key was provided.

Missing API Key

Close the dialog by clicking anywhere on the screen outside the dialog area. Then, click on the View button of the response message to see how the web service responded to the request.

No Auth Response

As we can see, despite not providing an API key, the operation succeeded, and the web service created a new user.

Looking at the implementation of the createUser operation we can see that there is no sign of any authentication mechanism.

Fix

Let's quickly fix this vulnerability and see what impact the fix has on the next test run. Open router implementation of the v1/user path of the API at routes/v1/user/index.js and add a simple authentication middleware.

danger

The authentication middleware we are about to implement is for demonstration purposes only! The API key will be easy to guess/brute-force, and the credential will be hard-coded. These are things you would never want to do in an application.

As shown below, we have created an authentication middleware named requireApiKey. It terminates processing and make the web service return a response with 401 error code to signal the authentication failure if the API key is not secret.

const Router = require ('koa-router');
const bodyParser = require('koa-body');

const parser = bodyParser({
jsonLimit: '100mb',
formLimit: '100mb',
textLimit: '100mb',
});

const {
createUser,
fetchUser,
deleteUser,
} = require('./controller');

//
const requireApiKey = async (ctx, next) => {
if (ctx.get('api_key') !== 'secret') {
ctx.throw(401, "Unauthorized");
}
await next();
}

const router = new Router({
prefix: '/user',
});

router.post('/', requireApiKey, parser, async (ctx) => {
ctx.body = await createUser(ctx);
});

Then, we add the requireApiKey middleware to the route handler to protect the createUser operation.

Re-test

We will encounter a very different behaviour if we rerun the test against the createUser operation.

  1. Start the test on the Test Assets page.
  2. Move to the Tests page. Notice that this time, the test stopped and did not disappear.
  3. Click the Show Details icon of the test to view the detailed test run screen where, we can find more information about what happened.

Authentication Failed

We can see from the Activity Log at the bottom of the screen that the Sanity Check picked up a problem. The service returned a response with the 401 HTTP status code. GUARDARA concluded that there was likely something wrong with the test configuration that prevented the testing of the operation.

We have fixed the authentication problem, and now GUARDARA must provide a valid API key to call the operation. Well done! One bug down, four more to go! But how will we test this operation now that it requires authentication? The answer is simple. We have to provide the expected API key in the Project configuration by updating the appropriate Authentication Variable.

  1. Click the Delete Test icon in the top-right corner of the screen to delete the test.
  2. Go back to the Test Assets page.
  3. Click the Edit button of the createUser project.
  4. Move to the Test Targets tab.
  5. Using the dropdown menu in the top-right corner of the screen, select our target (http://127.0.0.1:4141).
  6. Expand the Authentication section and provide to provide the API key (secret).

After completing the last step, your screen should look like the one in the picture below.

Set Credentials

Scroll down to the bottom of the screen and click the Update button to update the test configuration. Start the test again. You can see that there was no complaint from the Sanity Check this time, and the test started as expected. The test will finish quickly again, so let's look at the report again. You will see that the authentication issue is no longer present. We can move on to addressing the next issue in the report.

Unexpected HTTP Response Code

If you open the new report and have a look at the findings you will find that all of them are talking about the same problem: an unexpected HTTP response code. In this case, the unexpected response code is 400 Bad Request.

Triage

A 400 response code is a perfectly normal and this is how your web service should respond to malformed/incorrect HTTP requests.

If you take a look at the console output of the web service you will see that this response is triggered by the JSON parser that could not deal with the malformed requests.

...
SyntaxError: Unexpected token , in JSON at position 9
at JSON.parse (<anonymous>)
at parse (/webservice/node_modules/co-body/lib/json.js:62:17)
at /webservice/node_modules/co-body/lib/json.js:45:22
at runMicrotasks (<anonymous>)
at processTicksAndRejections (node:internal/process/task_queues:96:5)
at async requireApiKey (/webservice/routes/v1/user/index.js:20:5)
at async /webservice/index.js:25:5
...

From the perspective of GUARDARA, it is unclear why you did not document this response type. It could be because you forgot to document it or did not even know your service can respond this way. In general, independently of what the status code is, this is a problem. OpenAPI documents were supposed to be processed by applications and services to integrate with your service easily. Poorly documented services are difficult and time-consuming to integrate, and debugging issues when they arise is pretty challenging.

Fix

One thing you would want to do is to document this response type in the OpenAPI document. Another thing you may want to do is help the developers of the client application or service interacting with your web service to debug the issue. You can do this by updating the web service so that it returns a more informative response that instead of just saying Bad Request, for example, it says Invalid User Object received: not a valid JSON object.

tip

As an exercise, update the OpenAPI document to include 400 as a possible response.

As you can see, GUARDARA will not flood you with hundreds or thousands of findings at once. GUARDARA created the test configuration during the OpenAPI document import process so that you can improve your web service and its documentation in a manageable way. GUARDARA operates based on the assumption that you would like to build a reliable, secure and well documented web service, and it will guide you through this process step-by-step.

Suppose you wish to disable this feature so that GUARDARA does not check for undocumented/unexpected response codes and response content types. You can do so by removing the "Validate Content Type" callback from the Test Flow Template of the Project.

Internal Server Error

Assuming you have updated the OpenAPI document with the 400 response, start the test again.

This time the test will run slightly longer as it takes more effort from GUARDARA to trigger unexpected behaviour. This is good. It means your application is already in better shape.

Triage

This time you will find only three findings in the report. GUARDARA tested the username, email and password properties and found a bug that forced the server to respond with a 500 Internal Server Error response in all three cases.

Let's look at the test case of the first finding in the report that triggered this response.

Internal Error Trigger

It may be a bit difficult to spot at first, but there is a single quote (') right after the username (theUser).

If you look at the console output of the web service the nature of the problem becomes clear.

...
Error: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'john@email.com', '12345')' at line 1
at PromisePool.execute (/webservice/node_modules/mysql2/promise.js:359:22)
at createUser (/webservice/routes/v1/user/controller.js:7:51)
at /webservice/routes/v1/user/index.js:28:22
at dispatch (/webservice/node_modules/koa-compose/index.js:42:32)
at /webservice/node_modules/koa-body/index.js:148:14
at runMicrotasks (<anonymous>)
at processTicksAndRejections (node:internal/process/task_queues:96:5)
at async requireApiKey (/webservice/routes/v1/user/index.js:20:5)
at async /webservice/index.js:25:5
...

GUARDARA managed to break the syntax of the SQL query that supposed to create the new user in the database. As the error was not handled, the web service responded with a 500 status code. Being able to break the SQL syntax is a serious issue and signals the presence of a SQL Injection vulnerability.

Fix

The vulnerability is at line 7 of the createUser controller.

The best way to prevent SQL Injection is by using prepared statements. Luckily, the mysql2 library supports prepared statements so that we will have an easy job. The fixed controller is shown below.

async function createUser(context) {
const { username, email, password } = context.request.body;
const [insertResult] = await context.database.execute(
"INSERT INTO users VALUES(NULL, ?, ?, ?)",
[username, email, password]
);
if (insertResult.affectedRows === 0) return null;
const [result] = await context.database.execute(
'SELECT * FROM users WHERE id = LAST_INSERT_ID()'
);
return result[0];
}

Please note that you could address SQL Injection vulnerabilities by using a secure ORM library as well.

Re-test

Even though we could run the test again, let's try something different this time. We will use the Reproduce feature of GUARDARA to quickly check if we fixed the issue properly.

Reproduce Issue

Click on the blue Reproduce button in the top-left corner of the screen to start a quick re-test. You start a new test that only sends the test case(s) displayed under the Test Cases tab by clicking this button.

You may have noticed that the test only appeared on the Tests page for a couple of seconds and then disappeared. If it were possible to reproduce the issue, switching to the Replays tab of the finding would show the issue again. Fortunately, nothing is under the Replays tab, meaning we successfully fixed the vulnerability.

The Reproduce feature is equivalent to a regression test. Isn't it great? You do not have to write and maintain regression tests for issues found by GUARDARA. Even better, the Reproduce feature can be called using the API, thus perfectly suitable to be triggered from a CI/CD pipeline.

Are we done fixing bugs yet, you may ask? We can only know for sure if we run another test from scratch. So let's do that, start another test for the same operation and see what we get this time.

Unfortunately, we are not done yet as we ended up with three issues again.

Triage Round #2

Looking at the latest report, we can see that the the web service still does not handle the username, email and password values properly. If you haven't addressed the JSON object handling related exceptions earlier your console may be flooded by exceptions about the problem, thus it may be difficult to find the needle in the haystack. Fortunately, we can tell GUARDARA to reproduce the issue so we can see what was the problem on the console of the web service.

Open the first finding and click the Reproduce button. Go back to the console of the web service. In a couple of seconds you should see the exception we were looking for:

...
Error: Data too long for column 'username' at row 1
at PromisePool.execute (/webservice/node_modules/mysql2/promise.js:359:22)
at createUser (/webservice/routes/v1/user/controller.js:7:51)
at /webservice/routes/v1/user/index.js:28:22
at dispatch (/webservice/node_modules/koa-compose/index.js:42:32)
at /webservice/node_modules/koa-body/index.js:148:14
at runMicrotasks (<anonymous>)
at processTicksAndRejections (node:internal/process/task_queues:96:5)
at async requireApiKey (/webservice/routes/v1/user/index.js:20:5)
at async /webservice/index.js:25:5
...

No surprise, the other two issues are due to the same root cause: the value provided for the properties were too long. Let's look at the message that triggered the problem:

POST /v1/user HTTP/1.1
User-Agent: GUARDARA
Host: 127.0.0.1
Content-Type: application/json
Content-Length: 330
api_key: secret
Connection: keep-alive

{"id": 10, "username": "ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ", "email": "john@email.com", "password": "12345"}

The value of the username property was 256 characters long. If we check the users table's description, we can see why the above payload triggered an error.

mysql> desc users;
+----------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+----------+--------------+------+-----+---------+----------------+
| id | int | NO | PRI | NULL | auto_increment |
| username | varchar(255) | NO | | NULL | |
| email | varchar(255) | NO | | NULL | |
| password | varchar(255) | NO | | NULL | |
+----------+--------------+------+-----+---------+----------------+

The maximum length for the username, email and password fields is 255 characters.

Fix Round #2

It is time to think about input validation. We intentionally did not mention input validation when fixing the SQL Injection vulnerability. Now that we see this new issue, we understand when it is appropriate and required to implement input validation.

Let's assume that the web service expected usernames to be made up of alphanumeric characters, be at least 1 character long and should not be longer than 32 characters. Based on these rules we can create a method that check if the username is valid:

const isUsername = (username) => username.match(/^[a-zA-Z0-9]{1,32}$/) !== null;

When it come validation the email address, creating a regular expression that correctly validates email addresses is no easy task. It is likely best to use a popular library that has this sorted out. Based on a quick search email-validator seems to be a viable option. Let's install this library by executing the following command in the root folder of the web service codebase.

npm install --save email-validator

When it comes to the password field, we should not store passwords as clear text. Insteaf, we should store the salted hash of the passwords. The pbkdf2 library seems to be a good choice.

npm install --save pbkdf2

To summarize:

  1. We will use a custom username validator.
  2. We are going to use an open-source library to validate email addresses.
  3. We are going to use an open-source library that will create salted hashes from passwords. This also means, we do not have to enforce any restrictions on what characters passwords can contain, nor enforce any restriction on the length of the passwords. Therefore, we do not have to implement any validation, yay!

This is how the update updated createUser controller looks like:

const child_process = require('child_process');
const validator = require("email-validator");
const pbkdf2 = require('pbkdf2')
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));

const isUsername = (username) => username.match(/^[a-zA-Z0-9]{1,32}$/) !== null;
const isEmail = (email) => validator.validate(email);

async function createUser(context) {
const { username, email, password } = context.request.body;
if (!isUsername(username) || !isEmail(email)) {
context.throw(400, 'Bad Request');
}
// The hard-coded, static `salt` is insecure, never do it like this.
// We only keep a static, hard-coded salt to keep this example short
// and simple.
const securePassword = pbkdf2.pbkdf2Sync(password, 'salt', 1, 32, 'sha512');
const [insertResult] = await context.database.execute(
"INSERT INTO users VALUES(NULL, ?, ?, ?)",
[username, email, securePassword]
);
if (insertResult.affectedRows === 0) return null;
const [result] = await context.database.execute(
'SELECT * FROM users WHERE id = LAST_INSERT_ID()'
);

// Here we delete the `password` property of the user object so we do not
// return the salted hash.
const userObject = result[0];
delete userObject.password;

return userObject;
}

...

Because of pbkdf2 which produces a binary value, we either have to alter the users table or hex-string encode the generated value. The above implementation assumes that the table was updated as shown below.

ALTER TABLE users MODIFY password BINARY(255);

Re-test Round #2

If we run a test again against the updated createUser operation we will not find any issues. Congratulations, we have implemented a robust operation!

Robust Create User Operation

What's Next?

Try running tests against the getUserById and deleteUser operations as well and fix the issues identified. We recommend starting with the getUserById as it demonstrates how the Target Performance Baselining and Monitoring feature can help you detect application denial-of-service bugs and performance issues.