277 JavaScript Frontend Developer Interview Questions & Answers

133 top • 30 Amazon • 15 Apple • 29 Google • 18 Meta • 21 Microsoft • 20 Netflix • 11 NVIDIA

JavaScript Frontend Developer icon

Questions with Detailed ExplanationsWith Detailed Explanations

(Last Updated: August 28, 2026)

121. How does cross-site request forgery affect a cookie-authenticated frontend?SecurityEasy

Question Details

A banking page uses an automatically sent session cookie for POST /api/payments. Explain how a malicious site can cause the browser to send a state-changing request, what asset and authorization boundary are at risk, and how SameSite cookies, anti-CSRF tokens, origin checks, and request design reduce the attack. Distinguish CSRF from reading a cross-origin response through JavaScript.

Short Interview Answer (30-60 seconds)

CSRF abuses the browser's automatic cookie behavior. A malicious site can trigger a payment request that carries the victim's bank session cookie even though it normally cannot read the response. Use SameSite cookies, anti-CSRF tokens, Origin checks, safe request design, and server-side authorization.

Detailed Explanation

This question asks how another website can make your browser perform an action on a banking website while you are already signed in. The danger is not that the bad website can see your bank page. The danger is that your browser may automatically include proof that you are signed in when it sends a request. If the bank accepts that request without checking where it came from or whether it belongs to an expected user action, money could be moved or account information could be changed. The answer should explain how the bank recognizes and rejects these unwanted actions.

Useful Questions to Ask the Interviewer
  1. Can I assume the banking application uses a server-managed session stored in a cookie that the browser automatically sends when the cookie rules allow it?
  2. Should I focus on browser-based CSRF defenses for a same-site frontend calling the bank API?
  3. Can I assume POST /api/payments changes server state and therefore requires both authentication and server-side authorization?
How does cross-site request forgery affect a cookie-authenticated frontend? diagram
How to Explain It in an Interview
1. Start with the threat

Assume the user signs in to bank.example and receives a session cookie. The browser stores that cookie and may automatically attach it to later requests to the bank when the cookie's domain, path, security, and SameSite rules allow it.

Now the user visits evil.example. The malicious page tries to cause the browser to send a request to https://bank.example/api/payments. If the browser includes the bank's session cookie and the bank accepts the request without an additional CSRF check, the server may treat the request as belonging to the authenticated user even though the user did not intentionally create that payment from the bank application.

That is cross-site request forgery, or CSRF: another site causes the victim's browser to send an authenticated state-changing request.

2. Identify the asset and authorization boundary

The asset at risk is the user's authenticated authority to perform sensitive banking actions, such as creating a payment.

The trusted authorization boundary is the bank server. The session cookie can identify an authenticated session, but authentication answers only who the session belongs to. It does not prove that the user intended this payment. The server must separately authorize the payment, for example by checking that the authenticated account is permitted to perform the operation, and must reject requests that fail its CSRF defenses.

Frontend JavaScript is not the final authorization boundary because requests can reach the server without using the legitimate frontend code.

3. CSRF does not require reading the response

The browser's same-origin policy normally prevents JavaScript running on evil.example from reading protected response data from bank.example unless cross-origin access is explicitly permitted.

That does not mean the browser cannot send any cross-origin requests. Normal browser features such as form submission can cause cross-origin requests. If such a request includes an authenticated cookie and the server accepts it, state may change even though the malicious site's JavaScript cannot inspect the response.

CORS is therefore not the primary CSRF defense. CORS controls whether JavaScript is allowed to access certain cross-origin responses and whether some non-simple cross-origin requests are permitted after a preflight. It does not turn all cross-origin requests into blocked requests.

4. SameSite cookies reduce automatic cross-site cookie sending

The session cookie should normally use Secure, HttpOnly, and an appropriate SameSite value.

Secure means the cookie is sent only over HTTPS. HttpOnly prevents normal JavaScript from reading the cookie, which helps limit cookie theft through script access, although it does not itself stop CSRF.

SameSite=Strict generally prevents the cookie from being sent with cross-site requests. It gives strong CSRF protection but can interfere with legitimate flows that enter the site from another site.

SameSite=Lax is more permissive. It generally withholds the cookie on cross-site subresource requests and cross-site POST requests, while allowing it on some top-level cross-site navigations that use safe methods such as GET. Because state-changing actions must not be performed with GET, Lax provides useful protection for many applications.

SameSite=None allows the cookie to be sent in cross-site contexts and requires Secure. Applications that genuinely need cross-site cookies need strong explicit CSRF defenses.

SameSite is valuable defense in depth, but sensitive applications should still design their state-changing endpoints so that a browser cookie alone is not enough to authorize an unintended request.

5. Anti-CSRF tokens prove knowledge a malicious origin should not have

For cookie-authenticated state-changing requests, the server can require an unpredictable anti-CSRF token in addition to the session cookie. The trusted application obtains the token and sends it with the state-changing request, commonly in a custom request header or protected form field.

A malicious cross-origin page normally cannot read a properly protected token from the bank because of the same-origin policy. The server validates the submitted token using its chosen CSRF-token design and rejects the request before changing state if the token is absent or invalid.

The token must have enough entropy to resist guessing, must be associated correctly with the application's authenticated request model, and must not be exposed through URLs, logs, or other unsafe channels.

Using a custom request header can add another useful property: ordinary cross-origin HTML forms cannot set arbitrary custom headers. Cross-origin JavaScript attempting to send such a non-simple header is subject to CORS preflight rules, which the bank should not authorize for untrusted origins.

6. Origin checks add another server-side control

For sensitive state-changing endpoints, the server can inspect the Origin header and compare it with an explicit allowlist such as https://bank.example.

If a request is expected to come only from the bank's own origin and its Origin value is untrusted, the server should reject it before changing state. Where an application needs a fallback for requests without Origin, a carefully validated Referer policy may be used according to the server's documented request model.

Origin matching must compare parsed origins exactly. Weak substring or suffix checks can accidentally trust attacker-controlled domains.

7. Request design can make CSRF harder

State-changing operations must not use GET. A URL that can be activated by a link, image, preload, or navigation must never create a payment merely because it was requested.

Use an explicit state-changing method such as POST for payment creation. Require the expected content type and CSRF proof, and reject malformed or unexpected request shapes before changing state.

A JSON API that requires an anti-CSRF custom header can be harder to invoke from ordinary attacker-controlled HTML because HTML forms cannot add arbitrary headers and cannot directly submit application/json. This is useful defense in depth, but the server should still enforce explicit CSRF and authorization controls rather than assuming a content type alone is sufficient protection.

For high-impact transactions, an application may also require transaction-specific confirmation or stronger user verification. Those controls complement rather than replace authorization and CSRF protection.

8. Authentication, authorization, and CSRF protection answer different questions

Authentication asks, 'Which authenticated session sent this request?'

Authorization asks, 'Is this authenticated user allowed to perform this payment?'

CSRF protection asks, 'Does this browser request satisfy the evidence required for an expected request from the trusted application flow?'

The trusted server must enforce every required check before creating the payment. A successful session-cookie check alone must not be treated as sufficient evidence of user intent.

9. Safe failure, logging, and verification

If the anti-CSRF token is missing or invalid, or a required Origin check fails, the server should reject the request before creating the payment. Authorization failures must also fail before state changes occur.

Security logs can record information such as the endpoint, rejection reason, timestamp, and a safe correlation identifier. They should not record session-cookie values, anti-CSRF token values, credentials, or sensitive payment secrets.

Verification should include a legitimate request that is expected to succeed and negative tests that must fail. Test a cross-site request with no CSRF token, an incorrect token, an untrusted Origin, unexpected methods or content types, and the relevant SameSite cookie contexts. Confirm after each rejected request that no payment or other protected state change occurred.

Technical Approach
  1. Identify whether authentication uses cookies that the browser sends automatically.
  2. Identify every state-changing endpoint, such as POST /api/payments.
  3. Confirm that the trusted server authenticates the session and separately authorizes the requested action.
  4. Configure the session cookie with Secure, HttpOnly, and the strongest practical SameSite policy.
  5. Require and validate an unpredictable anti-CSRF token for protected state-changing requests.
  6. Validate the request Origin against an exact trusted-origin allowlist when the application flow supports it.
  7. Keep state changes off GET endpoints and require expected methods, headers, content types, and request shapes.
  8. Reject failed CSRF, authentication, or authorization checks before changing state.
  9. Log rejection metadata without secrets.
  10. Verify the controls with legitimate requests and simulated cross-site attacks.
Practical Insights

These protections add very little computation compared with normal request processing. SameSite enforcement is performed by the browser. Checking an Origin or validating a token usually requires only small comparisons or lookups for each request, with a small amount of extra request or session data. The larger cost is maintenance: choosing cookie behavior that does not break legitimate flows, implementing the token design consistently, maintaining trusted-origin rules, testing browser behavior, and ensuring every sensitive endpoint applies the required protections.

Why Interviewers Ask This

Interviewers want to know whether the candidate understands the browser trust boundary created by automatically sent cookies, the difference between authentication and authorization, why the same-origin policy and CORS do not by themselves prevent CSRF, and how layered controls such as SameSite cookies, anti-CSRF tokens, Origin validation, and safer request design reduce the risk.

Common interview mistakes

Common mistakes are saying that the same-origin policy or CORS automatically prevents CSRF; treating a valid session cookie as proof that the user intended the action; assuming POST alone prevents forgery; performing state changes with GET; relying only on frontend JavaScript checks; treating HttpOnly as a CSRF defense; relying only on SameSite without considering application requirements; accepting missing or invalid anti-CSRF tokens; using weak Origin matching; forgetting server-side authorization; logging session cookies or CSRF tokens; and confusing CSRF with XSS. XSS means attacker-controlled script executes in the trusted application's origin and can often perform actions with the same privileges as legitimate frontend code, while classic CSRF normally causes authenticated requests without needing to read the protected cross-origin response.

Interview tip

Explain CSRF as a browser trust-boundary problem: the browser may automatically attach authentication, but the trusted server still needs authorization and evidence that a sensitive request satisfies the application's CSRF policy. Clearly separate authentication, authorization, SameSite, anti-CSRF tokens, same-origin policy, CORS, and Origin checks, then finish with safe rejection and verification.

Interviewer may ask next
If the same-origin policy prevents a malicious site from reading the bank's response, why is CSRF still possible?

The same-origin policy mainly prevents one origin's JavaScript from reading protected data from another origin. It does not prevent every cross-origin request from being sent. For example, an HTML form can submit to another origin. If the browser includes an authenticated cookie and the bank accepts the request without adequate CSRF validation, server state can change even though the malicious site cannot read the bank's response.

Is SameSite=Lax enough to protect a cookie-authenticated payment endpoint from CSRF?

It blocks the session cookie in many common cross-site request contexts, including typical cross-site POST submissions, so it is a strong mitigation. For a sensitive payment endpoint, I would still use layered controls: server-side authorization, an appropriate anti-CSRF token or equivalent explicit CSRF proof, exact Origin validation where practical, and safe request design. This also makes the protection less dependent on one browser cookie setting and supports applications that may later require different cross-site behavior.

122. What security properties do `HttpOnly`, `Secure`, and `SameSite` cookie attributes provide?SecurityEasy

Question Details

For an authentication cookie issued to app.example, explain what each attribute restricts: JavaScript access, transport, and cross-site sending. Include the effects of host/domain scope, path scope, expiration, and a cross-site sign-in redirect. Identify which attacks each attribute can reduce and which, such as an already executing same-origin script, it does not fully solve.

Short Interview Answer (30-60 seconds)

HttpOnly prevents JavaScript from reading the cookie, Secure restricts it to secure transport, and SameSite limits cross-site sending. For authentication cookies, also use narrow host and path scope, sensible expiration, and server-side authorization. These controls reduce risk but do not completely prevent XSS or CSRF.

Detailed Explanation

These settings tell the browser how carefully it should handle a sign-in cookie. One setting stops page scripts from reading its value. Another makes sure the browser sends it only through a protected connection. A third decides whether the browser may send it when the user arrives or sends a request from another website. Other settings decide which website names and page areas can receive it and how long it lasts. Together, these limits reduce several common ways a sign-in cookie can be stolen or misused, but they cannot stop every attack by themselves.

Useful Questions to Ask the Interviewer
  1. Does the application need authentication to work through a cross-site sign-in redirect from an identity provider?
  2. Is the authentication cookie intended only for app.example, or must sibling subdomains also receive it?
  3. Does the application have any legitimate cross-site requests or embedded scenarios that require the authentication cookie?
What security properties do `HttpOnly`, `Secure`, and `SameSite` cookie attributes provide? diagram
How to Explain It in an Interview

For an authentication cookie, think about three separate browser restrictions: who can read the cookie value, how it can travel over the network, and whether it can be sent in a cross-site context.

HttpOnly restricts JavaScript access. If the trusted server sets an authentication cookie with HttpOnly, JavaScript cannot read that cookie through document.cookie. This reduces the chance that an XSS payload can directly steal and send the raw session cookie to an attacker. However, HttpOnly does not stop malicious JavaScript that is already executing in the application's origin from making requests as the user. The browser can still automatically attach the cookie to eligible requests. Therefore, HttpOnly reduces credential theft from XSS, but it is not an XSS defense by itself. The application still needs safe DOM construction, framework escaping, contextual output encoding, sanitization when intentionally allowing HTML, and controls such as CSP and Trusted Types where appropriate.

Secure restricts transport. A cookie marked Secure is sent only over secure connections such as HTTPS, subject to browser rules. This reduces accidental exposure of the cookie over plaintext HTTP. It does not encrypt the cookie value itself, does not protect the cookie after the server receives it, and does not stop JavaScript from reading it unless HttpOnly is also present.

SameSite controls cross-site sending. SameSite=Strict is the most restrictive mode: the browser generally withholds the cookie when a request is initiated from another site. This can interfere with flows where a user leaves the application for authentication and then returns from an external identity provider. SameSite=Lax is less restrictive. It generally allows the cookie on qualifying top-level cross-site navigations that use safe methods such as GET, while withholding it from many cross-site subrequests and state-changing requests. SameSite=None explicitly permits cross-site sending and must be paired with Secure in modern browsers.

For a cross-site sign-in redirect, SameSite=Strict can prevent an existing application cookie from being sent on the return navigation. SameSite=Lax commonly works when the identity provider redirects the browser back with a top-level GET. A sign-in flow that returns with a cross-site POST, or otherwise requires cross-site cookie delivery, may need a different design or SameSite=None; Secure. If cross-site cookies are required, the application must not rely on SameSite as its only CSRF defense and should use explicit protections such as unpredictable CSRF tokens or protocol-specific state validation where applicable.

Cookie host and domain scope are separate from these three attributes. If the server does not set a Domain attribute, the cookie is host-only. A host-only cookie issued by app.example is sent only to that host and not automatically to sibling hosts such as api.example. Setting an appropriate broader Domain allows the cookie to be sent to matching subdomains, which expands the trust boundary. For a sensitive authentication cookie, prefer host-only scope unless sharing across subdomains is genuinely required.

Path narrows the request paths for which a cookie is normally sent. For example, Path=/account makes the cookie eligible for matching paths under /account. However, Path is a cookie-routing rule, not a strong security boundary between applications on the same host, so it must not be treated as protection from malicious same-origin code.

Expiration controls cookie lifetime. A cookie without Expires or Max-Age is normally treated as a session cookie, although exact session restoration behavior can vary by browser. Max-Age or Expires can make the cookie persistent for a defined period. Authentication cookies should have the shortest practical lifetime. The trusted server should also support session expiration and revocation because removing or expiring the browser cookie does not by itself guarantee that a server-side session or credential has been invalidated.

The attack mapping is important. HttpOnly reduces direct theft of the cookie value by JavaScript. Secure reduces cookie exposure over plaintext transport. SameSite reduces many CSRF opportunities by preventing the browser from attaching cookies in disallowed cross-site contexts. None of these controls replaces authorization. Authentication establishes which user or session is making a request; authorization decides whether that authenticated identity is allowed to perform the requested action. The trusted server must enforce authorization on every protected operation.

For an authentication cookie, a strong default when the application's requirements allow it is similar to Set-Cookie: session=<opaque-value>; HttpOnly; Secure; SameSite=Lax; Path=/, with no Domain attribute so the cookie remains host-only. Some applications can use SameSite=Strict; others legitimately require SameSite=None; Secure. The correct choice depends on required cross-site behavior. The server should use HTTPS, rotate or invalidate sessions appropriately, avoid logging cookie values or other secrets, and fail authentication safely when a session is missing or invalid.

Verification should include checking the cookie attributes in browser developer tools and testing behavior rather than assuming the configuration works. Confirm that JavaScript cannot read an HttpOnly cookie, that the authentication cookie is not sent over insecure transport, that expected same-site and cross-site requests behave correctly, that the sign-in redirect still works, that expiration and logout invalidate access as intended, and that protected server endpoints reject unauthorized requests even if a client attempts to bypass frontend checks.

Technical Approach
  1. Decide whether frontend JavaScript ever needs to read the authentication credential. If not, make the cookie HttpOnly.
  2. Serve authentication over HTTPS and mark the cookie Secure.
  3. Choose the narrowest SameSite mode compatible with the required application flow: use Strict when possible, commonly Lax for applications that need top-level cross-site GET navigation, and None; Secure only when genuine cross-site cookie sending is required.
  4. Keep the cookie host-only by omitting Domain unless multiple trusted subdomains genuinely need it.
  5. Use the narrowest practical Path, while remembering that Path is not a strong same-origin security boundary.
  6. Set an appropriate lifetime with session behavior, Max-Age, or Expires, and support trusted server-side session expiration and revocation.
  7. Use explicit CSRF protection when the application's request model requires it instead of assuming SameSite covers every case.
  8. Prevent XSS independently because HttpOnly does not stop an already executing same-origin script from acting through the user's authenticated browser.
  9. Enforce authentication and authorization on the trusted server independently of browser cookie attributes.
  10. Verify JavaScript access, HTTPS transport, same-site and cross-site behavior, sign-in redirects, expiration, logout, and unauthorized-request rejection.
Practical Insights

The browser checks these cookie rules automatically, so they add no meaningful application-level time or memory complexity. The main cost is operational and maintenance work: developers must design and test HTTPS behavior, login redirects, subdomain sharing, expiration, logout, and cross-site requests. Stricter settings can break legitimate authentication flows, while broader Domain scope or SameSite=None increases the situations in which a cookie can be sent. The maintenance cost remains small when the application documents one clear cookie policy and tests it whenever authentication behavior changes.

Why Interviewers Ask This

Interviewers want to know whether the candidate understands that cookie security consists of several independent browser controls. A strong answer distinguishes protection from JavaScript access, network transport, and cross-site request behavior while also recognizing that Domain, Path, expiration, XSS, CSRF, and trusted server-side authorization remain separate concerns.

Common interview mistakes

Common mistakes are saying that HttpOnly prevents XSS, that Secure encrypts the cookie, or that SameSite completely eliminates CSRF. Another mistake is choosing SameSite=Strict without testing a cross-site sign-in redirect, or assuming Lax supports every redirect method. Setting a broad Domain unnecessarily expands which matching subdomains can receive the cookie. Treating Path as a strong security boundary is also incorrect. Developers may also forget that SameSite=None requires Secure in modern browsers, keep authentication cookies alive longer than necessary, expose session credentials to JavaScript without a real need, log sensitive cookie values, or assume cookie attributes replace trusted server-side authorization.

Interview tip

Explain the attributes as three independent controls: HttpOnly limits JavaScript access, Secure limits network transport to secure connections, and SameSite limits cross-site sending. Then mention host/domain scope, Path, lifetime, redirect compatibility, and the key limitation: these browser controls reduce risk, but the application must still prevent XSS and CSRF where relevant, while the trusted server enforces authorization.

Interviewer may ask next
Why might SameSite=Strict cause problems with a cross-site authentication redirect?

With SameSite=Strict, the browser generally withholds the cookie when the request follows a cross-site context. If the user authenticates at an external identity provider and navigates back to app.example, an existing application cookie may therefore be absent on that return request. SameSite=Lax commonly works better for a top-level return navigation using GET. A cross-site POST callback may not receive a Lax cookie, so the exact authentication protocol and redirect method must be tested rather than weakening SameSite automatically.

If an authentication cookie is HttpOnly, can an XSS attacker still act as the logged-in user?

Yes. HttpOnly prevents injected JavaScript from directly reading the cookie value through document.cookie, which makes stealing that credential harder. But malicious code already executing in the application's origin can often send requests to application endpoints, and the browser can attach eligible cookies automatically. That is why HttpOnly reduces credential theft but does not replace XSS prevention, CSRF defenses where relevant, or trusted server-side authorization checks.

123. How do input validation, output encoding, and HTML sanitization differ?SecurityEasy

Question Details

A profile editor accepts a plain-text display name, a URL for a personal site, and limited rich-text biography markup. For each field, define the attacker input, expected grammar, rendering context, protected asset, and the roles of allowlist validation, context-appropriate encoding or safe DOM APIs, and sanitization. Explain why client checks improve user experience but cannot replace server enforcement.

Short Interview Answer (30-60 seconds)

Validation checks whether input matches a field's allowed grammar. Encoding or safe DOM APIs make accepted data safe for its output context. Sanitization is only for intentionally allowed HTML and removes unsafe markup. Client checks improve feedback, but the trusted server must enforce security rules.

Detailed Explanation

A profile page accepts three different kinds of information: a name, a website address, and a biography that can contain a small amount of formatting. Each needs a different safety rule. First decide what values the field should accept. Then make sure accepted values cannot change the page in an unintended way when shown. If formatting is deliberately allowed, remove anything outside that approved formatting set. Browser checks can give fast feedback to the user, but they cannot be trusted for protection because someone can bypass the page and send requests directly.

Useful Questions to Ask the Interviewer
  1. Which HTML elements and attributes are intentionally allowed in the biography?
  2. Which URL schemes should the personal-site field allow, such as only HTTPS or both HTTP and HTTPS?
  3. Will any of these values be rendered in more than one context, such as visible text, an HTML attribute, or a link destination?
How do input validation, output encoding, and HTML sanitization differ? diagram
How to Explain It in an Interview

The practical difference is purpose.

Input validation asks: "Is this value allowed for this field?" It checks input against the expected grammar or business rules before accepting it.

Output encoding or safe DOM construction asks: "How do I place this accepted value into this particular output context without letting it become code or markup?" In frontend JavaScript, safe DOM APIs often avoid the need for manual encoding because they keep data separate from HTML markup.

HTML sanitization asks: "If HTML is intentionally allowed, which parts of that HTML may remain?" A sanitizer parses HTML and removes or neutralizes elements, attributes, and URL values that are outside an explicit safety policy.

For the plain-text display name, an attacker might submit a value such as <img src=x onerror=alert(1)>. The expected grammar is plain text with whatever length and character rules the product actually requires. The rendering context is normal text in the DOM. The protected assets include the integrity of the page, the user's session, and information or actions accessible to JavaScript running in the application's origin.

Use allowlist validation only for genuine field rules, such as a maximum length or a defined set of accepted characters when the product requires that restriction. Validation alone does not make the value safe to render. Display it with textContent, createTextNode, or normal framework text interpolation that escapes text by default. Do not pass the untrusted name to innerHTML. HTML sanitization is unnecessary because this field is not supposed to contain HTML.

For the personal-site URL, an attacker might submit javascript:alert(1) or another URL outside the application's permitted scheme policy. The expected grammar is a valid URL whose scheme and other required properties satisfy the product's policy. The rendering context is usually the destination of an anchor element. The protected asset is the user's browsing context and the trust the user places in links presented by the application.

Parse the URL rather than searching the string for suspicious text. Apply an allowlist policy to the parsed scheme, commonly https: and, only if the product requires it, http:. If relative URLs are not intended, require an absolute URL as part of the grammar. After validation, assign the accepted URL through a DOM property such as an anchor's href rather than building an HTML string. HTML sanitization is not the correct control because this field contains a URL, not rich HTML.

For the limited rich-text biography, an attacker might submit <script> elements, event-handler attributes such as onclick, dangerous URL schemes, or other markup outside the supported formatting set. The expected grammar is deliberately restricted HTML, for example paragraphs, emphasis, strong text, and links if those features are required. The rendering context is HTML because the product intentionally supports markup. The protected assets again include the DOM, the user's session, and any information or actions available to scripts running in the application's origin.

Length validation and other business checks can still apply, but ordinary input validation cannot safely transform arbitrary HTML into trusted HTML. Process the biography with a well-maintained HTML sanitizer configured with an explicit allowlist of permitted elements, attributes, and URL schemes. Only sanitized output should reach an HTML-parsing sink. Do not attempt HTML sanitization with regular expressions or a home-grown blacklist.

Trusted Types can provide additional protection in supporting browsers by restricting certain dangerous DOM sinks so that approved code paths must create trusted values. A Content Security Policy can also reduce the impact of some XSS mistakes. Both are defense-in-depth controls; neither replaces correct validation, safe DOM construction, contextual output handling, or sanitization.

The important distinction is that these controls are not interchangeable. Validation decides whether the application should accept a value. Contextual output encoding or safe DOM APIs determine how that value can safely be represented at a specific destination. Sanitization is a transformation for content where HTML is intentionally part of the allowed data model.

The output context matters. A value that is harmless when inserted as text is not automatically safe in an HTML attribute, URL, CSS value, or JavaScript source. Prefer APIs that keep data separate from markup, such as textContent, DOM properties, and framework rendering that escapes text by default. Avoid constructing HTML strings from untrusted data whenever HTML parsing is unnecessary.

Client-side validation improves user experience because it can catch mistakes immediately and avoid unnecessary requests. It cannot provide authoritative security enforcement. An attacker can disable JavaScript, modify frontend code, send an HTTP request directly, or use another client entirely. The trusted server must independently enforce all security-relevant validation before storing or acting on data.

The server must also enforce authorization when a profile is modified. Authentication answers who the user is. Authorization answers whether that authenticated user is allowed to modify the particular profile or resource. Client-side controls cannot enforce authorization because requests can bypass the frontend.

Safe failure behavior should reject values that violate the policy instead of trying to guess a safe interpretation. The application should return a clear field-level error and, where appropriate, preserve the previously accepted value. Rejected attacker-controlled data should not be reflected through an unsafe rendering context. Security logging may record enough information to investigate repeated failures, but it should avoid secrets, session tokens, credentials, or unnecessary personal information.

To verify the controls, test each field with both valid and malicious values. Confirm that attack-like display names appear only as literal text, disallowed URL schemes are rejected, valid URLs still work, unsafe biography elements and attributes are removed, approved formatting remains, and no XSS payload executes. Also bypass the browser and send equivalent requests directly to the server to confirm that server-side validation and authorization still reject invalid or unauthorized changes.

Technical Approach
  1. Define the expected grammar, attacker-controlled input, rendering context, and protected asset for each field.
  2. For the display name, enforce genuine field rules and render accepted data as text using textContent, createTextNode, or normal framework escaping.
  3. For the personal-site URL, parse the URL, allowlist permitted schemes and other required URL properties, and assign the accepted value through a safe DOM property.
  4. For the biography, enforce relevant size or business limits and sanitize intentionally allowed HTML with a maintained sanitizer configured with a narrow allowlist.
  5. Keep untrusted data away from HTML-parsing sinks unless sanitized HTML is intentionally required.
  6. Repeat all security-relevant validation on the trusted server and enforce authorization there.
  7. Fail safely, log without exposing secrets, and verify the controls with valid inputs, malicious payloads, sanitizer edge cases, and direct requests that bypass client checks.
Practical Insights

Validation for a display name or URL normally examines the input once, so its time grows roughly with the size of the value and it needs little extra memory. HTML sanitization costs more because the sanitizer must parse and inspect the biography markup, but profile biographies are normally small, so this is usually inexpensive. The more important production cost is maintenance: validation rules, URL policies, sanitizer configuration, and sanitizer dependencies must remain explicit, tested, and updated as requirements or browser behavior change.

Why Interviewers Ask This

Interviewers want to know whether the candidate understands that input validation, contextual output handling, and HTML sanitization protect different trust boundaries. They are testing whether the candidate can define each field's expected grammar, identify its rendering context, prevent XSS with safe DOM construction, safely support intentionally allowed HTML, enforce security rules on the trusted server, and explain why browser-side checks are useful for user experience but are not a security boundary.

Common interview mistakes

Common mistakes include treating validation, output encoding, and sanitization as interchangeable; trusting client-side validation as a security boundary; blocking a few suspicious strings instead of defining an allowed grammar; using innerHTML for plain text; accepting dangerous URL schemes; treating URL validation as simple substring matching; trying to sanitize HTML with regular expressions or a homemade blacklist; sanitizing HTML when the product does not need HTML at all; assuming data safe in one output context is safe in every context; bypassing normal framework escaping without a justified sanitization step; forgetting trusted-server authorization; logging secrets or tokens with validation failures; and treating CSP or Trusted Types as substitutes for correct data handling.

Interview tip

Structure the answer around the three profile fields. For each one, state the attacker input, expected grammar, rendering context, protected asset, and correct control. Finish by emphasizing that validation controls acceptance, safe output handling controls interpretation, sanitization is only for intentionally allowed HTML, and the trusted server must independently enforce security rules and authorization.

Interviewer may ask next
Why is safe output handling still necessary if input has already passed validation?

Validation and output handling protect different boundaries. Validation checks whether a value belongs in a field, but accepted characters may still have special meaning in the context where the value is later rendered. Safe DOM APIs or context-appropriate encoding keep the value as data instead of executable markup. For example, an accepted display name should still be rendered with textContent or normal framework escaping rather than inserted into an HTML string.

When should HTML sanitization be used instead of escaping all markup?

Use HTML sanitization when HTML formatting is an intentional product feature and approved markup must remain functional. A maintained sanitizer should parse the content and apply a narrow policy for allowed elements, attributes, and URL schemes before the result reaches an HTML-parsing sink. If HTML is not required, do not sanitize and render it as HTML; keep the value as plain text and use textContent or normal framework escaping instead.

124. How would you choose a session design for a browser application using bearer tokens?SecurityMedium

Question Details

The application currently stores a long-lived access token in localStorage, attaches it to API requests, and refreshes it with another JavaScript-readable token. Compare the threat from XSS, token exfiltration, CSRF, tab persistence, refresh replay, and logout. Propose a browser/server trust boundary using short lifetimes, rotation or secure cookies as appropriate, in-memory state, and server-side revocation. Include page refresh, multiple tabs, and failure behavior without claiming any storage option eliminates XSS.

Short Interview Answer (30-60 seconds)

I would keep short-lived access tokens in memory and use a Secure, HttpOnly, SameSite cookie for a rotating refresh credential. The server handles authorization, replay detection, revocation, and logout. This reduces token theft and persistence, but strong XSS defenses are still required.

Detailed Explanation

The application needs a safer way to remember that a person has signed in. Right now, important login credentials stay in browser storage for a long time, where harmful code running on the page could copy them. I would reduce how long those credentials remain useful, avoid keeping the most valuable credential where page code can read it, and let the server control renewal and logout. The design must also work after a page reload, across several open tabs, and when renewal fails, without pretending that any browser storage choice makes harmful page code harmless.

Useful Questions to Ask the Interviewer
  1. Is the browser application and API on the same site, or must they work across different sites or subdomains?
  2. Must users remain signed in after closing and reopening the browser, or only during the current browser session?
  3. Do we need immediate server-side logout and session revocation, for example after account compromise or a password change?
  4. Can the backend own a refresh-session endpoint and store refresh-session state for rotation and replay detection?
  5. Are multiple tabs expected to share one signed-in session?
How would you choose a session design for a browser application using bearer tokens? diagram
How to Explain It in an Interview

I would start by separating authentication from authorization. Authentication establishes who the user is. Authorization decides whether that user may perform a specific action. The browser can present credentials, but the trusted server must validate them and enforce authorization for every protected operation. Possessing a valid bearer token must not be treated as permission to perform arbitrary actions.

The current design has two important weaknesses. First, the long-lived access token is stored in localStorage. JavaScript executing in the same origin can read localStorage, so an XSS vulnerability or compromised same-origin script could exfiltrate that token. Second, the refresh token is also JavaScript-readable. If an attacker steals it, the attacker may be able to obtain new access tokens for much longer than the original access token's lifetime.

My preferred design is a short-lived access token held only in JavaScript memory, combined with a server-managed refresh credential in a Secure, HttpOnly cookie. The access token would normally live for only a few minutes. JavaScript attaches it to API requests with an Authorization: Bearer header. Because it is held only in memory, a normal page reload removes that tab's copy, and closing the browser does not persist the access token itself.

The refresh credential is different. I would put it in a cookie configured with Secure so it is sent only over HTTPS and HttpOnly so normal JavaScript cannot directly read the cookie value. I would also choose an appropriate SameSite policy. SameSite=Lax or SameSite=Strict can substantially reduce cross-site request forgery when the application flow permits those settings. If genuine cross-site cookie use requires SameSite=None, the cookie must also be Secure, and explicit CSRF protection becomes especially important.

I would scope the cookie as narrowly as practical. I would avoid an unnecessarily broad Domain attribute and use an appropriate Path. If the deployment allows it, limiting the refresh cookie to the refresh-session path reduces where the browser sends it. The server must never expose the refresh credential back to JavaScript.

On initial login, the server authenticates the user, creates server-side refresh-session state, sends the protected refresh cookie, and returns a short-lived access token to the frontend. The frontend keeps that access token only in memory.

For normal API requests, the frontend sends the short-lived access token in the Authorization header. Browsers do not automatically add an application's bearer Authorization header to an attacker-controlled cross-site form submission, so this part is generally less exposed to classic CSRF than cookie-authenticated state-changing requests. However, CORS is not authentication or authorization. The API must still validate the access token, including its integrity, expiry, issuer and audience where applicable, and then enforce authorization for the requested resource and action.

When the access token expires, the frontend calls the refresh endpoint. The browser automatically includes the HttpOnly refresh cookie. The server validates the refresh session, verifies that it is active and not revoked, rotates the refresh credential, invalidates the previous refresh credential, updates the protected cookie, and returns a new short-lived access token. Rotation means a successfully used refresh credential should not remain indefinitely reusable.

Refresh replay needs explicit handling. The server should keep enough state to recognize reuse of an already-rotated credential. A common design stores a hash or other non-secret verifier for the current refresh credential together with a session or token-family identifier, expiry, and revocation state. If a credential that should already be invalid appears again outside an accepted concurrency window, the server should reject it and can revoke the affected session or token family according to the application's risk policy.

The refresh endpoint is cookie-authenticated, so I would analyze CSRF separately. SameSite is useful but should not be described as universal protection. Depending on the deployment, the server can also require a CSRF token or another explicit request-verification mechanism for cookie-authenticated state-changing operations. Validating Origin for HTTPS browser requests can provide an additional check when appropriate. CORS must not be described as a complete CSRF defense because it mainly governs whether cross-origin JavaScript may read responses and use certain request patterns; it is not a substitute for authentication, authorization, or CSRF validation.

A page refresh intentionally removes the in-memory access token. When the application starts again, it enters an authentication-loading state and calls the protected refresh endpoint. If the refresh session is still valid, the server safely renews or rotates the refresh credential and returns a fresh access token. Only after that succeeds should the client consider the authenticated session restored. If it fails, the application should become signed out rather than trusting stale client-side state.

Multiple tabs need special handling because each tab has its own JavaScript memory while cookies are shared by the relevant browser context. I would not solve this by putting a long-lived bearer token back into localStorage. Each tab can bootstrap its own short-lived in-memory access token through the protected refresh flow. However, two tabs may attempt refresh at nearly the same time and both initially send the same cookie value before rotation completes. The server must have a defined concurrency policy, such as a very small grace mechanism tied to the same session, or the tabs can coordinate refresh activity using a non-secret mechanism such as BroadcastChannel. The design must distinguish expected near-simultaneous use from true replay.

If tabs coordinate, I would share only non-secret events such as 'session refreshed' or 'logged out' unless there is a carefully justified reason to share an access token. I would never broadcast the refresh credential. Sharing bearer access tokens between tabs increases JavaScript exposure and weakens the benefit of keeping credentials isolated in tab memory.

Logout must be meaningful on the server, not just local cleanup. The frontend should call a logout endpoint that revokes the server-side refresh session or token family and expires the refresh cookie. It then removes its in-memory access token and clears user-specific client state. Other tabs can be notified through BroadcastChannel or another non-secret coordination mechanism so that they also clear authenticated state.

A previously issued self-contained access token may remain usable until its short expiry unless the application also checks access-token revocation, performs introspection, or uses another server-side mechanism that can invalidate it immediately. This is an important tradeoff. Very short access-token lifetimes limit that window, while server-side refresh-session revocation prevents the attacker or logged-out browser from obtaining further access tokens.

Server-side revocation is useful for explicit logout, password changes, administrator action, suspicious refresh replay, account compromise, or other security events. The server can store a session identifier, a hash or verifier for the current refresh credential, token-family state, expiry, and revocation status. Raw bearer credentials should not be kept in logs, and refresh credentials should preferably be stored in a form that does not expose the original secret when only comparison is required.

This architecture improves token exposure, but it does not eliminate XSS. An HttpOnly cookie prevents normal JavaScript from directly reading the refresh cookie value, but malicious JavaScript running in the application's origin can still act as the user while the page is open. It may call authenticated endpoints, read sensitive data available to the application, steal an access token already present in memory, or invoke the refresh endpoint and potentially observe the newly returned access token. Therefore the cookie changes what an XSS attacker can directly exfiltrate; it does not make XSS harmless.

For XSS prevention, I would prefer textContent, safe DOM APIs, and normal framework escaping when displaying untrusted text. I would not put untrusted values into innerHTML. If the product intentionally accepts HTML, I would sanitize it with a well-maintained HTML sanitizer and still use the correct output handling for the destination context. Input filtering alone is not sufficient protection.

I would also use a restrictive Content Security Policy to reduce opportunities for unauthorized script execution and use Trusted Types where appropriate to make dangerous DOM injection sinks harder to reach accidentally. These are defense-in-depth controls, not replacements for safe DOM construction and correct framework use.

Third-party scripts require special attention because scripts intentionally allowed to execute in the application's origin usually have powerful access to the page. I would minimize them, restrict allowed script sources with CSP, review why they are required, and manage package and supply-chain risk. Dependency scanning helps discover known vulnerable packages, but it does not eliminate XSS or malicious third-party-script risk.

Clickjacking is relevant when an attacker could frame authenticated UI and trick the user into interacting with it. I would prevent unauthorized framing with CSP frame-ancestors and use X-Frame-Options where compatibility requirements justify it.

I would never place session secrets in URLs. URLs can appear in browser history, server logs, analytics systems, screenshots, copied links, and referrer information. I would also never put server secrets, signing keys, private API credentials, or database credentials into frontend JavaScript because anything delivered to the browser must be treated as discoverable.

Browser storage choices also affect persistence and exposure. localStorage survives normal page reloads and commonly survives browser restarts until it is cleared, and it is readable by same-origin JavaScript. sessionStorage has a different lifetime and is scoped to a browsing context, but it is still JavaScript-readable. IndexedDB is also JavaScript-readable. Moving a bearer credential among these stores changes persistence and sharing behavior but does not remove XSS exposure.

Failure behavior should be predictable and fail closed. If refresh fails because the refresh session is expired, revoked, replayed, malformed, or otherwise invalid, the application should clear its in-memory authentication state, stop treating the user as authenticated, avoid indefinitely retrying protected requests, and return to a signed-out state. It may preserve non-sensitive unsaved UI state when appropriate, but it should not keep showing stale privileged information as though authentication were still valid.

I would also prevent refresh storms. Several API requests can discover an expired access token at almost the same time. Within one tab, the frontend can collapse those events into one in-flight refresh operation. Waiting requests depend on that result. If refresh succeeds, eligible requests can retry once with the new access token. If refresh fails, all waiting requests fail safely instead of creating an infinite refresh loop.

Security logging belongs mainly on the trusted server. I would log security-relevant events such as login, logout, refresh failure, replay detection, revocation, and unusual session behavior using non-secret session identifiers or other safe references. I would not log passwords, raw access tokens, refresh credentials, complete Authorization headers, or other reusable secrets.

Finally, I would verify the design. I would check that localStorage, sessionStorage, and IndexedDB do not contain long-lived bearer credentials; JavaScript cannot read the HttpOnly refresh cookie; the cookie has the intended Secure, SameSite, Domain, and Path behavior; expired access tokens are rejected; refresh rotation invalidates old credentials; replay triggers the intended response; CSRF attempts against cookie-authenticated endpoints fail; logout revokes server state; page refresh restores a valid session correctly; concurrent tabs behave according to the documented policy; failed refresh signs the user out safely; raw credentials never appear in logs; and CSP or Trusted Types violations can be observed during testing.

The central tradeoff is complexity. This design requires more server state and more careful handling than storing two long-lived tokens in localStorage. The backend must support rotation, revocation, concurrency rules, cookie configuration, and replay detection. The frontend must manage temporary in-memory state, page bootstrap, refresh deduplication, and tab behavior. In return, long-lived credentials are no longer directly available through JavaScript-readable persistent storage, stolen access tokens have shorter value, refresh replay becomes detectable, and the trusted server has much stronger control over session termination.

Technical Approach
  1. Identify which credentials JavaScript can currently read, where they persist, and how long each remains useful.
  2. Make the trusted server authoritative for authentication state, authorization, refresh-session validity, rotation, replay detection, and revocation.
  3. Replace the long-lived localStorage access token with a short-lived access token held only in memory.
  4. Store the refresh credential in a Secure, HttpOnly cookie with the narrowest practical Domain and Path and an appropriate SameSite policy.
  5. Protect cookie-authenticated refresh and logout endpoints against CSRF according to the actual same-site or cross-site deployment model.
  6. Rotate refresh credentials after successful use and maintain server-side state sufficient to detect invalid reuse.
  7. Define a concurrency policy so legitimate near-simultaneous refreshes from multiple tabs are not confused with true replay.
  8. On page load, restore authentication through the protected refresh flow rather than persistent JavaScript-readable bearer storage.
  9. Keep access tokens tab-local when practical and coordinate only non-secret session events across tabs.
  10. Make logout revoke the server-side refresh session and expire the cookie, then clear client memory and notify other tabs without transmitting secrets.
  11. Fail closed when refresh is expired, revoked, replayed, or invalid, and prevent infinite retries and refresh storms.
  12. Reduce XSS risk with safe DOM construction, framework escaping, contextual output handling, sanitization only for intentionally allowed HTML, CSP, Trusted Types where appropriate, and careful control of third-party scripts and dependencies.
  13. Verify token lifetime, cookie flags, rotation, replay detection, authorization, CSRF behavior, logout, page refresh, multi-tab behavior, failure handling, and secret-free logging.
Practical Insights

Normal browser work is small. Attaching an access token to a request or checking whether one refresh is already running is constant-time application work, and each tab keeps only a small amount of session data in memory. The larger cost is operational. The server must track refresh-session state, rotation, expiry, replay, and revocation, which adds database or cache reads and writes around refresh and logout. Multi-tab concurrency also adds design and test cases. Maintenance includes cookie policy, CSRF protection, XSS defenses, CSP, dependency review, monitoring, and incident response. This costs more engineering effort than a localStorage-only design but provides stronger control over credential lifetime, replay, and logout.

Why Interviewers Ask This

The interviewer wants to see whether the candidate can choose a browser session architecture by comparing real threats instead of treating localStorage, cookies, or bearer tokens as automatically secure or insecure. The candidate should understand XSS-driven token theft, CSRF, token lifetime, refresh-token rotation, replay detection, multiple-tab behavior, page reloads, logout, server-side revocation, browser trust boundaries, and the difference between authentication and authorization. The interviewer is also testing whether the candidate understands that no browser storage choice eliminates XSS and that the trusted server must enforce authorization.

Common interview mistakes

A common mistake is saying HttpOnly cookies eliminate XSS. They prevent normal JavaScript from directly reading the cookie value, but malicious same-origin JavaScript can still act as the user, call endpoints, read accessible data, and potentially obtain a newly returned access token. Another mistake is moving a long-lived bearer token from localStorage to sessionStorage or IndexedDB and calling the problem solved even though those stores remain JavaScript-readable. Candidates also confuse CORS with CSRF protection, forget that cookie-authenticated refresh and logout endpoints may need CSRF defenses, reuse refresh credentials without rotation or replay detection, treat legitimate multi-tab refresh races as definite theft, implement logout only by clearing frontend state, ignore still-valid access tokens after logout, broadcast secrets between tabs, treat authentication as authorization, log raw credentials, use unnecessarily broad cookie scope, create infinite refresh loops, put server secrets in frontend code, rely on input filtering as the complete XSS defense, or insert untrusted content with innerHTML.

Interview tip

Lead with the trust boundary and threat tradeoff: keep the access token short-lived and in memory, protect and rotate the refresh credential with an HttpOnly cookie, and make the server authoritative for authorization, revocation, and replay detection. Then cover page refresh, multiple tabs, logout, CSRF, XSS, and failure behavior. Explicitly say that no storage choice eliminates XSS.

Interviewer may ask next
Why not store both the access token and refresh token in localStorage if CSP is enabled?

Because localStorage is readable by JavaScript executing in the application's origin. CSP can reduce some ways unauthorized scripts execute, but it does not guarantee that XSS, a compromised allowed script, or malicious supply-chain code can never run. If such code executes, it can read and exfiltrate both localStorage tokens. A stolen long-lived refresh token is especially valuable because it can extend access. Keeping the access token short-lived and in memory reduces persistence, while putting the refresh credential in a Secure, HttpOnly cookie prevents normal JavaScript from directly reading that credential. The application still needs CSP, safe DOM construction, Trusted Types where appropriate, framework escaping, and third-party dependency controls because an XSS attacker can still act as the user.

How would you handle two tabs refreshing at the same time when refresh tokens rotate after every use?

I would define concurrency behavior as part of the refresh protocol. Two tabs may send the same current cookie value almost simultaneously before either response updates the shared cookie, so a second request is not automatically proof of theft. The server can support a tightly bounded concurrency or grace policy associated with the same session, or the tabs can coordinate refresh activity with a non-secret mechanism such as BroadcastChannel so only one refresh is normally in flight. The refresh credential itself is never broadcast. Reuse outside the documented concurrency behavior should be rejected as replay and can revoke the affected session or token family. If refresh ultimately fails, every affected tab should clear authenticated state and fail safely rather than retry indefinitely.

125. How would you secure a rich-text profile biography?SecurityMedium

Question Details

Users may enter links, emphasis, lists, and paragraphs, and the saved HTML is rendered to every visitor under the application's origin. Define the attacker-controlled input, allowed markup and URL schemes, DOM sink, protected sessions and data, and where validation and sanitization occur. Describe testing for event attributes, malformed markup, encoded payloads, SVG or MathML if unsupported, and later mutation. Include a CSP defense-in-depth plan and explain why plain output encoding would remove the required formatting.

Short Interview Answer (30-60 seconds)

I would treat the biography as hostile HTML, allow only required tags, attributes, and URL schemes, sanitize it at the trusted server boundary, and render only sanitized output. I would add CSP and Trusted Types where supported, protect profile updates, and test stored-XSS bypasses and later mutation.

Detailed Explanation

A profile biography is written by one person but shown to many other people. The main danger is that someone may hide harmful instructions inside the saved biography so they run when another visitor opens the profile. I would clearly decide which formatting people may use, remove everything else, and make unsafe links fail closed. I would protect account actions and private information if a bad biography is ever displayed. I would also test unusual, broken, disguised, and changed input because harmful content can sometimes appear after saving or later editing.

Useful Questions to Ask the Interviewer
  1. Which formatting must biographies support: links, emphasis, lists, paragraphs, and anything else?
  2. Are images, SVG, MathML, embedded media, custom attributes, or style attributes intentionally supported?
  3. Should links allow only HTTP and HTTPS, or are schemes such as mailto required?
  4. Is sanitized HTML stored, or is raw user input retained and sanitized each time it is rendered?
  5. Can biographies be modified later by migrations, editors, plugins, imports, or other services after sanitization?
How would you secure a rich-text profile biography? diagram
How to Explain It in an Interview

I would start by defining the trust boundary. The entire biography supplied by the user is attacker-controlled input. That includes element names, attributes, attribute values, URLs, character references, malformed markup, and any content reconstructed later by an editor, import, migration, or other transformation. Because the application intentionally supports rich text, I cannot safely treat the biography as ordinary plain text.

First, I would define a small allowlist based only on the required formatting. If the product needs paragraphs, emphasis, lists, and links, I might allow elements such as p, strong, em, ul, ol, li, and a. Unsupported elements should be removed or rejected. I would not try to maintain a blacklist of known dangerous tags because HTML has too many parser behaviors and execution paths for that to be reliable. I would keep script, iframe, object, embed, style, SVG, and MathML out unless there is an explicit requirement and a separately reviewed policy for them.

Attributes need their own allowlist. Most of these formatting elements need no attributes. For links, I would allow only the minimum attributes the product requires, such as href and perhaps a tightly controlled rel value. Event attributes such as onclick, onerror, onload, and other on* attributes must not survive sanitization. I would also avoid arbitrary style, srcdoc, formaction, and other unnecessary active-content attributes.

URLs require separate validation even when the a element itself is allowed. I would parse each URL instead of relying on string-prefix checks. I would allow only schemes required by the product, normally HTTP and HTTPS, with mailto only if explicitly needed. If relative links are supported, I would resolve them against an expected application base URL before checking the resulting protocol and destination rules. Schemes such as javascript and data should be rejected unless there is an exceptional, separately reviewed requirement. Invalid, ambiguous, or disallowed URLs should fail closed.

Client-side validation can improve the editing experience by checking length, required formatting, or clearly unsupported content before submission, but it is not the security boundary. An attacker can bypass frontend JavaScript and send requests directly. The trusted server must therefore repeat all security-relevant validation and apply the canonical sanitization policy before content becomes trusted renderable HTML.

The server must also enforce authorization. Authentication answers who the user is. Authorization answers whether that authenticated user is allowed to change this specific profile. The frontend may hide editing controls for usability, but the trusted server must enforce the authorization decision on every update request.

For storage, one practical design is to store the sanitized representation that is approved for rendering. Raw source may also be retained if the product truly needs it for editing or auditing, but that raw value must remain explicitly untrusted and must never be rendered as HTML. Another valid design is to retain raw input and sanitize at every rendering boundary. Whichever design is chosen, the invariant is the same: every value that reaches an HTML-rendering sink must have passed the current sanitization policy after its most recent untrusted transformation.

That last point matters because sanitization is not permanent if later code mutates the result. Database migrations, rich-text editors, template transformations, imports, plugins, or other services can change previously safe markup. If later processing can invalidate the original guarantee, I would sanitize again at that new trusted boundary or immediately before the controlled rendering sink.

In the browser, I would keep the HTML sink extremely narrow. Raw biography input must never be assigned to innerHTML, outerHTML, insertAdjacentHTML, document.write, or equivalent framework escape hatches. Normal text fields should continue to use textContent, safe DOM APIs, or the framework's default escaping. Only the dedicated rich-text component should accept the already sanitized HTML representation.

Where the browser supports Trusted Types, I would use them as another enforcement layer around DOM XSS sinks. A policy should create TrustedHTML only from content that has passed the approved sanitization path, and a CSP directive such as require-trusted-types-for 'script' can help prevent ordinary strings from reaching covered injection sinks. Trusted Types are defense in depth and do not replace sanitization, especially because support is not uniform across all browsers.

Plain output encoding is not enough for this feature. Encoding characters such as <, >, &, quotes, and other context-sensitive characters is the correct approach when user input should be displayed only as text. Here, however, formatting is a product requirement. Encoding the entire biography would display allowed markup literally or otherwise remove its HTML meaning, so links, emphasis, lists, and paragraphs would stop working. Because some HTML is intentionally allowed, the correct primary control is strict HTML sanitization combined with a controlled rendering sink.

I would protect the visitor's session because successful stored XSS would execute under the application's origin. Session cookies should normally use Secure, HttpOnly, and an appropriate SameSite setting. HttpOnly helps prevent JavaScript from directly reading the cookie, but it does not stop injected code from making same-origin requests as the logged-in user, so it is not an XSS defense by itself.

If profile updates use cookie-based authentication, I would also use appropriate CSRF defenses for state-changing requests, such as SameSite cookies plus a server-validated CSRF token when the application's threat model requires it. CSRF and XSS are different problems: CSRF tricks a browser into sending an authenticated request, while XSS executes attacker-controlled content in the application's origin. An XSS vulnerability can often bypass normal CSRF protections, which is another reason sanitization remains essential.

The browser's same-origin policy and CORS are not primary defenses against this stored-XSS problem. If malicious code is already executing under the application's own origin, it is same-origin with the application's pages and APIs. CORS should still be configured narrowly for APIs that genuinely require cross-origin access, but changing CORS does not make unsafe biography HTML safe.

I would add a restrictive Content Security Policy as defense in depth. I would avoid unsafe-inline and unsafe-eval where practical, authorize application scripts using nonces or hashes, restrict script-src to required sources, use object-src 'none' when object content is unnecessary, and use base-uri 'none' or another strict value to prevent base-URL manipulation. A suitable frame-ancestors directive can also prevent unauthorized framing and reduce clickjacking risk. CSP should be tested and monitored, but it is not a substitute for removing the stored-XSS path.

Third-party scripts and dependencies are also relevant because any script intentionally allowed to run under the application's origin receives substantial access to the page and user session. I would minimize third-party JavaScript, keep the sanitizer and rich-text dependencies patched, review security advisories, pin dependency versions through the project's normal lockfile process, and remove packages that are no longer needed. For externally hosted resources, I would limit CSP sources and use Subresource Integrity where it is applicable and operationally appropriate.

I would avoid exposing sensitive credentials through frontend code or storage. Server secrets and long-lived credentials must never be embedded in JavaScript bundles. I would also avoid storing sensitive long-lived authentication tokens in localStorage simply for convenience because successful XSS can read browser-accessible storage. The exact session mechanism depends on the application architecture, but the biography feature must not introduce additional token exposure.

Testing is critical because HTML sanitization depends on parsing behavior, not just simple string matching. I would test every class of event-handler attribute, mixed-case variants, encoded characters, HTML character references, malformed and nested markup, broken quoting, control characters, unsafe URL schemes, unexpected namespace behavior, and parser-differential cases. I would verify the behavior in the actual browser rendering path rather than relying only on string-level unit tests.

If SVG and MathML are unsupported, tests should prove that they are completely removed or rendered harmless according to the chosen sanitizer policy. I would not merely test a few well-known payload strings. I would include nested and malformed namespace combinations because browsers can repair or reinterpret markup in ways a naive filter does not expect.

I would also test post-sanitization mutation. A payload might be harmless immediately after sanitization but become dangerous after the DOM parser, a rich-text editor, template code, or later application logic restructures it. Tests should render sanitized output through the real component and verify that later DOM mutations do not create executable attributes, URLs, or elements. Sanitized HTML must never be concatenated with new untrusted HTML fragments after the sanitization boundary.

Safe failure behavior should be explicit. If sanitization or URL validation cannot confidently produce acceptable output, the application should reject the update or remove unsupported markup while preserving harmless text according to the product contract. It must never fall back to rendering the original unsanitized HTML because the sanitizer failed.

Security logging should record useful facts such as a rejected element category, disallowed URL scheme, policy failure, or CSP violation without recording passwords, session tokens, authorization headers, server secrets, or unnecessary private biography content. Logs themselves should not become a new location for sensitive-data exposure.

Finally, I would verify the complete control with sanitizer unit tests, browser-level stored-XSS regression tests, authorization tests, CSRF tests where applicable, CSP reporting during deployment, and tests for every sanitizer bypass or mutation bug that is discovered. The design is layered: allowlisting and sanitization handle intentionally supported HTML, the controlled sink prevents raw-input rendering, Trusted Types can enforce safer sink usage in supporting browsers, the trusted server protects state changes, secure session controls reduce credential exposure, and CSP limits impact if another layer fails.

Key Insight / Why This Solution Works
  1. Mark the entire submitted biography and all later untrusted transformations as attacker-controlled.
  2. Define the exact allowed HTML elements from the formatting requirements.
  3. Define allowed attributes separately and remove event handlers and unnecessary active-content attributes.
  4. Parse and validate link URLs against the small set of schemes and relative-URL behavior the product intentionally supports.
  5. Use client-side validation only for user experience; repeat all security-relevant validation on the trusted server.
  6. Apply the canonical sanitizer at a trusted boundary before HTML becomes renderable or trusted for storage.
  7. Enforce authentication and server-side authorization for every profile update.
  8. Add CSRF defenses when cookie-based state-changing requests require them.
  9. Render only the sanitized representation through one controlled rich-text sink; never send raw user input to innerHTML or equivalent APIs.
  10. Use Trusted Types where supported to reduce accidental unsafe sink usage.
  11. Add a restrictive CSP and appropriate frame-ancestors policy as defense in depth.
  12. Re-sanitize after any migration, import, editor transformation, plugin, or other later mutation that can invalidate the original guarantee.
  13. Test event attributes, malformed HTML, encoded payloads, unsafe URLs, unsupported SVG and MathML, browser parser repair, and post-sanitization mutation.
  14. Fail closed, log security-relevant failures without secrets, and keep regression tests for discovered bypasses.
Why Interviewers Ask This

This question tests whether the candidate can reason about stored XSS across the full lifecycle of user-controlled rich text instead of relying on one filter. It evaluates trust-boundary identification, HTML and URL allowlisting, safe DOM rendering, trusted server enforcement, browser defenses such as CSP and Trusted Types, protected-session reasoning, mutation risks, and practical verification. It also checks whether the candidate understands why ordinary output encoding is correct for plain text but cannot preserve intentionally supported links, emphasis, lists, and paragraphs.

Common interview mistakes

Common mistakes are sanitizing only in frontend JavaScript; using a blacklist of known bad tags instead of a small allowlist; allowing an a element without validating its URL; trusting string-prefix URL checks; stripping script tags while leaving event attributes or dangerous namespaces; allowing unnecessary style, SVG, or MathML content; trusting stored HTML forever even after later mutation; passing raw content to innerHTML or a framework raw-HTML escape hatch; assuming HttpOnly cookies prevent authenticated XSS actions; treating CORS or the same-origin policy as an XSS defense; relying on CSP instead of fixing the unsafe HTML path; using unsafe-inline broadly; treating Trusted Types as a sanitizer; ignoring browser parser repair and mutation after sanitization; relying only on plain output encoding even though rich formatting is required; storing long-lived credentials in browser-accessible storage; logging sensitive tokens or private content; or failing open by rendering the original HTML when sanitization fails.

Interview tip

Start with the trust boundary and trace one biography from input to storage to rendering. Say clearly that rich text requires strict sanitization rather than encoding the whole value as text. Then explain server-side authorization, the controlled DOM sink, URL validation, mutation testing, and defense-in-depth controls such as CSP and Trusted Types.

Interviewer may ask next
Would you sanitize the biography when it is saved or every time it is rendered?

I would enforce sanitization at a trusted server boundary before the biography becomes trusted renderable HTML. Storing that sanitized representation can avoid repeated work, while raw source may be retained separately only if it remains explicitly untrusted. I would not assume the sanitized value remains safe forever. If migrations, imports, editors, plugins, or other systems can modify it later, I would re-sanitize after that untrusted transformation or before rendering. The key invariant is that every value reaching the HTML sink satisfies the current sanitizer and URL policy.

Why are CSP, Trusted Types, and HttpOnly cookies not enough if the biography still contains an XSS payload?

They are additional layers, not replacements for sanitization. CSP can block many execution paths, but a weak policy, allowed script gadget, browser difference, or future configuration error may reduce its protection. Trusted Types can restrict covered DOM sinks in supporting browsers, but the application still needs a policy that creates safe HTML and support is not universal. HttpOnly stops JavaScript from directly reading the session cookie, but injected same-origin code can still make authenticated requests. The primary control is therefore preventing dangerous markup from reaching an executable DOM sink.

126. How would you protect a cookie-authenticated money-transfer form from CSRF?SecurityMedium

Question Details

The form posts JSON to https://bank.example/api/transfers; the session cookie is sent automatically, and a separate marketing site must not initiate transfers. Define the authentication and origin boundaries, anti-CSRF token lifecycle and binding, SameSite setting, Origin or Referer validation, CORS behavior, content-type expectations, and failure response. Explain how the frontend obtains and submits the token without exposing it to unrelated origins, and how you would test a legitimate cross-site sign-in return separately.

Short Interview Answer (30-60 seconds)

I would require a session-bound CSRF token in a custom header, validate Origin, accept only JSON, use a Secure HttpOnly SameSite cookie, and deny the marketing origin through CORS. Any failed CSRF check returns 403 with no transfer. The server must also authorize the requested transfer.

Detailed Explanation

See the Code while reading this explanation.

A bank form moves money using a browser login that is sent automatically with each request. That creates a danger: another website might try to make the browser send a transfer without the customer intending it. The goal is to make the bank accept transfers only when the request really came from its own trusted page and from the signed-in customer. Several independent checks should agree before money moves. A failed check must stop the transfer safely, while a normal return from an outside sign-in page should still work through a separate path.

Useful Questions to Ask the Interviewer
  1. Does the sign-in provider return to the bank with a top-level GET redirect, or does it require a cross-site POST callback?
  2. Are the frontend and transfer API both under https://bank.example, or are there other trusted bank-controlled origins that must call the API?
  3. Can the backend keep CSRF state in the authenticated server session?
How would you protect a cookie-authenticated money-transfer form from CSRF? diagram
How to Explain It in an Interview

I would start by defining the trust boundaries. https://bank.example is trusted to initiate transfers. The separate marketing origin is not trusted to initiate them. The browser automatically attaching a valid session cookie proves that the request is associated with an authenticated session, but it does not prove that the user intentionally initiated the request from the bank application. The trusted backend must also perform authorization: after identifying the user, it must verify that this user may transfer from the requested source account and that the operation satisfies the bank's normal rules.

For the authentication cookie, I would use Secure and HttpOnly, with an appropriate SameSite value. Secure limits transmission to HTTPS. HttpOnly prevents normal frontend JavaScript from reading the session cookie. I would normally prefer SameSite=Lax when the application needs a normal top-level cross-site GET return from an identity provider. If the application's required navigation flows work with SameSite=Strict, that provides a tighter cross-site boundary. SameSite is defense in depth, not the only CSRF defense for a money-transfer endpoint.

The server would generate a cryptographically random anti-CSRF token and bind it to the authenticated session. The token should be issued after authentication or when CSRF state is initialized, replaced when the security-sensitive session context changes, and invalidated when the session ends. A token from one authenticated session must not validate for another session.

The frontend can obtain the token from a same-origin authenticated endpoint such as /api/csrf-token, or receive it in trusted same-origin server-rendered application data. Because the same-origin policy prevents an unrelated origin from reading same-origin responses unless the bank explicitly grants CORS access, the marketing site must not be allowed to read this token. I would keep the token in JavaScript memory when practical rather than placing it in a URL or unnecessarily persisting it in long-lived browser storage. The frontend then sends it in a custom header such as X-CSRF-Token with the transfer request.

The server must compare the submitted token with the value expected for the current authenticated session and reject a missing, malformed, expired, or wrong-session token. The token is not a substitute for authorization, and the frontend must never contain a server secret or long-lived privileged credential.

I would also validate the browser-provided Origin header. For this endpoint, the expected initiating origin is the exact trusted bank origin, for example https://bank.example. The comparison must be an exact origin comparison of scheme, host, and port rather than substring matching. If Origin is legitimately absent for a supported client, the server can validate the origin portion of Referer as a fallback. For a high-risk browser money-transfer endpoint, I would fail closed when the required origin evidence is unavailable or untrusted rather than silently accepting the request.

The transfer route should accept only the intended method and JSON media type. For example, it can require POST with an application/json media type, allowing only explicitly supported parameters such as an optional charset if the server stack permits them. It should not also accept application/x-www-form-urlencoded, multipart/form-data, or text/plain merely for convenience. JSON alone is not a complete CSRF defense, but requiring JSON together with a non-simple custom CSRF header prevents an ordinary cross-site HTML form from matching the accepted request shape.

CORS should not authorize the marketing origin to call the transfer API. If the transfer API needs no cross-origin browser callers, it should grant no cross-origin CORS access. If specific bank-controlled origins legitimately need access, the server should use an explicit allowlist, permit credentials only for those origins, and permit only the required methods and request headers. It must not reflect arbitrary origins. It also cannot combine credentialed requests with Access-Control-Allow-Origin: *.

A cross-origin JavaScript request using JSON plus X-CSRF-Token normally requires a browser CORS preflight. The marketing origin should receive no permission to continue that credentialed request. However, CORS is a browser access-control mechanism, not the server's primary CSRF validation. The server must still independently validate the CSRF token and request origin because those checks directly protect the state-changing operation.

The server-side processing order should fail safely. First authenticate the session. Then verify the allowed HTTP method and content type, validate Origin or the permitted Referer fallback, and validate the CSRF token bound to that session. Next validate the transfer fields and authorize the requested operation. Only after every required check succeeds should the server create the transfer.

If the CSRF token or origin check fails, I would return 403 Forbidden and perform no transfer or other partial state change. The response should not reveal token values or detailed information that helps an attacker distinguish token guesses. Security logs may record a safe correlation identifier, time, rejected origin, authenticated account identifier when appropriate, and a general failure category, but they should never log session cookies, CSRF tokens, credentials, or unnecessary sensitive transfer information.

I would also treat same-origin XSS as a separate but important threat because JavaScript executing inside the trusted bank origin could make authenticated requests and potentially obtain the CSRF token. CSRF tokens do not protect against arbitrary same-origin script execution. The application should therefore avoid innerHTML with untrusted content, use textContent, safe DOM APIs, or normal framework escaping, sanitize only where intentionally allowing HTML, and use defenses such as a restrictive CSP and Trusted Types where practical. Third-party scripts running with the bank page's privileges should be minimized and tightly controlled.

I would verify the CSRF design from a separate attacker-style origin. A normal cross-site HTML form should not be able to produce an accepted request because the transfer route does not accept simple form content types. Cross-origin JavaScript from the marketing origin should not be granted CORS permission and should not be able to read the same-origin CSRF-token response. Direct requests with a missing token, modified token, expired token, or token from a different session should return 403. Requests with an untrusted Origin, or without required trustworthy origin evidence, should fail. A legitimate same-origin request with the valid session, correct session-bound token, trusted origin, supported JSON request shape, valid data, and successful authorization should succeed.

I would test a legitimate cross-site sign-in return separately because it has a different security purpose from /api/transfers. For an OAuth 2.0 or OpenID Connect authorization flow, the application should validate the callback's dedicated state value and use PKCE where applicable; OpenID Connect flows may also use and validate a nonce. A top-level GET return commonly works with SameSite=Lax. If an identity provider genuinely requires a cross-site POST response, I would narrowly design the authentication callback and any temporary state or cookie needed for that flow. I would not weaken the transfer endpoint's SameSite, origin, CSRF-token, content-type, or authorization requirements merely to make the sign-in callback work.

Key Insight / Why This Solution Works
  1. Define https://bank.example as the trusted transfer-initiating origin and keep the marketing origin outside that boundary.
  2. Authenticate the request using the secure session cookie.
  3. Require the intended HTTP method and supported application/json media type.
  4. Validate Origin against the exact trusted bank origin, using an exact-origin Referer fallback only where that fallback is intentionally supported.
  5. Require a cryptographically random CSRF token in a custom header and verify that it is bound to the current authenticated session.
  6. Validate the JSON transfer fields.
  7. Authorize the authenticated user to perform the exact requested transfer.
  8. Perform the transfer only after every required check succeeds.
  9. On a CSRF or origin failure, make no state change, return a generic 403, and log only safe diagnostic metadata.
  10. Test cross-site authentication callbacks independently with their own anti-forgery state instead of weakening /api/transfers.
Code
let csrfToken = null;

async function getCsrfToken() {
  if (csrfToken) return csrfToken;

  // Read CSRF state only from the trusted same-origin bank endpoint.
  // The browser may send the HttpOnly authentication cookie, but JavaScript cannot read that cookie.
  const response = await fetch('/api/csrf-token', {
    method: 'GET',
    credentials: 'same-origin',
    headers: {
      Accept: 'application/json',
    },
  });

  // Fail closed if the server cannot establish valid CSRF state for the authenticated session.
  if (!response.ok) {
    throw new Error('Unable to initialize secure transfer request.');
  }

  const data = await response.json();

  // A missing token means the security precondition was not established, so no transfer is attempted.
  if (typeof data.csrfToken !== 'string' || data.csrfToken.length === 0) {
    throw new Error('Unable to initialize secure transfer request.');
  }

  // Keep the token in memory instead of exposing it in a URL or unnecessarily persisting it.
  // Same-origin policy and restrictive CORS must prevent unrelated origins from reading this response.
  csrfToken = data.csrfToken;
  return csrfToken;
}

async function createTransfer({ fromAccountId, toAccountId, amount }) {
  const token = await getCsrfToken();

  // The custom header supplies the anti-CSRF value that the server binds to this authenticated session.
  // The server must independently validate Origin, content type, input, and authorization before moving money.
  const response = await fetch('/api/transfers', {
    method: 'POST',
    credentials: 'same-origin',
    headers: {
      'Content-Type': 'application/json',
      Accept: 'application/json',
      'X-CSRF-Token': token,
    },
    body: JSON.stringify({ fromAccountId, toAccountId, amount }),
  });

  // Do not weaken or bypass security controls after a rejection.
  // A CSRF or origin failure should produce no state change on the trusted server.
  if (!response.ok) {
    if (response.status === 403) {
      // Drop cached CSRF state so a later legitimate attempt can obtain fresh session-bound state.
      // The server remains authoritative about whether the session itself is still valid.
      csrfToken = null;
    }

    throw new Error('Transfer was not accepted.');
  }

  return response.json();
}
Why Interviewers Ask This

This tests whether the candidate understands why automatically sent cookies create CSRF risk and can combine browser and server controls correctly. A strong answer distinguishes authentication from authorization, defines trusted and untrusted origins, explains the CSRF token lifecycle and binding, uses SameSite and CORS as defense in depth instead of substitutes for server validation, specifies safe failure behavior, and handles a legitimate cross-site sign-in return separately.

Common interview mistakes

Common mistakes are relying on SameSite alone; treating CORS as the primary CSRF defense; accepting a CSRF token that is not bound to the authenticated session; exposing the token in a URL or unnecessarily persistent browser storage; allowing the marketing origin to read the token endpoint; using substring or suffix matching for Origin; reflecting arbitrary CORS origins; accepting form-encoded or text/plain requests on a JSON-only transfer endpoint; assuming a JSON content type alone prevents CSRF; logging cookies or CSRF tokens; returning detailed token-validation information; confusing authentication with authorization; assuming CSRF protection also stops same-origin XSS; and weakening the sensitive transfer endpoint merely to support an unrelated cross-site sign-in callback.

Interview tip

Explain the protection as independent layers with clear jobs: the cookie authenticates the session, server authorization decides whether the transfer is allowed, the session-bound token and exact Origin check prove acceptable browser request context, the JSON/custom-header contract narrows request shape, SameSite and CORS add browser defenses, and the sign-in callback gets its own dedicated anti-forgery validation.

Interviewer may ask next
Is SameSite=Lax enough by itself to protect the transfer endpoint from CSRF?

No. SameSite=Lax is useful defense in depth because it prevents cookies from being sent with many cross-site requests while still supporting common top-level GET navigations. But I would not use it as the only control for a money-transfer endpoint. I would still require a CSRF token bound to the authenticated session, validate the exact trusted Origin or an intentionally supported Referer fallback, restrict the accepted request shape, and authorize the transfer on the server.

What would you do if the identity provider requires a cross-site POST back to the bank?

I would isolate that authentication callback from the money-transfer endpoint. The callback would use its own narrowly scoped anti-forgery state, such as a validated OAuth or OpenID Connect state value, with PKCE where applicable and an OpenID Connect nonce where applicable. Any cookie or temporary state needed for that callback should have the minimum scope and lifetime required. I would not make /api/transfers accept cross-site POSTs or remove its CSRF, origin, content-type, or authorization checks.

127. How would you configure a credentialed cross-origin frontend API safely?SecurityMedium

Question Details

The application runs at https://app.example.com and calls https://api.example.com with a session cookie; no other origins should read responses. Specify allowed origins, credential headers, preflight handling, methods and headers, cache variation, cookie scope and SameSite behavior, and error handling. Identify why Access-Control-Allow-Origin: * cannot be combined safely with credentials and why CORS still does not replace API authorization.

Short Interview Answer (30-60 seconds)

I would allow only https://app.example.com, return that exact origin with Access-Control-Allow-Credentials: true, restrict preflight methods and headers, add Vary: Origin, and scope the cookie safely. The frontend uses credentials: "include", while the API still enforces authentication, authorization, and appropriate CSRF protection.

Detailed Explanation

This question asks how to let one trusted website use a person's signed-in session when talking to a separate service, while stopping other websites from reading the replies. The service should recognize only the approved website, accept only the actions and request information it really needs, and reject everything else. The person's sign-in information should be protected and limited in where it is sent. Stored copies of replies must not mix rules for different websites. Most importantly, permission for a website to read a reply does not decide what the signed-in person is allowed to do.

Useful Questions to Ask the Interviewer
  1. Are app.example.com and api.example.com the only production origins, or must development and staging origins also be supported?
  2. Which HTTP methods and request headers does the frontend actually need?
  3. Is the session cookie intended only for these same-site HTTPS subdomains, or must it also work in a truly cross-site context?
  4. What CSRF protection is expected for state-changing cookie-authenticated requests?
How would you configure a credentialed cross-origin frontend API safely? diagram
How to Explain It in an Interview

The first decision is an exact server-side origin allowlist. For the stated production requirement, the allowed origin is only https://app.example.com. When the API receives a cross-origin request, it checks the Origin header against that allowlist using an exact comparison. If the origin is approved, the API can return Access-Control-Allow-Origin: https://app.example.com and Access-Control-Allow-Credentials: true. It must never blindly copy an arbitrary Origin value into the response.

For an origin that is not approved, the API must not return an Access-Control-Allow-Origin value that grants that origin access. For a failed preflight, it should reject the request or return a response without CORS permission. Protected API endpoints must still apply their normal authentication and authorization rules regardless of the CORS result. CORS is enforced mainly by browsers; it is not a security boundary against direct HTTP clients.

Access-Control-Allow-Origin: * cannot be combined with credentialed CORS. When the credentials flag is true, browsers require a specific permitted origin rather than the wildcard. The wildcard also contradicts the requirement that only https://app.example.com may read these credentialed responses.

On the frontend, a cross-origin fetch that needs the session cookie uses credentials: "include". The API opts into credentialed CORS with Access-Control-Allow-Credentials: true. Both sides are needed: the frontend controls whether credentials are included in the cross-origin fetch, while the server controls whether the browser may expose the credentialed cross-origin response to that origin.

For requests that require a CORS preflight, the browser first sends an OPTIONS request. It includes the intended method in Access-Control-Request-Method and, when needed, proposed request header names in Access-Control-Request-Headers. The API should validate the request origin, requested method, and requested headers against explicit allowlists. A successful preflight returns the exact allowed origin, Access-Control-Allow-Credentials: true, only the necessary Access-Control-Allow-Methods, and only the necessary Access-Control-Allow-Headers.

For example, if this application only needs GET and POST, do not advertise PUT, PATCH, or DELETE. If JSON POST requests use Content-Type: application/json, permit Content-Type when requested. Do not use Access-Control-Allow-Headers: * as a substitute for deciding which non-simple request headers the application actually needs. A reasonable Access-Control-Max-Age can reduce repeated preflights, but a very long value makes policy changes slower to take effect in clients.

Because the response's CORS policy depends on the request origin, include Vary: Origin. This tells shared HTTP caches that responses selected for different Origin values can require separate cache variants. Without appropriate cache variation, an intermediary could reuse origin-dependent response headers incorrectly. Normal cache rules for authentication and private user data must also be configured independently; Vary: Origin does not by itself make sensitive responses safe to cache publicly.

The session cookie should be created by the trusted server over HTTPS with Secure and normally HttpOnly. Secure restricts transmission to secure connections. HttpOnly prevents frontend JavaScript from reading the cookie value directly, which reduces session-token exposure if script execution is compromised. Prefer a host-only cookie by omitting the Domain attribute when broader subdomain sharing is unnecessary, and use the narrowest practical Path.

https://app.example.com and https://api.example.com are different origins because their host names differ, so browser CORS rules apply. However, they are normally same-site because both use HTTPS and share the same registrable domain, example.com. Therefore SameSite=None is not automatically required merely because the request is cross-origin. Choose Strict or Lax when the required application flow works with it. Use SameSite=None; Secure only when the cookie genuinely must be sent in a cross-site context.

Cookie-based authentication also requires attention to CSRF for state-changing operations. SameSite cookies reduce some CSRF exposure, but the application should use an explicit CSRF defense when its threat model requires one, such as a server-validated anti-CSRF token. CORS should not be presented as the authorization system or as the only CSRF control. Some cross-origin requests can be transmitted even when browser JavaScript is not allowed to read their responses.

Authentication and authorization are separate. A valid session cookie can authenticate who the user is. The trusted API must then authorize each protected operation by checking whether that authenticated user may access the requested resource or perform the requested action. CORS only controls whether browser code from a particular origin may access a cross-origin response. It does not give the user permission to read or modify API resources.

The system should fail closed. An unexpected origin must not receive permissive CORS headers. An invalid preflight must not broaden the method or header policy. Invalid sessions should receive the normal authentication failure, and authenticated users without permission should receive the application's normal authorization failure. Logs may include the rejected origin, route, method, status, and a request identifier, but they should not contain session cookies, access tokens, CSRF tokens, or other secrets.

I would verify the configuration with browser and server tests. A credentialed request from https://app.example.com should succeed. A request from an unapproved browser origin must not receive CORS permission to expose the response. A preflight asking for an unapproved method or header should fail. Responses should contain the intended Vary: Origin behavior. The cookie should have the expected Secure, HttpOnly, Domain or host-only, Path, and SameSite attributes. Finally, direct API tests should prove that authentication and resource-level authorization still work correctly even when CORS is irrelevant.

Technical Approach
  1. Define an exact server-side origin allowlist containing https://app.example.com and only separately approved development or staging origins if required.
  2. For a CORS request, compare the supplied Origin exactly with the allowlist. Do not use substring matching, unsafe suffix matching, or blind reflection.
  3. For an approved origin, return that exact value in Access-Control-Allow-Origin and return Access-Control-Allow-Credentials: true.
  4. Include Vary: Origin when the response's CORS headers vary according to Origin.
  5. Handle OPTIONS preflights by validating the origin, Access-Control-Request-Method, and requested header names against explicit allowlists.
  6. Return only the HTTP methods and request headers the frontend actually needs.
  7. Optionally use a reasonable Access-Control-Max-Age when the operational tradeoff is acceptable.
  8. Set the session cookie from the trusted server with Secure, normally HttpOnly, the narrowest practical Path, and preferably host-only scope unless a Domain attribute is truly required.
  9. Choose the least permissive SameSite value that supports the required site relationship; do not assume cross-origin means cross-site.
  10. Use credentials: "include" on frontend fetch requests that require the session cookie.
  11. Apply appropriate CSRF protection to state-changing cookie-authenticated operations.
  12. Enforce authentication and resource-level or action-level authorization on the API independently of CORS.
  13. Fail closed for unknown origins and invalid preflights, and log diagnostic information without credentials or secrets.
  14. Test approved and rejected origins, method and header restrictions, preflight behavior, cache variation, cookie attributes, CSRF controls, authentication, and authorization.
Practical Insights

The origin, method, and header checks are tiny compared with normal API work. With a small allowlist, their CPU and memory cost is effectively constant for each request. Some requests need an extra OPTIONS preflight, which adds one network round trip until the browser can reuse a cached preflight. Vary: Origin can create more cache variants, and authenticated responses may need stricter private or no-store caching anyway. The main long-term cost is maintenance: allowed origins, methods, headers, cookie settings, CSRF defenses, and authorization tests must stay synchronized as the application changes.

Why Interviewers Ask This

This question tests whether the candidate understands the boundary between browser CORS enforcement and trusted server security. A strong answer should correctly configure one permitted frontend origin, credentialed requests, preflights, methods, headers, cache variation, cookies, and safe failure behavior. It also tests whether the candidate can distinguish authentication from authorization, understands why a wildcard origin cannot be used with credentialed CORS, and knows that CORS is not an API authorization mechanism.

Common interview mistakes

Common mistakes include combining Access-Control-Allow-Origin: * with credentialed requests; reflecting any incoming Origin without validating it; forgetting Access-Control-Allow-Credentials: true; forgetting credentials: "include" on the frontend; allowing unnecessary methods or request headers; accepting any requested preflight method or header; mishandling OPTIONS; omitting Vary: Origin on origin-dependent CORS responses; assuming Vary: Origin makes authenticated data safe for public caching; assuming every cross-origin request requires SameSite=None; setting an unnecessarily broad cookie Domain or Path; exposing a session token to JavaScript when an HttpOnly cookie can be used; treating SameSite or CORS as complete CSRF protection; treating CORS as authentication or authorization; and logging cookies, tokens, or other secrets. Another serious mistake is checking origins with weak string matching such as endsWith("example.com"), which can accidentally trust attacker-controlled domains.

Interview tip

Start with the trust boundary: only https://app.example.com may receive CORS permission for credentialed browser responses. Then explain exact origin matching, credentials, narrow preflight rules, Vary: Origin, secure cookie scope and SameSite behavior, CSRF, and fail-closed errors. Finish by saying that CORS is a browser response-access policy, while the trusted API must independently enforce authentication and authorization.

Interviewer may ask next
Why is `SameSite=None` not automatically required when app.example.com calls api.example.com?

app.example.com and api.example.com are different origins, so CORS applies, but with HTTPS on both hosts they are normally same-site because they share the registrable domain example.com. SameSite is based on the site relationship rather than the origin relationship. Therefore Lax or Strict may work depending on the required flow. SameSite=None; Secure is needed when the cookie must be sent in a genuinely cross-site context. The safest choice is the least permissive SameSite value that still supports the application's required behavior.

If CORS blocks an attacker from reading the response, why does the API still need CSRF protection and authorization?

CORS mainly controls whether browser JavaScript from another origin may access a cross-origin response. It does not guarantee that every unwanted cross-origin request cannot be transmitted, so cookie-authenticated state-changing operations can still require CSRF protection. CORS also says nothing about whether an authenticated user may access a particular resource or perform an action. The trusted server must authenticate the session, validate appropriate CSRF defenses for state-changing requests, and authorize every protected operation independently of CORS.

128. How would you review a frontend that hides privileged controls by role?SecurityMedium

Question Details

The UI receives { id, role: 'viewer' }, removes edit and delete buttons, but still ships code that can call the mutation endpoints. Map the untrusted browser, user-modifiable state, protected records, and server authorization boundary. Define frontend behavior for denied actions, API response handling, route access, direct-request tests, and audit logging expectations. Explain which role checks remain useful for presentation and which security decisions must be enforced outside the client.

Short Interview Answer (30-60 seconds)

I would use role checks in the frontend only to improve the user experience. The browser is untrusted, so hidden buttons and protected routes are not authorization. Every protected mutation must be authorized by the trusted server, with safe denial handling, direct-request tests, and security logging.

Detailed Explanation

This question asks whether hiding buttons is enough to protect important actions. It is not. Anything sent to a person's browser can be changed by that person. They can make hidden controls appear, change saved values, or send requests without using the page at all. The important records therefore need protection somewhere the user cannot control. The page should still make the experience clear by hiding actions that are unavailable and explaining when an action is refused. The review should also check that refused attempts are tested and recorded safely so unusual activity can be investigated later.

Useful Questions to Ask the Interviewer
  1. Does the server already authenticate users and enforce permissions for every mutation endpoint?
  2. Are permissions based only on a global role, or can access also depend on the specific record, owner, tenant, or resource state?
  3. What response convention does the API use for unauthenticated and unauthorized requests, such as 401 and 403?
  4. Should unauthorized users be prevented from viewing protected data as well as editing or deleting it?
  5. What security events must be recorded when an authorization check fails?
How would you review a frontend that hides privileged controls by role? diagram
How to Explain It in an Interview

I would start by drawing the trust boundary around the server, not the browser. The browser receives { id, role: 'viewer' }, but that object is user-modifiable state. A user can change role in DevTools, modify JavaScript, call application functions directly, or ignore the frontend completely and send an HTTP request to the mutation endpoint. Because of that, the browser must never be the authority that decides whether a protected record may be changed.

Authentication and authorization are different. Authentication answers, "Who is making this request?" Authorization answers, "Is this authenticated identity allowed to perform this action on this specific resource?" The trusted server must enforce the security decision. For every edit or delete request, the server should identify the requester from a trusted authentication mechanism, identify or load the target resource, evaluate the applicable permission policy, and reject the operation when permission is missing. It must not trust a role, user ID, owner ID, tenant ID, or permission flag supplied by the frontend as proof of access.

Frontend role checks are still useful for presentation. If the current user's session information says the user is a viewer, the interface can omit edit and delete buttons, disable irrelevant menu choices, avoid showing forms that cannot succeed, and redirect normal navigation away from editing screens. This reduces confusion and unnecessary requests. However, those checks provide user experience, not security. A client-side route guard is also only a navigation convenience because users can bypass it or call the API directly.

The frontend should fail safely when the server denies an action. A 401 response normally means the request is not authenticated or no longer has valid authentication, so the application can move the user into the appropriate sign-in or session-recovery flow. A 403 response normally means the server recognizes the requester but does not permit that action, so the application should keep the protected operation failed and show a clear permission message. The UI must not assume that hiding a button makes a later denial impossible. It should handle denial wherever a protected API call can occur.

I would also review whether sensitive record data is being returned unnecessarily. Preventing mutation is not enough if a viewer receives data that they are not permitted to read. The server should authorize reads as well as writes and return only the information that the requester is permitted to access. Frontend code cannot make already-delivered confidential data secret.

For verification, I would test the authorization boundary without relying on the UI. I would authenticate as a viewer using the normal application flow and then send the edit and delete requests directly using browser developer tools or an API test client. I would try the normal target record, other protected records when record-level permissions apply, manipulated client role values, and direct navigation to privileged routes. The important result is that the server rejects every unauthorized mutation even when the frontend checks are completely bypassed. I would also test that an authorized identity can perform the allowed operation so the policy is not accidentally blocking legitimate users.

I would review audit logging as a trusted server responsibility. Authorization failures should record enough information for investigation, such as the time, authenticated principal identifier, attempted action, target resource identifier when appropriate, request or correlation identifier, and authorization outcome. Logs should not contain passwords, session cookies, bearer tokens, secrets, or unnecessary sensitive record contents. Logging should support detection and auditing without creating another source of sensitive-data exposure.

I would also verify that browser-side convenience controls do not accidentally create new security problems. Privileged URLs or mutation functions may exist in shipped JavaScript, and that is acceptable only because knowing an endpoint or function must not grant permission. No server secrets or long-lived credentials should be embedded in frontend code. If authentication uses cookies, normal protections such as Secure, HttpOnly where appropriate, SameSite behavior, and any required CSRF defense should be reviewed separately, but they do not replace authorization. Likewise, CORS and the browser same-origin policy can restrict browser behavior but are not authorization controls because direct HTTP requests can still be made outside the normal UI.

The main tradeoff is deliberate duplication. The frontend may contain role or capability checks to provide a clean experience, while the server independently performs the real authorization check. That duplication is acceptable because the two checks have different purposes. Client checks improve presentation and reduce failed actions. Server checks protect the records even when the client is modified or bypassed.

Technical Approach
  1. Identify the protected assets: the records and mutation operations that unauthorized users must not be allowed to change.
  2. Mark the entire browser as untrusted, including the received role, JavaScript state, hidden controls, route state, browser storage, and shipped mutation code.
  3. Separate authentication from authorization and confirm that the trusted server determines the requester identity and permission for the specific requested action and resource.
  4. Review every protected mutation endpoint and verify that authorization occurs server-side before protected state changes.
  5. Review protected read endpoints as well so unauthorized users are not sent sensitive data that the UI merely hides.
  6. Keep frontend role checks only for presentation, such as hiding unavailable controls and guiding normal route navigation.
  7. Define consistent handling for authentication and authorization failures, especially 401 and 403 responses, without pretending the action succeeded.
  8. Test the security boundary by bypassing the UI and sending direct edit and delete requests as an unauthorized user.
  9. Test manipulated client state, direct privileged routes, valid but unauthorized resource identifiers when applicable, and normal authorized requests.
  10. Confirm denied attempts are logged on the trusted side with useful identifiers and outcomes but without credentials, tokens, secrets, or unnecessary sensitive data.
Practical Insights

The browser checks are cheap because they are simple presentation decisions, but they must not replace server checks. The server performs an authorization decision for every protected request, so the operational cost depends on how permissions are stored and evaluated. A simple role check may be very small, while record-level or tenant-level permissions can require a database or policy lookup. Direct authorization tests add test cases but prevent serious access-control regressions. Maintaining both frontend presentation rules and server authorization rules creates some duplication, so teams should keep permission meanings consistent while treating the server policy as the security source of truth. This approach does not require meaningful extra browser memory beyond ordinary UI state.

Why Interviewers Ask This

This question tests whether the candidate understands that browser code, role values, hidden buttons, routes, and client-side state are controlled by the user and therefore cannot enforce authorization. It also evaluates whether the candidate can separate authentication from authorization, place the real security boundary on the trusted server, design safe frontend behavior for denied actions, verify controls by bypassing the UI, and define useful security logging without exposing secrets or sensitive information.

Common interview mistakes

A common mistake is treating a hidden or disabled button as access control. Another is trusting role, userId, ownerId, tenantId, or an isAdmin value supplied by the browser when deciding whether a mutation is allowed. Client-side route guards are also sometimes mistaken for security even though direct API requests bypass them. Other mistakes include checking only whether a user is authenticated instead of authorizing the requested action on the specific resource, returning sensitive records to unauthorized users and merely hiding them in the UI, treating CORS or the same-origin policy as authorization, handling every 401 or 403 as a generic application error, failing to test endpoints directly, and logging tokens, cookies, secrets, or sensitive record contents when authorization fails.

Interview tip

State the trust boundary first: the browser is untrusted and the server owns authorization. Then explain that frontend role checks are still valuable for user experience, walk through 401 and 403 behavior, and finish with direct-request tests and safe audit logging. This makes both the security decision and the verification method clear.

Interviewer may ask next
If the frontend removes the edit button and also blocks the edit route, is that enough if the user cannot reach the form normally?

No. Both controls run in the untrusted browser. A user can alter the JavaScript, manually enter a route, call the mutation function from developer tools, or send the HTTP request directly. The trusted server must independently authenticate the requester and authorize the requested edit against the target resource before changing any protected state. The route guard and hidden button should remain as user-experience controls only.

Should the API return 401, 403, or 404 when a viewer tries to modify a record they cannot access?

Use the API's documented security policy consistently. A 401 normally means the request lacks valid authentication, while 403 normally means an authenticated requester is not authorized for the action. Some systems intentionally return 404 for resources whose existence should not be disclosed to unauthorized users. That is a server-side information-disclosure decision. Regardless of the chosen response, the mutation must not occur, the frontend must fail safely, and logs should record the denied attempt without credentials or unnecessary sensitive data.

129. Design a Trusted Types rollout for a legacy frontend with many HTML sinks.SecurityHard

Question Details

The application has hundreds of innerHTML and insertAdjacentHTML calls, a reviewed rich-text sanitizer, third-party widgets, and a report-only CSP. Design an inventory and migration that introduces named Trusted Types policies, limits policy creation, replaces plain-text sinks with safe DOM APIs, and routes intentional HTML through one audited sanitizer. Cover framework escape hatches, browser support and fallback, violation reporting, test payloads, ownership, staged enforcement, and the risk of a permissive default policy hiding unresolved injection paths.

Short Interview Answer (30-60 seconds)

I would inventory HTML sinks, replace text-only cases with safe DOM APIs, and route intentional HTML through one audited sanitizer and narrowly named Trusted Types policies. I would restrict policy creation with CSP, monitor report-only violations, assign owners, test XSS payloads, and enforce gradually without a permissive default policy.

Detailed Explanation

This question asks how to make an old web application safer when many parts of it can place generated content directly onto the page. The goal is not to change everything at once. First, find every risky place and decide whether it really needs formatted content. Simple text should use safer methods. Content that truly needs formatting should pass through one carefully reviewed cleaning path. Changes should be introduced gradually, measured for breakage, assigned to clear owners, and tested before stronger protection is turned on. Shortcuts that silently allow old unsafe behavior should be avoided.

Useful Questions to Ask the Interviewer
  1. Which browsers must the application support, and can Trusted Types enforcement initially target browsers that support it?
  2. Which rich-text sanitizer has already been reviewed, and what elements, attributes, and URL schemes is it intentionally allowed to preserve?
  3. Which third-party widgets and framework escape hatches currently write raw HTML, and which of those integrations can be changed or upgraded?
  4. Is there already a CSP reporting endpoint and an ownership process for assigning violations to teams?
  5. Can enforcement be rolled out by route, application area, or deployment cohort instead of enabling it everywhere at once?
Design a Trusted Types rollout for a legacy frontend with many HTML sinks. diagram
How to Explain It in an Interview

The main threat is cross-site scripting, especially DOM-based XSS, where attacker-controlled data reaches an HTML-capable browser sink. Trusted Types adds a browser-enforced type boundary around supported injection sinks. When require-trusted-types-for 'script' is enforced, those sinks cannot normally accept arbitrary strings. Trusted Types does not sanitize content by itself. The important trust boundary is the small amount of reviewed code allowed to create trusted values such as TrustedHTML.

I would start with an inventory. Static analysis should find direct HTML-writing APIs such as innerHTML, outerHTML, insertAdjacentHTML, and document.write, plus framework APIs that intentionally bypass normal escaping. I would combine that with Trusted Types violations collected through the existing report-only CSP because runtime reports can reveal dynamically executed paths, lazy-loaded features, and third-party code that static searches miss.

Every finding should record the sink, the source of its value, its owning team, whether HTML is truly required, and its migration state. I would classify sinks into four useful groups: plain text, intentional rich HTML, framework escape hatches, and third-party code. That classification determines the fix instead of blindly wrapping every sink in a Trusted Types policy.

For plain text, I would remove the HTML sink. Use textContent, createTextNode, normal DOM element creation, or the framework's ordinary escaped rendering. Attribute and URL values need their own context-specific handling. For example, using a non-event attribute API does not make an unsafe URL trustworthy; URL-bearing attributes still require an allowed destination and scheme. HTML sanitization is not a universal replacement for contextual output encoding or validation.

For content that intentionally needs rich HTML, I would create one audited sanitization path around the application's existing reviewed sanitizer. The sanitizer should use an explicit policy for the HTML that the product intentionally allows, including appropriate handling of elements, attributes, and URL schemes. Only the sanitized result should be converted to TrustedHTML. Application features should call this reviewed boundary rather than directly creating trusted values themselves.

I would keep the number of Trusted Types policies small. Named policies should represent genuine trust boundaries, not individual components. For example, the application might have one named policy for reviewed rich-text HTML and, only when unavoidable, a separately reviewed policy for a specific integration with different requirements. The CSP trusted-types directive should list the permitted policy names so unrelated code cannot create arbitrary named policies.

I would be especially careful with the default policy. When a Trusted Types default policy exists, the browser can invoke it when a string is passed to a protected sink. A permissive default policy that returns strings unchanged or performs weak transformation can make legacy code appear compatible while unresolved injection paths remain. That hides migration work and weakens the security boundary. I would therefore avoid a permissive default policy. If a temporary default policy is used only as a migration aid, it should not blindly trust input, should be tightly reviewed and observable, and should have an explicit removal plan.

The rollout should begin with report-only behavior rather than immediate enforcement. Add the Trusted Types requirement to the report-only CSP and collect violations centrally. Reports should contain enough information to identify the sink and source location but should avoid unnecessarily recording secrets, tokens, personal information, or complete attacker-controlled values. Reports should be deduplicated, correlated with source maps where available, grouped by component or owner, and tracked until each production path is fixed or explicitly reviewed.

Framework escape hatches need explicit migration. Normal framework templates usually escape text, while raw-HTML APIs deliberately bypass that protection. Each raw-HTML path should be removed when HTML is unnecessary or changed so its value comes from the same audited sanitization and TrustedHTML boundary. A framework API that says a value is intentionally raw HTML is not proof that the value is actually safe.

Third-party widgets require their own compatibility review because they may assign strings to protected sinks internally. First, upgrade or configure the widget if its current version supports Trusted Types. If application code controls the integration, route only the necessary HTML through a narrowly reviewed boundary. If the widget cannot be made compatible, isolate it where practical or replace it. I would not add a broad default policy or weaken the application's global Trusted Types restrictions merely to keep one widget working. Third-party scripts remain a supply-chain risk because code executing in the application's origin may be able to use any Trusted Types policy creation paths that CSP and application code expose.

Browser support is a defense-in-depth issue. Browsers that implement Trusted Types can enforce the sink restriction. Browsers without that enforcement must still remain safe because the underlying application uses framework escaping, safe DOM construction, contextual handling, and sanitization when HTML is intentionally allowed. A compatibility layer cannot provide the same browser-enforced security boundary in a browser that does not implement Trusted Types, so the fallback is the secure rendering design itself rather than reliance on a polyfill for equivalent protection.

Testing should verify both security and product behavior. Unit-test the sanitizer against the HTML that must remain allowed. Add integration tests around each trusted rendering boundary and end-to-end tests with representative XSS payloads. Useful cases include script elements, inline event handlers, dangerous URL schemes, malformed markup, parser edge cases, and SVG-related payloads when SVG is within the sanitizer's supported content. The tests should prove that dangerous behavior cannot execute while expected rich-text formatting still renders correctly.

Ownership prevents report-only mode from becoming permanent. Every violation should have a component or team owner, migration decision, priority, and target state. CI should detect newly introduced dangerous sinks through linting or static analysis. Creation of new Trusted Types policies should require review because every policy expands the trusted code surface. A migration dashboard can track unresolved violations by owner and application area without exposing sensitive payload data.

Enforcement should be staged. First establish the inventory and reporting. Next remove unnecessary HTML sinks and centralize intentional HTML through the sanitizer. Then restrict policy creation to the reviewed named policies. Once a route, application area, or deployment cohort produces no unexpected violations, enable require-trusted-types-for 'script' there with an enforcing CSP. Expand coverage gradually until the application is fully enforced. If deployment must be rolled back, roll back or narrow that enforcement stage instead of introducing a policy that accepts arbitrary strings.

CSP and Trusted Types complement each other. CSP controls such as script restrictions reduce which code can execute, while Trusted Types reduces the chance that strings reaching supported injection sinks become executable markup. Neither mechanism replaces the trusted server's authorization checks. Authentication establishes who a user is; authorization decides what that user may do, and the server must enforce it. CSRF protection, secure cookies, CORS, the same-origin policy, clickjacking controls, browser storage rules, dependency controls, privacy protections, and secret handling remain important at their own boundaries when relevant, but they do not replace the sink migration described here. Server secrets and long-lived credentials must never be placed in frontend code.

The rollout succeeds when normal features still work, expected rich text survives the audited sanitizer, representative attack payloads cannot execute, production Trusted Types violations reach zero or a small explicitly reviewed exception set, unauthorized policy creation is blocked, and enforcing CSP can remain enabled without depending on a permissive default policy. Logs and reports should support investigation without recording secrets or unnecessary sensitive data.

Technical Approach
  1. Inventory Trusted Types-relevant DOM sinks with static analysis and report-only runtime telemetry.
  2. Classify each finding as plain text, intentional HTML, framework escape hatch, or third-party code, and assign an owner.
  3. Replace plain-text HTML writes with textContent, DOM construction, or normal framework escaping.
  4. Route intentional HTML through the single reviewed sanitizer and convert only its sanitized output to TrustedHTML.
  5. Keep the Trusted Types policy set small and use descriptive names for genuine trust boundaries.
  6. Restrict allowed policy names with the CSP trusted-types directive.
  7. Upgrade, adapt, isolate, or replace incompatible framework escape hatches and third-party widgets instead of weakening the global policy.
  8. Collect and triage violations through report-only CSP without logging secrets or unnecessary sensitive values.
  9. Add sanitizer tests, representative XSS tests, integration tests, CI checks for new dangerous sinks, and review requirements for new policy creation.
  10. Enable require-trusted-types-for 'script' gradually for clean routes or cohorts and expand enforcement until the application no longer depends on unsafe string-to-HTML paths or a permissive default policy.
Practical Insights

The browser cost of replacing text-only HTML writes with safe DOM APIs is usually small. Sanitizing intentional HTML takes work roughly related to the size and structure of the markup, so very large rich-text values should not be sanitized repeatedly without need. The biggest cost is operational: hundreds of sinks must be found, classified, assigned, changed, tested, and monitored. Centralizing sanitization and keeping only a few named policies creates migration work at first but reduces long-term security review and maintenance. Violation reporting also needs storage, deduplication, source-location mapping, privacy controls, and an ownership workflow.

Why Interviewers Ask This

This question tests whether the candidate can migrate a large legacy frontend toward stronger XSS prevention without breaking the application. It evaluates understanding of dangerous DOM sinks, Trusted Types, CSP policy restrictions, sanitization boundaries, framework escape hatches, third-party compatibility, browser support, violation reporting, testing, ownership, staged enforcement, and the security risk of using a permissive default policy as a compatibility shortcut.

Common interview mistakes

Common mistakes include enabling enforcement before building an inventory; wrapping every old sink in a Trusted Types policy instead of removing unnecessary HTML writes; creating many policies that unnecessarily enlarge the trusted code surface; duplicating sanitization rules across policies; assuming Trusted Types sanitizes content automatically; treating a framework raw-HTML escape hatch as proof that content is safe; assuming setAttribute makes every attribute value safe regardless of context; treating a polyfill as equivalent to native browser enforcement; weakening the global policy for an incompatible third-party widget; logging complete attacker-controlled or sensitive values in violation reports; failing to assign owners and deadlines; testing only successful rendering; and keeping a permissive default policy that silently converts unresolved strings and hides remaining injection paths.

Interview tip

Present this as a migration of trust boundaries, not as simply turning on a CSP directive. Walk through inventory, classification, safe sink replacement, one audited sanitizer, restricted named policies, framework and third-party migration, reporting, ownership, testing, browser fallback, and staged enforcement. Explicitly explain why a permissive default policy can hide unresolved XSS paths.

Interviewer may ask next
Why is a permissive default Trusted Types policy dangerous during migration?

A default policy can be called when an ordinary string is passed to a Trusted Types-protected sink. If that policy simply returns the string or performs weak transformation, legacy injection paths keep working and their failures may disappear from the migration signal. That creates a compatibility bypass rather than a meaningful trust boundary. Prefer fixing each sink and using explicit named policies. If a temporary default policy is used for migration support, it should not blindly trust input, should be observable and tightly reviewed, and should have a firm removal plan.

How would you handle a third-party widget that still assigns strings to innerHTML after the rest of the application is ready for enforcement?

First check whether a current widget version or supported configuration works with Trusted Types. If application code controls the integration, adapt the smallest possible boundary so intentional HTML goes through the reviewed sanitizer and an approved named policy. If the widget cannot be made compatible, isolate it where practical or replace it. I would not add a broad default policy or globally weaken policy restrictions for that dependency. Any temporary exception should have a clear owner, narrow scope, monitoring, tests, and a removal plan.

130. Design a nonce-based CSP for dynamically loaded frontend code.SecurityHard

Question Details

The application serves server-generated HTML, loads ES modules and dynamic imports, starts a worker, applies styles, connects to APIs, and includes one reviewed analytics provider. Define the response-generated nonce lifecycle, relevant CSP directives, module and chunk loading behavior, worker and connection origins, style strategy, frame restrictions, reporting, caching constraints, and fallback for unsupported features. Explain how to test injection blocking while ensuring that a reusable or client-generated nonce cannot authorize attacker markup.

Short Interview Answer (30-60 seconds)

I generate one strong nonce on the server for each HTML response and put it in both the CSP header and approved tags. I then restrict modules, workers, styles, APIs, framing, and analytics, prevent nonce-bearing HTML reuse, and test that fake, old, or client-created nonce values cannot authorize injected code.

Detailed Explanation

This question asks how to give each page response a fresh approval value that lets the browser run only code and styling approved by the trusted server. The page must still load extra program files, start background work, contact approved services, and use one reviewed measurement provider. The design must also stop harmful injected page content, control which outside locations may be contacted, prevent another response from reusing the same approval value through caching, record blocked attempts safely, and provide a reasonable fallback when a browser does not understand newer protection features.

Useful Questions to Ask the Interviewer
  1. Are all application modules and dynamically imported chunks served from the same origin, or is there a separate asset origin?
  2. What exact origins are required for API calls, WebSockets, workers, styles, and the reviewed analytics provider?
  3. Does the page require any inline scripts or inline style blocks, or can most code and CSS be external?
  4. Must the application be embedded by another site, or can all framing be denied?
  5. Is the server-generated HTML cached by a CDN or reverse proxy, and can that layer preserve a fresh per-response nonce safely?
  6. Must we support older browsers that understand only earlier CSP versions, or only current evergreen browsers?
Design a nonce-based CSP for dynamically loaded frontend code. diagram
How to Explain It in an Interview

I would make the trusted server own the nonce lifecycle. For every generated HTML response, the server creates a cryptographically strong, unpredictable nonce, preferably with at least 128 bits of randomness. It puts that nonce into the Content-Security-Policy response header and copies the same value only onto server-approved <script nonce="..."> elements and, when required, approved <style nonce="..."> elements. The next HTML response gets a new nonce. The important security property is unpredictability before the response is created and non-reuse across responses.

I would not describe the nonce as a secret that must remain permanently hidden from browser JavaScript. The browser necessarily receives it as part of the document. The security boundary is that attacker-supplied markup cannot choose what nonce the trusted HTTP response header authorizes. If an attacker injects <script nonce="made-up-value">, that script is blocked because its value does not match a nonce source already present in the CSP header. A random value generated later by client JavaScript is equally useless unless that value was already authorized by the delivered policy. Reusing a nonce across responses is dangerous because a previously observed value could then become useful in a later injection.

I would begin the policy from a deny-by-default position such as default-src 'none', then open only the resource types the application actually needs. A representative policy might conceptually include a nonce in script-src, tightly scoped compatibility host sources where required, script-src-attr 'none', a narrow style-src, explicit worker-src, explicit connect-src, object-src 'none', a restrictive base-uri, and frame-ancestors 'none' unless legitimate embedding is required. I would also define other directives, such as image or font sources, only when the actual application needs them rather than allowing them broadly.

For the initial ES module, the server-rendered <script type="module" nonce="..."> element receives the response nonce. Dynamic import() requests and module dependencies still need to resolve to locations permitted by the effective CSP rules in the target browsers. I would therefore inventory the actual production module graph and chunk URLs and explicitly test the built application. I would prefer same-origin chunks, or one narrowly defined asset origin if deployment requires it. I would not add wildcard hosts, broad schemes, or unrelated CDNs simply because a chunk failed to load.

I would consider 'strict-dynamic' only when its trust-propagation model is useful for the application's script-loading pattern. In CSP3-capable browsers, 'strict-dynamic' changes how script-src source expressions are interpreted and can allow scripts trusted by a nonce or hash to load additional scripts through supported script-loading mechanisms. It also causes host and scheme allowlists in that directive to be ignored by supporting browsers. I would not claim that it automatically authorizes every ES-module dependency or dynamic import in every browser. I would test the real module-loader behavior and keep required module origins compatible with the deployed browser baseline.

That distinction matters for the analytics provider. A reviewed third-party script is still a supply-chain trust boundary. I would give it only the permissions it needs and avoid making it a general-purpose loader. If 'strict-dynamic' is part of the design, I would verify whether the analytics integration can indirectly introduce more executable code and decide whether that enlarged trust is acceptable. Its network destinations should also be restricted separately through connect-src or another applicable directive.

Workers have their own source control. I would set worker-src to only the required worker location, normally 'self' when the worker file is hosted with the application. I would not rely on script-src as the intended worker control when worker-src is available. If the implementation wants blob-based workers, I would add blob: only after deciding that the feature is genuinely required, because allowing blob worker URLs broadens the worker source policy.

For API and analytics traffic, connect-src should contain only the origins required by fetch, XMLHttpRequest, WebSocket, EventSource, beacon-style reporting where applicable, and the analytics integration. CSP does not authenticate users and does not authorize protected API operations. The trusted server must still authenticate requests when needed and enforce authorization for every protected resource or action. The same-origin policy and CORS still govern cross-origin browser reads, and CSRF protection is still required for applicable cookie-authenticated state-changing requests. connect-src is an additional browser restriction, not a replacement for those controls.

For styles, I would prefer external same-origin CSS with style-src 'self'. If server-rendered inline <style> blocks are necessary, I would authorize only those blocks with the same per-response nonce and include the corresponding nonce source in style-src. I would use style-src-attr 'none' if the application does not need inline style attributes. I would avoid 'unsafe-inline' because enabling it broadly weakens the protection that the nonce-based style policy is intended to provide.

For framing, I would use frame-ancestors 'none' when the application must never be embedded. If legitimate embedding is required, I would replace it with the smallest explicit ancestor allowlist. This is the CSP control used to reduce clickjacking risk. If the application itself never loads frames, I would also keep frame-src closed rather than opening arbitrary frame destinations.

I would normally set object-src 'none' because legacy plugin content is unnecessary for a modern frontend. I would also use a restrictive base-uri, commonly base-uri 'none' when the page does not need a <base> element. This prevents injected markup from changing how relative URLs are resolved through an attacker-controlled base URL.

CSP is defense in depth for XSS, not permission to create unsafe DOM content. Untrusted strings should normally be rendered with textContent, safe DOM construction APIs, or framework escaping. I would not assign untrusted content to innerHTML. If the product intentionally accepts HTML, I would use a reviewed sanitizer designed for that context before inserting it into an HTML sink. Contextual output encoding is still necessary where data is rendered into HTML, attributes, URLs, JavaScript, or CSS contexts.

Where supported, I would also consider Trusted Types with require-trusted-types-for 'script' and a small set of reviewed Trusted Types policies. Trusted Types can make dangerous DOM injection sinks harder to reach accidentally, but it does not replace CSP, safe rendering, contextual encoding, or sanitization. A browser that does not support Trusted Types will ignore that protection, so the underlying application must still be safe without it.

I would keep secrets out of frontend code. CSP source rules, analytics identifiers, public API identifiers, and the nonce are not substitutes for server credentials. Server secrets, signing keys, private tokens, and long-lived privileged credentials belong on trusted servers. If authentication uses cookies, I would prefer appropriate Secure, HttpOnly, and SameSite settings where the architecture allows them so JavaScript exposure and cross-site request risk are reduced.

For CSP reporting, I would first test the proposed policy in Content-Security-Policy-Report-Only so legitimate application behavior can be discovered without immediately breaking the page. After fixing expected violations, I would enforce the policy. For modern reporting, I would configure a Reporting API endpoint using Reporting-Endpoints with CSP's report-to mechanism where supported. During a compatibility period, report-uri can also be retained when older CSP reporting support matters. The report collector should validate, rate-limit, and sanitize incoming reports, and logs should avoid authentication tokens, secrets, sensitive URLs, or unnecessary personal data.

Caching is one of the most important nonce design constraints. A shared cache must not serve a nonce-bearing HTML body in a way that makes the same nonce reusable across unrelated responses. One straightforward design is to prevent shared caching of dynamic nonce-bearing HTML while continuing to cache immutable JavaScript, CSS, images, and module chunks normally. If HTML caching is required, the serving layer must ensure that each delivered HTML response receives a fresh nonce and that the CSP header and every authorized nonce-bearing element are rewritten consistently as one operation. I would test this behavior at the real CDN or reverse-proxy boundary, not only at the origin server.

For older-feature fallback, I would use CSP's backward-compatible parsing behavior rather than weaken the modern policy. For example, a script-src can contain a nonce, 'strict-dynamic', and carefully selected host source expressions. A browser that supports CSP3 and 'strict-dynamic' applies the modern trust model, while an older browser that does not recognize 'strict-dynamic' can still use source expressions it understands. The exact fallback source list must remain narrow. I would not add 'unsafe-inline' simply to make old browsers execute inline code. Unsupported Trusted Types directives are ignored, so safe DOM construction remains necessary regardless of browser support.

I would verify the policy with positive and negative tests. Positive tests confirm that the real module entry point, static dependencies, dynamic imports, worker, required styles, APIs, WebSockets if present, and analytics integration all work. Negative tests inject an inline <script> without a nonce, an inline event handler such as onclick, a script with a fake nonce, a script with a nonce generated by client code, a nonce copied from an older HTML response, and an external script from an unauthorized origin. Every unauthorized case must remain blocked.

I would also test an unauthorized worker source, blocked API origin, prohibited frame ancestor, unsafe DOM insertion attempt, and unauthorized inline style when style restrictions are enabled. I would inspect browser developer tools and collected CSP reports to confirm which directive blocked each attempt. I would request multiple HTML responses and verify that each response contains a different nonce while the CSP header and authorized elements within one response use the same value.

The safe failure behavior is to block the unauthorized resource rather than silently relax the policy. A production monitoring system can record useful CSP violation metadata, but it should not log secrets or sensitive user data. If a legitimate deployment change starts failing, I would update the explicit resource inventory and policy only after reviewing why the new origin or behavior is necessary instead of weakening the policy globally.

Technical Approach
  1. Inventory every script, module, dynamic chunk, worker, connection, style, frame, and third-party resource the page genuinely needs.
  2. Generate a cryptographically strong nonce on the trusted server for every generated HTML response.
  3. Put the nonce in the CSP response header and apply the same value only to server-approved script and required style elements.
  4. Start from default-src 'none' and open only the exact resource types and origins required.
  5. Authorize the ES-module entry point with the nonce and test the real static and dynamic module graph instead of assuming every dependency inherits trust.
  6. Decide deliberately whether 'strict-dynamic' fits the loader design and test its behavior in the supported browser set.
  7. Restrict workers through worker-src, network destinations through connect-src, styles through style-src, and framing through frame-ancestors.
  8. Treat the analytics provider as a separate third-party trust boundary and minimize its executable and network permissions.
  9. Keep safe DOM construction, contextual encoding, sanitization where intentional HTML is allowed, and Trusted Types where supported.
  10. Prevent shared reuse of nonce-bearing HTML, or safely regenerate the CSP header and markup nonce together at the serving edge.
  11. Deploy reporting in report-only mode first, then enforce the validated policy.
  12. Run positive application tests and negative injection tests, including missing, fake, reused, old, and client-generated nonce values.
Practical Insights

The browser's CSP checks add very little application-level time or memory cost. Generating one cryptographically strong nonce per HTML response is also inexpensive. The larger cost is operational and maintenance work: templates must use the nonce correctly, the CSP header must stay synchronized with the page, the module and worker origins must be known, and every new third-party script, API, asset host, or runtime loader may need review. Static JavaScript, CSS, and chunks can still be cached aggressively. The main caching complication applies to the server-generated HTML that contains the per-response nonce.

Why Interviewers Ask This

This tests whether the candidate understands that a CSP nonce is a per-response authorization value controlled by the trusted server, not merely a random HTML attribute. It also evaluates practical knowledge of CSP directives, ES modules and dynamic imports, workers, styles, third-party scripts, browser compatibility, shared caching, DOM XSS defenses, reporting, and how to verify that attacker-controlled markup cannot become executable by inventing or reusing a nonce.

Common interview mistakes

Common mistakes are generating the nonce only in browser JavaScript; reusing the same nonce across responses; putting a nonce attribute in markup without a matching nonce source in the CSP header; treating the nonce as a permanent secret instead of a per-response authorization value; assuming 'strict-dynamic' automatically authorizes every ES-module dependency; assuming a nonce on the module entry point removes the need to test dynamic-import and chunk origins; serving cached nonce-bearing HTML to unrelated requests; adding 'unsafe-inline', *, broad schemes, or unnecessary hosts when something breaks; forgetting worker-src, connect-src, style restrictions, frame-ancestors, object-src, or base-uri; giving an analytics provider more script-loading authority than necessary; assuming CSP replaces server authorization, CSRF protection, CORS, safe DOM APIs, output encoding, or sanitization; using innerHTML with untrusted content; putting server secrets in frontend code; collecting sensitive information in CSP reports; and testing only successful application behavior instead of proving attacker-controlled markup is blocked.

Interview tip

Present the nonce as a fresh server-issued authorization value for one HTML response, not as a random frontend attribute or permanent secret. Then walk through scripts and modules, workers, connections, styles, framing, third parties, reporting, caching, fallback, and negative tests. Explicitly mention that a fake or client-created nonce cannot change the CSP header that the browser already received.

Interviewer may ask next
Why is a client-generated nonce not a valid way to authorize a CSP-blocked script?

Because script authorization is determined by the CSP policy that the browser received in the trusted HTTP response. If JavaScript later generates a random value and places it on a new script element, that value does not match any nonce source authorized by the existing policy unless the server had already chosen the same value. The client cannot update the response header retroactively. The nonce therefore must be generated by the trusted response-producing side, used consistently in that response, and not reused across later responses.

How would you keep nonce-based CSP safe when server-generated HTML is delivered through a CDN?

The CDN must not cause the same nonce-bearing HTML response to be reused across unrelated deliveries. The simplest design is to prevent shared caching of that dynamic HTML while continuing to cache static JavaScript, CSS, images, and chunks normally. If HTML caching is required, the edge must generate a fresh nonce for each delivered response and update both the CSP header and every authorized nonce-bearing element consistently. I would test the actual CDN path repeatedly, verify that separate HTML responses have different nonces, and confirm that an old nonce cannot authorize injected markup in a new response.

More questions load as you scroll

Disclaimer: This interview guide is for educational and informational purposes only. It is designed to help readers prepare, but it does not guarantee any interview result, hiring decision, offer, or outcome. Interview questions, hiring criteria, and preferred answers can vary by employer, interviewer, industry, location, and time. The examples and explanations reflect the authors' research and judgment, are provided without warranties of any kind, and should not be treated as the only correct approach. Diagrams are simplified illustrations intended to highlight the main components and their interactions; actual systems and implementations may be more complex. Alternative approaches may be equally valid or better suited to a particular question, context, or interviewer. To the fullest extent permitted by applicable law, the author, contributors, and publisher are not liable for decisions made, actions taken, or losses incurred based on this guide.