Writing abuse cases is an exercise in “thinking like the enemy.” It’s a great way to help secure your software and systems and stay ahead of attacks.
Use cases have become common practice in agile software development to help developers deliver code that meets intended feature requests. Product managers draft use cases to ensure the code they write meets their business objectives. Developers build the application to those specs. In a perfect world, it prevents unnecessary and redundant work.
Unfortunately, the security world hasn’t evolved in the same ways. There are no protections in place to help developers anticipate what a malicious user might do with a feature. But there should be.
Use cases create a path of least resistance for users to get what they want. But you also need to put the most resistance between bad people and the data/access they want. And you want to do this while minimizing the impact on legitimate users.
What you need, then, are the opposite of use cases: abuse cases. You need to think like an attacker at the unit, system, and infrastructure levels. To prepare your codebases for use cases that compromise sensitive information. To prevent embarrassing business failures.
Sure, the bulk of this burden falls to your security and product management teams. It’s impossible to define thorough abuse cases entirely within development. But developers can do a few things now to get the process started. Let’s break these down.
Developers write unit tests for use cases. For each function or method, they write a test or two to ensure the function does what it’s supposed to.
To switch to “abuse case” thinking, ask yourself: How can I interrogate the system for useful information by feeding it unexpected input: the wrong type or size? What happens if I feed it null input? How could I harm the system by calling a function repeatedly?
Include at least one abuse case for every positive test case you have. If the security benefits of coding defensively like this are not immediately apparent, look at it this way: Thinking small will help you think large. If you practice thinking about how to break your functions, then you’ll have an easier time with the following sections.
Unit tests are great for solidifying individual components of your application. But security errors often happen at the intersection of two components. For that, we’ll need something a little more integrated.
The system view is how most of the bad people will access an application—interfacing with it as a user or as a scripted farce of a user.
System-level abuses cases are a bit trickier, but with enough thought, you can avoid a number of scary scenarios and build protections into your automated testing suite. Selenium has become the default for testing web applications. Using Selenium, you can test for a number of major flaws, including SQL injections, cross-site scripting attacks, and session hijacking.
Next, look at your authentication:
Your system is your code, but your system is not ONLY your code. Remember human factors too:
Another method of attacking your application happens only at scale: the dreaded DDoS attack. Hackers are finding increasingly creative ways to bring your system down by maxing out resources—commonly CPU, bandwidth, or RAM (whichever breaks first). When multiple attackers work together, it’s called a distributed denial-of-service or DDoS attack.
A 2015 DDoS attack leveraged the sizable daily traffic to China’s Baidu search engine. Hackers inserted malicious code into the search results page, causing anybody using the search engine to unwittingly participate in a DDoS against GitHub.
You also might remember an early episode of “Mr. Robot,” where they took this one step further. After society’s DDoS attack caused the servers to reboot, they installed a rootkit that gave them control over the Evil Corp servers.
Organizations try to combat high load with auto-scaling, which leads to a secondary vector of attack—bankrupting a company with infrastructure costs. Oh, and people can also attack hard drive space too.
These types of exploits can only happen at scale. The abuse cases are obvious:
There are plenty of load-testing tools that you can use, from the simple siege tool to the more complex WebLOAD, and more. Remember, with abuse cases you are trying to break your app in as many ways you can think of.
It’s not a pragmatic use of your time to attempt to document all the ways your system could be hacked and all the methods people might use. Luckily, just like Alan Turing didn’t do all his cryptography by hand, you can get a computer to do this for you.
Enter fuzzing.
Developed at the University of Wisconsin, fuzzing is a technique where random input, pseudorandom input, or user behavior is applied to a system. This technique is incredibly useful when it comes to helping you detect the unexpected.
Let’s take a look at this example. Here’s a painfully simple PHP function:
function doubleNumber($x) {
return $x * 2;
}
A simple, deterministic test might run it through a hundred or so variations and just ensure that it’s working.
public function testPoorly() {
for($i = -100; $i < 100; $i++) {
$this->assertEquals(doubleNumber($i), $i * 2);
}
}
This is obviously ridiculous, but it becomes more interesting when we add fuzzing to it:
public function testMultiplicationProperties() {
for($i = 0; $i < 50000; $i++) {
$positiveRand = mt_rand(1, 1000000);
$isDoubleBigger = $positiveRand < doubleNumber($positiveRand);
$this->assertEquals($isDoubleBigger, true);
}
for($i = 0; $i < 50000; $i++) {
$negativeRand = mt_rand(-1000000, -1);
$isDoubleSmaller = doubleNumber($negativeRand) < $negativeRand;
$this->assertEquals($isDoubleSmaller, true);
}
}
So here we have a much wilder and more random test of our simple function that tests random numbers between 1 and 1 million, 50,000 times in a row. While it’s doubtful that you’ll run into any flaws trying to double a number in PHP, this becomes infinitely more complex the instant you start writing “real functions” that do “real things.”
Note that another emergent property of fuzzing is that you end up testing properties of your code instead the direct results. In the above example, the code tests:
Now our very simple example has become fairly interesting. Fuzzing has that effect on testing. You’ll start to notice some unexpected results soon, even at the unit test level. You can even apply fuzzing at the system level with Selenium.
Hacks can happen at every layer, so be prepared. Writing misuse or abuse cases is an exercise in “thinking like the enemy.” It’s a great way to train yourself to have a security-first mindset. If you’re actively thinking of ways that your system may be compromised, then you’re that much further ahead in the great arms race of software security.