If you’re pentesting web applications, you certainly come across a lot of JavaScript. Nearly every web application nowadays is using it. Frameworks like Angular, React and Vue.js place a lot of functionality and business logic of web applications into the front end. Thus, to thoroughly pentest web applications, you have to analyze their client-side JavaScript.
This blog post shows you how. It covers the basics of static and dynamic analysis, introduces obfuscation and deobfuscation, and explains how to bypass code protection mechanisms while giving practical examples and suggesting the proper tools for particular tasks.
Note that this blog post is quite long. You can and should skip the topics you already are familiar with. Just use the Table of Contents above to easily navigate through the document. Now, without further ado, let’s dive in! ⤵️
Static Analysis
Static analysis is the analysis of software without its execution. The objectives of static analysis can be numerous. URLs within the code may increase the attack surface and reveal broken access controls. Code could also contain sensitive information like passwords, secrets or API keys. Also, the use of dangerous functions or outdated software might introduce vulnerabilities to the application.
Gather JavaScript Code
To perform a static analysis, you first need to gather the JavaScript code. The easiest way I know of is to use Burp Suite as follows:
-
Filter Proxy HTTP history to only show files with the js extension:
Burp Suite’s proxy history
-
Mark the resulting list of JavaScript files and Copy URLs:
Proxy history’s filter settings
-
Save the URLs to a text file:
Context menu to copy selected URLs
-
Use
wget -i urls.txt
to download them:Batch download with wget
Alternatively, you can use the developer tools of your browser, to download files one by one:
Chromium’s developer tools
Identify Endpoints
To discover endpoints and parameters in JavaScript files, you can use LinkFinder. Results can be either saved to HTML or printed to stdout:
$ python linkfinder.py -i 'js/*' -o result.html
$ python linkfinder.py -i 'js/*' -o cli
The combination with other command line tools can also be beneficial:
$ python linkfinder.py -i 'js/*' -o cli | sort -u | grep rest
/rest/admin
/rest/captcha
/rest/chatbot
/rest/continue-code
/rest/continue-code/apply/
/rest/continue-code-findIt
/rest/continue-code-findIt/apply/
/rest/continue-code-fixIt
/rest/continue-code-fixIt/apply/
/rest/country-mapping
/rest/deluxe-membership
/rest/image-captcha/
/rest/memories
/rest/order-history
/rest/products
/rest/repeat-notification
/rest/saveLoginIp
/rest/track-order
/rest/user
/rest/user/authentication-details/
/rest/user/change-password?current=
/rest/user/login
/rest/user/reset-password
/rest/user/security-question?email=
/rest/user/whoami
/rest/wallet/balance
Detect Secrets
To detect secrets in code, you can use TruffleHog.
Earlier, TruffleHog focused on secrets within git repositories.
Nowadays, it natively supports filesystems and more.
Just make sure to use the sub-command filesystem
.
$ ./trufflehog filesystem ~/Downloads/js --no-verification --include-detectors="all"
🐷🔑🐷 TruffleHog. Unearth your secrets. 🐷🔑🐷
Found unverified result 🐷🔑❓
Detector Type: AWS
Decode Type: PLAIN
Raw result: AKIAIOSFODNN7EXAMPLE
File: ~/Downloads/js/main.js
Burp Suite Professional users can also use JS Miner to detect endpoints and secrets.
Issues will show up in Burp Suite’s Issue dashboard as soon as JavaScript files are passively analyzed.
Burp Suite’s Issue dashboard
In my experience, JS Miner delivers the best results for detecting secrets and endpoints in JavaScript.
Detected secrets of JS Miner
If you are searching for something specific, you can of course also use basic command line tools like grep
.
Keywords you might want to try are:
password
admin
login
token
user
auth
key
Locate Dangerous Functions
An open-source analysis tool to detect vulnerabilities in code is Semgrep. You can configure your own detection rules or use rules created by the community. These are able to detect secrets but also the use of potentially vulnerable methods:

Semgrep detecting a JWT and an innerHTML function
Interesting functions and properties in JavaScript are for example:
Element.innerHTML
eval()
window.postMessage()
window.addEventListener()
window.localStorage
window.sessionStorage
document.cookie
Discover Outdated Libraries
Outdated JavaScript libraries often contain vulnerabilities. A common example is jQuery. Often, you can find version information in the path or file name of the library or as a version string in the file itself.

Version strings in path and file
To check whether vulnerabilities are published, you can use online services such as snyk.io.

Results from snyk.io for jQuery 2.2.4
Burp Suite Professional has a built-in dependency checker that automates this procedure. Additionally, the Burp Suite Professional extension Retire.js can be used.

Outdated jQuery version detected by Retire.js
Attention: Before reporting, verify that the web application is actually vulnerable! Vulnerabilities in libraries often affect only specific functions. If the web application does not use these functions, it is not vulnerable despite including the library. Search the web application’s JavaScript for vulnerable functions with the methods described above.
Dynamic analysis
Dynamic analysis is the analysis of software during its execution. Generally, you do not want to analyze the entire software but only a specific part or function. This lets you reconstruct its functionality and is superior to static analysis for complex computations.
Basic Tools
The basic tools for dynamic analysis of JavaScript are your browser’s developer tools. I prefer the developer tools of Chromium to Firefox’s due to their higher performance.
You can open them either via the main menu bar or by pressing F12.
Opening developer tools in Chromium
This will open a toolbar in your current active tab. Choose the Sources tab.
It consists of 4 parts:
-
The source files of the current page
-
The content of the selected file
-
Tools for debugging
-
The interactive console (open with ESC if not present)
Developer tool’s Sources tab
Example Application
I programmed a small example application to demonstrate the basic techniques.
It features a simple ping service, where you can enter a host and receive the output of the ping command.
Such a service ought to be vulnerable to command injection. So, let’s take a closer look.
Exemplary ping service
Client-Side Filtering
First, I analyze the request that is sent when the submit button is pressed.
A JWT is sent to the server that among other values contains the host to be pinged.
JWTs are signed. This makes it impossible for us to manipulate the host value within Burp Suite or perform an active scan effectively without invalidating the signature.
Request being sent in Burp Suite
Thus, I try to send the basic command injection payload Clarified command injection and inserted payload into input field127.0.0.1;id
from the web application itself.
The image below shows how the injection is supposed to work.
But the special character Filtered request in Burp Suite;
is filtered on the client-side. The host
value inside the JWT contains the value 127.0.0.1id
.
Finding the Entry Point
To analyze what’s happening when the submit button is pressed, I right-click it and choose Inspect.
Inspect HTML element
This opens the Elements tab in the developer tools.
The buttons’ HTML does not reveal anything as there is no Event Listeners tab in developer toolsonclick
property.
But choosing the Event Listeners tab shows that an event listener was registered to call the submit function in line 3 of secure.js.
I switch back to the Sources tab by clicking the link to Breakpoints in the Sources tabsecure.js:3
.
To analyze the function, I create a breakpoint in line 4 by clicking the line number on the left.
The breakpoint is displayed in the toolbar on the right.
The Debugger
I click Submit again.
The JavaScript execution stops at line 4 and the debugger starts.
I step through the particular JavaScript statements by pressing F9 or clicking the appropriate symbol on top of the toolbar.
Debugger tools to step through instructions
After each statement that contains a variable, its contents are displayed next to it after being executed, highlighted in yellow.
A regular expression seems to remove all special characters but Variable values are displayed inside the debugger._-
.
Before the sanitized host value is submitted as payload to the JWT, I use the Scope section of the toolbar to change the value of the variable back to the original input.
Changing the value back to
127.0.0.1;id
Hitting the play button resumes code execution as normal and shows that the command injection was successful.
Results of the
id
command
General Advice
Three more notes:
-
To detect the entry point, it sometimes is helpful to set a breakpoint on Any XHR/fetch via the toolbar. This way, the execution pauses before the XHR is sent and all of the computed variables of the previous statements are displayed.
Add XHR/fetch breakpoint
-
The signing key is of course also present in the JavaScript. It could be extracted and used in Burp Suite to calculate the correct signature there. I’ll cover this in a follow-up thread.
-
DOM Invader is an extension to Burp Suite’s built-in browser that helps to test for DOM XSS. PortSwigger’s resources to it are pretty good - as usual. If you want me to cover it in a blog post as well, just reach out.
Obfuscation & Deobfuscation
Before we get into obfuscation techniques, let’s start with the concepts of minification and beautification.
Minification
Minification is the process of reducing the file size of JavaScript, while maintaining its functionality. This is achieved by removing unnecessary characters like white spaces or new lines. Sometimes minification also includes shortening variable names and refactoring the source code. Many JavaScript frameworks minify code to be published by default.
The goal of minification is to increase the efficiency of the JavaScript’s transmission. The following is a (somewhat artificial) example of minified code, created with UglifyJS, but clarifies the point.

Example of minification
The file size decreased by a notable amount of ~40%, by performing the following transformations:
- White spaces, new lines, comments and unnecessary curly brackets were removed.
- The local variable
input
was renamed toe
. - The if-else-statement was replaced with the so-called ternary operator (x?a:b).
By the way, you can often recognize minified sources by their extension .min.js
.
Beautification
Beautification is the process of making minified code human-readable again. There are several ways how to beautify minified code:
- The Chromium debugger pretty-prints JavaScript sources by default. This adds indents and line breaks to the code.
- To beautify local JavaScript files, you can use a tool like js-beautify:
# Install $ pip install jsbeautifier # Usage $ js-beautify main.min.js > main.js
- There also is an online service:
However, note that variable renaming cannot be reversed with this process.
Source Maps
A way to completely recover minified code are so-called source maps.
Source map files just contain a JSON object as can be seen below.
The crucial part is the Example of source mapsmappings
string, which contains the mapping between the original and the minified code.
Mappings are VLQ Base64 encoded, which I will not discuss in detail here.1
You can visualize the mapping via source map visualizers.
For example, Example of source map visualization23->2:5
means that the word at column 23 e=
maps to the word at line 2, column 5 input=
.
Sometimes source maps are shipped along with the minified code.
This is done by adding a comment to the minified JavaScript that contains the location of the source map.
The source map will only be loaded when developer tools are opened.
Reference to source map from minified JavaScript
Minification and Analysis
During the analysis of JavaScript, minification is not that big of a problem. Yes, without source maps, the code is less comprehensible due to irreversible transformations. However, secrets, links, and calls to vulnerable functions can still be detected by a static analysis, as discussed above.
During dynamic analysis, the lack of descriptive identifiers is compensated by the debugger in Chromium’s developer tools. It displays the values of variables next to the JavaScript statements after their execution.
Obfuscation
Obfuscation of JavaScript has the goal to prevent its analysis, be it static or dynamic, while maintaining its functionality.
To achieve this, even a loss in the code’s performance is accepted.
Often, obfuscators put words and symbols from within the code into an array.
During the execution, the original code is rebuilt by recurring references to that array.
The image shows an example of this so-called packing.
Example of a packed array in obfuscated code
document.getElementById("input").value
➡️
document[d(0x0)](d(0x1))[d(0x2)];
d
is a function that references the array, defined in function a
.
0x0
is the first string, 0x1
is the second, and so on.
Some other transformations that are frequently used:
- Renaming identifiers to hexadecimal strings
- Self-defending code that breaks when beautified
- Injecting dead code that increases the file size
Some tools even offer debug protection making it “almost impossible to use the debugger function of the developer tools”2. There are several obfuscators across the Internet. A popular one is javascript-obfuscator You can use it from the command line or online.
Many more obfuscators in diverse variations exist.
A prominent one is JSFuck.
It uses only these six characters: Example of obfuscation with JSFuck![]+()
Deobfuscation
Deobfuscation is the process of making obfuscated code human-readable again. As you can imagine from the examples above, this is not trivially possible. Beautifying obfuscated code is only one step and still leaves the code nearly unreadable.
Additionally, deobfuscators often rely on the following techniques:
- Unpacking arrays
- Replacing proxy functions
- Removing dead code branches
- Renaming identifiers
There also are a lot of deobfuscators available on the Internet. An example is javascript-deobfuscator. It’s usable from the command line or online again. I also like the results of JSNice. It renames identifiers based on a statistical model, which often helps to understand code better.
Obfuscation and Analysis
Unfortunately, deobfuscated results usually don’t get anywhere near the original JavaScript.
Still, they often help to simplify the code flow, which is an advantage for a subsequent dynamic analysis.
The following image illustrates the transformations from plain to obfuscated and finally deobfuscated code.
Transformation from plain to obfuscated and deobfuscated code
Hands-On: Analyze obfuscated code
After having covered the topics static analysis, dynamic analysis and obfuscation of JavaScript, I will go through the dynamic analysis of obfuscated JavaScript using an example application. In each step, I will explain my approach, procedure and conclusions.
Before diving into the tools for dynamic analysis, I want to lose a few words about the appropriate mindset. Dynamic analysis can be a painstaking process. Sometimes it helps to recall that whatever client-side function you’re searching for definitely IS present in the client-side code. As an example: If a web application signs data before sending it to the server, the signing key MUST be in the client-side code. This attitude helps you to try harder.
Example Application
The application I am going to analyze is the ping service from above but with obfuscated JavaScript.
The user submits a host and receives the ping response of the server.
The interception of the request in Burp Suite shows that the application sends a JWT, containing the host (IP) of the target server.
Request being sent in Burp Suite
As JWTs are signed, it is not possible to manipulate the host value without invalidating the signature. This prevents me from manipulating the request within Burp Suite, which is essential for a pentest. But as the JWT is calculated with JavaScript, the key must be somewhere in the client-side code. The aim is to extract the secret key to perform a proper analysis of the endpoint.
Finding the Entry Point
First, I want to find out what happens when the Submit button is clicked.
For that, I visit the Elements tab of developer tools and check for event listeners.
This reveals a click event listener.
Event listener in Chromium’s developer tools
A click on the link opens the Sources tab and highlights the position of the listener function.
Note how Chromium’s developer tools beautified the obfuscated code, which actually is written inside a single line.
Beautified minified JavaScript with the highlighted submit function
I put a breakpoint on the first statement within the submit function and click the Submit button.
The breakpoint triggers and pauses the execution.
This means, I found the correct position and can now go through the execution step by step.
Paused execution on the breakpoint
Locating the Value of Interest
At some time, the ready-to-send JWT must be stored inside a JavaScript variable.
I use the Step over next function call button of the developer tools to find this position.
The highlighted lines below contain the JWT header, payload and finally the encoded JWT, starting with the characteristic string Locating the calculation of the JWTey[...]
The signing must happen somewhere in the line in which the JWT is calculated.
I put a breakpoint in that line and delete the old one to skip unnecessary statements during the analysis.
Afterwards, I click the Submit button again.
New breakpoint on calculation of JWT
The Unpacking Function
Now, the tricky part begins.
I use the Step button to proceed through the execution.
This leads to the unpack function of the obfuscated code, where the JavaScript symbols and strings are loaded from the array.
Function
0x246a()
contains the packed array
The entire array is referenced by a variable now.
This can be observed from the Scope section of the developer tools on the right.
The packed array observable from the Scope section
This step shows that the 20th entry of the Array is going to be returned.
The unpacking function doing its work
Indeed the value Returned variable JWS
is returned, which had the index 20.
0x51f223
contains the value JWS
Reconstructing the Signing Call
Using the same method iteratively, I observe that the next unpacked values from the array are:
sign
HS256
6465623837323564656533323462383134656535386133626434353431373866
Last step of unpacking before JWT calculation
This lets me reconstruct the call that is used to calculate the JWT:
The structure of the line that calculates the JWT is KJUR['jws'][a][b](c,d,e,f)
.
The variables a
, b
, c
and f
are the unpacked values from above.
The variables d
and e
are the header and payload of the JWT.
Putting all together and replacing array- with dot-notation leads to the following function call:
Origins of the obfuscated variables and function callsKJUR.jws.JWS.sign("HS256", {header}, {payload}, "6465[...]3866")
Proceeding to the next step validates this.
The scope shows which variables have been passed to the function.
The scope of the
KJUR.jws.JWS.sign()
function
At this point, the simplest method is to research whether this function comes from a JavaScript library and what JWS.sign
exactly is doing. A quick Google search shows that indeed, a library is being used. It is called jsrsasign by the GitHub user kjur.
The documentation shows that the method generates a JWS signature with the specified key.
The key, at position 4 of the function call, is a hexadecimal value in this case.
This means that the long integer from above is the key.
Documentation of jsrsasign
Decoding the Key
I decode the hexadecimal key and encode the result to base64 as this is what I need for the Burp Suite extension JWT Editor.
Using
xxd
and base64
for decoding and encoding
Signing Manipulated JWTs
In the JWT Editor extension, I create a new symmetric key, as HS256 is a symmetric signing method.
I paste the key from the previous step into the Creating a new symmetric key in JWT Editor"k"
field.
I send a request from Burp Suite’s proxy history to the Repeater and switch to the JSON Web Token tab.
Here, I replace the host Using the key to sign the manipulated request127.0.0.1
with infosec.exchange
and click the Sign button.
I send the request with the updated signature to the server and receive the ping output for Ping output for infosec.exchange in the server’s responseinfosec.exchange
.
This means that the signature calculation succeeded and I can now manipulate the value as needed.
Using the Burp Suite extension CSTC, I can now automate the process of signing and perform an active scan on the target, which detects the command injection vulnerability. But this is content for another blog post.
Local Overrides
Local overrides are a way to keep changes to JavaScript across page loads. In certain situations, this may be helpful for the analysis.
Typical use cases are:
- Changing the code flow to bypass client-side protections
- Adding
console.log
statements to log variable contents - Refactoring code for beautification and deobfuscation
Temporary Changes in Developer Tools
Chromium’s developer tools allow you to change the content of sources right in the Sources tab.
I’ll use the ping service from above for demonstration purposes.
Lines 5 and 6 implement a client-side filter that removes special characters from the host value.
Client-side filter highlighted in the Sources tab
To bypass the filter, I commented out both lines and saved the changes with Ctrl+S.
The warning sign in the top bar states that these changes are not persistent.
Temporary code changes
As you can see, the filter was successfully bypassed and allows command injection from the web application itself.
Disabled filter allows command injection
However, as soon as the page is reloaded the changes are gone. Real-world web applications often reload pages when navigating through them. To pentest the application efficiently, it is useful to make changes to JavaScript persistent.
Persistent Changes in Developer Tools
Chromium and Chrome have the so-called Local Overrides feature for this purpose.
This lets you override page assets with files from a local folder.
Configure it via the Overrides tab within the Sources tab.
The Overrides tab within the Sources tab
After allowing Chromium access to the local folder, you can save sources via the item Save for overrides in the context menu.
Save file for overrides
This will add a folder for the path within the overrides folder and save the source to it.
Changes to this file are now persistent as the purple dot indicates.
Persistent code changes
Persistent Changes in Burp Suite
Sadly, this feature is not usable from within Burp Suite’s embedded browser, as the Overrides tab stays empty.
The reason for this seems to be that the browser is launched with the --disable-file-system
flag.
A feature request has been submitted 2.5 years ago.
But fortunately, there is a BApp for that! The extension HTTP Mock lets you define responses that will be returned instead of the real ones. It works like this:
-
Send the request-response pair to the extension.
Context menu for request editor to mock an HTTP response
-
Make your changes and don’t forget to press the Save button.
Burp Suite’s HTTP Mock extension
-
Reload the page in the embedded browser and you can see that the changes have been adopted.
Persistent code changes delivered to browser
Bypass code protection
JavaScript obfuscators offer features to protect obfuscated code from deobfuscation and analysis. As an example, obfuscator.io has the following protection measures:
- Self-defending: Breaks code when beautified or deobfuscated
- Debug protection: Prevents the use of the debugger statement
- Disable console output
This section shows you how to bypass all of these measures and provides tips on how to do so with other obfuscators.
Setup
One after another, I will apply protection measures to the default options of obfuscator.io and analyze the result.
The screenshot below shows the options for obfuscation.
Options of obfuscator.io
My demo application uses JavaScript to write input of a text field to the page like this:
function process() {
let input = document.getElementById("input").value;
document.getElementById("output").innerHTML = input;
}
In each step, I will deobfuscate the obfuscated script using deobfuscate.io. Note that I am making changes to the JavaScript code directly in the source files. In a real-world example, you would probably use local overrides for this, as explained above.
Self Defending
This option makes the output code resilient against formatting and variable renaming. If one tries to use a JavaScript beautifier on the obfuscated code, the code won’t work anymore, making it harder to understand and modify it.
When visiting the page in Chromium, it freezes, giving me no way to analyze what’s happening at all.
Chromium page is unresponsive
Next attempt: Firefox
The result is different and more promising:
The error message Error message in Firefox’s JavaScript consoleUncaught InternalError: too much recursion
and a stack trace that reveals the cause of the problem are displayed.
Line 41 contains the regular expression Obfuscated code with a regular expression that sticks out(((.+)+)+)+$
, which exhibits catastrophic backtracking.
As you can see, the function Obfuscated code with a regular expression that sticks out_0x3b1622
is called in line 43, before the actual JavaScript code of the web page, which starts in line 44.
Thus, the bypass is as simple as commenting out line 43 (or line 41 respectively).
As a result, the page is loading in Chromium and is still functional.
This approach is generalizable to any self-defending code of obfuscator.io.
The template of the self-defending function is defined here.
The regular expression is hard coded into it.
Template of self-defending code from obfuscator.io
As a result, the bypass can be as simple as removing this certain regular expression, for example with sed:
sed -i 's/(((.+)+)+)+\$//g' file.js
Debug protection
This option makes it almost impossible to use the debugger function of the Developer Tools.
Visiting the page in Chromium while the developer tools are opened, immediately pauses the execution and opens the debugger.
Also, one seems to be stuck in an anonymous function that is calling the debugger again and again.
With closed developer tools, everything works as usual.
Debug protection opens debugger in anonymous functions
The easiest way to get rid of this is to prevent the call of the anonymous function you’re stuck in.
For this, I recommend using the Call Stack of the developer tools.
Click through the particular calls until you find one that seems easy to comment out without breaking something.
The further you proceed through the stack, the more likely it is that the function is relevant for the web page to work.
Thus, stay as close to the top as possible.
The call stack yielding valuable information
In this case, I decided to delete the Disabled debug protection by removing the else
statement in line 54 as it contains the call _0x3c9b48(0)
, which eventually led to endless calls of the debugger.
And indeed, the page is now loading in Chromium without starting the debugger.
else
statement
Disable console output
Disables the use of
console.log
,console.info
,console.error
,console.warn
,console.debug
,console.exception
andconsole.trace
by replacing them with empty functions. This makes the use of the debugger harder.
For demonstration purposes, I added a A console.log
statement into the process function that should print the value of the input field to the console.
However, the console output stays empty due to the disabled console output of obfuscator.io.
console.log
statement added to the obfuscated code
There are multiple ways to bypass this measure.
-
Remove the symbol
console
from the packed array. This works because the obfuscated code needs to override the console object, for which it needs its name. Still, the packed array could be obfuscated in a way such that this is not simply possible.The packed array contains the symbol
console
-
The template of the function to disable console output is here. It contains the name of the console functions to be overridden:
log
,warn
,info
,error
,exception
,table
,trace
.Methods to be overridden from obfuscator.io
The obfuscated array of methods to be overridden
-
Use the console function from an embedded iframe. This is by far the most reliable method. It works by appending an iframe to the current document via JavaScript. This iframe has its own internal DOM, which can be accessed via its
contentWindow
property. The following code shows this:var iframe = document.createElement("iframe"); document.body.appendChild(iframe); iframe.contentWindow.console.log("This is logged!")
I added this code to the obfuscated code.
The above code added to the obfuscated code
console
method of the document with the one from the iframe like this:console = iframe.contentWindow.console;
The following screenshot shows that console output is working again.
Re-enabled console output
General advice
Depending on the degree of obfuscation and the obfuscator used, the above techniques might not be applicable. If possible, you should always try to find out which obfuscator has been used. Minimal examples as above give a great idea of how the obfuscation works and can be generalized to larger code bases.
Always try to run and analyze JavaScript in different browsers or JavaScript engines. As seen above, the results might slightly differ. In this case, Chromium froze and Firefox threw the error with the essential piece of information: The line number with the recursion. You can also use services like jsconsole.com.
Recall that whatever client-side protection is in place can also be removed. In the end, that’s the fun thing about JavaScript hacking 😏
Wrapping Up
This wraps up my blog post JavaScript Analysis for Pentesters. Did you find it valuable?
-
Share it with your friends and colleagues!
-
Follow me on Mastodon for early access to my web security content!
Some References
-
Source maps: Introduction, Update, Production
-
You can read more about it in Chrome’s Introduction to JavaScript Source Maps. ↩︎
-
This is stated by https://obfuscator.io/ under
debugProtection
↩︎