For the last year, I’ve been working hard on creating what I believe to be the next stage in the evolution of tooling for penetration testers, web application security testers, security analysts, and security engineers—in short, anyone who spends time testing web applications. And today, I’m happy to announce that it’s finally available for everybody to use!
The post will be split into multiple parts, covering two major domains in which jswzl runs.
This integration makes jswzl a seamless addition to your existing workflow. It operates quietly in the background, working its magic. Plus, it enables two-way communication to perform tasks like fetching source maps and pre-fetching packed chunks, which I'll describe in more detail below.
Addressing the challenge of displaying source code with rich context led me to the realization that existing tools already solve the problem efficiently. Why reinvent the wheel?
With a straightforward extension for VS Code, you can view all files that jswzl has analyzed and see a request history. All your favorite existing extensions work flawlessly, providing a very natural experience. The days of trying to browse code in a tiny panel in Chrome Dev Tools are behind us.
The analysis engine is where the real magic happens. A lot goes on here that might not be immediately apparent. Let's examine some of its key components.
To provide the most accurate and comprehensive analysis possible, it's crucial to process all relevant code and initially, maintain its original form as closely as possible. This is accomplished in two stages:
First, jswzl fetches source maps. Whenever a .js file is requested, jswzl sends another request to see if a .js.map file also exists. If it does, that source map is applied to the source. In many cases, you can even retrieve the original source in its entirety!
Secondly, jswzl looks for chunks that are lazy-loaded. This part is surprisingly complex, given how Webpack functions. Heuristics will detect lazy-chunk-loading code and evaluate it in a small custom runtime/sandbox to compile a list of all chunk names. These chunks are then loaded.
Loading chunks eagerly is particularly crucial in larger applications, where most of the source is loaded as needed. This way, you won't need to manually spider the application to make it load all its chunks.
Sometimes, the source packing retains the original file structure in the code. This means we can often extract the entire source tree with high fidelity, making it much easier to obtain a comprehensive overview of the application's structure.
It’s easy to lose out on meaningful data in source code if it heavily uses variables needlessly (see example in the next section)—jswzl ships with a simple interpreter, which can dereference variables for analysis. The next step (Optimize) clearly demonstrates why this is important.
This code is equivalent to the code seen below. Since there's no semantic difference between the two, jswzl will rewrite it, giving this more straightforward form.
Minified code can be impossible to read. Therefore, jswzl always consistently prettifies the code to enhance readability.
Having skimmed over a lot of detail, we now reach the stage that results in the output you see in the tool: extracting descriptors.
To understand a descriptor's nature, you must know that it represents a part of the AST and can take the following forms:
- String Expression
- Object Schema
- Call Expression
Each of these forms is more complex than the last. For instance, a string literal can contain a path (String Expression), an object can contain a path and headers to set for an HTTP request (Object Schema), and a method call to make an HTTP request includes arguments through an object (Call Expression).
By performing multiple passes over the AST, we first tag nodes with String Expressions, then Object Schemas, and finally, Call Expressions.
While this overview isn't exhaustive, it provides a glimpse into the components that make up jswzl. But how does this impact you, and how can it enhance your testing workflows?
Imagine you're testing a large and complex Single-Page Application (SPA). One of the first things jswzl will do is provide you with a list of all path-looking strings. You can simply copy these values and drop them into Burp Intruder to get an overview of existing endpoints.
You've now identified paths that might interest you. But how do you interact with these endpoints in a meaningful way? By identifying the call sink, jswzl can often show you the data structure that the endpoint expects, thereby making manual testing far more straightforward and faster in most cases.
Today is just the beginning of the journey. Thanks to all those who helped beta test the tool, it's already significantly aiding testers in their daily quest to find cool bugs.
However, I have many plans to evolve the tool further and provide previously unachievable capabilities. Initially, the focus will be on ensuring the tool is easy to pick up and use. After that, I'm excited to explore the many fascinating opportunities for scaling this across teams and organizations based on the concept of taking source code and reducing it to a set of descriptors.
This concept opens up a whole new world of possibilities that I've likely not even considered yet. Feedback from the community and users will be pivotal in determining the roadmap. I'm always eager to hear from people and understand how to empower testers to do their best work.