# Leveraging TDD to Achieve Production-Ready API Security :::info :warning: This article only represent me, an indie software engineer who likes to explore new things. Read it with a grain of salt! ::: I know the title might be controversial, who would use TDD in real agile-production anyway? I too honestly think that TDD should not be used, **until** I deal with (developing) application which has many API endpoints with sensitive informations. In such case, application security is critically **crucial**, because basically when you add new API endpoint to your app -- consequently -- you are "adding" more attack surface / entrypoint to your app, therefore putting your application sensitive data and feature onto risk. In this article we will discuss, how we as software engineers can and *should* leverage TDD when creating APIs to make our app achieve -- bare minimum -- production-ready application security. ## :green_book: Test-Driven Development Test-Driven Development or TDD is a paradigm in software engineering that mandates us to create features based on sets of test that has already made beforehand, therefore the test cases will "drive" our development goals and pace when creating related features. In a nutshell, we develop features that should passes all the test listed, therefore achieving application that's -- supposedly -- robust and production-ready. In practice , the idea of TDD is we have to first think of scenario that our features will "meet" in production. we then create tests based on this. After that, we will focus developing feature that fulfills or passses all of the test cases. lastly, we will optimize our feature (and of course it should passes all the tests) ### TDD Cycle Based the steps above, we can divide the development onto 3 parts, they are **RED**, **GREEN** and **REFACTOR** stage. These stages might sounds not intuitive and indefinitive, but all it defines is the state of our feature with respect to the test cases. ![TDD cycle](https://content.codecademy.com/programs/tdd-js/articles/red-green-refactor-tdd.png) <p style="text-align:center;font-style:italic"> TDD Cycle taken from <a href="https://www.codecademy.com/article/tdd-red-green-refactor" target="_blank">codecademy</a> </p> #### 1. RED phase In this phase you will focus on creating test based on your imagination -- or insight beforehand -- scenario that your feature will face in production. If this is your initial cycle, It's not necessary for you to create such complex and comprehensive test cases hollistically, as long as your test cases covers the core functionality. On the other hand, If you've done at least 1 cycle, you will eventually come back to this phase, because usually you will then find bugs or some cases where your feature doesn't work as intended, called edge or corner cases. To prevent this, you need something called positive-negative test where you have to create test cases that should expect your feature does task A correctly when given correct inputs, and handle error for task A when given incorrect inputs. Let's use example to understand this. suppose you are trying to create function to add 2 number and return the result. ```js= function addNumber(x1, x2) { //TODO } ``` inside your mind, you might think easily that our test case will just then have to test that `x1 + x2` equals to something. But what if the inputs are actually not numbers, rather numerical strings? what if the inputs are neither numbers nor numerical strings, but rather some string? your test cases in any testing framework should look like ```js= const expected = //SOMETHING ; const notExpected = "0xDEADBEEF"; const result = addNumber(x1, x2) assert.equal(result, expected) assert.notEqual(result, notExpected) ``` See, these kind of '*what if*' imagination can helps you generate many test cases that is high quality. You can imagine yourself if your function or feature is complex, how many cases including pos-neg corner cases you can generate. At this stage you haven't implemented any functionality, therefore your tests are still failing and usually it would prompt you the test log with **red** colored warning, the reason why it's called **RED phase** #### 2. GREEN phase In this stage you are actually starting to develop your feature, which design and functionality should follows the test cases made beforehand in RED phase. Your ultimate **goal** is to implement feature that should be or find the solution of given task and pass the tests without worrying about optimization and design pattern Let's use the same example in **red** phase ```js= function addNumber(x1, x2){ let result = null if (typeof x1 === 'number'){ result = x1 }else if (!isNaN(parseFloat(input))){ result = parseFloat(input) }else{ return "can't add x1 and x2, invalid number on x1" } if (typeof x1 === 'number'){ result += x1 }else if (!isNaN(parseFloat(input))){ result += parseFloat(input) }else{ return "can't add x1 and x2, invalid number on x2" } return result } ``` The function above should pass all of the 3 cases covered in red phase above. notice that your current implementation need not to be clean and optimized, as long as you pass all the test cases. #### 3. REFACTOR phase When you have implemented the function or feature that passes all the tests, at the end of the day, you will have to try to refactor your code to make it optimized, cleaner and easier to maintain. Let's use the same example as before ```js= function isNumber(x){ return typeof x === 'number' } function isNumeric(x){ return !isNaN(parseFloat(x)) } function getNumber(x){ if(isNumber(x)){ return x }else if (isNumeric(x)){ return parseFloat(x) } else { return null } } function addNumber(x1, x2){ x1 = getNumber(x1) x2 = getNumber(x2) if (x1 == null) { return "can't add x1 and x2, invalid number on x1" }else if (x2 == null){ return "can't add x1 and x2, invalid number on x2" }else { return x1 + x2 } } ``` This way your function is now easier to understand and maintain if changes arise in the future ### Unit Testing In testing scope, there are actually many type of test, such as unit testing, integration testing, regression testing and many more. each of this test scope has the same purpose, that is, to achieve a robust production-ready application, but using different approach. In unit testing, we will test the feature as a granular unit, fractions of your feature that does a single or relatively-simple task. in unit test, you can nitpick, creating as many as possible cases to test the functionality of respective fraction. In fact, the example we did before is considered as unit test. ### Integration Testing When you have done developing features, and each feature satisfies the requirement also passed the unittest individually, you will then have to try integrating all of those feature onto a single or several bigger component. You will be surprised when it turns out that although your feature passed the unittests, it won't be the fact when you test them hollistically as a group of feature. The reason of this is that, when you do unittest you are actually only testing a scope of individual feature, that is considered small and now your features has to communicate each other. Of course, what do you expect? It's also our job to make sure the integrated features works seamlessly and robust. ## :closed_lock_with_key: TDD in API Security After we discussed TDD in previous part, you should've at least get the intuition why we need TDD when developing APIs. Right, by testing our APIs using the TDD cycle, we can then ensure the robustness of our API when given inputs there are not expected. In practice, usually hacker will do bruteforce on sets of API endpoint like giving random payload, common known attack payload or even FUZzing to our API endpoint. when they've found something interesting, they proceed to specially craft payload matching our API vulnerability. This is not something we would like, especially in sensitive API. let's discuss some of sensitive functionalities that's usually affected by attack. ### Authentication and Authorization The first important feature that's commonly attacked is **auth** (refers to both) feature. When we are talking about authentication, what comes to mind usually is login, logout and perhaps checkCredential feature. login feature pretty much contain database access, therefore it's vulnerable to SQL Injection which will be discussed after auth part. Let's talk the checkCredential and authorization feature. > For this example we are going to use JWT to serve both of the auth purposes. When your application auth design is to use JWT, It means for every request sent to endpoint that is guarded by `Authwall`, your application will actually check the JWT right. JWT or Json Webtoken is actually you can think of it as json string encoded to `base64` string and is signed by some cryptographic function to ensure it's integrity ![JWT structure](https://research.securitum.com/wp-content/uploads/sites/2/2019/10/jwt_ng1_en.png) <p style="text-align:center;font-style:italic"> JWT structure taken from <a href="https://research.securitum.com/jwt-json-web-token-security" target="_blank">securitum</a> </p> For authorization (and authentication), several data needed to mark the level of authority of a user is included in the `Payload` section of JWT. for example `ROLE : Admin`, `username: Admin` or `ROLE: authenticated-user`. On basic implementation of checkCredential, the flow would look like this. first, it parse the `Header` part of JWT to check the `type` of token and the `cryptographic algorithm` used for JWT signing. Then using the the chosen algorithm it will try to verify the JWT signature. After that, it will then parse data from the JWT `Payload`, check the token expiration and finally consume the user related data for further process. Now, there are 2 common problem or caveat of JWT, they are 1. Unverified JWT misconfigruation 2. JWT Verification bypass for the 1st one, it's most of the time the developer's fault where they forget to implement verification feature. When the application received JWT , Instead of verifying the JWT `sign` first, the app directly parse the data from JWT `Payload`. This is silly, but is critically dangerous, because then the whole idea of JWT as security measure is pointless. User (attacker) now can impersonate as other role and might gain admni authority by setting `role : ADMIN` in the JWT `Payload` The second issue is related to JWT `Header`. As we discussed before, your app will know what algorithm to verify the `sign` by looking for entry `alg` inside the JWT `Header`. Do you know What if the alg is set to `None`? exactly, the verification process will then just do nothing since the `alg` is set to None, which leads to the same situation as the first issue. This vulnerability is commonly known as it is easy to overlook ![Forging JWT with alg set to None](https://hackmd.io/_uploads/rJm1yt-66.png) <p style="text-align:center;font-style:italic"> Forging JWT with alg set to None </p> For these reasons, You might and should implement TDD on you jwt verification feature. You can rest assured if you are using the latest JWT libraries, since they have already patch this issues. ### SQL Injection on CR(UD) feature Let's say you have endpoint to get user account data ``` /api/v1/user/account?username='STRING' ``` The underlying implementation, is the `username` value will be passed onto query string to database, perhaps like ```js const query = `SELECT * FROM users.account WHERE username='${username}'` ``` Now, whenever your API receive any input from user that is meant to be passed as argument for query string, you must, i repeat, **YOU MUST** sanitize the input with whatever methods you can think of. *Why is that necessary*? you might think. Well let's just use example. what if i as a user make queries like this ``` /api/v1/user/account?username=a' OR 1=1 -- ``` The username passed to query string now will be like this ```js const query = `SELECT * FROM users.account WHERE username='a' OR 1=1 -- ` ``` This is equivalent to `SELECT * FROM users.account` since the OR 1=1 will evalute true for every row inside the `users.account` table. I end up can exfiltrate all data and credential for users. This is only READ query, what will happen if pass UPDATE or even DELETE query :face_palm: Therefore you need to make sure, ***test*** your API to handle and how to behave when given malicious input to prevent this ### Arbitrary (Read) Access The last common attack surface we are going to discuss is Arbitrary access to resource of the application. There are actually many type of this vulnerability, but for the sake of simplicity so you can follow, we are going to use "*Insecure Direct Object Reference*" (IDOR) as the example. Suppose you are building an e-commerce app, and of course you need an endpoint to get user order, transaction, and payment history. let's say you have this endpoint ``` api/v1/user/transcation/payment?payment_id=1 ``` The underlying implementation would look the same as we discussed in `SQL Injection` part before, except now, your query has been sanitized so it's not vulnerable to SQL Injection. *So, it should be safe now*, right? RIGHT?? Whoops, Turns out you might forgot to implement authority-checking for the request correctly. so now i can enumerate `from 1 until n` where i will send request for that endpoint with `payment_id` value ranges from `1 until n` i can access all user's payment id, ended up getting their credit card numbers, debit card, and many more. The common patch for this kind of issue is to use randomized id that is crypthographically secure, like **uuidv4** so now i can't enumerate. But this is actually not enough, your API should do authority-checking for any request made through that endpoint, for example, when I send request to get payment detail with `payment_id=12`, Your app should verify that if such data exist, I am actually the authenticated and authorized user related to that payment detail data(s) before returning the response. ## :books: Conclusion TDD Might not be the best Software Engineering Practice Paradigm. Yes, you and I can hate it, but we have to acknowledge it does serve some purpose that can be critical and crucial to your application. You can leverage TDD to ensure robustness and security in your Application by creating pos-neg and corner cases test cases with malicious inputs to simulate the attack that might be happen to your production app. Therefore, you can achieve the bare-minimum production ready and secure API