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.
How should a mobile app store access tokens and refresh tokens on the device? Explain the storage choices, encryption-at-rest considerations, appropriate token lifetimes, strategies to protect tokens on a rooted or jailbroken device, and the logout behavior you would recommend to reduce the risk of token reuse after a compromise.
Sample Answer
Direct answer: Access and refresh tokens on a mobile device should live in the platform's hardware-backed secure storage (iOS Keychain, Android Keystore-backed encrypted storage), never in plain preferences/UserDefaults, with short access-token lifetimes and a rotation strategy that limits how long a stolen token remains useful.
Structured elaboration.
Storage choice. iOS Keychain and Android's Keystore-backed EncryptedSharedPreferences provide OS-level, hardware-backed encryption at rest, tied to the device and (optionally) to biometric/passcode unlock, meaning even a rooted or jailbroken device makes extraction meaningfully harder than a plain preferences file would. Plain SharedPreferences/NSUserDefaults store data as unencrypted plist/XML on disk, trivially readable by anyone with filesystem access (via backup extraction, or trivially on a rooted device) - never store a token there.
Encryption-at-rest considerations. Even within Keychain/Keystore, prefer requiring the key be hardware-backed (Secure Enclave on iOS, StrongBox/TEE on Android where available) over software-only key storage, since hardware-backed keys can't be extracted even with full device compromise in many threat models, only used in-place.
Token lifetimes. Access tokens should be short-lived (minutes to low hours) so a leaked one has a narrow exploitation window; refresh tokens are necessarily longer-lived (days to weeks) to avoid forcing frequent re-authentication, which is exactly why they need the strongest storage protection and a server-side revocation mechanism.
Protecting tokens on rooted/jailbroken devices. No client-side storage mechanism is fully bulletproof against a determined attacker with root access to the device itself; the realistic goal is raising the cost and adding detection, not achieving perfect prevention. Combine strong storage with: detecting root/jailbreak status and adjusting risk posture (declining to cache highly sensitive data, or requiring step-up authentication) on a detected-compromised device, and short token lifetimes so even a successful extraction has limited value before the token naturally expires.
Recommended logout behavior. Logout should clear the token from local storage AND explicitly revoke the refresh token server-side (not just "forget" it locally) - a client-side-only logout leaves a still-valid refresh token that an attacker who separately extracted it from the device (say, from a backup taken before logout) could still use, since the server never learned the session ended.
A concrete worked trace. Say the app issues a 15-minute access token and a 14-day refresh token at login, both written to Keychain. If an attacker extracts the device's encrypted backup 2 minutes after issuance, they get both tokens: the access token is usable immediately and remains valid for the remaining ~13 minutes of its life, while the stolen refresh token could mint new access tokens for up to 14 days unless something stops it - which is exactly why short access-token life alone is not sufficient and the refresh token needs its own protections (rotation-with-reuse-detection, and server-side revocation on logout). Now suppose the app additionally checks for a rooted/jailbroken device at the moment of that same extraction attempt: if the extraction happened through a backup-restore path the app can't observe in real time, root detection at launch doesn't help retroactively, but it DOES mean any FUTURE use of the app on that same compromised device (including an attacker trying to use the stolen tokens through the app's own UI rather than a raw API call) can be blocked or degraded, and a step-up re-authentication requirement forces the attacker to also have the user's actual login credentials, not just the extracted tokens, to gain full access again after the access token expires.
Trade-offs and pitfalls: Keychain/Keystore items can persist across app reinstalls (iOS Keychain specifically, by default) and can be included in device backups depending on configuration - decide deliberately whether that's the behavior you want (a token surviving a reinstall is convenient for UX but means "uninstall the app" isn't a reliable way for a user to end their session on that device) and configure backup-exclusion for sensitive Keychain items where it isn't.
Design a secure, scalable certificate-pin rotation system that lets you update pins without forcing an app update. Explain client-side pin validation, a server protocol for distributing signed pin manifests, pin TTL and expiry semantics, backup pins and grace periods, integrity checks on the pin bundle, and the emergency rollback procedure for a key-compromise scenario.
Sample Answer
Direct answer
A scalable, field-updatable certificate-pin rotation system separates two roles cleanly: the server signs a versioned "pin manifest" (the current pins, a monotonically increasing sequence number, and a validity window) with a rotation key kept offline except when signing, and the client only ever verifies that signature against a baked-in public key before trusting the manifest's contents, never trusting network transport alone. Everything else, backup pins, grace periods, integrity checks, emergency rollback, is a consequence of designing around that one verify-don't-trust-transport rule.
Structured elaboration
Client behavior for validating pins
sequenceDiagram
participant App as Client app
participant Sec as Baked-in verification public key
participant Server as Manifest server
App->>Server: Fetch pin manifest (If-None-Match cached etag)
Server-->>App: Signed manifest (pins, sequence, issued_at, expires_at, signature)
App->>Sec: Verify signature over manifest bytes
Sec-->>App: Signature valid
App->>App: Check sequence > last stored sequence (replay guard)
App->>App: Check now within [issued_at, expires_at] plus grace window
App->>App: Store manifest, use pins for TLS validation
Note over App,Server: Emergency path
Server-->>App: Emergency manifest signed by separate offline emergency key
App->>App: Verify against baked-in emergency key, apply immediately, bypass normal sequence check only for this key
On startup, and on a periodic timer thereafter, the app fetches the manifest (using a cache-validation header so an unchanged manifest costs almost nothing to re-check). Before trusting anything in it, the app verifies the signature against a public key compiled into the app itself, never a key fetched over the network. Only after signature verification passes does the app check the manifest's sequence number against the last one it stored (rejecting anything not strictly newer, the replay guard) and its validity window. If all three checks pass, the pins in the manifest become the active pin set for Transport Layer Security (TLS) certificate validation; if any check fails, the app keeps using its last successfully verified pin set (or the static baseline pins it shipped with, if it has never successfully fetched a manifest) rather than falling back to no pinning at all.
Server protocol for distributing signed pin manifests
The server signs a manifest containing {sequence, issued_at, expires_at, pins[]} with the rotation key, and serves it over an ordinary Hypertext Transfer Protocol Secure (HTTPS) endpoint, supporting conditional requests (an ETag/If-None-Match pair) so that clients polling on a schedule do not re-download an unchanged manifest. The manifest server itself is not a trust anchor in this design, its job is availability and caching, not security, since the client's trust decision rests entirely on the signature, not on how the manifest was transported. This is deliberate: it means the manifest can be served from a content delivery network (CDN) or even pushed via a mobile platform's notification service to accelerate rollout, without either of those channels becoming a new thing the client has to trust.
Pin TTL and expiry semantics
Each manifest carries its own issued_at and expires_at. A short TTL (time-to-live), commonly 24 to 72 hours, gives rotation agility: a compromised or soon-to-expire certificate can be pinned around quickly, since clients naturally re-check often. The pins THEMSELVES (as opposed to the manifest wrapper) can have a longer effective lifetime than any single manifest's TTL, because a new manifest reissued before the old one expires simply re-asserts the same pins with a later expires_at, extending their effective life without the pins ever having to change.
Backup pins and grace periods
Every manifest lists at least one backup pin generated from a different key pair than the primary, so a planned or emergency key change can be adopted instantly by clients that already have the backup pin in their currently trusted set, no new manifest fetch required at the exact moment of rotation. A grace period extends how long a client accepts an expired manifest if it cannot reach the network to fetch a fresh one (commonly up to seven days), trading a small window of staleness for not bricking connectivity for an offline device; the grace period is bounded, not indefinite, because an unbounded grace period would let a permanently offline, already-compromised device keep trusting pins forever.
Integrity checks on the pin bundle
The signature covers the entire manifest payload (not just the pin list), so an attacker cannot selectively tamper with one field (extend the expiry, add a pin) without invalidating the whole signature. The sequence number, checked strictly greater than the last one the client stored, is what actually prevents an attacker who has captured a previously valid, correctly signed OLD manifest from replaying it later to roll a client back to a pin set that has since been intentionally retired.
Emergency rollback procedure for a key-compromise scenario
The rotation key (used for routine updates) and the emergency key (used only for rollback) are different keys, and the emergency key is kept offline (a hardware security module, HSM, or an air-gapped store) except at the moment it is used. If the rotation key is ever suspected compromised, an emergency manifest signed by the EMERGENCY key, which every client can already verify because its public counterpart was baked in from day one, is pushed out; this manifest is validated against its OWN independent sequence counter, entirely separate from the rotation key's sequence counter, because the whole point of the emergency path is that it must work even if the rotation key's sequence state is itself in an unknown or attacker-influenced condition.
Independent counters alone are not sufficient, though: if the client simply resumed accepting rotation-key manifests afterward, an attacker who still holds the compromised rotation key (or who merely captured an old, validly-signed rotation manifest before the compromise was discovered) could sign or replay a rotation-key manifest and silently overwrite the emergency pin set the moment the client next polls, since that manifest's signature genuinely verifies. The rollback therefore also needs to be a REVOCATION, not just an override: once a client has applied any emergency-signed manifest, it must reject every rotation-key-signed manifest outright, valid signature or not, until a rotation-key change is itself authorized by a subsequent emergency-signed manifest. This is what actually makes the rollback durable against a compromised (or merely previously-captured) rotation key, rather than a one-time pin swap an attacker can immediately undo.
Worked example
The demo below implements and exercises the verification logic above end to end, using HMAC-SHA256 (Python's standard-library hmac/hashlib) as an explicit stand-in for the asymmetric signature (ECDSA or Ed25519) a real deployment would use; a production implementation must use a vetted asymmetric-signature library, since HMAC requires the verifier to hold the same secret the signer used, which does not fit this system's "only the server can sign, any client can verify" trust model. The verification LOGIC exercised (signature check, replay guard, validity window, grace period, emergency-key path, and rotation-key revocation after rollback) is identical in shape regardless of which primitive backs the signature, which is what this demo is actually testing.
import hashlib
import hmac
import json
ROTATION_KEY = b"stand-in-secret-for-the-rotation-signing-key"
EMERGENCY_KEY = b"stand-in-secret-for-the-OFFLINE-emergency-key"
def sign(payload: dict, key: bytes) -> str:
body = json.dumps(payload, sort_keys=True).encode()
return hmac.new(key, body, hashlib.sha256).hexdigest()
def make_manifest(sequence: int, issued_at: int, expires_at: int, pins, key: bytes) -> dict:
payload = {
"sequence": sequence,
"issued_at": issued_at,
"expires_at": expires_at,
"pins": pins,
}
return {"payload": payload, "signature": sign(payload, key)}
class ClientState:
def __init__(self):
# Separate, independent sequence counters per signing key: a manifest
# signed by one key is never compared against, or allowed to reset,
# the other key's counter.
self.last_rotation_sequence = 0
self.last_emergency_sequence = 0
self.active_pins = ["PRIMARY_PIN_A"]
# Once true, the rotation key is presumed compromised: every
# rotation-key-signed manifest is rejected outright, including ones
# validly signed and issued BEFORE the emergency event, until a new
# rotation key is itself re-authorized by a future emergency-signed
# key-rotation manifest (not modeled here).
self.rotation_key_revoked = False
def apply(self, manifest: dict, now: int, key: bytes, key_role: str,
grace_seconds: int = 0) -> str:
payload = manifest["payload"]
expected_sig = sign(payload, key)
if not hmac.compare_digest(expected_sig, manifest["signature"]):
return "REJECT: bad signature"
if key_role == "rotation":
if self.rotation_key_revoked:
return "REJECT: rotation key revoked since an emergency rollback, no longer trusted"
if payload["sequence"] <= self.last_rotation_sequence:
return (f"REJECT: replay (rotation sequence {payload['sequence']} "
f"<= last seen {self.last_rotation_sequence})")
else: # key_role == "emergency"
if payload["sequence"] <= self.last_emergency_sequence:
return (f"REJECT: replay (emergency sequence {payload['sequence']} "
f"<= last seen {self.last_emergency_sequence})")
if now < payload["issued_at"]:
return "REJECT: not yet valid"
if now > payload["expires_at"] + grace_seconds:
return "REJECT: expired (even with grace window)"
if key_role == "rotation":
self.last_rotation_sequence = payload["sequence"]
else:
self.last_emergency_sequence = payload["sequence"]
self.rotation_key_revoked = True # any emergency manifest revokes the rotation key
self.active_pins = payload["pins"]
return f"ACCEPT: active pins now {self.active_pins}"
if __name__ == "__main__":
client = ClientState()
now = 1_000_000
valid = make_manifest(
sequence=2, issued_at=now - 10, expires_at=now + 3600,
pins=["PRIMARY_PIN_A", "BACKUP_PIN_B"], key=ROTATION_KEY,
)
print("1) Valid manifest: ", client.apply(valid, now, ROTATION_KEY, "rotation"))
replay = make_manifest(
sequence=1, issued_at=now - 100, expires_at=now + 3600,
pins=["OLD_PIN"], key=ROTATION_KEY,
)
print("2) Replayed old manifest: ", client.apply(replay, now, ROTATION_KEY, "rotation"))
expired_within_grace = make_manifest(
sequence=3, issued_at=now - 20000, expires_at=now - 10000,
pins=["PRIMARY_PIN_A"], key=ROTATION_KEY,
)
print("3) Expired, within 7-day grace window: ", client.apply(
expired_within_grace, now, ROTATION_KEY, "rotation", grace_seconds=604800
))
expired_beyond_grace = make_manifest(
sequence=4, issued_at=now - 20000, expires_at=now - 10000,
pins=["PRIMARY_PIN_A"], key=ROTATION_KEY,
)
print("3b) Expired, beyond 1-hour grace window:", client.apply(
expired_beyond_grace, now, ROTATION_KEY, "rotation", grace_seconds=3600
))
tampered = make_manifest(
sequence=5, issued_at=now - 10, expires_at=now + 3600,
pins=["ATTACKER_PIN"], key=ROTATION_KEY,
)
tampered["payload"]["pins"] = ["ATTACKER_PIN_SWAPPED_AFTER_SIGNING"]
print("4) Tampered manifest: ", client.apply(tampered, now, ROTATION_KEY, "rotation"))
emergency = make_manifest(
sequence=1, issued_at=now - 5, expires_at=now + 60,
pins=["EMERGENCY_ROLLBACK_PIN"], key=EMERGENCY_KEY,
)
print("5) Emergency rollback manifest: ", client.apply(
emergency, now, EMERGENCY_KEY, "emergency"
))
print(" state -> last_rotation_sequence:", client.last_rotation_sequence,
"last_emergency_sequence:", client.last_emergency_sequence,
"rotation_key_revoked:", client.rotation_key_revoked,
"active_pins:", client.active_pins)
# The attack this design has to close: replaying a PREVIOUSLY VALID,
# correctly-signed rotation-key manifest issued before the compromise
# (captured here in `valid` from scenario 1), replayed AFTER the
# emergency rollback, must not be able to undo the rollback.
print("6) Replay pre-emergency rotation manifest AFTER rollback:", client.apply(
valid, now, ROTATION_KEY, "rotation"
))
print(" state -> active_pins:", client.active_pins)
Running this produces:
1) Valid manifest: ACCEPT: active pins now ['PRIMARY_PIN_A', 'BACKUP_PIN_B']
2) Replayed old manifest: REJECT: replay (rotation sequence 1 <= last seen 2)
3) Expired, within 7-day grace window: ACCEPT: active pins now ['PRIMARY_PIN_A']
3b) Expired, beyond 1-hour grace window: REJECT: expired (even with grace window)
4) Tampered manifest: REJECT: bad signature
5) Emergency rollback manifest: ACCEPT: active pins now ['EMERGENCY_ROLLBACK_PIN']
state -> last_rotation_sequence: 3 last_emergency_sequence: 1 rotation_key_revoked: True active_pins: ['EMERGENCY_ROLLBACK_PIN']
6) Replay pre-emergency rotation manifest AFTER rollback: REJECT: rotation key revoked since an emergency rollback, no longer trusted
state -> active_pins: ['EMERGENCY_ROLLBACK_PIN']
Each scenario exercises a different rule: (1) a well-formed newer rotation manifest is accepted and becomes the active pin set; (2) an older, previously-superseded manifest is rejected purely on its sequence number, regardless of its own signature being valid, which is exactly the replay protection integrity checks are meant to provide; (3) a manifest past its own expires_at is still accepted because a grace window covers the gap, demonstrating why the grace period exists for intermittently-offline devices; (3b) the same shape of expired manifest is rejected once the gap exceeds the grace window, showing the boundary actually holds; (4) a manifest whose payload was altered after signing fails signature verification outright, regardless of every other field looking plausible; (5) the emergency manifest, signed by the separate emergency key and validated against its own independent sequence counter, is accepted, becomes the new active pin set, AND revokes the rotation key; (6) the exact manifest accepted in scenario 1, still cryptographically valid and not a "replay" by sequence number alone, is nonetheless rejected once the rotation key is revoked. Scenario 6 is the one that actually earns the "key-compromise scenario" the question asks about: without the revocation flag, this same replay would silently overwrite the emergency pin set the moment it arrived (confirmed by running the same design without the revocation check: the replay is accepted and active_pins reverts to ['PRIMARY_PIN_A', 'BACKUP_PIN_B'], undoing the rollback), which is the exact failure mode a rollback mechanism exists to prevent.
Trade-offs and pitfalls
- Making the manifest-serving endpoint itself a trust anchor. If the client trusts "came from the right URL" instead of "signature verifies," a compromised content delivery network (CDN) or a Domain Name System (DNS) hijack becomes a full pin-rotation attack; this design deliberately keeps transport untrusted and puts all the trust in the signature check.
- Using the same key for routine rotation and emergency rollback. If one key serves both purposes, compromising it removes the emergency recovery path at exactly the moment it is needed most; keeping them separate, with the emergency key offline, is what makes rollback trustworthy even in a rotation-key-compromise scenario.
- Setting the grace period too generous. A long grace period is convenient for offline devices but extends how long an already-stale (and potentially already-revoked) pin set stays trusted; the boundary demonstrated in scenario 3b above only protects the system if the grace period is chosen deliberately, not left unbounded.
- Treating HMAC (or any symmetric scheme) as production-ready for this design. As noted in the worked example, a shared-secret scheme requires the verifier to hold the signing secret, which defeats the "many clients verify, only the server signs" model; production requires an asymmetric signature scheme where the verification key can be public.
Design a privacy-preserving telemetry and error-reporting pipeline for a mobile app that supports security incident response without exposing PII. Specify which fields to collect for security purposes (events, hashes, stack traces), client-side redaction and hashing rules, encryption in transit, consent/opt-in flows, retention and deletion policies, and the server-side analysis and alerting capability this telemetry feeds.
Sample Answer
Direct answer
The design principle is to redact and hash on the device, before anything leaves it, rather than collecting raw data and trying to scrub it centrally afterward, since centralized scrubbing has to be perfect on every code path forever while on-device redaction only has to be correct at the single point of collection. What reaches the server should be the minimum set of fields a security investigator actually needs (event types, sanitized diagnostics, correlation hashes) under explicit consent, encrypted in transit, and deleted on a defined schedule, not an open-ended archive of everything the client happened to see.
Structured elaboration
Fields to collect for security purposes. Scope collection to what an investigator can actually act on:
- Event metadata: event type, timestamp (Coordinated Universal Time, UTC), app version, operating system and version, network type. None of this identifies a specific person on its own.
- Security context signals: authentication-failure counts, flagged suspicious application programming interface (API) calls, permission changes, root/jailbreak detection result. These are the fields most directly useful for the anomaly detection and alerting this pipeline exists to feed.
- Error diagnostics, sanitized: exception type, module name, and a redacted stack trace (see redaction rules below), not a raw, unfiltered trace.
- Correlation keys: a rotating per-installation identifier (not a stable, cross-app-linkable device identifier), so related events from the same installation can be grouped for investigation without that identifier itself being a durable tracking handle.
Deliberately excluded: user-entered content, contact lists, precise location beyond what a specific security investigation explicitly justifies, and any field whose only purpose is general product analytics rather than security investigation, since mixing the two purposes under one collection umbrella is exactly what makes a security telemetry pipeline start to look like broad surveillance to both users and regulators.
Client-side redaction and hashing rules. This is the step that actually delivers the "without exposing personally identifiable information (PII)" requirement, and it happens before transmission, not after:
- Strip or mask obvious PII patterns (email addresses, phone numbers, full file-system paths that could contain a username) from any free-text field, such as an exception message, using a maintained pattern list, applied on-device.
- Replace anything that must be retained for correlation but could otherwise identify a person with a salted Hash-based Message Authentication Code (HMAC-SHA256) digest, where the salt is rotated periodically and held server-side, never shipped in the client, so the hash is useful for grouping related events but is not reversible by anyone without server-side access to the current salt, and stops being linkable at all once the salt rotates.
- For stack traces specifically: keep function and file names (needed to actually debug the underlying issue) but hash file paths below the project root, and bucket line numbers into ranges (0 to 10, 11 to 50, and so on) rather than reporting the exact line, since an exact line number combined with other fields can, in some cases, narrow down enough context to be more identifying than it needs to be for the diagnostic value it adds.
- Hashes are deterministic within one salt period but keyed by the rotating per-installation identifier, specifically so the same underlying value hashes differently across different installations, preventing cross-app or cross-user linkage through a shared hash value.
Encryption in transit. Transport Layer Security (TLS) 1.2 or higher for the transport itself, with certificate pinning given this pipeline's sensitivity, plus an additional application-layer encryption step for the payload (hybrid encryption: a fresh symmetric key generated per batch, itself encrypted with the server's public key) so the payload remains protected even in an environment where TLS is terminated somewhere before it reaches the system that actually needs to read it (a load balancer or content delivery network layer, for example), a real architectural consideration for telemetry pipelines that often route through general-purpose ingestion infrastructure before reaching a security-specific processing system.
Consent and opt-in flows. Present a clear choice at first run or first relevant feature use, not buried in a general terms-of-service acceptance: critical security telemetry (the fields above, tied directly to detecting compromise) can reasonably default to on, since it exists to protect the user's own account and device, but should still be clearly disclosed and separately toggle-able from general product analytics, which should default to off and require explicit opt-in. Respect platform-level privacy signals (an operating system's own tracking-permission state) and provide an in-app control to review and change the choice later, not just at install time. On opt-out, stop client-side collection immediately and send a deletion request for previously collected data tied to that installation's identifier.
Retention and deletion policies. Tiered retention matching investigative need against exposure risk: raw, individually-correlatable event data for a short window (on the order of 30 days) sufficient for active incident response, then either deletion or reduction to aggregated, hashed-only indicators (crash-signature clusters, anomaly counts) retained longer (on the order of 90 days) for trend detection without the individually-correlatable detail. An automated deletion job enforces this on schedule rather than relying on manual cleanup, and a dedicated data-deletion capability handles explicit user erasure requests (a General Data Protection Regulation, GDPR, right-to-erasure request, or an opt-out as described above) outside the normal retention schedule.
Server-side analysis and alerting. The pipeline exists to feed detection, not just to archive data:
- Ingest into an access-restricted environment, with the security operations team, not general engineering, holding read access to individually-correlatable event data.
- Run anomaly detection on the security-context signals (a spike in root/jailbreak detections, a cluster of authentication failures across many installations in a short window, a new crash signature clustering across previously-unrelated installations) and route qualifying anomalies to an alerting system (a paging or chat-based alert to the security on-call).
- For the rare case where an investigation genuinely needs the raw, pre-hash detail behind a specific correlation hash (a confirmed active incident, or a legal request), require multi-person approval and log the access itself as an auditable security event, so decrypting anything back toward individual-level detail is itself a reviewable, accountable action, not a routine capability any single engineer can exercise unilaterally.
Worked example
A mobile banking app detects a spike in "suspicious API call" security-context events:
- Collection. A device experiencing repeated unusual authorization-header manipulation attempts (a sign of a compromised or tampered client probing the API) generates security-context events:
event_type: suspicious_api_call,auth_failure_count: 4, timestamp, app version, OS version, and a correlation hashHMAC-SHA256(current_salt, installation_id). No email, no username, no precise location. - Client-side redaction. Before batching for transmission, the client checks any accompanying error message text against the PII pattern list; suppose one event's diagnostic string incidentally contains a file path with the device's username in it (a common accidental leak from OS-level error strings), that path is stripped down to a hashed representation before the batch is assembled.
- Transit. The batch is encrypted at the application layer with a fresh per-batch symmetric key (itself encrypted to the server's public key) and sent over a pinned TLS connection.
- Server-side ingestion and analysis. The security telemetry pipeline, isolated from general product-analytics ingestion, receives the batch. Anomaly detection notices that 40 distinct installations (identified only by their rotating, non-linkable correlation hashes, not any durable device identifier) reported the same
suspicious_api_callpattern within a 10-minute window, a cluster inconsistent with normal usage. - Alerting. This crosses the defined threshold for the specific anomaly rule and pages the security on-call, who begins investigating a possible coordinated attack against the API, using only the aggregated pattern (event type, timing, volume) without needing, or having easy access to, any individually identifying detail about which specific users were affected, unless the investigation later escalates to the multi-person-approved raw-detail access path.
- Retention. The individual events age out of raw retention after 30 days; the aggregated "cluster detected on this date, this many installations, this pattern" record persists in the 90-day aggregated tier to support trend analysis (was this a one-off, or part of a longer campaign) without retaining the per-installation detail past its useful investigative window.
Trade-offs and pitfalls
- Collecting broadly and redacting centrally instead of on-device. Sending raw diagnostic data to the server and relying on a central scrubbing pipeline to catch PII is fragile: every new field, every new error-message format from a new code path, has to be independently caught by the central scrubber's pattern rules, whereas on-device redaction at the point of collection is a single, auditable choke point.
- Using a stable device identifier for correlation instead of a rotating one. A durable identifier makes cross-session correlation easier for the investigator but also makes the telemetry itself a persistent tracking mechanism, exactly the property a privacy-preserving design needs to avoid; rotation with server-held salts gives most of the correlation value within a bounded window without the durable-tracking downside.
- Treating security telemetry and general product analytics as one collection pipeline. Bundling them under one consent flow and one retention policy means the broader, less-justified analytics collection inherits the default-on posture that is only actually appropriate for the narrower, protective security telemetry, and it also means a user who wants analytics off has no way to keep security telemetry on, or vice versa.
- No access control on the raw-detail escalation path. If any engineer with production access can decrypt a correlation hash back to individual-level detail without review, the entire on-device-redaction and rotating-hash design is undermined by an unrestricted backdoor around it; the multi-person-approval requirement is not bureaucratic overhead, it is what makes the rest of the design's privacy claims actually hold.
- Retention windows set by convenience rather than actual investigative need. Keeping raw data far longer than active incident response realistically requires "just in case" increases exposure risk (a future data-store compromise has more to lose) without a corresponding security benefit; tie retention explicitly to what each tier is actually used for, as the 30-day-raw versus 90-day-aggregated split above does.
Design a secure and user-friendly logout and token-revocation strategy for a mobile app. Cover client-side removal of credentials, server-side revocation of refresh tokens, handling multiple signed-in devices, push- or polling-based notification to other devices, and techniques to ensure tokens are unusable after logout even if an attacker had already extracted them.
Sample Answer
Direct answer
Logout has to succeed instantly from the user's point of view (clear local credentials the moment the button is tapped, regardless of network state) while also being backed by a server-side revocation that does not depend on that instant local clear alone, because an attacker who already extracted a copy of the token before logout would be unaffected by the device wiping its own copy. The design that closes both gaps is: clear locally first for responsive user experience (UX), revoke the refresh token server-side so the session cannot be extended past its current access token, and keep access tokens short-lived so even an already-stolen, not-yet-expired access token has a small, bounded window rather than an indefinite one.
Structured elaboration
Client-side removal of credentials. The moment logout is triggered, the app deletes the access and refresh tokens from platform-backed secure storage (iOS Keychain, Android Keystore-backed storage), clears any in-memory session state, and cancels background jobs that would otherwise silently refresh the session again. This happens unconditionally and immediately, without waiting for the server call in the next step to succeed, because a user who tapped "log out" should see a logged-out app even if they are offline at that moment.
Server-side revocation of refresh tokens. The device sends a revoke request identifying the specific refresh token (or its session) to the server, which marks the corresponding session row revoked in the authoritative store. From that point, no future refresh call for that session can produce a new access token: the server checks session status on every refresh, so the compromise is bounded going forward even though it does nothing about tokens issued before the revoke.
Handling multiple signed-in devices. Sessions are per-device, not per-user, so each device has its own session row, refresh token, and revocation status. This makes two distinct actions both first-class operations rather than one being a hack on top of the other:
- "Log out this device": revoke only the current session; other devices are untouched.
- "Log out everywhere": revoke every session row for the user in one transaction, which is the operation a user reaches for after realizing their account was compromised, and it needs to actually be atomic across all sessions, not a loop of independent per-device calls that could partially fail.
Push- or polling-based notification to other devices. Revoking a session server-side stops that device's next refresh from succeeding, but it does not by itself tell the device to stop acting on its still-valid, not-yet-expired access token, or update its UI to a logged-out state. Two complementary mechanisms close this gap:
- Push: the server sends a silent, data-only push (Apple Push Notification service, APNs, or Firebase Cloud Messaging, FCM) to the affected device's registered push token, instructing the app to clear local credentials and show the logged-out screen immediately, without waiting for the user to notice.
- Polling fallback: since push delivery is best-effort (a device can be offline, in airplane mode, or have background delivery throttled by the operating system), the app also checks session status on foreground and at a low-frequency background interval, so a device that missed the push still self-corrects the next time it is actually used.
Ensuring tokens are unusable after logout even if already extracted. This is the harder half of the question, because "the device deleted its copy" says nothing about a copy an attacker took earlier (malware, a debug log, a memory dump). Four techniques combine to close that gap:
- Short-lived access tokens. If access tokens live for minutes, a stolen access token is only useful for however many minutes remain on it, an upper bound that exists regardless of whether logout ever happens.
- Server-side refresh-token revocation, checked on every refresh. Even if the attacker has the refresh token too, revoking it server-side means their next refresh attempt is rejected, so they cannot extend the stolen access indefinitely.
- Storing only a hash of the refresh token server-side. If the token store itself is ever read (a backup leak, a compromised replica), what is exposed is not directly usable, mirroring password-hashing practice.
- An access-token denylist for the remaining lifetime of the current token, for high-sensitivity actions. Since a short-lived access token cannot itself be "revoked" the way a refresh token can (its validity is normally judged only by its own signature and expiry, with no per-request database lookup, which is what makes it fast), a denylist keyed by the token's unique identifier (
jti) and checked only on sensitive endpoints gives you the ability to immediately invalidate even an unexpired access token, at the cost of that one lookup, without giving up the no-lookup fast path for ordinary requests.
Worked example
A user logs into a note-taking app on their phone and a tablet, then loses the phone and reports it stolen from the tablet:
- Normal logout on the phone (before it was lost). User taps "log out." The app immediately clears the Keychain-stored tokens and shows the login screen. In parallel, it calls
POST /auth/logoutwith the phone's session id. The server marks that sessionrevokedand returns success; if this call had failed (no network), the phone would still show logged-out locally and would retry the server call on next connectivity. - The theft scenario. The phone is stolen while still logged in (never got to run step 1's local clear). From the tablet, the user opens "Manage devices," sees "iPhone, last active 2 hours ago," and taps "Log out this device."
- Server-side action. The server revokes the phone's session row and publishes a revocation event. Because access tokens for this app are short-lived, and because the phone's already-issued access token might still be technically unexpired for a few more minutes, the server also adds that token's
jtito the denylist so sensitive endpoints reject it immediately rather than waiting out its natural expiry. - Notification. The server sends a silent push to the phone's registered push token. If the thief has the phone online and connected, the app receives it, clears local credentials, and shows the logged-out screen without any user interaction on that device (the thief sees the app log itself out). If the phone is offline (airplane mode, powered off), nothing happens until it reconnects; at that point its next API call is rejected by the denylist check or, once the access token naturally expires, its next refresh attempt is rejected by the revoked session row.
- Bound on exposure. From the moment the tablet's "log out this device" action completes, the thief has, at most, however long it takes them to make one more API call before hitting either the denylist (if the endpoint checks it) or the natural access-token expiry (if it does not), never an indefinite window.
Trade-offs and pitfalls
- Treating "revoke the refresh token" as sufficient on its own. A revoked refresh token stops the session from being extended, but says nothing about an access token issued before the revoke that has not expired yet. This is the single most common gap: teams implement refresh-token revocation, verify it works on the next login attempt, and stop there, leaving a live window equal to the access-token's full remaining lifetime.
- Making the access-token denylist the default check on every request. Checking a denylist on every single API call reintroduces the per-request database lookup that short-lived, self-verifying access tokens were designed to avoid, at scale. Reserve it for sensitive endpoints (payments, account settings, data export) where the cost is worth the immediate-invalidation guarantee, and let ordinary reads rely on the token's short natural expiry instead.
- Blocking the local logout UI on the server call. If the app waits for
POST /auth/logoutto succeed before clearing local state, a network failure at exactly the wrong moment leaves the user looking logged-in when they are not, which is both a confusing user experience and, if they hand the device to someone else believing they logged out, a real security gap. Clear locally first, unconditionally, and reconcile the server call asynchronously. - "Log out everywhere" implemented as a loop of per-device revoke calls. If one call in the loop fails partway through, the user is left with a false sense of security, believing every device is logged out when one is not. This needs to be a single atomic operation against the session store, not a client-side or naive server-side loop.
- Relying on push as the actual security boundary rather than a UX improvement. As with any push-based notification, delivery is not guaranteed. The denylist and revoked-session checks are what actually make a device stop working; the push just makes it stop working sooner, when it arrives.
A critical vulnerability is discovered in a popular third-party SDK your app depends on. As the mobile developer lead, outline your incident-response plan: immediate triage steps, risk assessment, temporary mitigations (feature flags, disabling the SDK), communication with stakeholders and users, expedited release management, backend-side mitigations (input validation, feature disabling), and the post-mortem and remediation timeline.
Sample Answer
Direct answer
As mobile lead, the first job is buying safety without waiting for a full understanding: reach for whatever kills the exposed behavior fastest (a feature flag, a backend-side block) before reaching for a client release, since an app-store release is measured in hours to days and an attacker's exploitation window is not. Everything after that, from stakeholder communication to the eventual code fix, proceeds in parallel with that immediate containment already in place, not sequentially after it.
Structured elaboration
Immediate triage. Within the first hour: convene the people who can actually act (an engineering lead, security, quality assurance, product, and whoever owns communications and legal for a user-facing incident), confirm which app versions and platforms are actually affected (not every SDK integration touches every screen), and get a working reproduction if a proof of concept exists, owned by security, not by every engineer independently trying to trigger it on their own device. In parallel, freeze unrelated releases so the incident response is not competing with routine ship cadence for review bandwidth or store review slots.
Risk assessment. Determine what the vulnerability actually exposes: what data the SDK can access, what network calls it makes, what permissions and background capabilities it holds, and what the realistic impact category is, data exfiltration, remote code execution, or privilege escalation within the app's own sandbox. Prioritize by exposure, not just severity in the abstract: which users are affected right now (active sessions using the vulnerable code path), and whether any user segment (enterprise customers under a stricter security agreement, for example) needs separate, faster handling.
Temporary mitigations, both client- and backend-side. This is the actual containment step, and it should not wait for risk assessment to fully complete if the SDK's exposure is already clear enough to act on:
- Client-side, if the app has a feature-flag or remote-config system: disable the SDK-dependent feature at runtime through a server-controlled flag, which takes effect without a store release at all, typically within minutes of the flag change propagating.
- If no such flag exists for this specific feature, this incident is also the argument for building one going forward; in the meantime, an expedited release removing or disabling the SDK integration is the fallback, accepting the longer timeline.
- Backend-side mitigations, independent of what the client can do: tighten input validation on any endpoint the SDK's traffic touches, rate-limit or block traffic patterns specific to the vulnerable SDK behavior, and revoke or rotate any tokens that could have been exposed through the vulnerable path. These often land faster than any client-side change, since they require no app release at all, and they remain effective even against users who have not updated and never will.
Communication. Three audiences, three different cadences and content:
- Internal: a running incident channel with frequent updates (hourly, tapering as the situation stabilizes), so engineering, support, and leadership are working from the same current state rather than stale assumptions.
- Stakeholders (product, sales, legal, executive): a clear impact summary and remediation timeline, updated as the picture solidifies, since they need to make their own downstream decisions (customer commitments, regulatory notification obligations) based on it.
- Users: a transparent advisory through whatever channels reach them (in-app notice, release notes, support scripts), stating what happened at a level that informs without handing an unpatched attacker a roadmap, and what action, if any, they should take (update the app, or nothing if the mitigation is already fully server-side and requires no user action).
Expedited release management. When a client release is needed (the SDK must be replaced or patched, not just disabled), keep the change minimal and scoped to the fix, run the automated test suite plus focused manual testing on the specifically affected flows rather than a full regression pass that would slow the release without proportionate benefit, and roll out in stages (a small percentage first, then wider) with active monitoring, so a mistake in the rushed fix itself is caught before it reaches every user. Keep a tested rollback plan ready, and coordinate timing with the app stores in advance if an expedited review is available and warranted.
Post-mortem and remediation timeline. Within roughly the first 72 hours, hold a blameless post-mortem covering the actual timeline (when the vulnerability was introduced, discovered, contained, and fixed), what worked and what did not in the response, and concretely, what let a vulnerable third-party SDK reach production in the first place, since that is the systemic question a single hotfix does not answer. The remediation timeline distinguishes the immediate fix (contained within hours via flags/backend mitigations, patched within one to a few days via an expedited release) from the longer-term structural fix (replacing the SDK entirely, or working with the vendor on a validated patch, which can take one to two weeks or more depending on the vendor). Longer-term, feed findings into the vetting process for future third-party SDK adoption: dependency scanning, a defined update cadence, and a documented risk tier per SDK based on what access it holds.
Worked example
A fitness-tracking app depends on a third-party analytics SDK; a researcher discloses that it silently exfiltrates a broader set of device identifiers than documented, via an insecure endpoint.
- Hour 0-1: Security confirms the report reproduces, engineering lead convenes the incident channel, unrelated releases are frozen. Risk assessment (running in parallel, not sequentially first) identifies this affects both platforms, all versions with the SDK, and that the exposed data includes device identifiers but not authentication credentials, an important distinction for the severity call.
- Hour 1-3: The app happens to have a remote-config flag gating the SDK's initialization. That flag is flipped off for all users; the SDK stops initializing app-wide within minutes of the config propagating, no app-store release needed. In parallel, the backend team blocks the SDK's specific outbound endpoint pattern at the network edge, so even a client that has not yet picked up the new remote config cannot complete the exfiltrating call.
- Hour 3-8: Internal stakeholders get an impact summary; legal assesses whether the exposed identifier set triggers a regulatory notification obligation (it depends on jurisdiction and exact data categories, a legal determination, not an engineering one). A user-facing advisory is drafted, factual about what was exposed and that it has already been mitigated server-side, with no action required from users, since the containment does not depend on them updating.
- Day 1-3: Engineering builds an expedited release that removes the vulnerable SDK integration entirely (not just gated by the flag, since the flag is a mitigation, not a fix), tested against the affected flows specifically, and rolled out in stages over the following two days with monitoring for regressions.
- Day 3: A blameless post-mortem covers why this SDK's data access was not scoped tighter at integration time, and the team adopts a policy requiring a documented data-access review for any new third-party SDK going forward, plus adding a remote-config kill switch to every future SDK integration by default, precisely because having one this time is what made the hour-1 containment possible at all.
Trade-offs and pitfalls
- Waiting for a complete risk assessment before taking any mitigation action. If the exposure is already clear enough to justify disabling a feature, do that immediately and let the fuller risk assessment continue in parallel; sequencing containment strictly after a complete assessment costs real exposure time for no corresponding gain in decision quality.
- Treating the app-store release as the primary containment mechanism. Store review and staged rollout take real time that an active exploitation window does not wait for; the backend-side and flag-based mitigations above are what actually buy time, with the client release as the durable fix that follows.
- Over-disclosing exploit details in the user-facing advisory. Transparency matters, but a technically detailed public description of exactly how the vulnerability worked can itself function as a how-to for anyone who has not yet exploited it, before every affected user has updated or the mitigation has fully propagated.
- Skipping the systemic post-mortem question in favor of just shipping the patch. Fixing this one SDK's specific flaw without asking how it got this level of access in the first place, or how it went unnoticed, means the same class of incident is likely to recur with the next third-party dependency.
- Under-resourcing the incident with only engineering. A vulnerability with user-data exposure implicates legal, communications, and often support simultaneously; treating it as a purely technical problem for engineering to solve alone misses the coordination the worked example's Day 1 legal determination and user advisory both depend on.
Unlock Full Question Bank
Get access to all 30 Secure Coding and Application Security interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.