Skip to content

Cookies on Jobverse

We use cookies that are needed to sign you in and keep the site working. With your permission we’d also use an analytics cookie to count job views, and Google advertising cookies to show ads. The site works the same whatever you choose. Read the Cookie Policy.

Jobverse
Sign in

Cybersecurity Engineer Top 100 Interview Questions

100 cybersecurity engineer interview questions with structured answers, comparison tables, flow diagrams, worked calculations and runnable code. Covers security fundamentals, cryptography, network and application security, identity, detection, incident response and vulnerability management, from the basics to design and scenario questions.

100 questions

Questions 41-60 of 100

  1. Q41.What is the difference between authentication and authorization?

    Authentication proves who someone is; authorization decides what that proven identity is allowed to do. Authentication happens first, but passing it says nothing about which data or actions a user should reach.

    AuthenticationAuthorization
    Answers: who are you?Answers: what may you do?
    Evidence: password, key, token, biometricEvidence: roles, permissions, ownership, policy
    Runs at login or session startRuns on every request to a protected resource
    Fails when credentials are stolen or guessedFails when a user reads another user's records
    HTTP 401 Unauthorized on failureHTTP 403 Forbidden on failure

    Points that matter in practice:

    • Most serious web application flaws are authorization failures: the user is genuinely logged in but reaches data that is not theirs by changing an ID.
    • Authorization must be enforced on the server for every request. Hiding a button in the user interface is not access control.
    • Keep authorization rules in one shared place, such as a policy module, so they are applied consistently.

    Interview tip: the confusing status code names are a common trick question; 401 actually means "not authenticated", while 403 means "authenticated but not allowed".

  2. Q42.What makes multi-factor authentication phishing-resistant?

    Multi-factor authentication is phishing-resistant when the user cannot hand a usable secret to a fake website. The strongest methods sign a challenge with a private key that never leaves the device, and the signature is bound to the real website's address.

    Text message codeAuthenticator app codePush approval
    Possession of a phone numberPossession of a devicePossession of a device
    Low phishing resistanceLow phishing resistanceMedium phishing resistance
    Weakness: SIM swapping, codes typed into fake sitesWeakness: codes relayed in real time by a phishing proxyWeakness: approval fatigue from repeated prompts
    Push with number matchingHardware security key or passkey
    Possession plus a user checkPossession, often plus biometric or PIN
    Medium phishing resistanceHigh phishing resistance
    Weakness: still approvable if the user is fooledWeakness: recovery processes and lost devices

    Why the strongest methods resist phishing:

    • The browser tells the authenticator which website is asking. A look-alike domain gets a signature that is useless on the real site.
    • There is no code for the user to read out or type, so nothing can be relayed.
    • The private key cannot be exported, so it cannot be stolen from a server breach either.

    The weak point then becomes account recovery. Protect recovery and help-desk resets as strongly as the login itself.

  3. Q43.How do SAML, OAuth 2.0 and OpenID Connect differ?

    SAML is an XML-based standard for single sign-on, mostly used for enterprise web applications. OAuth 2.0 is a framework for delegated authorization, letting an app call an API on a user's behalf. OpenID Connect adds a standard login layer on top of OAuth 2.0.

    SAMLOAuth 2.0OpenID Connect
    For authentication and single sign-onFor delegated access to APIsFor authentication on top of OAuth 2.0
    Token: a signed XML assertionToken: an access token, often opaqueToken: an ID token as a signed JWT, plus an access token
    Used for workforce sign-on to business appsUsed when an app reads a user's calendarUsed for sign-in to web and mobile apps
    Poor fit for mobile apps and APIsGood fit for mobile apps and APIsGood fit for mobile apps and APIs
    Tells the app who the user isDoes not by itself tell the app who the user isTells the app who the user is

    When to use which:

    • Use SAML when integrating with enterprise applications that only support it.
    • Use OAuth 2.0 when an application needs scoped access to an API.
    • Use OpenID Connect for new login integrations, especially for mobile and single-page apps.

    A common mistake is treating an OAuth access token as proof of who logged in. Access tokens are meant for the API, so use the OpenID Connect ID token to establish identity.

  4. Q44.How does the OAuth authorization code flow with PKCE work?

    The application sends the user to the authorization server to log in, receives a short-lived authorization code, and exchanges that code for tokens. PKCE adds a one-time secret so that only the app that started the flow can redeem the code.

    1. The app creates a random code_verifier and derives code_challenge = BASE64URL(SHA256(code_verifier)).
    2. It redirects the user to the authorization server with its client ID, redirect address, scopes, a random state value and the code challenge.
    3. The user logs in and consents; the server redirects back with a code and the same state.
    4. The app checks that state matches, which blocks forged callbacks.
    5. The app sends the code plus the original code_verifier to the token endpoint.
    6. The server hashes the verifier, compares it with the stored challenge, and only then returns tokens.
    sequenceDiagram
      participant U as User browser
      participant A as App
      participant S as Authorization server
      A->>U: Redirect with code challenge and state
      U->>S: Log in and consent
      S->>U: Redirect back with code and state
      U->>A: Code and state
      A->>S: Code plus code verifier
      S->>S: Hash verifier and compare with challenge
      S->>A: Access token and refresh token

    PKCE matters because codes can leak through browser history, logs or a malicious app registered for the same redirect address. A leaked code is useless without the verifier, which never left the app.

    Example

    Deriving the PKCE challenge from a verifier, using the published test values for the method:

    import base64
    import hashlib
    
    verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
    challenge = base64.urlsafe_b64encode(hashlib.sha256(verifier.encode()).digest()).rstrip(b"=").decode()
    
    print(challenge)  # E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
    print(len(verifier), len(challenge))  # 43 43

    The app sends only the challenge at the start. An attacker who intercepts the code cannot compute the verifier from the challenge, because the hash cannot be reversed.

  5. Q45.What is a JSON Web Token, and what mistakes make JWT validation unsafe?

    A JSON Web Token is a compact, signed set of claims written as header.payload.signature, with each part Base64URL-encoded. The signature proves the claims were issued by the key holder and not changed, but the payload is readable by anyone.

    Mistakes that make validation unsafe:

    • Trusting the token's alg header: accepting none, or letting an attacker switch from an asymmetric algorithm to an HMAC one keyed with the public key. Pin the expected algorithm on the server.
    • Skipping claim checks: always validate the expiry, the issuer and the audience, so a token for another service is rejected.
    • Weak HMAC secrets: a short secret can be brute-forced offline from any captured token.
    • Following key URLs from the header: fetching keys from a location the token names lets attackers supply their own key.
    • Putting secrets in the payload: it is encoded, not encrypted.
    • Long lifetimes with no revocation: keep access tokens short-lived and use refresh tokens that the server can revoke.

    Use a maintained library with an explicit configuration for algorithm, issuer, audience and clock skew, rather than decoding tokens by hand.

    Example

    A minimal issuer and a strict verifier, tested against the common attacks:

    import base64
    import hashlib
    import hmac
    import json
    
    KEY = b"a-long-random-server-side-signing-key-0042"
    
    
    def b64url(data: bytes) -> str:
        return base64.urlsafe_b64encode(data).rstrip(b"=").decode()
    
    
    def b64url_decode(text: str) -> bytes:
        return base64.urlsafe_b64decode(text + "=" * (-len(text) % 4))
    
    
    def sign(header_b64: str, payload_b64: str) -> str:
        return b64url(hmac.new(KEY, f"{header_b64}.{payload_b64}".encode(), hashlib.sha256).digest())
    
    
    def issue(claims: dict) -> str:
        header = b64url(json.dumps({"alg": "HS256", "typ": "JWT"}).encode())
        payload = b64url(json.dumps(claims).encode())
        return f"{header}.{payload}.{sign(header, payload)}"
    
    
    def verify(token: str, audience: str, now: int) -> dict:
        header_b64, payload_b64, signature = token.split(".")
        if json.loads(b64url_decode(header_b64)).get("alg") != "HS256":
            raise ValueError("unexpected algorithm")  # pinned, never taken from the token
        if not hmac.compare_digest(sign(header_b64, payload_b64), signature):
            raise ValueError("bad signature")
        claims = json.loads(b64url_decode(payload_b64))
        if claims.get("aud") != audience:
            raise ValueError("wrong audience")
        if now >= claims["exp"]:
            raise ValueError("expired")
        return claims
    
    
    NOW = 1_700_000_000
    token = issue({"sub": "user-17", "aud": "orders-api", "exp": NOW + 900})
    header, payload, signature = token.split(".")
    forged_payload = b64url(json.dumps({"sub": "admin", "aud": "orders-api", "exp": NOW + 900}).encode())
    unsigned_header = b64url(json.dumps({"alg": "none"}).encode())
    
    cases = {
        "valid": (token, "orders-api", NOW),
        "changed payload": (f"{header}.{forged_payload}.{signature}", "orders-api", NOW),
        "alg none": (f"{unsigned_header}.{payload}.", "orders-api", NOW),
        "other service": (token, "billing-api", NOW),
        "expired": (token, "orders-api", NOW + 901),
    }
    for name, (candidate, audience, at) in cases.items():
        try:
            print(f"{name}: accepted as {verify(candidate, audience, at)['sub']}")
        except ValueError as error:
            print(f"{name}: rejected, {error}")
    print(json.loads(b64url_decode(payload))["sub"])  # user-17
    # Output:
    # valid: accepted as user-17
    # changed payload: rejected, bad signature
    # alg none: rejected, unexpected algorithm
    # other service: rejected, wrong audience
    # expired: rejected, expired
    # user-17

    The last line decodes the payload without any key, which is why tokens must never carry secrets.

  6. Q46.Which cookie attributes protect a session, and what does each one prevent?

    Session cookies carry the user's authenticated session, so their attributes decide who can read them, where they are sent and how long they live. Set them deliberately rather than relying on defaults.

    HttpOnlySecureSameSite=Lax or Strict
    Scripts in the page cannot read the cookieSent only over HTTPSNot sent on most cross-site requests
    Reduces session theft through cross-site scriptingReduces sniffing on the networkReduces cross-site request forgery
    No Domain attribute__Host- name prefixMax-Age or Expires
    The cookie stays on the exact host that set itThe browser enforces Secure, Path=/ and no DomainLimits how long a stolen cookie works
    Reduces exposure to other subdomainsStops subdomains overwriting the cookieReduces long-lived session reuse

    Session handling beyond attributes:

    • Issue a new session ID at login to prevent session fixation, where an attacker plants a known ID before the victim logs in.
    • Invalidate the session on the server at logout; deleting the cookie alone leaves a stolen copy valid.
    • Apply both an idle timeout and an absolute lifetime, shorter for sensitive applications.
    • Bind sensitive actions, such as changing a password, to a fresh re-authentication.
  7. Q47.How do role-based and attribute-based access control differ?

    Role-based access control grants permissions to roles and assigns users to roles. Attribute-based access control decides each request from attributes of the user, the resource, the action and the context, evaluated against policies.

    Role-basedAttribute-based
    Decides based on the user's rolesDecides based on attributes and policy rules
    Example: support agents can view ticketsExample: users can edit documents they own, from managed devices, in working hours
    Coarse to medium granularityVery fine granularity
    Simple to audit: list a role's membersHarder to audit: depends on rule logic and data
    Problem: role explosion as special cases growProblem: complex policies that are hard to test

    When to use which:

    • Use roles for most applications and for administrative permissions; they are easy to explain and review.
    • Add attributes where access depends on data, such as ownership, region, data classification or tenant.
    • Many systems combine both: roles decide which actions exist for a user, and attribute or ownership checks decide which records those actions apply to.

    Whichever model you use, centralize the decision in one policy component and log every denial.

  8. Q48.How does privileged access management reduce the risk of admin accounts?

    Privileged access management controls, monitors and limits the accounts that can change systems, so that a stolen or misused admin credential does far less damage. The aim is to remove standing admin power wherever possible.

    Core practices:

    1. Inventory every privileged account, including service accounts, shared root accounts and cloud administrators.
    2. Separate accounts: administrators use a dedicated admin identity, never their everyday email and browsing account.
    3. Store credentials centrally: shared and service passwords live in a secrets manager, are checked out when needed, and rotate automatically after use.
    4. Grant just-in-time access: elevation is requested, approved for a stated task and expires after a set period.
    5. Require strong authentication with phishing-resistant multi-factor authentication for every elevation.
    6. Record and monitor sessions, so unusual commands raise alerts and investigations have evidence.
    7. Keep break-glass accounts for emergencies, with alerts whenever they are used.

    The measure of success is how few people hold admin rights at any given moment. Attackers who steal a normal account then have nothing powerful to escalate into.

  9. Q49.How do you defend a login page against credential stuffing and brute-force attacks?

    Layer controls that make each guess expensive, spot automated patterns across many accounts, and make a correct password insufficient on its own. Different attacks need different signals.

    Brute forcePassword sprayingCredential stuffing
    Many passwords against one accountA few common passwords against many accountsLeaked username and password pairs from other sites, spread over many addresses
    Stop with progressive delays per account and multi-factor authenticationStop by blocking common and breached passwords, and alerting on one source failing across many accountsStop with multi-factor authentication, breached-credential checks, bot detection and device signals

    Further controls:

    • Rate limits by account, by source address and by network range, since stuffing attacks rotate addresses.
    • Uniform responses: the same message and timing for unknown users and wrong passwords, so accounts cannot be enumerated.
    • Careful lockouts: hard lockouts let attackers lock real users out, so prefer temporary delays or extra challenges.
    • Monitoring: watch the global login success rate; a sudden drop with a spike in attempts signals an attack in progress.
    • Notify users about new-device sign-ins so they can report account takeover quickly.
  10. Q50.Implement a time-based one-time password generator.

    A time-based one-time password is an HMAC of the current 30-second time step, keyed with a secret shared between the server and the user's authenticator app, shortened to six digits.

    1. Decode the shared secret, which apps exchange in Base32 inside a QR code.
    2. Compute the counter: counter = floor(unix_time / 30), packed as an 8-byte big-endian integer.
    3. Compute HMAC-SHA1(secret, counter).
    4. Dynamic truncation: take the low 4 bits of the last byte as an offset, read 4 bytes from that offset and clear the top bit.
    5. Take that number modulo 10^digits and pad with leading zeros.

    Points for a production implementation:

    • Allow small clock drift by also accepting the previous and next time step, but no more.
    • Reject reuse: remember the last accepted step per user so an intercepted code cannot be replayed within its window.
    • Rate-limit attempts, because six digits give only a million combinations.
    • Compare in constant time and store the shared secrets encrypted.

    These codes stop password-only attacks, but they can still be phished in real time. Use them where phishing-resistant keys are not yet possible.

    Example

    A generator checked against the published test vectors for the algorithm, which use the ASCII secret "12345678901234567890":

    import base64
    import hashlib
    import hmac
    import struct
    
    
    def totp(secret_base32: str, unix_time: int, digits: int = 6, step: int = 30) -> str:
        key = base64.b32decode(secret_base32)
        counter = unix_time // step
        digest = hmac.new(key, struct.pack(">Q", counter), hashlib.sha1).digest()
        offset = digest[-1] & 0x0F
        number = struct.unpack(">I", digest[offset:offset + 4])[0] & 0x7FFFFFFF
        return str(number % 10**digits).zfill(digits)
    
    
    SECRET = base64.b32encode(b"12345678901234567890").decode()
    print(SECRET)  # GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ
    print(totp(SECRET, 59, digits=8))  # 94287082
    print(totp(SECRET, 1111111109, digits=8))  # 07081804
    print(totp(SECRET, 1234567890, digits=8))  # 89005924
    print(totp(SECRET, 59))  # 287082

    The 8-digit results match the published vectors, and the 6-digit code is the same number's last six digits.

  11. Q51.How does SQL injection work, and how do parameterized queries prevent it?

    SQL injection happens when an application builds a query by pasting user input into SQL text, so specially crafted input changes the query's meaning. A parameterized query sends the SQL and the values separately, so input is always treated as data and can never become code.

    How the attack works:

    1. The code builds ... WHERE name = ' + input + '.
    2. The attacker enters x' OR '1'='1, closing the quote and adding a condition that is always true.
    3. The database runs the altered query and returns every row. Other payloads can read other tables, change data or, on some databases, run commands.

    How to prevent it:

    • Parameterized queries or prepared statements for every value, in every query.
    • Allowlists for identifiers, because table and column names cannot be parameters. Map user choices such as a sort field to fixed names in code.
    • Least-privilege database accounts, so a successful injection reaches as little as possible.
    • Avoid building SQL by hand in stored procedures and query builders too; string concatenation inside them is just as vulnerable.

    Escaping input by hand is fragile and differs between databases, so treat it as a last resort.

    Example

    The same lookup written unsafely and safely, run against an in-memory database:

    import sqlite3
    
    db = sqlite3.connect(":memory:")
    db.execute("CREATE TABLE users (name TEXT, role TEXT)")
    db.executemany("INSERT INTO users VALUES (?, ?)", [("alice", "admin"), ("bob", "user")])
    
    
    def find_unsafe(name: str) -> list:
        query = f"SELECT name, role FROM users WHERE name = '{name}'"  # input pasted into SQL
        return db.execute(query).fetchall()
    
    
    def find_safe(name: str) -> list:
        return db.execute("SELECT name, role FROM users WHERE name = ?", (name,)).fetchall()
    
    
    attack = "x' OR '1'='1"
    print(find_unsafe("bob"))  # [('bob', 'user')]
    print(find_unsafe(attack))  # [('alice', 'admin'), ('bob', 'user')]
    print(find_safe(attack))  # []

    With the parameter, the database looks for a user literally named x' OR '1'='1 and finds none.

  12. Q52.What are the types of cross-site scripting, and how do you prevent each one?

    Cross-site scripting lets an attacker run their script in another user's browser on your site, where it can steal data, act as the user or change the page. The three types differ in where the malicious script comes from.

    StoredReflectedDOM-based
    The script is saved on the server and shown to other usersThe script is echoed back from the current requestThe script is written into the page by client-side code
    A comment containing a script tagA search page that prints the query unescapedScript copies the URL fragment into the page as HTML
    Prevent by encoding output for its context when renderingPrevent by encoding output and never echoing raw inputPrevent with safe APIs that set text, not HTML

    Prevention that covers all three:

    • Context-aware output encoding: HTML body, attributes, JavaScript strings and URLs each need different encoding. Templating engines with automatic escaping handle most cases.
    • Avoid dangerous sinks such as assigning to innerHTML or building code from strings; use textContent and DOM methods instead.
    • Sanitize rich text with a well-maintained HTML sanitizer when users must be able to format content.
    • Content Security Policy as a second layer, blocking inline scripts and limiting where scripts load from.
    • HttpOnly cookies to limit what a successful attack can steal.

    Input validation helps but cannot replace output encoding. The same value can be safe in one context and dangerous in another.

  13. Q53.How does cross-site request forgery work, and which defenses stop it?

    Cross-site request forgery tricks a logged-in user's browser into sending a request the user did not intend. It works because browsers automatically attach the site's cookies to requests, even when another website triggers them.

    sequenceDiagram
      participant U as Victim browser
      participant E as Attacker page
      participant B as Bank site
      U->>B: Log in, receive session cookie
      U->>E: Visit attacker page
      E->>U: Hidden form that posts a transfer to the bank
      U->>B: Transfer request with session cookie attached
      B->>B: Cookie is valid, transfer accepted
    SameSite cookiesAnti-forgery tokenOrigin or Referer checkRe-authentication
    The browser does not send the cookie on most cross-site requestsEach form carries a secret the attacker's page cannot readThe server rejects state changes from foreign originsSensitive actions need the password or a second factor again

    Further points:

    • Never change state with GET requests, because links and images can trigger them.
    • APIs that read a token from a custom header, not from a cookie, are not exposed to this attack, since attacker pages cannot set that header across sites.
    • Cross-site scripting defeats every forgery defense, because injected script runs on your own origin.
  14. Q54.What is server-side request forgery, and why is it dangerous in cloud environments?

    Server-side request forgery happens when an attacker makes your server send requests to a destination of the attacker's choosing, usually by supplying a URL to a feature such as "import from link" or "fetch a preview". The server's network position and credentials then work for the attacker.

    Why it is dangerous in the cloud:

    • Instance metadata: cloud servers can reach a local metadata address, such as 169.254.169.254, that can return temporary credentials for the server's role.
    • Internal services: admin panels, databases and other services trust requests from inside the network.
    • Port scanning from inside: response times and errors reveal the internal network layout.
    flowchart LR
      A["Attacker"] -->|URL to fetch| B["Vulnerable app server"]
      B -->|request| C["Metadata address"]
      C -->|temporary credentials| B
      B -->|response shown| A
      A -->|uses credentials| D["Cloud storage and APIs"]

    Defenses:

    1. Allowlist the destinations and schemes the feature may fetch, rather than blocking known-bad addresses.
    2. Resolve and check the address before connecting, reject private, loopback and link-local ranges, and connect to the address you checked, so a DNS change cannot swap it.
    3. Disable redirects, or validate every hop again.
    4. Require session tokens on the metadata service and give servers minimal roles.
    5. Restrict egress from servers that fetch user-supplied URLs.

    Example

    How a link preview feature became a credentials leak:

    1. A chat app fetched any URL a user pasted to show a preview card with the page title.
    2. An attacker pasted a link to the metadata address 169.254.169.254, with the path that returns the server role's credentials.
    3. The server fetched it and placed the response, a temporary access key and secret, into the "page title" field of the preview.
    4. With those credentials, the attacker listed and downloaded the company's storage buckets.

    The fix combined an allowlist of public schemes and ports, address validation after DNS resolution, token-protected metadata and a narrower server role.

  15. Q55.What is broken object-level authorization, and how do you test for it?

    Broken object-level authorization means an API returns or changes a record without checking that the caller is allowed to access that specific record. The user is properly logged in, but changing an ID in the request reaches someone else's data.

    Why it is common:

    • Frameworks handle authentication globally, but object ownership checks must be written per endpoint, and some get missed.
    • Hard-to-guess IDs such as random identifiers make it less obvious but do not fix it, because IDs leak through links, logs and other responses.

    How to test for it:

    1. Create two test accounts in the same role, and if possible a third in another tenant.
    2. With account A, create resources and record their IDs.
    3. With account B's token, request each of A's resources through every method: read, update, delete and any custom actions.
    4. Try IDs in every location: the path, query string, request body and nested objects.
    5. Check list and search endpoints, and exports, for records that leak across accounts.
    6. Expect a 403 or 404 response every time. A 200, or a change that took effect, is a finding.

    How to fix it: enforce ownership in a shared data-access layer, for example by always filtering queries on the caller's tenant and user, rather than in each controller.

    Example

    A short test session against an order API:

    1. Account A creates order 8812; account B creates order 8813.
    2. GET /api/orders/8812 with B's token returns 200 and A's address. Finding.
    3. PATCH /api/orders/8812 with B's token and a new delivery address returns 200, and the change is saved. Critical finding: B can redirect A's deliveries.
    4. GET /api/orders?customerId=A with B's token returns A's order history. Finding.

    All three endpoints trusted the ID in the request. The fix added the condition "order belongs to the caller" to the shared order lookup used by every endpoint, and the test was added to the automated suite.

  16. Q56.How do you prevent path traversal when serving files?

    Path traversal happens when user input becomes part of a file path and sequences such as ../ walk out of the intended folder. The reliable defense is to resolve the final path and confirm it is still inside the allowed base directory before opening it.

    Key points:

    • Avoid user-supplied paths entirely when possible: store files under generated IDs and look up the real path in a database.
    • Resolve, then check: compute the absolute, normalized path, following any symbolic links, and verify it starts inside the base folder. Checking the raw string for ../ misses encoded and nested variants.
    • Reject absolute paths and drive letters, which can replace the base path when joined.
    • Decode before checking, and decode only once, so %2e%2e%2f cannot slip past.
    • Run with least privilege so that even a successful traversal cannot read secrets such as configuration files or keys.

    Archive extraction has the same flaw: entries named ../../file can be written outside the target folder, so apply the same resolve-and-check rule to every entry.

    Example

    A resolver that blocks traversal attempts, using a temporary folder as the allowed root:

    import tempfile
    from pathlib import Path
    
    root = Path(tempfile.mkdtemp()).resolve()
    (root / "reports").mkdir()
    (root / "reports" / "q1.pdf").write_text("report")
    
    
    def safe_path(user_path: str) -> Path:
        candidate = (root / user_path).resolve()
        if not candidate.is_relative_to(root):
            raise PermissionError("outside the allowed folder")
        return candidate
    
    
    for request in ["reports/q1.pdf", "../../app/config.env", "reports/../../secret.txt", "reports/./q1.pdf"]:
        try:
            print(request, "->", safe_path(request).relative_to(root).as_posix())
        except PermissionError as error:
            print(request, "->", "blocked:", error)
    # Output:
    # reports/q1.pdf -> reports/q1.pdf
    # ../../app/config.env -> blocked: outside the allowed folder
    # reports/../../secret.txt -> blocked: outside the allowed folder
    # reports/./q1.pdf -> reports/q1.pdf

    The third request starts inside the allowed folder and still escapes, which is why the check runs on the resolved path rather than the input text.

  17. Q57.How do you secure a file upload feature?

    Treat every uploaded file as hostile: check what it really is, limit it, store it where it cannot execute, and serve it so the browser cannot treat it as part of your site.

    1. Allowlist file types by checking the file's content signature, not just its extension or the content type the client claims.
    2. Limit size and count per file and per user, and reject compressed files that expand enormously.
    3. Rename files to random identifiers and keep the original name only as metadata.
    4. Store outside the web root, ideally in separate object storage, so an uploaded script can never be executed by the web server.
    5. Serve from a separate domain with Content-Disposition: attachment and X-Content-Type-Options: nosniff, so a disguised HTML file cannot run scripts on your main domain.
    6. Scan for malware before making files available to other users.
    7. Re-encode images and strip metadata, which removes hidden payloads and location data.
    8. Keep parsers patched, because image, document and archive libraries are a frequent source of serious vulnerabilities.

    Process uploads asynchronously in an isolated worker with minimal permissions. A parser exploit then lands in a sandbox rather than on the main application server.

  18. Q58.Why is deserializing untrusted data dangerous?

    Many languages have native serialization formats that can recreate arbitrary objects and call their methods while doing so. If an attacker controls the serialized data, they can choose which objects are built, and chains of existing classes, called gadget chains, can end in running commands on the server.

    What can go wrong:

    • Remote code execution: the most severe outcome, from object construction or "magic" methods that run automatically.
    • Authentication bypass: tampered session objects that claim a different user or role.
    • Denial of service: deeply nested or self-referencing structures that exhaust memory or CPU.

    How to prevent it:

    1. Use data-only formats such as JSON for anything crossing a trust boundary, and validate it against a schema.
    2. Never deserialize native objects from cookies, request bodies, message queues or files that users can influence.
    3. If you cannot avoid it, sign the data with an HMAC and verify the signature before deserializing, and restrict the classes allowed.
    4. Run deserialization with least privilege and keep libraries patched, since new gadget chains are found regularly.

    Interview tip: point out that the danger lies in the format's ability to create objects, not in the parsing itself; that is why switching to a data-only format fixes the class of bug.

  19. Q59.Why is allowlist input validation stronger than denylist validation?

    An allowlist defines exactly what valid input looks like and rejects everything else; a denylist tries to list what is bad and accepts the rest. Attackers only need one variant the denylist forgot, while an allowlist fails safe.

    AllowlistDenylist
    Accepts only known-good patternsRejects known-bad patterns
    Rejects unknown inputAccepts unknown input
    Hard to bypass with encoding tricksOften bypassed with encoding tricks
    Changes when the business rules changeGrows forever as new attacks appear
    Example: a postcode must match a strict formatExample: block input containing <script>

    When to use which:

    • Use allowlists wherever the valid set is known: IDs, dates, enums, sort fields, file types, country codes and redirect targets.
    • Use denylists only as an extra layer, such as blocking known-malicious addresses.
    • For free text, such as names and comments, strict allowlists break legitimate input. Validate length and character encoding, then rely on output encoding and parameterized queries for safety.

    Validate on the server, after decoding and normalizing the input. Client-side validation is for user experience only.

  20. Q60.Which HTTP security headers should a web application set?

    Security headers tell the browser to apply protections that the page itself cannot enforce. They are cheap to add and close whole classes of attacks, though they never replace fixing the application.

    Strict-Transport-SecurityContent-Security-PolicyContent-Security-Policy frame-ancestorsX-Content-Type-Options
    Forces HTTPS for future visitsLimits where scripts and other content load fromStops other sites framing the page, preventing clickjackingStops the browser guessing content types
    max-age=31536000; includeSubDomainsScript sources restricted to self and noncesframe-ancestors 'none'nosniff
    Referrer-PolicyPermissions-PolicyCache-Control
    Limits URL data sent to other sitesDisables browser features the site does not useKeeps sensitive pages out of shared caches
    strict-origin-when-cross-origincamera=(), microphone=()no-store on account pages

    Rolling them out:

    • Start a Content Security Policy in report-only mode, collect violation reports, fix them, then enforce.
    • Add HSTS with a short max-age first, because a mistake locks users into HTTPS for the whole period.
    • Do not rely on deprecated headers such as the old browser cross-site scripting filter; modern browsers ignore it.

    Example

    A response header set for an account page, and what each line buys:

    Strict-Transport-Security: max-age=31536000; includeSubDomains
    Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-r4nd0m'; object-src 'none'; frame-ancestors 'none'; base-uri 'self'
    X-Content-Type-Options: nosniff
    Referrer-Policy: strict-origin-when-cross-origin
    Permissions-Policy: camera=(), microphone=(), geolocation=()
    Cache-Control: no-store

    The policy allows only the site's own scripts plus inline scripts carrying this response's nonce, so an injected script tag without the nonce is blocked. frame-ancestors 'none' stops the page being loaded invisibly inside an attacker's frame, and no-store keeps account data out of shared caches.