Practice, learn, and master web security coding with real-world browser-based JS flaws, explained for all levels.
Broken Access Control occurs when an application fails to enforce what authenticated users are allowed to do, leading to unauthorized information access or actions. This was ranked #1 in OWASP 2021 after 94% of tested applications were found to have some form of access control weakness. Attackers exploit these flaws to elevate privileges or access other users’ data.
<!-- index.html (fragment) -->
<input type="hidden" id="userRole" value="user">
<button id="deleteBtn" onclick="deleteAccount()">Delete Account</button>
<script>
function deleteAccount() {
// Check role on client-side (vulnerable - can be manipulated)
const role = document.getElementById('userRole').value;
if (role === 'admin') {
alert("Account deleted!");
// ... perform deletion ...
} else {
alert("Access denied. Admins only.");
}
}
</script>
Description: The code relies on a client-side role check (a hidden field) to restrict admin functionality. An attacker can modify the DOM to change userRole to "admin", then perform unauthorized deletion. This exemplifies broken access control by trusting client-side checks.
Task: Identify and explain the vulnerability. Why is relying on hidden fields or client-side checks for authorization insecure? Propose a proper fix.
// Server-side pseudocode:
if (currentUser.role === 'admin') {
deleteUserAccount();
} else {
return 403; // Forbidden
}
// profile.js
function loadUserProfile() {
const userId = new URLSearchParams(location.search).get('id');
// Vulnerable: no authorization check for ID
fetch(`/api/users/${userId}`)
.then(resp => resp.json())
.then(data => {
document.getElementById('profileName').innerText = data.name;
});
}
Description: The code retrieves a user profile based on an id from the URL, without verifying that the current user is authorized to view it. This is an IDOR flaw; attackers can view other users' data by changing the id.
Task: Explain the risk of using a user-controlled id for access. Suggest a fix so users can only access their own data.
// Server-side pseudocode:
function getUserProfile(req) {
if (req.user.id !== req.query.id && !req.user.isAdmin) {
return 403;
}
// return profile data
}
// auth.js
// WARNING: JWT token not verified on client-side
let token = localStorage.getItem('authToken');
if (token) {
// Decode payload (without verifying signature)
const payload = JSON.parse(atob(token.split('.')[1]));
if (payload.role === 'admin') {
showAdminPanel();
}
}
Description: This code reads a JWT from localStorage and trusts its payload without verifying the signature. An attacker can forge a token granting themselves admin rights.
Task: Why is this approach unsafe? How could an attacker abuse it? What is the correct way to check tokens?
// On the server:
const jwt = require('jsonwebtoken');
jwt.verify(token, SECRET_KEY, (err, payload) => {
if (!err && payload.role === 'admin') { /* allow admin */ }
});
Cryptographic Failures (formerly Sensitive Data Exposure) cover failures to protect data in transit or at rest, using weak or no encryption, poor key management, or weak randomness, leading to exposed or guessable data.
// crypto.js
function encryptPassword(pwd) {
// Insecure: using Base64 as "encryption"
return btoa(pwd);
}
const secret = "Pa$$w0rd!";
console.log("Encrypted password:", encryptPassword(secret));
Description: Uses Base64 as encryption. Base64 is reversible and offers no real protection. Anyone can decode it and retrieve the original password.
Task: Why is using Base64 for "encryption" insecure? Demonstrate decoding, and suggest a secure way to protect passwords.
// Demonstrate decoding:
atob('UEEkJHcwcmQh'); // returns 'Pa$$w0rd!'
// Use bcrypt on the server for hashing.
// token.js
function generateResetToken(username) {
// Insecure: predictable token generation
const token = username + Math.floor(Math.random() * 1000000);
return token;
}
Description: Uses Math.random(), which is predictable and weak. An attacker could brute-force or guess reset tokens.
Task: Explain why predictable tokens are dangerous. Rewrite using a cryptographically secure random generator.
function generateResetToken() {
return crypto.randomUUID();
}
// config.js
// Hard-coded secret key in client-side code
const PAYMENT_GATEWAY_KEY = "sk_test_51H8***RED4CTED***";
function submitPayment(details) {
// Insecure: using secret key on frontend
fetch("https://api.paymentgateway.com/charge", {
method: "POST",
headers: { "Authorization": `Bearer ${PAYMENT_GATEWAY_KEY}` },
body: JSON.stringify(details)
});
}
Description: Secret API keys must never be exposed in frontend code. Attackers can extract and misuse them.
Task: Identify the risk. Describe how to design this securely.
// Use public key on frontend to create payment intent, send to backend for final charge.
Injection flaws occur when untrusted data is interpreted as code/commands by an application. Includes SQL injection, XSS, and similar flaws, leading to arbitrary code execution.
<!-- comment_form.html -->
<form onsubmit="postComment(); return false;">
<input id="userName" placeholder="Your name">
<input id="comment" placeholder="Add a comment...">
<button type="submit">Post</button>
</form>
<div id="comments"></div>
<script>
function postComment() {
const name = document.getElementById('userName').value;
const comment = document.getElementById('comment').value;
// Vulnerable: inserting user input directly into HTML (XSS risk)
document.getElementById('comments').innerHTML +=
`<p><strong>${name}:</strong> ${comment}</p>`;
}
</script>
Description: User input is inserted directly into innerHTML, allowing for XSS attacks. An attacker can inject HTML or scripts.
Task: Demonstrate an XSS payload. Refactor the code so it is safe.
let p = document.createElement('p');
let strong = document.createElement('strong');
strong.textContent = name + ": ";
p.appendChild(strong);
p.appendChild(document.createTextNode(comment));
commentsDiv.appendChild(p);
// search.js
const params = new URLSearchParams(location.search);
const query = params.get('q') || '';
// Vulnerable: inserting search query into page without escaping
document.getElementById('result').innerHTML = `Results for "<b>${query}</b>"`;
Description: Reflects unsanitized search query from URL into HTML, leading to reflected XSS.
Task: Show how a malicious query can cause XSS. Fix the code to prevent this.
document.getElementById('result').innerText = `Results for "${query}"`;
// eval_demo.js
function calculate(formula) {
// Vulnerable: using eval on untrusted input
try {
const result = eval(formula);
return "Result: " + result;
} catch(e) {
return "Error in formula";
}
}
let userInput = prompt("Enter a calculation:");
alert( calculate(userInput) );
Description: Using eval on user input allows arbitrary code execution. Any JS code entered will run with full privileges.
Task: Show how an attacker can abuse eval. Refactor the function to be safe.
function calculate(formula) {
if(!/^[0-9+\-*/().\s]+$/.test(formula)) {
return "Invalid input";
}
try {
const func = new Function(`return (${formula})`);
return "Result: " + func();
} catch {
return "Error in formula";
}
}
Insecure Design refers to flaws resulting from missing or weak security controls in the application's architecture. This includes lack of brute-force protection, relying on guessable secrets, or trusting client logic for critical functions.
// login.js (simplified)
let failedAttempts = 0;
function login(username, password) {
const user = db.findUser(username);
if (user && user.password === password) {
failedAttempts = 0;
return "Welcome, " + username;
} else {
failedAttempts++;
return "Incorrect credentials, please try again.";
}
}
Description: No lockout or brute-force protection is implemented. Attackers can try unlimited passwords.
Task: Why is unlimited login attempts dangerous? Propose a fix.
if (failedAttempts >= 5) {
return "Account locked. Try again in 15 minutes.";
}
// password_reset.js
function resetPassword(user, answer) {
// Insecure design: security question for reset
if (answer.toLowerCase() === user.securityAnswer.toLowerCase()) {
user.password = prompt("Enter new password:");
return "Password reset successful for " + user.username;
} else {
return "Incorrect answer. Cannot reset password.";
}
}
Description: Security questions are guessable or obtainable by attackers, making this reset process weak.
Task: Why are security questions insecure? Propose a better password reset process.
function sendResetEmail(user) {
const token = crypto.randomUUID();
EmailService.send(user.email, `Reset: https://example.com/reset?token=${token}`);
}
<!-- checkout.html (fragment) -->
<input type="hidden" id="itemPrice" value="100.00">
Quantity: <input id="quantity" type="number" value="1">
<button onclick="purchase()">Buy</button>
<script>
function purchase() {
const price = parseFloat(document.getElementById('itemPrice').value);
const qty = parseInt(document.getElementById('quantity').value);
const total = price * qty;
// Insecure: trusting client-calculated price
fetch(`/api/buy?total=${total}&qty=${qty}`)
.then(res => res.text())
.then(msg => alert(msg));
}
</script>
Description: Critical logic (price calculation) is enforced only on the client; attackers can change price or qty.
Task: What can go wrong if users control price/qty? Propose a more secure design.
// Server-side:
function handlePurchaseRequest(user, itemId, qty) {
const item = database.getItem(itemId);
const price = item.price;
const total = price * qty;
// ...
}
Security Misconfiguration refers to improper implementation or setup of security controls, use of default credentials, verbose errors, or unpatched default settings, leading to exploitable weaknesses.
// adminSetup.js
const adminUsername = "admin";
const adminPassword = "admin"; // Default password still in use
function adminLogin(user, pass) {
if (user === adminUsername && pass === adminPassword) {
return "Admin access granted";
} else {
return "Access denied";
}
}
Description: Uses unchanged default admin credentials. Attackers can easily guess and use these to gain admin access.
Task: Why are default credentials dangerous? How should this be fixed?
const adminPassword = process.env.ADMIN_PASS; // Securely set via environment variable.
// errorHandler.js
function handleError(err) {
// Misconfiguration: exposing detailed errors in production
console.error("StackTrace:", err);
return `Error: ${err.message} (at ${err.fileName}:${err.lineNumber})`;
}
// Example usage:
try {
// ... some code that throws ...
} catch(e) {
const msg = handleError(e);
document.getElementById('errorBox').innerText = msg;
}
Description: Returns stack traces and internal error details to the user, revealing implementation details useful to attackers.
Task: Why is showing detailed errors to users dangerous? How to handle errors securely?
function handleError(err) {
if (ENV === 'production') {
console.error(err);
return "An error occurred. Please contact support.";
} else {
return `Error: ${err.message}\n${err.stack}`;
}
}
// debugMode.js
const DEBUG_MODE = true; // Misconfiguration: should be false in production
function isAdminUser(user) {
if (DEBUG_MODE && user === undefined) {
// Debug backdoor: if no user, treat as admin
console.warn("Debug mode: treating undefined user as admin");
return true;
}
return user?.isAdmin === true;
}
// In admin panel:
if (isAdminUser(currentUser)) {
showAdminControls();
}
Description: Debug mode with a backdoor is enabled in production. Attackers can exploit this to gain admin rights.
Task: How could an attacker exploit this? How should debug/test code be handled in production?
const DEBUG_MODE = false; // Always false in production; remove debug branches.
Vulnerable and Outdated Components refers to the use of software libraries, frameworks, or modules that contain known vulnerabilities or are unmaintained. Attackers exploit these to compromise applications through well-documented flaws.
<!-- Including an outdated library -->
<script src="https://code.jquery.com/jquery-1.8.3.min.js"></script>
<script>
$('#submit').on('click', sendData);
</script>
Description: The page uses jQuery 1.8.3, a very old version with multiple known XSS vulnerabilities. Attackers can exploit these flaws, sometimes even if your own code is safe.
Task: Research and list a known vulnerability in jQuery 1.8.3. Propose how to fix or mitigate the risk.
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="/static/js/vue.js"></script> <!-- Vue.js v2.5.13 --> <script src="/static/js/my-app.js"></script>
Description: Vue.js 2.5.13 is several years old and contains XSS and other vulnerabilities. Attackers can discover the version and craft exploits based on known CVEs.
Task: Find out if Vue 2.5.13 is vulnerable. How could an attacker identify the version? What is the remediation?
<script src="/static/js/vue-2.7.14.js"></script>
// package.json excerpt
"dependencies": {
"express": "4.16.1",
"lodash": "4.17.4",
"ckeditor": "4.5.9"
}
Description: Multiple dependencies are outdated and have known vulnerabilities (DoS, prototype pollution, XSS, etc.). Failing to update puts the app at risk.
Task: Identify at least one vulnerability for each dependency. Propose a process to keep dependencies secure.
// Update dependencies to latest secure versions (e.g., lodash 4.17.21, express 4.18.x, CKEditor 4.16+)
Identification and Authentication Failures involve flaws in the way user identities and sessions are handled, such as weak password policies, session fixation, credential exposure, or failure to invalidate sessions on logout.
// clientAuth.js
function clientSideLogin(username, password) {
// Hardcoded credential check on client side
if (username === "admin" && password === "Secr3t!") {
showAdminDashboard();
}
}
Description: Login logic and credentials are exposed in client-side code. Attackers can easily discover and bypass authentication.
Task: Why is this insecure? What is the correct approach to authentication?
// Use a server-side API for authentication; no hardcoded credentials in JS.
// sessionStorageAuth.js
sessionStorage.setItem('authToken', 'XYZ12345');
function getUserData() {
const token = sessionStorage.getItem('authToken');
return fetch('/api/user/data', {
headers: { 'Authorization': 'Bearer ' + token }
});
}
Description: Storing auth tokens in sessionStorage exposes them to theft via XSS. If an attacker injects code, they can steal the token and hijack the session.
Task: Explain the risk of storing tokens in Web Storage. Suggest a safer alternative.
// Set session token as a HttpOnly cookie from the server. Remove from sessionStorage.
// loginPage.js
const urlParams = new URLSearchParams(window.location.search);
const sessionId = urlParams.get('sessionId');
if (sessionId) {
document.cookie = `JSESSIONID=${sessionId}; path=/; Secure`;
}
Description: Accepts session IDs from URL parameters, enabling session fixation attacks.
Task: What is session fixation? How does this code enable it? How should session management be handled?
// Do not set session IDs from URL params. Always generate a fresh session on login.
Software and Data Integrity Failures occur when code, dependencies, or data are not verified for integrity before being trusted or executed. Examples include fetching scripts over HTTP, lack of SRI, or using eval on fetched data.
<!-- index.html --> <!-- Insecure: loading script over HTTP --> <script src="http://cdn.example.com/lib/awesome-widget.js"></script>
Description: Loads a third-party script over HTTP, allowing attackers to tamper with it in transit.
Task: Why is this risky? How can it be mitigated?
<script src="https://cdn.example.com/lib/awesome-widget.js" integrity="sha384-..." crossorigin="anonymous"></script>
// config-loader.js
function loadConfig() {
return fetch("/config.json")
.then(res => res.text())
.then(txt => {
// Insecure: eval on fetched data
eval(txt);
});
}
Description: Uses eval on untrusted data from a config file, which could lead to code execution if tampered.
Task: Why is using eval on config dangerous? What is a safe alternative?
.then(txt => JSON.parse(txt));
// pluginLoader.js
function loadPlugin(url) {
const script = document.createElement('script');
script.src = url;
document.head.appendChild(script);
}
loadPlugin(prompt("Enter plugin URL to load:"));
Description: Dynamically loads and executes scripts from arbitrary URLs, allowing RCE if an attacker can supply the URL.
Task: What risks does this pose? How can dynamic plugin loading be made secure?
// Only load plugins from a trusted list of URLs, or implement digital signature checks.
Security Logging and Monitoring Failures happen when events are not recorded, analyzed, or alerted upon, making it difficult to detect and respond to attacks or suspicious activity.
// login.js
function login(user, pass) {
// No logging of failed login attempts
if (user === 'admin' && pass === 'letmein') {
return "Welcome";
}
return "Invalid credentials";
}
Description: There is no logging or alerting for failed login attempts, so brute-force attacks or suspicious activity go undetected.
Task: Why is logging important? Suggest what should be logged for authentication events.
function login(user, pass) {
if (user === 'admin' && pass === 'letmein') {
return "Welcome";
}
logEvent("Failed login for user: " + user);
return "Invalid credentials";
}
// audit.js
function deleteUser(userId) {
// No audit trail
usersDB.remove(userId);
return "User deleted";
}
Description: Sensitive actions (like user deletion) are not logged, so malicious or accidental actions may go unnoticed.
Task: How would you add auditing for critical actions? Why is this necessary?
function deleteUser(userId, actor) {
logEvent(`User ${actor} deleted user ${userId}`);
usersDB.remove(userId);
}
// monitoring.js
function processPayment(amount, user) {
// No monitoring of suspicious transactions
if (amount > 10000) {
// should trigger an alert
}
// process payment...
}
Description: There is no alerting for suspicious or abnormal events, such as very large payments, which could indicate fraud.
Task: How can monitoring be improved to detect abuse or attacks in real time?
if (amount > 10000) {
alertSecurityTeam(`Large payment: ${amount} by ${user}`);
}
Server-Side Request Forgery (SSRF) occurs when an application fetches remote resources based on user input without proper validation, allowing attackers to make requests on behalf of the server to internal or protected resources.
// fetcher.js
function getWebsiteInfo() {
const url = document.getElementById('url').value;
fetch('/api/info?target=' + encodeURIComponent(url))
.then(resp => resp.text())
.then(data => displayInfo(data));
}
Description: The backend likely fetches the provided target URL, which an attacker could set to internal resources (e.g., http://localhost/admin).
Task: What is SSRF? How could an attacker exploit this code?
// Backend should validate the target is a public URL; deny localhost, 127.0.0.1, etc.
// imageProxy.js
app.get('/proxy', (req, res) => {
const imgUrl = req.query.url;
// No restriction on imgUrl
request(imgUrl).pipe(res);
});
Description: No validation of imgUrl; attacker can request internal services or metadata endpoints via proxy.
Task: Suggest mitigation steps to prevent SSRF.
// Validate imgUrl is external, and not localhost or internal IP.
// openRedirect.js
app.get('/redirect', (req, res) => {
const dest = req.query.url;
res.redirect(dest); // no validation
});
Description: Open redirect can be leveraged for phishing or SSRF if the backend follows untrusted URLs.
Task: What risks are posed by open redirects? How can this be safely implemented?
// Only allow redirects to whitelisted domains, or relative paths.