Skip to main content

Security engineer coding interview questions

10 questions on coding for security engineer candidates. Expect prompts such as “Detect a timing side-channel in a cryptographic comparison function” and “Implement server-side CSRF token generation and validation”, each with a worked answer outline and the follow-ups interviewers push on.

Questions

Showing 1 to 10 of 10 coding questions.

As asked

Write a function to detect whether a given string comparison function is constant-time by measuring its execution time across multiple inputs. How would you statistically analyze the results to determine if a timing oracle exists?

Sample answer outline

A strong answer implements high-resolution timing around the comparison function, runs hundreds of trials per input pair (same prefix length, different suffix), groups measurements by prefix length, and applies a t-test or Mann-Whitney U test to detect statistically significant timing differences between groups. The candidate should discuss measurement noise from CPU caches, scheduler jitter, and why many trials are required, and explain that a p-value below 0.01 would indicate a timing oracle.

Reference implementation (python)

Python
import time
import statistics

def measure_comparison(fn, a, b, trials=500):
    times = []
    for _ in range(trials):
        start = time.perf_counter_ns()
        fn(a, b)
        times.append(time.perf_counter_ns() - start)
    return times

def unsafe_compare(a, b):
    return a == b

times_match = measure_comparison(unsafe_compare, "secret", "secret")
times_miss  = measure_comparison(unsafe_compare, "secret", "xxxxxx")

print(statistics.mean(times_match), statistics.mean(times_miss))
# TODO: apply t-test to determine if difference is significant

Expect these follow-ups

  • How does the Welch t-test differ from the Mann-Whitney U test for detecting timing differences, and when would you choose one over the other given non-normal measurement distributions?
timing-attackcryptographystatisticspythontesting

As asked

Implement a CSRF token generation and validation system in Python. The token must be cryptographically random, tied to the user session, and validated in constant time. Handle the case where the token is missing or expired.

Sample answer outline

A strong answer generates a cryptographically random token using secrets.token_hex, stores it in the server-side session keyed to the user, validates it on state-changing requests using hmac.compare_digest for constant-time comparison, and returns 403 with a specific error code (not a redirect that leaks state) on failure. The candidate should discuss token rotation after successful use, expiry via session expiry, and why per-form tokens provide better isolation than a single per-session token.

Reference implementation (python)

Python
import secrets
import hmac
from flask import session, request, abort

CSRF_TOKEN_LENGTH = 32

def generate_csrf_token():
    token = secrets.token_hex(CSRF_TOKEN_LENGTH)
    session['csrf_token'] = token
    return token

def validate_csrf_token():
    submitted = request.form.get('csrf_token', '')
    expected  = session.get('csrf_token', '')
    # TODO: add expiry check
    if not hmac.compare_digest(submitted.encode(), expected.encode()):
        abort(403)

Expect these follow-ups

  • How would you implement the Double Submit Cookie pattern and what are its weaknesses compared to synchronizer tokens?
csrfcryptographypythonappsecauthn

As asked

Implement a sliding window rate limiter in Redis that limits login attempts to 5 per IP per 60-second window. It should be atomic, handle Redis failure gracefully, and not allow bypass via distributed timing.

Sample answer outline

A strong answer uses a Redis sorted set keyed on the IP, with timestamps as both score and member, a ZREMRANGEBYSCORE to drop old entries, ZCARD to count current attempts, ZADD to record the new attempt, and EXPIRE to clean up idle keys. The entire operation should be wrapped in a Lua script or Redis transaction to guarantee atomicity. On Redis failure the candidate should decide on a fail-open vs fail-closed policy and justify it: fail-open prevents lockout but allows burst attacks during outage.

Reference implementation (python)

Python
import redis, time

r = redis.Redis()

RATE_LIMIT = 5
WINDOW_SEC = 60

def is_rate_limited(ip: str) -> bool:
    key = f'login_rl:{ip}'
    now = time.time()
    window_start = now - WINDOW_SEC
    pipe = r.pipeline()
    pipe.zremrangebyscore(key, 0, window_start)
    pipe.zadd(key, {str(now): now})
    pipe.zcard(key)
    pipe.expire(key, WINDOW_SEC)
    try:
        results = pipe.execute()
        count = results[2]
        return count > RATE_LIMIT
    except redis.RedisError:
        return False  # fail-open; discuss tradeoff

Expect these follow-ups

  • How would you extend this to distribute the rate limit across a login attempt from different IPs but the same username (to detect credential stuffing)?
rate-limitingredispythonappsecdistributed-systems

As asked

Write a function that validates a redirect URL to prevent open redirect vulnerabilities. The function should allow redirects to paths on the same origin and to a configurable allowlist of trusted domains, and must handle common bypasses like scheme variations and unicode confusables.

Sample answer outline

A strong answer parses the URL with the standard library urlparse, compares the scheme and netloc (hostname) against an allowlist of trusted domains, and defaults to a safe path for any URL that fails validation rather than throwing an error. Bypass mitigations include normalizing the URL before parsing (decode percent-encoding, lowercase scheme), checking for double-slashes or missing scheme tricks like //evil.com, and handling internationalized domain names by converting to punycode before comparison.

Reference implementation (python)

Python
from urllib.parse import urlparse

ALLOWED_HOSTS = {'example.com', 'api.example.com'}

def safe_redirect_url(url: str, default: str = '/') -> str:
    if not url:
        return default
    try:
        parsed = urlparse(url)
        # relative path only
        if not parsed.scheme and not parsed.netloc:
            return url if url.startswith('/') else default
        if parsed.scheme not in ('http', 'https'):
            return default
        host = parsed.hostname or ''
        if host.lower() in ALLOWED_HOSTS:
            return url
    except Exception:
        pass
    return default

Expect these follow-ups

  • How does the unicode confusable attack work specifically and how does punycode normalization prevent it?
open-redirecturl-validationpythonappsecbypass

As asked

Implement a webhook receiver in Python that verifies the HMAC-SHA256 signature on incoming webhooks, rejects replayed requests older than 5 minutes, and handles both the signature verification and replay prevention in a way that is safe against timing attacks.

Sample answer outline

A strong answer computes HMAC-SHA256 over the concatenation of the timestamp and raw request body, compares it to the header signature using hmac.compare_digest, and checks that the timestamp in the header is within a 5-minute tolerance of the current server time. The replay prevention via timestamp works because the body includes the timestamp in the signed message. The candidate should note that the raw body must be used (not a re-serialized version) and discuss that nonce-based replay prevention is stronger if clock skew is a concern.

Reference implementation (python)

Python
import hmac, hashlib, time
from flask import request, abort

WEBHOOK_SECRET = b'your-secret-key'
TOLERANCE_SECONDS = 300

def verify_webhook():
    sig_header = request.headers.get('X-Signature-256', '')
    ts_header  = request.headers.get('X-Timestamp', '')
    if not sig_header or not ts_header:
        abort(400)
    try:
        ts = int(ts_header)
    except ValueError:
        abort(400)
    if abs(time.time() - ts) > TOLERANCE_SECONDS:
        abort(400)  # replay
    body = request.get_data()  # raw bytes, not parsed
    expected = hmac.new(
        WEBHOOK_SECRET,
        f'{ts_header}.'.encode() + body,
        hashlib.sha256
    ).hexdigest()
    if not hmac.compare_digest(f'sha256={expected}', sig_header):
        abort(403)

Expect these follow-ups

  • What happens if the webhook receiver is behind a load balancer that re-serializes the JSON body?
hmacwebhookspythonauthnappsec

As asked

Write a function that sanitizes user-supplied strings before they are written to structured log output, preventing log injection attacks including CRLF injection, ANSI escape code injection, and JSON log field escaping.

Sample answer outline

A strong answer strips or escapes carriage returns and newlines that would create fake log lines, removes or encodes ANSI escape sequences (regex matching ESC bracket sequences) that could manipulate terminal output, and in a JSON logging context ensures user input is always a quoted string value rather than injecting into the JSON structure. The candidate should discuss that the proper fix is using a structured logging library that handles escaping automatically rather than manual sanitization.

Reference implementation (python)

Python
import re, json

ANSI_ESCAPE = re.compile(r'\x1b\[[0-9;]*[mGKHF]')

def sanitize_for_log(value: str) -> str:
    if not isinstance(value, str):
        value = str(value)
    value = ANSI_ESCAPE.sub('', value)
    value = value.replace('\r', '\\r').replace('\n', '\\n')
    return value

def build_log_entry(user_input: str) -> str:
    entry = {'message': sanitize_for_log(user_input)}
    return json.dumps(entry)  # proper JSON escaping

Expect these follow-ups

  • How does log injection interact with a SIEM that parses log lines into structured events?
log-injectioninput-validationpythonappsecsanitization

As asked

You need to build a query builder in Python that allows callers to filter a users table by any combination of optional fields (username, email, role, created_after). Write it in a way that is safe against SQL injection regardless of the combination of filters supplied.

Sample answer outline

A strong answer builds the WHERE clause by accumulating condition strings with named or positional placeholders into a list, appending corresponding values to a parameters list, and joining the conditions with AND before passing both to the database driver's parameterized execute method. The candidate should explain that column names (not values) cannot be parameterized and must be validated against an explicit allowlist of known columns. They should also demonstrate handling of zero filters gracefully.

Reference implementation (python)

Python
import psycopg2

ALLOWED_FILTERS = {'username', 'email', 'role', 'created_after'}

def find_users(conn, **filters):
    conditions = []
    params = []
    for col, val in filters.items():
        if col not in ALLOWED_FILTERS:
            raise ValueError(f'Unknown filter: {col}')
        if col == 'created_after':
            conditions.append('created_at > %s')
        else:
            conditions.append(f'{col} = %s')
        params.append(val)
    sql = 'SELECT id, username, email FROM users'
    if conditions:
        sql += ' WHERE ' + ' AND '.join(conditions)
    with conn.cursor() as cur:
        cur.execute(sql, params)
        return cur.fetchall()

Expect these follow-ups

  • Why can column names not be safely parameterized in most database drivers and what is the safe alternative?
sql-injectionpythondatabasesparameterizationappsec

As asked

Without using a JWT library, write code to parse a JWT, verify its RS256 signature using the provided public key, and return the decoded claims. What attacks would a naive implementation be vulnerable to?

Sample answer outline

A strong answer base64url-decodes the three parts, parses the header JSON first to check the alg field and rejects anything other than RS256 explicitly, reconstructs the signing input as header.payload, verifies the signature bytes against the public key using RSA-PKCS1v15 or RSASSA-PSS, and only then parses the payload claims. Vulnerable patterns include trusting the alg field from the token (algorithm confusion), accepting alg:none, using symmetric verification with a public key as the secret (RS256 to HS256 swap), and not checking exp/iss/aud claims.

Reference implementation (python)

Python
import base64, json
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives.serialization import load_pem_public_key

def b64url_decode(s):
    s += '=' * (4 - len(s) % 4)
    return base64.urlsafe_b64decode(s)

def verify_jwt(token: str, public_key_pem: bytes) -> dict:
    parts = token.split('.')
    if len(parts) != 3:
        raise ValueError('malformed jwt')
    header = json.loads(b64url_decode(parts[0]))
    if header.get('alg') != 'RS256':
        raise ValueError('unsupported algorithm')
    signing_input = f'{parts[0]}.{parts[1]}'.encode()
    signature = b64url_decode(parts[2])
    pub_key = load_pem_public_key(public_key_pem)
    pub_key.verify(signature, signing_input, padding.PKCS1v15(), hashes.SHA256())
    return json.loads(b64url_decode(parts[1]))

Expect these follow-ups

  • What is the algorithm confusion attack specifically and how does a missing algorithm allowlist enable it?
jwtcryptographypythonauthnvulnerability-research

As asked

You need to check whether an incoming IP address falls within any of a million CIDR ranges in a blocklist. Implement an efficient IP trie (also called a radix tree or Patricia trie) for this lookup. What is the time complexity of your approach?

Sample answer outline

A strong answer builds a binary trie over the 32 bits of an IPv4 address, inserts CIDR ranges by traversing to the prefix length and marking that node as blocked, and looks up a single IP by traversing the full 32 bits while checking if any ancestor node is marked. Lookup is O(32) or O(128) for IPv6 regardless of blocklist size, which is optimal. The candidate should discuss memory optimization via path compression (Patricia trie) and the tradeoff against a sorted interval tree or hash-based approach for different dataset characteristics.

Reference implementation (python)

Python
class TrieNode:
    def __init__(self):
        self.children = [None, None]
        self.blocked = False

class IPBlocklist:
    def __init__(self):
        self.root = TrieNode()

    def insert_cidr(self, cidr: str):
        ip, prefix = cidr.split('/')
        bits = self._ip_to_bits(ip)
        node = self.root
        for i in range(int(prefix)):
            b = bits[i]
            if node.children[b] is None:
                node.children[b] = TrieNode()
            node = node.children[b]
        node.blocked = True

    def is_blocked(self, ip: str) -> bool:
        bits = self._ip_to_bits(ip)
        node = self.root
        for b in bits:
            if node.blocked:
                return True
            if node.children[b] is None:
                return False
            node = node.children[b]
        return node.blocked

    def _ip_to_bits(self, ip: str):
        n = int.from_bytes(bytes(int(x) for x in ip.split('.')), 'big')
        return [(n >> (31 - i)) & 1 for i in range(32)]

Expect these follow-ups

  • How would you extend this to support both IPv4 and IPv6 in the same data structure?
algorithmsdata-structuresnetworkingpythontrie

As asked

Write a function that estimates the effective entropy of a password given a set of detected patterns (dictionary words, keyboard walks, date formats, repeated characters). How would you use this in a real-time password strength meter?

Sample answer outline

A strong answer models each detected pattern's entropy based on the search space it requires (e.g. a dictionary word from a 10k-word list contributes log2(10000) bits, a 4-digit year contributes about 5 bits), sums the entropy of independent segments rather than treating the whole password as random characters, and applies a leet-speak multiplier for common substitutions. The function should return bits of entropy and map to a qualitative strength label. In a real-time meter the bottleneck is pattern detection speed, so the candidate should discuss short-circuiting on common patterns.

Reference implementation (python)

Python
import math

DICT_SIZE = 100_000
KEYBOARD_WALKS = 1000
DATE_COMBOS = 365 * 100  # days * years

def estimate_entropy(patterns: list) -> float:
    """
    patterns: list of {type: 'dict'|'walk'|'date'|'random', length: int}
    Returns entropy in bits.
    """
    total_bits = 0.0
    for p in patterns:
        if p['type'] == 'dict':
            total_bits += math.log2(DICT_SIZE)
        elif p['type'] == 'walk':
            total_bits += math.log2(KEYBOARD_WALKS)
        elif p['type'] == 'date':
            total_bits += math.log2(DATE_COMBOS)
        else:  # random chars
            total_bits += p['length'] * math.log2(95)  # printable ASCII
    return total_bits

Expect these follow-ups

  • How does zxcvbn implement this approach and what limitations does it have compared to a full Markov model?
cryptographyentropypythonpassword-securityauthn