August 28, 2026
JavaScript for Pentesters Part 2
In my last article, JavaScript for Pentesters: The Way I Finally Understood It, I tried to make JavaScript less scary.

By Humairah Adamu Sidi
8 min read
Variables became containers.
Functions became something like ordering shawarma.
APIs became waiters.
Promises became food orders.
Webpack became packing for a trip.
That helped me understand what these JavaScript concepts actually meant.
But there was still one problem.
Okay, I understand what a variable is. Now what exactly am I supposed to do when I open a real application's JavaScript file?
Because nobody is going to give you this during a pentest:
let userId = 25;let userId = 25;and conveniently tell you:
"Hello pentester, please test this variable for IDOR."
I wish lol.
Real applications look more like this:
const userId = localStorage.getItem("userId");
const response = await fetch(`/api/users/${userId}`, {
headers: {
Authorization: `Bearer ${token}`
}
});
const data = await response.json();
displayProfile(data);const userId = localStorage.getItem("userId");
const response = await fetch(`/api/users/${userId}`, {
headers: {
Authorization: `Bearer ${token}`
}
});
const data = await response.json();
displayProfile(data);The first time you see something like this, your brain might immediately say:
Abeg, what is going on here?
But I am beginning to understand that when reading JavaScript for security testing, I do not necessarily need to understand every single line.
I need to learn how to follow the data.
And that changed how I started looking at source code.
Think of Data Like a Package Being Sent From Abuja to Lagos
Imagine I give a package to a driver in Abuja.
The driver takes it to the motor park.
Another person loads it into a vehicle.
It gets transported to Lagos.
Someone receives it there.
The package may have passed through five different people.
But it is still the same package.
When analyzing JavaScript, data behaves similarly.
Something may start here:
const userId = "123";const userId = "123";Then enter a function:
getProfile(userId);getProfile(userId);Then that function puts it inside an API request:
fetch(`/api/users/${userId}`);fetch(`/api/users/${userId}`);Then the server receives:
GET /api/users/123GET /api/users/123The important thing is not memorizing all the JavaScript syntax in between.
The important thing is following:
123
โ
userId
โ
getProfile(userId)
โ
/api/users/123
โ
Server123
โ
userId
โ
getProfile(userId)
โ
/api/users/123
โ
ServerAs a pentester, this immediately gives me a much more interesting question:
Where did 123 come from and can I control it?
That is where things start getting interesting.
Sources and Sinks โ Two Words That Used to Confuse Me
While learning security and code review, you will eventually hear people talking about:
Sources
and
Sinks
These terms sounded complicated to me initially.
They really are not.
A Source Is Where Data Comes From
For example:
const name = document.getElementById("name").value;const name = document.getElementById("name").value;The user typed something into a form.
That form is the source of the data.
Another example:
const search = location.search;const search = location.search;The information came from the URL.
Another source.
Or:
const token = localStorage.getItem("token");const token = localStorage.getItem("token");The value came from LocalStorage.
Another source.
Sources can therefore include things like:
Form inputs
URL parameters
Cookies
localStorage
sessionStorage
API responses
HTTP headers
WebSocket messages
postMessage
Uploaded filesForm inputs
URL parameters
Cookies
localStorage
sessionStorage
API responses
HTTP headers
WebSocket messages
postMessage
Uploaded filesThe security question becomes:
Can an attacker influence this data?
Then What Is a Sink?
A sink is basically where that data eventually goes or gets used.
Imagine:
const username = document.getElementById("username").value;
document.getElementById("welcome").innerHTML = username;const username = document.getElementById("username").value;
document.getElementById("welcome").innerHTML = username;Let's follow it.
User types something
โ
username
โ
innerHTML
โ
Browser displays itUser types something
โ
username
โ
innerHTML
โ
Browser displays itThe input field is our source.
innerHTML is an interesting sink because data is being inserted into the page as HTML.
Now my pentester brain starts waking up.
Instead of just thinking:
"This code displays a username."
I start asking:
"What happens if the username contains HTML?"
That is how following data can eventually lead us toward vulnerabilities such as DOM-based XSS.
Notice something important.
I did not begin by shouting:
XSS!
I followed the data first.
That's a habit I am trying to develop.
Let's Read Some JavaScript Together
Suppose I find this while reviewing an application's JavaScript:
const params = new URLSearchParams(window.location.search);
const userId = params.get("id");
async function loadUser() {
const response = await fetch(`/api/users/${userId}`);
const user = await response.json();
document.getElementById("name").textContent = user.name;
}
loadUser();const params = new URLSearchParams(window.location.search);
const userId = params.get("id");
async function loadUser() {
const response = await fetch(`/api/users/${userId}`);
const user = await response.json();
document.getElementById("name").textContent = user.name;
}
loadUser();Before, I might look at this entire thing and think:
"I don't understand JavaScript enough."
Now, I break it down.
Line 1
const params = new URLSearchParams(window.location.search);const params = new URLSearchParams(window.location.search);JavaScript is looking at the URL.
Suppose the page is:
https://example.com/profile?id=25https://example.com/profile?id=25The interesting part is:
?id=25?id=25Line 2
const userId = params.get("id");const userId = params.get("id");JavaScript says:
Find the parameter called
idand give me its value.
So:
id=25id=25becomes:
userId = 25userId = 25Now stop.
This is already interesting.
Who controls the URL?
Me.
Therefore, potentially:
I control userId.
Then We Reach This
const response = await fetch(`/api/users/${userId}`);const response = await fetch(`/api/users/${userId}`);Remember what userId contains?
2525So JavaScript effectively creates:
GET /api/users/25GET /api/users/25Now everything connects.
URL
?id=25
โ
JavaScript reads id
โ
userId = 25
โ
fetch()
โ
GET /api/users/25URL
?id=25
โ
JavaScript reads id
โ
userId = 25
โ
fetch()
โ
GET /api/users/25Now I have an attack hypothesis.
What happens if:
?id=25?id=25becomes:
?id=26?id=26Will JavaScript request:
GET /api/users/26GET /api/users/26Probably.
But here's the important part:
That still does not mean there is an IDOR vulnerability.
This is something I had to understand properly.
Changing an ID is not automatically IDOR.
The real question is:
Will the server allow my authenticated account to access user 26?
If the server checks:
Logged-in user = 25
Requested user = 26
DENYLogged-in user = 25
Requested user = 26
DENYGood.
But if the server simply says:
You are logged in?
Yes.
Okay, here is user 26.You are logged in?
Yes.
Okay, here is user 26.Now we may have a broken object-level authorization problem.
This distinction matters.
Authentication Is Not the Same as Authorization
This is one of those concepts that sounds obvious until you start testing applications.
Imagine entering an office building.
Security asks:"Do you work here?"
You show your staff ID.
They allow you inside.
That is authentication.
The system knows who you are.
But imagine you now walk into the Managing Director's office and start opening confidential files.
Someone should ask:"Yes, you work here, but are you allowed to access THIS?"
That is authorization.
Applications sometimes get the first one right and mess up the second.
For example:
GET /api/account/123
Authorization: Bearer eyJhbGci...GET /api/account/123
Authorization: Bearer eyJhbGci...The server may verify that your token is valid.
Excellent.
You are authenticated.
But what happens when you send:
GET /api/account/124
Authorization: Bearer eyJhbGci...GET /api/account/124
Authorization: Bearer eyJhbGci...The question is no longer:
Who are you?
The question becomes:
Are you allowed to access account 124?
That difference is at the heart of many access-control vulnerabilities.
Another Example: Following Data From LocalStorage
Suppose I find:
const customerId = localStorage.getItem("customerId");
fetch(`/api/customer/${customerId}/transactions`);const customerId = localStorage.getItem("customerId");
fetch(`/api/customer/${customerId}/transactions`);Let's translate it into normal English.
JavaScript says:
"Browser, check LocalStorage."
"Find something called customerId."
"Take whatever value is there."
"Put that value inside this API endpoint."
Suppose LocalStorage contains:
customerId = 8372customerId = 8372The resulting request becomes:
GET /api/customer/8372/transactionsGET /api/customer/8372/transactionsInteresting.
Now I have questions.
Can I change:
83728372to:
83738373using DevTools?
What request does the application make afterwards?
Does the backend independently determine which customer belongs to my session?
Or does it trust the customer ID supplied by the browser?
Again:
Finding this JavaScript does not prove a vulnerability.
It gives me a test case.
And I think that's one of the biggest differences between automated scanning and manual testing.
JavaScript gives us clues.
We investigate the clues.
This Is Why Browser DevTools Is Becoming My Friend
If you are learning JavaScript for pentesting, your browser's Developer Tools is basically your laboratory.
Open a website you are authorized to test.
Press:
F12F12or:
Ctrl + Shift + ICtrl + Shift + IDepending on your browser.
There are a few tabs I now care about a lot.
Network
This shows requests the application is making.
I can click:
LoginLoginand watch what request appears.
Maybe:
POST /api/auth/loginPOST /api/auth/loginI click:
ProfileProfileMaybe:
GET /api/users/meGET /api/users/meI update my phone number.
Maybe:
PUT /api/profilePUT /api/profileSuddenly the application starts explaining itself.
Sources
The Sources tab lets me inspect JavaScript loaded by the application.
I might see:
main.js
app.js
runtime.js
vendor.js
main.483728.js
chunk.839201.jsmain.js
app.js
runtime.js
vendor.js
main.483728.js
chunk.839201.jsInitially those random filenames can look intimidating.
But now I know what I am searching for.
Try searching for strings like:
/api/
login
register
password
reset
otp
verify
admin
userId
customerId
accountId
token
authorization
localStorage
sessionStorage
fetch(
axios/api/
login
register
password
reset
otp
verify
admin
userId
customerId
accountId
token
authorization
localStorage
sessionStorage
fetch(
axiosWhy?
Because I do not necessarily need to read a 40,000-line JavaScript bundle from top to bottom.
I am looking for interesting application behaviour.
Let's Say I Search "password"
And I discover:
const resetPassword = async (email) => {
return await api.post("/api/auth/reset-password", {
email: email
});
};const resetPassword = async (email) => {
return await api.post("/api/auth/reset-password", {
email: email
});
};Fantastic.
I just discovered:
POST /api/auth/reset-passwordPOST /api/auth/reset-passwordand one parameter:
{
"email": "?"
}{
"email": "?"
}Now JavaScript has given me another testing path.
Questions:
Does the response reveal whether an email exists?
Is there rate limiting?
How is the reset token generated?
Where does the reset token go?
Does it expire?
Can it be reused?
Is the new password associated securely with the intended account?
Again, JavaScript has not given me the vulnerability.
It has shown me where to investigate.
Follow the Function Too
Sometimes data does not go directly into fetch().
You may see:
const email = form.email.value;
resetPassword(email);const email = form.email.value;
resetPassword(email);Then somewhere else:
function resetPassword(userEmail) {
sendResetRequest(userEmail);
}function resetPassword(userEmail) {
sendResetRequest(userEmail);
}Then:
function sendResetRequest(value) {
return fetch("/api/password/reset", {
method: "POST",
body: JSON.stringify({
email: value
})
});
}function sendResetRequest(value) {
return fetch("/api/password/reset", {
method: "POST",
body: JSON.stringify({
email: value
})
});
}At first glance, that looks like three different pieces of code.
But follow the package.
form.email.value
โ
email
โ
resetPassword(email)
โ
userEmail
โ
sendResetRequest(userEmail)
โ
value
โ
JSON body
โ
POST /api/password/resetform.email.value
โ
email
โ
resetPassword(email)
โ
userEmail
โ
sendResetRequest(userEmail)
โ
value
โ
JSON body
โ
POST /api/password/resetSame package.
Different names.
This is data flow.
And learning to trace this is beginning to make source-code review much less intimidating for me.
A Variable Name Can Change โ The Data Doesn't
This one is important.
Consider:
const id = 25;
getUser(id);const id = 25;
getUser(id);Then:
function getUser(customer) {
loadProfile(customer);
}function getUser(customer) {
loadProfile(customer);
}Then:
function loadProfile(identifier) {
fetch(`/api/profile/${identifier}`);
}function loadProfile(identifier) {
fetch(`/api/profile/${identifier}`);
}We had:
ididthen:
customercustomerthen:
identifieridentifierBut they're all carrying:
2525So when reviewing code, don't get too attached to variable names.
Follow the value.
What Exactly Am I Looking for as a Pentester?
Now this is the part I care about.
When I see user-controlled data, I want to know where it eventually reaches.
For example:
USER INPUT
โ
Where does it go?USER INPUT
โ
Where does it go?Does it reach:
innerHTMLinnerHTMLInteresting.
Could be worth investigating for DOM XSS.
Does it become:
fetch(`/api/users/${id}`)fetch(`/api/users/${id}`)Interesting.
Could lead to an authorization test.
Does it become:
window.location = redirect;window.location = redirect;Interesting.
Could be an open redirect candidate depending on how the value is validated.
Does it become part of an API request like:
{
"name": "Humairah",
"role": "user",
"isAdmin": false
}{
"name": "Humairah",
"role": "user",
"isAdmin": false
}Interesting.
Why is the client sending:
roleroleor:
isAdminisAdminat all?
Can those fields be manipulated?
Does the backend trust them?
Potential mass-assignment/authorization testing territory.
The Question I Now Ask Every Time
Instead of looking at JavaScript and asking:
"Do I understand all this code?"
I am training myself to ask:
1. Where did this data come from?
Was it:
URL?
Form?
Cookie?
LocalStorage?
API?URL?
Form?
Cookie?
LocalStorage?
API?2. Can the user control it?
If yes, interesting.
3. Where does it go?
Into:
HTML?
API request?
Redirect?
Authentication function?
Database-related request?HTML?
API request?
Redirect?
Authentication function?
Database-related request?4. What assumption is the developer making?
Maybe:
"Nobody will change userId."
Or:
"Only admins can see this button."
Or:
"Nobody will modify LocalStorage."
Or:
"The frontend already validated the amount."
Those assumptions are exactly what I want to challenge.
5. Does the server verify the assumption?
This is the big one.
Because the browser belongs to the user.
And if I control the browser, I can modify requests before they reach the server.
A Small Practical Exercise
Do not just read this article.
Try this.
Use an application you own, a training lab, Juice Shop, PortSwigger Academy, or another environment where you have permission to test.
Open:
Developer ToolsDeveloper ToolsGo to:
NetworkNetworkNow perform one normal action.
For example:
View ProfileView ProfileFind the corresponding request.
Write down:
Endpoint:
Method:
Parameters:
Authentication:
Response:Endpoint:
Method:
Parameters:
Authentication:
Response:Then go to:
SourcesSourcesSearch for part of that endpoint.
If the request was:
/api/profile/api/profilesearch:
/api/profile/api/profileTry to locate the JavaScript responsible for making the request.
Then trace backwards.
Ask:
Where did the values inside this request come from?
That's it.
The goal of the exercise is simply:
Browser action โ JavaScript โ API request
If you can trace that successfully, you are already practizing source-code analysis.
One More Thing I Am Learning: Don't Chase Every Line
This was another mistake I was making.
I thought source-code review meant:
Start from line 1 and understand everything.
Imagine opening a JavaScript bundle with 50,000 lines and trying to understand every function.
You will just close your laptop .
Instead, I am learning to work backwards from interesting functionality.
If I am testing password reset:
Search:
password
reset
forgot
otp
verifySearch:
password
reset
forgot
otp
verifyIf I am testing authorization:
Search:
role
admin
permission
userId
accountId
customerIdSearch:
role
admin
permission
userId
accountId
customerIdIf I am mapping APIs:
Search:
/api/
fetch(
axios
baseURLSearch:
/api/
fetch(
axios
baseURLNow I am not randomly reading JavaScript.
I have a reason for every search.
My first lesson was:
I do not need to become a frontend developer to use JavaScript during pentesting.
My second lesson is becoming:
I do not need to understand every line of JavaScript to begin analyzing an application.
I need to understand enough to follow:
DATA ENTERS
โ
JAVASCRIPT PROCESSES IT
โ
SOMETHING HAPPENSDATA ENTERS
โ
JAVASCRIPT PROCESSES IT
โ
SOMETHING HAPPENSThen ask:
Can I control the data?
Can I change it?
Where does it end up?
Does the application trust it?
Does the server verify it?
And most importantly:
What happens if I break the developer's assumption?
That last question is slowly changing the way I look at web applications.
Because now when I see:
const userId = localStorage.getItem("userId");const userId = localStorage.getItem("userId");I do not just see JavaScript anymore.
I see the beginning of a trail.
And my job is to follow it.
Next in the series: I want to go deeper into the browser itself LocalStorage, SessionStorage and Cookies because applications keep interesting things there and understanding what belongs in the browser versus what the server should actually trust makes a huge difference during web application testing. Stay hooked!