Picture this: You’ve written a C library to parse complex file formats. You’ve meticulously reviewed the code, run it through static analyzers like Coverity and PVS Studio, and fixed every warning.Â
Confident in its robustness, you deploy it. Months later, a malformed input triggers a heap overflow, leading to a critical remote code execution vulnerability.
This scenario is not hypothetical. It’s a recurring theme in software built with memory-unsafe languages like C/C++.Â
Static Application Security Testing (SAST) tools are indispensable, but they operate in a vacuum, analyzing code without executing it. To uncover vulnerabilities that lurk in runtime behavior, you need a dynamic approach: fuzzing.
‍
‍
SAST tools are great! They’re like spellcheck for code. They’ll tell you that scanf() might be unsafe, or that your malloc() is missing a free().Â
But here’s the problem: they can’t see runtime behavior. Besides this they suffer from two burdening issues:
Take FreeRadius, the most widely used RADIUS server in the world powering major Internet Service Providers and telecommunications companies. For years, they relied on secure coding standards and industry-standard SAST tools like Coverity, PVS Studio, cpp-check, and others.Â
Then, in 2017, they got fuzzed. The result? A parade of vulnerabilities they never knew existed like buffer overflows triggered by malformed packets. SAST had missed them because the bugs only surfaced when the code was executed with specific inputs.
SAST is like checking if your car has seatbelts. Fuzzing is like crashing it into a wall to see if the airbags work.
‍
‍
Fuzzing or fuzz testing is an automated dynamic analysis technique designed to discover bugs by injecting invalid, unexpected, or random inputs into a program.Â
Unlike SAST which just inspects source code, fuzzing tracks how the software behaves during execution.
Modern fuzzers operate in three key phases:
Fuzzing is not random. It’s a systematic exploration of your program’s attack surface, which uncovers edge cases that human auditors and SAST tools overlook.
While John is being promoted from developer to customer by his manager, let’s crush denialism with some statistics and evidence:
The data is clear. From Curl to the Linux kernel, fuzzing has become the most effective way to uncover memory corruption flaws in C/C++ codebases.
‍
Now that you’re convinced about the benefits of fuzzing, here’s some homework till the next post:
This is an overview of a standard fuzzing campaign. It’s not always this straightforward. The real challenge in a fuzzing campaign is writing the harness and ensuring best coverage.Â
In the next series of posts, we’ll explore the internals of a fuzzer and practise fuzzing hands-on by writing some harnesses for prominent and complex open-source software.
‍
Fuzzing isn’t a silver bullet or a replacement for SAST or code reviews. It’s a complementary force multiplier.Â
By dynamically probing your code’s execution paths, it exposes vulnerabilities that static analysis cannot see. For projects in memory-unsafe languages like C/C++, it’s a critical line of defense against exploits that could lead to breaches, data loss, or regulatory penalties.
In Part 2, we’ll explore the internals of a fuzzer and some history of fuzzing. In part 3 we’ll get hands-on with advanced techniques, writing effective harnesses for stateful systems, integrating fuzzing into CI/CD, and interpreting crash reports to diagnose root causes.
Remember that fuzzing is your strongest weapon in the fight against memory corruption, but it's not a universal solution. Security begins with developer education and a commitment to secure-by-design principles, and AppSecEngineer is your best-friend in this endeavour.
Fuzz testing (or fuzzing) is an automated testing technique that injects random, malformed, or unexpected inputs into a program to find vulnerabilities like buffer overflows, memory leaks, and undefined behavior. Unlike static analysis, which only inspects code, fuzzing actually runs the program to uncover runtime bugs that could lead to security exploits.
SAST analyzes source code without executing it, identifying potential vulnerabilities based on coding patterns and rules. However, it cannot detect runtime issues like memory corruption, race conditions, or logic flaws that only manifest during execution. Fuzzing, on the other hand: Executes the program dynamically. Finds vulnerabilities triggered by real-world inputs. Identifies memory safety issues in languages like C/C++.
Fuzzing techniques generally fall into these categories: Mutation-Based Fuzzing: Takes existing valid inputs and mutates them to generate test cases (e.g., AFL, libFuzzer). Generational Fuzzing: Uses models of input formats (e.g., file formats, protocols) to generate structured test cases (e.g., Peach Fuzzer). Coverage-Guided Fuzzing: Uses code coverage feedback to generate inputs that explore new execution paths (e.g., AFL++, libFuzzer).
Fuzzing is highly effective at discovering: Buffer overflows (stack/heap overflows). Use-after-free vulnerabilities. Integer overflows and underflows. Memory leaks and null pointer dereferences. Race conditions and concurrency issues. Logic errors triggered by malformed inputs.
OpenSSL: Fuzzing discovered CVE-2022-3602 and CVE-2022-3786, critical buffer overflow flaws that could have led to remote code execution. Windows TCP/IP Driver: CVE-2021-24086, a fuzzing-discovered RCE vulnerability that allowed remote system takeover. Linux Kernel: Google’s Syzkaller found over 4,500 security bugs, including privilege escalation and memory corruption vulnerabilities. Curl: Fuzzing revealed multiple RCE flaws, such as CVE-2021-22901 and CVE-2023-38545, in its TLS and SOCKS5 implementations.
The effectiveness of fuzzing depends on runtime and coverage. Ideally, a fuzzer should run continuously for hours to days to maximize bug discovery. Critical open-source projects (e.g., Linux, OpenSSL) run fuzzing campaigns indefinitely as part of their CI/CD pipelines.
Yes. Fuzz testing can be integrated into DevSecOps workflows using: Google’s OSS-Fuzz (for open-source projects). GitHub Actions or GitLab CI/CD for running fuzzers regularly. Custom scripts that execute fuzzing on new commits and flag crashes.
While fuzzing is most effective for memory-unsafe languages (C, C++), it can also detect logic errors in languages like Python, Java, and Rust using tools like: Python: Atheris (libFuzzer-based). Java: Jazzer (for JVM fuzzing). Rust: cargo-fuzz (libFuzzer-based).
Fuzzing is a powerful tool but has some limitations: It requires good test harnesses: Poor harness design can limit code coverage. Not all bugs are found: Some vulnerabilities require human analysis and additional testing methods. High resource consumption: Long fuzzing runs can be CPU-intensive. Limited for non-memory-related bugs: Fuzzing excels at memory safety issues but isn’t always effective for business logic flaws.
Fuzz testing is automated and systematic, while penetration testing is manual and targeted: Fuzzing: Finds low-level software vulnerabilities like buffer overflows and memory leaks. Penetration Testing: Identifies security weaknesses in applications, networks, and configurations. For robust security, both approaches should be used together.