OWASP Top 10 JavaScript Security Exercises

Practice, learn, and master web security coding with real-world browser-based JS flaws, explained for all levels.

A01 — Broken Access Control

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.

HTML
A01 Beginner
<!-- 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.

Teacher Notes:
The application uses a hidden input to determine if the user is admin, which can be manipulated by attackers. Access control must always be enforced on the server. Remove or ignore the userRole field in the client, and enforce the check server-side.
Solution:
// Server-side pseudocode:
if (currentUser.role === 'admin') {
  deleteUserAccount();
} else {
  return 403; // Forbidden
}

JS
A01 Intermediate
// 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.

Teacher Notes:
Trusting user-supplied identifiers without ownership checks violates least privilege. Server should validate the resource belongs to the authenticated user, or infer the ID from the session/token.
Solution:
// Server-side pseudocode:
function getUserProfile(req) {
  if (req.user.id !== req.query.id && !req.user.isAdmin) {
    return 403;
  }
  // return profile data
}

JS
A01 Advanced
// 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?

Teacher Notes:
Never trust JWTs (or any token) unless verified by signature on the server. Clients must not grant access based on unverified tokens; token validation must always occur on the server.
Solution:
// On the server:
const jwt = require('jsonwebtoken');
jwt.verify(token, SECRET_KEY, (err, payload) => {
  if (!err && payload.role === 'admin') { /* allow admin */ }
});

A02 — Cryptographic Failures

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.

JS
A02 Beginner
// 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.

Teacher Notes:
Base64 is encoding, not encryption. Passwords should be hashed using a secure algorithm (e.g., bcrypt) with a salt, and handled on the server.
Solution:
// Demonstrate decoding:
atob('UEEkJHcwcmQh'); // returns 'Pa$$w0rd!'
// Use bcrypt on the server for hashing.

JS
A02 Intermediate
// 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.

Teacher Notes:
Tokens should use crypto.randomUUID() or crypto.getRandomValues for unpredictability. Predictable tokens allow unauthorized resets.
Solution:
function generateResetToken() {
  return crypto.randomUUID();
}

JS
A02 Advanced
// 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.

Teacher Notes:
Secrets must never be in browser JS. Frontend should send payment tokens to backend, which uses the real secret to talk to gateway.
Solution:
// Use public key on frontend to create payment intent, send to backend for final charge.

A03 — Injection

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.

HTML
A03 Beginner
<!-- 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.

Teacher Notes:
Use textContent or safe DOM APIs instead of innerHTML for user input. Never render untrusted data as HTML.
Solution:
let p = document.createElement('p');
let strong = document.createElement('strong');
strong.textContent = name + ": ";
p.appendChild(strong);
p.appendChild(document.createTextNode(comment));
commentsDiv.appendChild(p);

JS
A03 Intermediate
// 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.

Teacher Notes:
Avoid innerHTML with user input; use textContent or escape special characters. Output encoding is crucial.
Solution:
document.getElementById('result').innerText = `Results for "${query}"`;

JS
A03 Advanced
// 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.

Teacher Notes:
Never use eval on user input. For math, restrict input to digits and operators, or use a safe parser.
Solution:
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";
  }
}

A04 — Insecure Design

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.

JS
A04 Beginner
// 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.

Teacher Notes:
Implement lockout or CAPTCHA after several failures. Security controls must be designed in from the start.
Solution:
if (failedAttempts >= 5) {
   return "Account locked. Try again in 15 minutes.";
}

JS
A04 Intermediate
// 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.

Teacher Notes:
Security questions are weak; use time-limited tokens sent to a user's verified email or phone instead.
Solution:
function sendResetEmail(user) {
  const token = crypto.randomUUID();
  EmailService.send(user.email, `Reset: https://example.com/reset?token=${token}`);
}

HTML
A04 Advanced
<!-- 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.

Teacher Notes:
The server must calculate price and total, not the client. Never trust client-sent price/qty; only accept itemId and qty, look up price server-side.
Solution:
// Server-side:
function handlePurchaseRequest(user, itemId, qty) {
  const item = database.getItem(itemId);
  const price = item.price;
  const total = price * qty;
  // ...
}

A05 — Security Misconfiguration

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.

JS
A05 Beginner
// 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?

Teacher Notes:
Always change defaults before production, and do not hardcode credentials. Use strong, unique passwords from secure storage (env variables, etc.).
Solution:
const adminPassword = process.env.ADMIN_PASS; // Securely set via environment variable.

JS
A05 Intermediate
// 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?

Teacher Notes:
In production, show only generic messages to users; log detailed errors internally only.
Solution:
function handleError(err) {
  if (ENV === 'production') {
    console.error(err);
    return "An error occurred. Please contact support.";
  } else {
    return `Error: ${err.message}\n${err.stack}`;
  }
}

JS
A05 Advanced
// 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?

Teacher Notes:
Never leave debug code or test backdoors in production. Use separate configs for prod/dev and remove or disable all debug logic before deploying.
Solution:
const DEBUG_MODE = false; // Always false in production; remove debug branches.

A06 — Vulnerable and Outdated Components

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.

HTML
A06 Beginner
<!-- 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.

Teacher Notes:
Outdated libraries are low-hanging fruit for attackers. Always update to the latest supported versions. jQuery 1.x <3.5 has multiple known CVEs.
Solution:
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>

HTML
A06 Intermediate
<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?

Teacher Notes:
Update all third-party libraries regularly, subscribe to security advisories, and use SRI where possible.
Solution:
<script src="/static/js/vue-2.7.14.js"></script>

JSON
A06 Advanced
// 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.

Teacher Notes:
Use npm audit, retire.js, Dependabot, and similar tools to track and patch vulnerable dependencies. Remove unused libraries.
Solution:
// Update dependencies to latest secure versions (e.g., lodash 4.17.21, express 4.18.x, CKEditor 4.16+)

A07 — Identification and Authentication Failures

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.

JS
A07 Beginner
// 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?

Teacher Notes:
Authentication must be done on the server, not in exposed JavaScript. Always send login info over HTTPS to a backend API.
Solution:
// Use a server-side API for authentication; no hardcoded credentials in JS.

JS
A07 Intermediate
// 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.

Teacher Notes:
Use HttpOnly, Secure cookies for tokens, as these are not accessible via JavaScript and mitigate XSS risk.
Solution:
// Set session token as a HttpOnly cookie from the server. Remove from sessionStorage.

JS
A07 Advanced
// 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?

Teacher Notes:
Session IDs must be generated and rotated by the server. Never accept a session ID from untrusted input like URLs.
Solution:
// Do not set session IDs from URL params. Always generate a fresh session on login.

A08 — Software and Data Integrity Failures

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.

HTML
A08 Beginner
<!-- 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?

Teacher Notes:
Always use HTTPS and add Subresource Integrity (SRI) for external scripts.
Solution:
<script src="https://cdn.example.com/lib/awesome-widget.js" integrity="sha384-..." crossorigin="anonymous"></script>

JS
A08 Intermediate
// 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?

Teacher Notes:
Never use eval on fetched data. Parse as JSON or a safe format instead.
Solution:
.then(txt => JSON.parse(txt));

JS
A08 Advanced
// 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?

Teacher Notes:
Only load plugins from vetted, whitelisted sources. Consider using signed plugins or CSP.
Solution:
// Only load plugins from a trusted list of URLs, or implement digital signature checks.

A09 — Security Logging and Monitoring Failures

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.

JS
A09 Beginner
// 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.

Teacher Notes:
Always log failed login attempts, password reset requests, and suspicious activity. Set up alerting for excessive failures.
Solution:
function login(user, pass) {
  if (user === 'admin' && pass === 'letmein') {
    return "Welcome";
  }
  logEvent("Failed login for user: " + user);
  return "Invalid credentials";
}

JS
A09 Intermediate
// 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?

Teacher Notes:
Record who performed what action and when, especially for sensitive operations. Logs should be protected from tampering.
Solution:
function deleteUser(userId, actor) {
  logEvent(`User ${actor} deleted user ${userId}`);
  usersDB.remove(userId);
}

JS
A09 Advanced
// 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?

Teacher Notes:
Set up automated alerts for suspicious actions, and review logs regularly. Consider integrating with a SIEM.
Solution:
if (amount > 10000) {
  alertSecurityTeam(`Large payment: ${amount} by ${user}`);
}

A10 — Server-Side Request Forgery (SSRF)

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.

JS
A10 Beginner
// 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?

Teacher Notes:
Always validate and restrict URLs fetched by the server. Block localhost, internal IPs, or private network addresses.
Solution:
// Backend should validate the target is a public URL; deny localhost, 127.0.0.1, etc.

JS
A10 Intermediate
// 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.

Teacher Notes:
Implement allow-lists, block private IP ranges, and check DNS results before making a request.
Solution:
// Validate imgUrl is external, and not localhost or internal IP.

JS
A10 Advanced
// 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?

Teacher Notes:
Only redirect to allowed URLs, or require destination paths (not full URLs). Never allow arbitrary external redirects.
Solution:
// Only allow redirects to whitelisted domains, or relative paths.