In our March 2nd blog, we discuss what can be learned from a red teamer when finding a vulnerability in an operational system. Red teamers are expected to find imperfections in systems and configurations, as their role is inherently vulnerability-discovery based.
But this is not always true for quality assurance (QA) testers as part of the development lifecycle. This is a common oversight in many organizational Secure Development Lifecycle (SDLC) processes.
Taking a security-oriented approach earlier in the development cycle has become known as “shifting left.” The further “left” an organization shifts in its security mindset in the development process, the more the shared security responsibility reveals itself across multiple perspectives in the organization.
Stepping back to digest security insight from these perspectives can shed valuable light on where vulnerabilities and misconfigurations exist across a system’s lifecycle — truly helping identify, and correct, faults earlier in the process.
Highlighting three valuable perspectives in secure development is the goal of this discussion. The unique points of view, or perspectives, inform one another to strengthen overall cyber resilience in an organization, maturing the systems we rely upon.
In this series, using a 2017 web application T-Mobile case study, we will examine these through:
- A red teamer’s point of view of a production analysis on a system
- A development-focused point of view examining the late stages of a SDLC with emphasis on Quality Assurance
- A holistic point of view of the design phase and application threat modeling
Through this method, we’ve started at the end and now continue to work our way backwards to show how a real-life case study that was found in production could have been found earlier.
This installment, we discuss that through a QA Tester’s POV.
Why the QA Tester’s POV Matters:
QA Testers significantly contribute to security efforts through testing that code functions as intended. Testing code that’s ostensibly ready to be deployed is their primary mission. Accomplishing this means creating and implementing a test plan, then testing applications and environments based on that plan.
The QA Tester answers the question “does our code function as desired” prior to a code’s deployment, resolving functionality issues during the most cost-effective part of the development cycle with the least reputational harm.
This identifies issues before they make it to the public — identifying and fixing security issues before the application goes into production. The QA Tester catches potential vulnerabilities during the early stage of development when empowered with a security mindset and tools, rather than being viewed as a purely functionality-oriented role.
Analysis — A QA Tester’s Journey
With the T-Mobile Case Study in mind, imagine being the Quality Assurance Tester on a development team. All test plans are focused on an application’s correctness, which can be defined in different ways. T-Mobile’s would be no different.
Our first step would begin with a typical QA plan, but more specifically, it would start with defining what “correctness” looks like for this application before a plan can be set in place.
Defining “correctness”
During a traditional QA process, we are focused on an application’s defined behavior and interactions. By defining “correctness” in terms of the way actors would interact with the code we can collaterally test security of normal operations. Because of this focus on behavioral correctness, however, this first pass at a test plan leaves out behavior that may only be exercised under anomalous conditions. That is something we’ll want to consider when building out a more complete test plan.
While a lot of the time our QA plan is focused on testing functional business requirements, today one of our main objectives is to expand the definition of “correctness” to include security-based requirements. Before we start building out the full suite of tests in our test plan, it is important to distinguish between our traditional functional business testing and security-focused tests. This could be described as:
- Traditional testing (Metrics based on behaviors, code coverage, etc. from a purely functional perspective, employing unit tests, end-to-end tests, etc.)
- Security-focused tests (Common flaws, issues, mitigations, etc. and based on threat modeling, bounds testing, and a search for edge and corner cases in application behavior)
Defining the QA test plan
Now that we are aware of the definition of “correctness” for this QA plan, we need to begin to formulate our test plan with two main measurement approaches in mind: behavioral analysis and coverage-based analysis.
To create a test plan to assess the T-Mobile web application we would want to start with creating tests which cover the greatest amount of use cases (with the least amount of effort) to verify that bugs are not accidentally being coded in.
This is called coverage-based analysis and we use this to trust but verify that developers (even if they are the best developers out there) have a sanity check on the thousands of lines of code they write. We can approach this by creating unit tests, each of which focuses on a specific function in, or subset of the code.
Let’s provide an example of a test we could write to test the code of the T-Mobile web application:
Generate a session ID, and call the “get line information” function using that session ID, an invalid session ID, and an expired session ID. For the first case, verify that a correct value is returned. For the second and third, verify that an error is returned.
We would continue to create test cases like this one to more effectively catch bugs as they are being developed and moved into the “testing” phase of the development lifecycle.
Think of it with this analogy: creating coverage-based analysis test cases is like checking that a house’s doors and windows are all shut and locked. This focus on covering the most code quickly does not mean that the “crown jewel” of your data is necessarily secure — if you chase the metric of coverage percent, you may miss crucial avenues of attack, particularly around deeper error handling.
Adding coverage-based analysis during development and before deployment allows you to check if your figurative doors, windows, and other entry points are secure before a burglar breaks in and steals your valuable assets. A single missed window on your top floor could be how they sneak in.
Once we feel our coverage-based analysis has good coverage (ha!), we will want to move to the next approach in our QA test plan process: behavioral analysis.
Looking at the T-Mobile application, we will want to begin to create positive test cases and negative test cases. The positive tests will check for correct behaviors under normal operation, while the negative test cases will validate that error conditions and edge cases are properly handled and caught. Simply put, test that basic software functionality is behaving as expected when prompted with correct AND incorrect inputs.
For each set of behaviors we identify by decomposing the macro steps in a workflow or user story into the individual actions that enable that workflow, we can create these sets of tests to determine the application’s reaction to good and bad.
Let’s fast-forward to the fault identified in this case study. One workflow that exists within the application allows a logged-in user to view information about the lines on their account; this is a subset of the user profile user story. A positive test case we would create here is a validation of a user attempting to access their own account information — in which case, they would be returned and presented with their information by the system. This is the intended functionality.
What would happen if we stopped testing there? Well, we could mistakenly assume that the functionality is working as intended. This misses the opportunity to discover and fully understand the current functionality, and limitations, of the application.
Let’s walk through what we call a negative test case. In this instance we would want to create a test case where a user attempts to access information that is not theirs (dun dun dunnn). In turn we hope (unless we are a malicious user) that the system does not return the information — perhaps giving an error instead. Once we have created our test cases, we must… go test them!
When we move to the negative test case, we discover something quite interesting. A user can input information that is not theirs, such as a phone number belonging to another T-Mobile user. As we indicated above, this should return some kind of error, rather than user information. Frighteningly, the system returns valid information about the requested user — even though the requesting user’s authentication token does not match the owner of the requested information. In other words, the user can now view information that they do not own.
If we had not gone down the path of performing a negative test case, particularly with the concept of data ownership in mind, this functionality would not have been discovered. While this sounds simple, there are many possible reasons this vulnerability wasn’t caught in the real world.
How might this vulnerability get missed?
Understanding the traditional and the more mature development cycle and the role of QA in that process, the vulnerability at the focus of this T-Mobile use case still seems perplexing. Yet when looking at how the vulnerability works, and how test cases — even in the mature environment — are built, it is more understandable.
As a QA Tester, your concern is in the site’s functionality. What happens though if a site functions as intended, but still has a vulnerability? Your test plans could all come back complete, and this would still not be found.
In particular, the root cause of this vulnerability is one unique to an environment utilizing APIs. As developers and architects have moved away from single monolithic applications and begun creating applications that separate front-end behavior from back-end data access and functionality, implications to data ownership have appeared that weren’t a concern in other more traditional design paradigms.
Among other things, a shift from the context of “single application, single user” to “single application, multiple user contexts” means a need for multiple layers of authorization — from the obvious functionality-level considerations (does this user have permission to execute this functionality at all?) to the less obvious object or data level considerations (ok, they’re allowed to call this function, but are they allowed to access the data object they requested?).
This shift to a hierarchical access model means the possible introduction of new families of vulnerabilities, including this one: Broken Object Level Authorization, or BOLA. No longer is function-level authorization enough. We need authorization in depth.
“Shifting Left” in Early Phases of Development with Threat Modeling
The attention around “moving security left” is helping security and development organizations to work together to solve these problems. By adding a focus not only on intended behavior, but on how the application reacts to unintended inputs and flows, security improvements come earlier, short-circuiting the identification-triage-design-implement loop that previously occurred post-release — if at all. If these teams move security even further left, they may see even more benefits.
By beginning with the end in mind, development teams can look at the various parts of their application holistically and anticipate where there may be vulnerabilities in the process, independent of the code. Then, they can decide how to create their code most securely and functionally in a manner that mitigates the vulnerabilities before they are even typed onto a screen. This practice is known as Security by Design, and one major component is Application Threat Modeling.
In our next blog, we’ll dive deeper into Threat Modeling, looking at how it is implemented, how it can be brought into the development lifecycle even after code has been pushed to production, and how we can train the next generation of developers to think about security before they even start writing their first functions.