September 7, 2026
JavaScript for Pentesters: The Final Part โ Turning Code Into Security Findings
When I started this JavaScript learning then, my biggest problem was not necessarily JavaScript itself.

By Humairah Adamu Sidi
5 min read
It was understanding why I needed JavaScript as a pentester.
I did not want to become a frontend developer.
I was not trying to build the next React application.
I simply wanted to be able to open an application's JavaScript and understand enough to say:
What is this application doing?
Where is this data coming from?
Where is it going?What does the frontend trust?
And most importantly, what does the backend actually enforce?
After going through variables, functions, objects, arrays, APIs, promises, async/await, browser storage and following data through an application, I realized that this is really the whole point.
JavaScript gives us clues about how the application works.
Our job as pentesters is to follow those clues.
Stop Trying to Read Every Line
One thing that used to scare me was opening a production JavaScript file and seeing something like:
!function(e){var t={};function n(r){if(t[r])return t[r].exports...!function(e){var t={};function n(r){if(t[r])return t[r].exports...My first reaction was:
Omo, what is this?
Real production JavaScript is often minified or bundled.
Instead of beautiful code like:
async function loginUser(email, password) {
return fetch("/api/auth/login");
}async function loginUser(email, password) {
return fetch("/api/auth/login");
}you may see:
function e(t,n){return fetch("/api/auth/login",{method:"POST"})}function e(t,n){return fetch("/api/auth/login",{method:"POST"})}The mistake is thinking you need to understand everything.
You do not.
Instead, search for things connected to the feature you are testing.
For APIs:
/api/
fetch(
axios
/v1/
/v2//api/
fetch(
axios
/v1/
/v2/For authentication:
login
auth
token
refreshToken
logoutlogin
auth
token
refreshToken
logoutFor account recovery:
forgot
reset
password
otp
verifyforgot
reset
password
otp
verifyFor authorization:
admin
role
permission
userId
accountId
customerIdadmin
role
permission
userId
accountId
customerIdFor browser storage:
localStorage
sessionStorage
document.cookielocalStorage
sessionStorage
document.cookieYou are not reading the entire application.
You are looking for interesting behavior.
Follow the Data
This became the biggest lesson for me.
Imagine I find:
const userId = localStorage.getItem("userId");
fetch(`/api/users/${userId}/profile`);const userId = localStorage.getItem("userId");
fetch(`/api/users/${userId}/profile`);I do not need to understand every JavaScript concept surrounding it.
I can already follow:
LocalStorage
|
userId
|
API endpoint
|
GET /api/users/123/profileLocalStorage
|
userId
|
API endpoint
|
GET /api/users/123/profileNow I have security questions.
Where did userId come from?
Can I control it?
Does the server trust it?
If I change:
123123to:
124124what happens?
But this is where another important lesson comes in.
Changing an ID does not automatically mean:
_IDOR!_The real question is:Does the backend allow my account to access another user's object?If it returns:
403 Forbidden403 Forbiddengood.
If it returns another user's private data, now we have something worth investigating.
The root issue is not the number.
The root issue is broken authorization.
Authentication and Authorization Are Different
Authentication asks:
Who are you?
Authorization asks:
What are you allowed to do?
Imagine security lets me into an office building after checking my staff ID.
That does not mean I should now be able to enter the Managing Director's office.
Same thing with an application.
Maybe my request contains:
Authorization: Bearer VALID_TOKENAuthorization: Bearer VALID_TOKENThat proves I am authenticated.
It does not automatically mean I should access:
/api/admin/users/api/admin/usersor another user's:
/api/accounts/8291/api/accounts/8291The backend should still check permissions.
JavaScript can help us find where those boundaries exist.
Frontend Restrictions Are Not Security Controls
This one appears everywhere.
Imagine JavaScript contains:
if (user.role === "admin") {
showAdminButton();
}if (user.role === "admin") {
showAdminButton();
}If I am not an admin, the button disappears.
Nice.
But the real question is:
What happens if I request the admin API directly?
Or:
if (!user.isVerified) {
withdrawButton.disabled = true;
}if (!user.isVerified) {
withdrawButton.disabled = true;
}Again, nice user interface.
But what happens if the withdrawal endpoint receives a request from an unverified account?
The frontend may say:
YOU CANNOT DO THISYOU CANNOT DO THISwhile the backend says:
Sure, no problem.Sure, no problem.That difference is where vulnerabilities can exist.
JavaScript Can Reveal Business Logic
Imagine I find:
if (investment.status === "matured") {
showWithdrawButton();
}if (investment.status === "matured") {
showWithdrawButton();
}I just learned one of the application's business rules:
Investment must mature
|
Before withdrawalInvestment must mature
|
Before withdrawalNow I have a question:
Does the withdrawal API independently enforce that rule?
Or:
if (user.kycCompleted) {
enableInvestment();
}if (user.kycCompleted) {
enableInvestment();
}Now I know:
KYC
|
InvestmentKYC
|
InvestmentMaybe the frontend hides the investment feature until KYC is complete.
But what if the API itself does not check KYC status?
That is the mindset behind business logic testing.
Do not only ask:
Is this parameter vulnerable?
Ask:
What rule is the application trying to enforce, and where is that rule actually enforced?
Password Reset Is Another Authentication Flow
Another thing JavaScript can help us understand is password recovery.
A normal flow might be:
Enter email
|
OTP sent
|
Verify OTP
|
Set new passwordEnter email
|
OTP sent
|
Verify OTP
|
Set new passwordIt looks simple.
But there are several security questions hiding inside it.
How many OTP attempts are allowed?
Does the OTP expire?
Can an old OTP still work after a new one is requested?
Is the OTP tied to the correct account?
What proves OTP verification succeeded?
Can the password-reset endpoint be called without completing verification?How many OTP attempts are allowed?
Does the OTP expire?
Can an old OTP still work after a new one is requested?
Is the OTP tied to the correct account?
What proves OTP verification succeeded?
Can the password-reset endpoint be called without completing verification?JavaScript can reveal endpoints such as:
/api/forgot-password
/api/verify-otp
/api/reset-password/api/forgot-password
/api/verify-otp
/api/reset-passwordThen I can map the whole process rather than testing each endpoint randomly.
That is much more useful.
Browser Storage Can Reveal How Authentication Works
Imagine:
localStorage.setItem(
"accessToken",
response.accessToken
);localStorage.setItem(
"accessToken",
response.accessToken
);Later:
const token =
localStorage.getItem("accessToken");
fetch("/api/profile", {
headers: {
Authorization: `Bearer ${token}`
}
});const token =
localStorage.getItem("accessToken");
fetch("/api/profile", {
headers: {
Authorization: `Bearer ${token}`
}
});Now the flow becomes clear:
Login
|
Token returned
|
Token stored
|
Token retrieved
|
Authorization header
|
Authenticated API requestLogin
|
Token returned
|
Token stored
|
Token retrieved
|
Authorization header
|
Authenticated API requestThat gives me additional questions:
What happens after logout?
Does the token still work?
How long does it last?
Is there a refresh token?
What information is inside the JWT?
How does the backend perform authorization?What happens after logout?
Does the token still work?
How long does it last?
Is there a refresh token?
What information is inside the JWT?
How does the backend perform authorization?Again, JavaScript is not necessarily giving me the vulnerability.
It is giving me a map.
CORS: Another Good Example of Why Context Matters
I used to see:
Access-Control-Allow-Origin: *Access-Control-Allow-Origin: *and immediately think:
CORS vulnerability.
But I later realized that is not enough.
Imagine the endpoint simply returns:
[
"Nigeria",
"Ghana",
"Kenya"
][
"Nigeria",
"Ghana",
"Kenya"
]Public information.
Allowing other websites to read it may be completely intentional.
The better question is:
Can an untrusted website make the victim's browser access sensitive authenticated information and actually read the response?
That is a very different question.
And it taught me another important lesson:
A suspicious configuration gives me something to investigate.
Impact gives me a vulnerability.
DOM XSS Uses the Same Data-Flow Thinking
Suppose:
const name =
new URLSearchParams(
location.search
).get("name");
document.getElementById(
"welcome"
).innerHTML = name;const name =
new URLSearchParams(
location.search
).get("name");
document.getElementById(
"welcome"
).innerHTML = name;Again, follow the data.
URL
|
name
|
innerHTML
|
PageURL
|
name
|
innerHTML
|
PageThe URL is user-controlled.
innerHTML is an interesting destination.
Now I have a source-to-sink relationship worth investigating.
This is why learning JavaScript has started making more sense to me.
The same thinking keeps appearing:
SOURCE
|
DATA
|
FUNCTION
|
SINK / API
|
SECURITY QUESTIONSOURCE
|
DATA
|
FUNCTION
|
SINK / API
|
SECURITY QUESTIONMy JavaScript Source-Code Review Process Now
If I open an application today, my approach is becoming much simpler.
First, I use the application normally.
Maybe:
Login
View Profile
Update Profile
Forgot Password
Make Transaction
LogoutLogin
View Profile
Update Profile
Forgot Password
Make Transaction
LogoutThen I watch the Network tab.
I note the requests.
POST /api/login
GET /api/profile
PUT /api/profile
POST /api/password/resetPOST /api/login
GET /api/profile
PUT /api/profile
POST /api/password/resetThen I go into the JavaScript.
I search for those endpoints.
I look at what data is passed into them.
Then I ask:
Where did this value come from?
Can the user control it?
What does the application assume?
What decision depends on it?
Does the server enforce that decision?Where did this value come from?
Can the user control it?
What does the application assume?
What decision depends on it?
Does the server enforce that decision?That has become my basic process.
A Small Practice Exercise
Use a lab such as OWASP Juice Shop, PortSwigger Academy, or any environment where you have permission to test.
Pick one feature only.
Maybe:
ProfileProfileUse it normally.
Watch the Network tab.
Find the request.
Then find the same endpoint inside the JavaScript.
Suppose you see:
const id =
localStorage.getItem("userId");
fetch(`/api/users/${id}`);const id =
localStorage.getItem("userId");
fetch(`/api/users/${id}`);Write:
Source:
LocalStorage
Value:
userId
Destination:
/api/users/{id}
Security Question:
Does the backend verify that this user
is authorised to access the requested ID?Source:
LocalStorage
Value:
userId
Destination:
/api/users/{id}
Security Question:
Does the backend verify that this user
is authorised to access the requested ID?That's it.
You do not need to find a vulnerability every time.
The goal is learning how to move from:
JAVASCRIPTJAVASCRIPTto:
SECURITY QUESTIONSECURITY QUESTIONThat is the skill.
What JavaScript Now Means to Me as a Pentester
When I started this series, JavaScript looked like:
Variables
Functions
Arrays
Objects
Promises
Async/Await
DOMVariables
Functions
Arrays
Objects
Promises
Async/Await
DOMBasically a programming course.
Now I see something different.
JavaScript
|
Application behaviour
|
APIs
|
Parameters
|
Authentication
|
Authorization
|
Business rules
|
Trust decisionsJavaScript
|
Application behaviour
|
APIs
|
Parameters
|
Authentication
|
Authorization
|
Business rules
|
Trust decisionsAnd that is why I needed JavaScript.
Not to become a frontend engineer.
Not to memorize syntax.
Not to write huge applications.
But to be able to look at frontend code and understand enough to ask better security questions.
The question is no longer:
"Do I understand every line of this JavaScript?"
It is:
"Do I understand enough of this JavaScript to know what the application is doing and what I should test?"
If the answer is yes, then I am making progress.
And I think this is a good place to end my JavaScript for Pentesters series.
Maybe JavaScript was not as scary as I thought after all.