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.
For a payment service that stores PII and handles card transactions, enumerate the OWASP Top Ten and relevant CWE categories that are most applicable, and propose mitigations by design (network segmentation, tokenization, encryption, least privilege). Describe your testing strategy (SAST/DAST, penetration-test scope) and how these controls map to PCI-DSS control objectives.
Sample Answer
Direct answer
A payment service that stores personally identifiable information (PII) and handles card transactions concentrates almost every high-severity Open Worldwide Application Security Project (OWASP) category in one system, because it combines sensitive data at rest, high-value transactions, and a large third-party integration surface (payment gateways, fraud services). The right response is to enumerate the applicable categories, attach concrete Common Weakness Enumeration (CWE) identifiers and design-time mitigations to each, then show that the resulting control set already satisfies most of the Payment Card Industry Data Security Standard (PCI-DSS) requirement objectives, since PCI-DSS is itself largely a codification of these same application-security controls for the payments domain.
Structured elaboration
Applicable OWASP categories, CWE mapping, and design mitigations
Using the current OWASP Top 10:2025 taxonomy:
| OWASP category | Representative CWE | Why it applies here | Design-time mitigation |
|---|---|---|---|
| A01 Broken Access Control | CWE-284 (Improper Access Control), CWE-639 (Authorization Bypass Through User-Controlled Key) | Card and PII data must be reachable only by the exact service/role that needs it, per transaction. | Least privilege: service accounts scoped to a single database schema or token vault, no shared "admin" credentials between services; row-level checks that never trust a client-supplied identifier alone. |
| A02 Security Misconfiguration | CWE-16 (Configuration) | Payment infrastructure spans many components (gateway, database, message queue) each with their own default settings. | Network segmentation: the cardholder data environment (CDE) sits in its own network segment with default-deny ingress/egress rules, separate from general application infrastructure. |
| A04 Cryptographic Failures | CWE-327 (Broken or Risky Cryptographic Algorithm), CWE-311 (Missing Encryption of Sensitive Data) | Card numbers and PII are the textbook case for data that must never exist in a readable form outside of tightly controlled processing. | Tokenization and encryption: replace the primary account number (PAN) with a non-reversible token everywhere except the minimal payment-processing path; encrypt any PII at rest with a vetted algorithm and centrally managed keys. |
| A05 Injection | CWE-89 (SQL Injection) | Any endpoint that queries transaction or PII records by a client-supplied filter is a classic injection surface. | Parameterized queries/ORM (object-relational mapping) enforced by lint rule, never string-built SQL against the PII store. |
| A07 Authentication Failures | CWE-287 (Improper Authentication), CWE-613 (Insufficient Session Expiration) | Both customer-facing auth and internal service-to-service auth guard access to the same sensitive data. | Multi-factor authentication (MFA) for any human administrative access to the CDE; short-lived service credentials instead of static shared secrets. |
| A09 Security Logging and Alerting Failures | CWE-778 (Insufficient Logging) | Payment fraud and breach investigations depend entirely on having an audit trail. | Immutable, centrally shipped audit logs for every read/write of cardholder or PII data, with alerting on anomalous access volume. |
Testing strategy
- Static Application Security Testing (SAST): run on every pull request touching the payment or PII code paths, with CWE-89 and CWE-327 findings in that code path treated as merge-blocking rather than advisory.
- Dynamic Application Security Testing (DAST): scheduled against a staging environment that mirrors production configuration, scoped explicitly at the payment API surface (authorization header handling, parameter tampering, error-message information leakage).
- Penetration-test scope: an annual (at minimum) engagement covering the full cardholder data environment: the public-facing payment API, the tokenization service, internal service-to-service authentication between the payment service and the fraud/ledger systems, and the admin console used by support staff. Scope should explicitly include testing that a compromised low-privilege service account cannot reach the token vault, since that is the single highest-value target in the system.
Mapping to PCI-DSS control objectives
PCI-DSS groups twelve requirements under six goals. The application-security controls above map directly onto several of them:
| PCI-DSS goal (requirement range) | How the design mitigations above satisfy it |
|---|---|
| Build and maintain a secure network (Req. 1-2) | Network segmentation isolating the CDE; secure configuration baselines instead of vendor defaults. |
| Protect cardholder data (Req. 3-4) | Tokenization removes the PAN from most of the environment; encryption covers what remains, both at rest and in transit. |
| Maintain a vulnerability management program (Req. 5-6) | SAST/DAST in the development pipeline, patch management for dependencies. |
| Implement strong access control measures (Req. 7-9) | Least-privilege service accounts and role-scoped access map to "restrict access by business need to know"; MFA maps to "identify and authenticate access to system components." |
| Regularly monitor and test networks (Req. 10-11) | Immutable audit logging maps to continuous monitoring; the SAST/DAST/penetration-test program maps to required regular security testing. |
| Maintain an information security policy (Req. 12) | The design-review and testing-gate process above only works if it is codified as organizational policy, not left to individual engineer discretion. |
Worked example
Trace the highest-value asset, the token vault, through the whole stack: a customer's card is submitted once at checkout, immediately exchanged for an opaque token by a dedicated tokenization service running in its own network segment (network segmentation), with the token (not the PAN) stored in the orders database (least privilege: the orders service never receives PAN-level access, so a SQL injection in the orders search endpoint, even if one existed, could not exfiltrate a real card number, only an already-useless token). The mapping from PAN to token lives in a hardware security module (HSM)-backed vault, encrypted at rest with keys the orders service cannot read (encryption). Every lookup against that vault is logged with the requesting service identity and timestamp (A09 mitigation), and the SAST pipeline blocks any pull request that would let a new service call the vault without going through the existing scoped client library. This single flow demonstrates all four requested design mitigations working together and satisfies PCI-DSS Requirements 3 (protect stored account data), 7 (restrict access by business need to know), and 10 (log and monitor access) simultaneously.
Trade-offs and pitfalls
- Tokenizing late instead of at the earliest possible point. If the PAN travels through several internal services before being tokenized, every one of those services and the network path between them falls inside PCI scope, which defeats the main cost benefit of tokenization: shrinking the audited surface area.
- Treating PCI-DSS compliance as equivalent to security. PCI-DSS defines a compliance floor built around cardholder data specifically; a system can pass an assessment while still having weak controls around PII that is not itself a card number (a common gap, since PII protection is a broader legal/privacy obligation, not solely a PCI-DSS one).
- Over-scoping the cardholder data environment. Bringing unrelated services into the CDE "to be safe" multiplies the audit and monitoring burden without improving security, and often signals the tokenization boundary was drawn in the wrong place.
- Skipping penetration testing of the service-to-service boundary. Teams often scope pentests at the public API only; the highest-impact finding in a payment system is frequently a low-privilege internal service that turns out to have unnecessary reach into the token vault, which only shows up if internal boundaries are explicitly in scope.
An attacker used SSRF to reach an internal configuration service, retrieved credentials, and exfiltrated them by appending them to application log URLs visible to a third-party log-aggregation service. Describe the full attack chain, and for each step name a concrete control that would have broken it (prevention or detection), including how you would harden secrets handling and logging to prevent this exfiltration path specifically.
Sample Answer
Direct answer
This is a five-step chain (initial Server-Side Request Forgery (SSRF), internal reconnaissance, credential retrieval, log-based exfiltration, and off-network collection), and the reason it worked end to end is that every step relied on a control that either did not exist or was implemented as a single point of failure. Breaking the chain does not require stopping the attacker at step one; any one of the five controls listed below, applied correctly, ends the attack before exfiltration completes, which is the central lesson for how to prioritize the fix: harden the step that is cheapest to fix and closest to the exfiltration path first (secrets handling and logging), even while the SSRF entry point itself also gets fixed.
Structured elaboration
The chain, step by step, with the control that would have broken it:
| Step | Attacker action | Concrete control that breaks it | Prevention or detection |
|---|---|---|---|
| 1 | Finds a server-side URL-fetch feature and uses it to reach an internal configuration service the attacker cannot reach directly | Destination allow-list on the fetch, validated after DNS resolution, plus network-layer egress filtering blocking the config service from this workload's segment | Prevention |
| 2 | Retrieves credentials from the internal configuration service's response | Configuration service requires its own authentication (a service-to-service token or mTLS client certificate), not "reachable from the internal network" as its only access control | Prevention |
| 3 | Exfiltrates the retrieved credentials by appending them to a URL that the application later logs | Structured, allow-listed logging: log a fixed schema of fields, never a raw caller-influenced string interpolated into a URL or free-text field | Prevention |
| 4 | Those logs are shipped to a third-party log-aggregation service where the attacker can view them | Redaction/masking at the logging pipeline's ingestion point (pattern-match credential shapes and scrub before they leave the trust boundary), and least-privilege access control on who can read the aggregator | Prevention, with detection as backstop |
| 5 | Attacker retrieves the credentials from the third-party aggregator | Data-loss-prevention alerting on the aggregator side for credential-shaped strings appearing in log content, and short-lived credentials so anything captured is already expired by the time it is retrieved | Detection |
Why the chain worked despite several plausible-looking safeguards. Each step individually might have looked "covered" in a checklist audit: the fetch feature had some allow-list (just not one that covered the internal configuration service), the configuration service was "internal only" (which is a network-reachability property, not an authentication control), and logging was "structured" in the sense of using a logging library (but the URL field itself was still a raw string containing whatever the upstream response happened to embed). The chain exploits the gap between "looks secure" and "is authenticated/validated at every hop," which is exactly why a defense needs to be redundant across steps rather than relying on getting one step perfectly right.
Step 1 in depth (the entry point). This is an ordinary SSRF bug: a caller-influenced URL fed to a server-side fetch, with the internal configuration service being one of the internal targets a properly-scoped allow-list or egress policy would have excluded. The load-bearing point for this question is narrower: step 1 being fixed is necessary but not sufficient, because steps 3 through 5 form an independent exfiltration path that would still work even against a different initial compromise (a leaked internal credential from any other source), so they need to be hardened on their own merits, not treated as "downstream of an SSRF bug that we've now patched."
Step 2 and 3 in depth (secrets handling). The retrieved credential should never have been able to reach a URL in the first place. Concretely:
- Configuration services that hand back secrets should require the caller to authenticate as a specific, audited service identity, not merely originate from an internal Classless Inter-Domain Routing (CIDR) range; "on the internal network" is a network property, and SSRF exists precisely to let an attacker borrow that network property without borrowing an identity.
- Once a secret is in application memory, treat it as radioactive: never string-concatenate it into anything that might be logged, including URLs, query parameters, and exception messages. A common failure mode is a retry or error-handling path that logs "failed request to
<url>" using the exact URL object that was constructed with the secret embedded in it; the secret leaks through a code path nobody thought of as "the logging code." - Prefer a secrets-management pattern where the credential is fetched just-in-time at the call site that needs it and scoped narrowly (short time-to-live, single-purpose), rather than being retrieved once and passed around through several layers of the request lifecycle where any one of those layers might log it.
Step 3 and 4 in depth (logging hardening specifically for this exfiltration path). The specific mechanism here (appending secrets to log-visible URLs) points at two independent fixes that should both ship:
- At the point of construction: never log a URL, header, or any field sourced from a value that might contain caller- or upstream-influenced content without passing it through an allow-list of expected characters/format first. If the field is meant to be a resource path, validate it looks like one before it is eligible to be logged at all.
- At the logging pipeline's boundary: run outbound log records through a redaction filter matching known credential shapes (AWS-style access keys, JSON Web Tokens (JWTs), high-entropy strings above a length threshold in fields not expected to contain them) before they leave the trust boundary to the third-party aggregator. This is a backstop for exactly the case where the first fix was missed somewhere, and it is cheap to apply broadly because it does not require finding every individual logging call site.
Step 5 in depth (limiting blast radius at the collection point). Even with steps 1 through 4 fixed, defense in depth means assuming a future, different bug reaches this far. Two controls specifically limit damage here: restrict who can query the third-party aggregator's raw log content (most engineers need dashboards and metrics, not raw payloads), and prefer credentials that are short-lived and scoped narrowly enough that a credential sitting in a log for the retention window is not still valid or not valid for anything consequential by the time anyone could retrieve it.
Worked example
Walking the chain with the specific mechanism named in the question: the attacker's initial SSRF request reaches http://internal-config.svc/api/db-credentials, which (because it trusts network origin instead of requiring service authentication) returns {"username":"app","password":"S3cr3t!"}. The application then makes a follow-up request to an upstream service for logging/analytics purposes, and the attacker has arranged (via the same SSRF primitive, or a separate parameter also under their control) for that follow-up URL to be https://upstream.example.com/track?ref=S3cr3t!. The application's request logger writes GET https://upstream.example.com/track?ref=S3cr3t! verbatim to the access log, which ships to the third-party log aggregator, where the attacker (having separately obtained read access, or the aggregator misconfigured as broadly readable) retrieves the password from the log entry, never having touched the database directly. Every step used a legitimate-looking feature (a config lookup, a tracking pixel, structured logging) doing exactly what it was built to do; the vulnerability is entirely in the missing validation at each handoff, not in any single obviously "broken" component.
Trade-offs and pitfalls
- Fixing only the SSRF entry point. This is the most common shallow response to this class of incident, and it leaves steps 2 through 5 exploitable by the next unrelated bug that manages to reach the configuration service or construct a log-visible URL. Prioritize the secrets-handling and logging fixes as independently valuable, not merely as SSRF cleanup.
- Redaction as the only logging control. Pattern-matching redaction is a backstop, not a primary control: it will miss credential formats it was not written for, and a false sense of security here ("we redact logs, so this is covered") is worse than knowing redaction has gaps. The primary control is never letting the secret reach a loggable field in the first place.
- Treating "internal only" as an access control. This is the root misconception the whole chain exploits. A service reachable only from inside the network is not authenticated; it is merely harder for an external attacker to reach directly, and SSRF exists specifically to erase that difficulty for anyone who can reach any internal-network-adjacent code path.
- Assuming third-party log aggregators inherit your access controls. Data shipped to an external SaaS (Software as a Service) logging platform is now governed by that platform's access model, which is frequently broader (more engineers, less granular roles) than the originating team assumes. Auditing who can read production logs at the aggregator, not just who can read them in the source system, is a distinct and often-skipped step.
You discover a legacy Java service deserializes untrusted data using native Java serialization. Explain how an attacker could craft a gadget chain to achieve remote code execution, how you would assess whether risky gadget classes (for example from common libraries like Apache Commons Collections) are present on the classpath, and enumerate robust mitigation options that are safe to apply in a legacy environment that cannot be rewritten quickly.
Sample Answer
Direct answer
Native Java serialization runs attacker-chosen code as a side effect of object reconstruction, not as a separate execution step, because ObjectInputStream.readObject() invokes each deserialized class's own readObject() method (or its default field-setting equivalent) before the caller gets any chance to inspect or reject what came back; a gadget chain is simply a sequence of already-present, individually harmless classes whose readObject/toString/hashCode/equals methods, when triggered in the right order, walk from "arbitrary object graph the attacker built" to "call a method the attacker fully controls," most famously Runtime.exec(). Assessing a legacy service for this risk means scanning its full classpath for known chain-building classes (Apache Commons Collections is the classic example, but far from the only one), and because rewriting a legacy service quickly is usually not realistic, the mitigations that matter most here are the ones that can be applied without touching a line of business logic: serialization filtering and, failing that, removing the dangerous classes from the classpath entirely.
Structured elaboration
How a gadget chain actually achieves remote code execution. ObjectInputStream reconstructs an object graph purely from the type names and field data embedded in the byte stream; it has no concept of "the type I expected" versus "the type the stream claims to contain," so if a class implementing Serializable is anywhere on the classpath, an attacker can instruct the stream to construct an instance of it, regardless of whether the application's own code ever references that class directly. A gadget chain exploits this by chaining several such classes together: the deserialized root object's readObject() (or a method invoked incidentally during deserialization, like hashCode() on a HashMap entry being rebuilt) calls a method on a field object, whose own method calls another field object's method, and so on, until the chain reaches a class whose behavior is attacker-steerable enough to do something dangerous with attacker-controlled data, most commonly reflectively invoking an arbitrary method by name (InvokerTransformer in Commons Collections literally exists to invoke a named method via reflection, which is exactly the primitive a chain needs at its terminal step).
The canonical example: Commons Collections "CommonsCollections1." The publicly documented ysoserial chain for older Apache Commons Collections versions composes: a LazyMap (whose get() triggers a configured Transformer whenever a key is looked up, and whose reconstruction during deserialization itself triggers a get() call as a side effect of AbstractMapDecorator/HashMap internals), a ChainedTransformer (runs a list of transformers in sequence, feeding each one's output to the next), an InstantiateTransformer and ConstantTransformer (build up a reflective call to Runtime.getRuntime()), and an InvokerTransformer (the primitive that actually performs the reflective method call). None of these classes is individually dangerous; LazyMap and ChainedTransformer are ordinary collection utilities used all over the ecosystem for entirely legitimate reasons. The danger is emergent from the combination being reachable purely by constructing the right object graph, which is exactly why "we don't call any dangerous methods in our code" is not a defense: the attacker's serialized bytes call them, not the application's source code.
Assessing whether risky gadget classes are present on the classpath. This is a mechanical, automatable check: known gadget-chain building-block classes have fixed, published fully-qualified names (from the ysoserial project's chain catalog, which documents CommonsCollections1 through 6+, CommonsBeanutils1, Groovy1, Spring1/2, and others), so scanning every jar on the classpath for those class names tells you, cheaply, which jars need a closer look. This does not by itself prove exploitability (the chain also needs a reachable, unfiltered ObjectInputStream.readObject() call on attacker-influenced bytes, and in patched library versions the chain may be present but a defensive check added upstream may block the specific invocation path), but it is the fast, high-signal first pass before any deeper analysis:
#!/usr/bin/env python3
"""Scan every jar on a classpath for classes that are known building blocks
of published Java deserialization gadget chains (ysoserial-catalogued).
Presence does NOT by itself prove exploitability; it tells you which jars
need the follow-up check (usually: is this a Runtime.exec sink reachable
from an unfiltered ObjectInputStream) and which don't."""
import sys, zipfile, os
KNOWN_GADGET_CLASSES = {
"org/apache/commons/collections/functors/InvokerTransformer.class":
"ysoserial CommonsCollections1/5/6 chain link (arbitrary method invocation via reflection)",
"org/apache/commons/collections/functors/ChainedTransformer.class":
"ysoserial CommonsCollections1 chain link",
"org/apache/commons/collections/functors/ConstantTransformer.class":
"ysoserial CommonsCollections1 chain link",
"org/apache/commons/collections/functors/InstantiateTransformer.class":
"ysoserial CommonsCollections1 chain link",
"org/apache/commons/collections/map/LazyMap.class":
"ysoserial CommonsCollections1 trigger (readObject calls Map.get)",
"org/apache/commons/collections4/functors/InvokerTransformer.class":
"commons-collections4 equivalent (CommonsCollections3/4)",
"org/apache/commons/beanutils/BeanComparator.class":
"ysoserial CommonsBeanutils1 chain link",
}
def scan_jar(path):
hits = []
try:
with zipfile.ZipFile(path) as zf:
names = set(zf.namelist())
for gadget_path, note in KNOWN_GADGET_CLASSES.items():
if gadget_path in names:
hits.append((gadget_path, note))
except zipfile.BadZipFile:
pass
return hits
def main(targets):
for jar in targets:
hits = scan_jar(jar)
if hits:
print(f"[FLAGGED] {jar}")
for gadget_path, note in hits:
print(f" {gadget_path.replace('/', '.').removesuffix('.class')} -- {note}")
else:
print(f"[clean] {jar}")
if __name__ == "__main__":
main(sys.argv[1:])
Run against a real, historically vulnerable version and a real, unrelated library as a negative control:
$ python3 scan_gadgets.py commons-collections-3.2.2.jar
[FLAGGED] commons-collections-3.2.2.jar
org.apache.commons.collections.functors.InvokerTransformer -- ysoserial CommonsCollections1/5/6 chain link (arbitrary method invocation via reflection)
org.apache.commons.collections.functors.ChainedTransformer -- ysoserial CommonsCollections1 chain link
org.apache.commons.collections.functors.ConstantTransformer -- ysoserial CommonsCollections1 chain link
org.apache.commons.collections.functors.InstantiateTransformer -- ysoserial CommonsCollections1 chain link
org.apache.commons.collections.map.LazyMap -- ysoserial CommonsCollections1 trigger (readObject calls Map.get)
$ python3 scan_gadgets.py jackson-databind-2.17.2.jar
[clean] jackson-databind-2.17.2.jar
Both runs are the real, captured output of scanning the actual jars against each other (commons-collections-3.2.2.jar downloaded from Maven Central, and jackson-databind as an unrelated library serving as the negative control), confirming the scanner correctly flags the known-dangerous jar and stays silent on one with no gadget-relevant classes. One important nuance worth assessing after the classpath scan, not before: Commons Collections 3.2.2 specifically already ships a defensive fix, and decompiling the actual class confirms exactly what it does. InvokerTransformer still declares implements Serializable unconditionally, but its readObject(ObjectInputStream) method now calls FunctorUtils.checkUnsafeSerialization(InvokerTransformer.class) as the very first thing it does, before defaultReadObject() runs; that helper throws UnsupportedOperationException unless the system property org.apache.commons.collections.enableUnsafeSerialization is set to true on the running JVM. So classpath presence of these classes in 3.2.2+ is a "needs further check" result (is that property set anywhere in this deployment's startup configuration), not an automatic confirmed finding; earlier 3.1/3.2.1 versions have no such guard and deserialize unconditionally. This is exactly the kind of version-specific property that has to be verified rather than assumed, because the same class name across two point releases can mean two different risk levels.
Robust mitigation options for a legacy environment that cannot be rewritten quickly. In priority order, cheapest and least invasive first:
- Serialization filtering (
ObjectInputFilter, standardized since Java 9, JEP 290). Wrap everyObjectInputStreamused on untrusted input with a filter that allow-lists only the specific classes the endpoint legitimately expects, rejecting everything else beforereadObject()is invoked on it. This is the single highest-leverage fix because it requires no changes to business logic, no library upgrades, and no classpath changes; it just needs every call site identified and wrapped, which is itself a mechanical, scriptable refactor. - Remove or shade the dangerous classes from the classpath, where the application does not actually use the vulnerable classes' functionality (many services pull in Commons Collections transitively without ever calling
InvokerTransformerorLazyMapthemselves). This closes the specific chain without touching application code, at the cost of needing to verify nothing legitimate depends on the removed classes. - Upgrade the library to a version with the serialization guard enabled by default where available (Commons Collections 3.2.2+ with the system property left unset, or migrating to
commons-collections4, which restructured the package and requires a deliberate opt-in for the dangerous behavior). This is safer than a full rewrite but still carries the normal dependency-upgrade regression risk. - Global JVM-wide filtering via the
jdk.serialFiltersystem property or security property (available since Java 9, backported to 8u121+), which applies a default filter process-wide without needing to find and modify every individualObjectInputStreamcall site. This is the pragmatic choice when call sites are too numerous or too poorly understood to enumerate confidently in the available time, at the cost of being coarser-grained than a per-call-site filter. - Network and architectural isolation as a backstop, not a fix: if the deserializing endpoint does not need to be reachable from untrusted networks at all, restricting its exposure reduces the practical attack surface while the above fixes are rolled out, but this should never be the only mitigation, since internal attackers and lateral movement from an already-compromised host both bypass network-layer isolation entirely.
Worked example
The worked example is the executed scan above: a real, vulnerable-version Commons Collections jar correctly flagged with the five specific classes that make up the CommonsCollections1 chain, each annotated with its role, against a real unrelated jar (Jackson) correctly returning clean. This is precisely the first-pass triage step a security engineer would run across a legacy service's lib/ directory or dependency tree before deciding where to spend limited remediation time: the flagged jars get the ObjectInputFilter treatment and a closer look at whether the specific vulnerable version property applies; the clean jars are deprioritized.
Trade-offs and pitfalls
- Treating classpath presence as a confirmed finding. As the Commons Collections 3.2.2 case shows, the same class names can be present but gated behind a version-specific defensive property, or simply never reachable because the application never calls
ObjectInputStream.readObject()on untrusted bytes at all. Classpath scanning is triage, not proof; the proof requires confirming an actual reachable, unfiltered deserialization sink. - Assuming the published gadget catalog is exhaustive.
ysoserialdocuments known, publicly disclosed chains; new chains are discovered periodically in libraries that have not yet been catalogued. A scanner built only against today's known list will always have false negatives against novel chains, which is a reason to prefer serialization filtering (which defends against unknown future chains by restricting reachable types, not by matching known-bad ones) over a purely detection-based posture. - Removing a "dangerous" class that turns out to be load-bearing.
LazyMapandChainedTransformerare ordinary, widely-used utility classes; blindly stripping them from a shaded jar without checking real usage can break legitimate functionality. Verify actual call-graph usage (or at minimum, run the full test suite) before removing classes rather than assuming "gadget-chain-relevant" means "unused." - Global
jdk.serialFilteras a substitute for understanding the endpoints. A process-wide filter is a reasonable stopgap, but it is coarser than per-endpoint filtering and can either be too permissive (default-allow patterns that still admit a chain) or too restrictive (breaking a legitimate, narrow serialization use case elsewhere in the same JVM); it buys time for the real per-call-site fix, and should be tracked as a stopgap in the remediation plan rather than closed out as "done."
Tell me about a time you convinced senior leadership to fund an application security initiative such as SAST, DAST, or SCA tooling. Describe the context and stakeholders, the major objections you faced, the data and metrics you used to make your case, how you addressed the concerns, and the measurable outcome.
Sample Answer
Direct answer
This question is scored on whether the candidate can turn a security investment into a business case that survives contact with skeptical stakeholders, not on the specific tool they funded. Structure the answer with the Situation, Task, Action, Result (STAR) method: name the real context and stakeholders, be specific about the objections actually raised (cost, developer velocity, "we haven't been breached"), show the data used to counter each objection, and close with an outcome the candidate can defend if the interviewer asks a follow-up question about it.
Structured elaboration
What each part of STAR should contain here
- Situation: the state of the organization before the initiative, stated in terms the objection-raisers would recognize (a recent finding from an external assessment, a compliance deadline, a near-miss incident, a competitor's public breach) rather than an abstract "we wanted better security."
- Task: the candidate's specific responsibility, ideally something they owned end to end (proposing the initiative and getting it funded), not something they merely supported.
- Action, broken into the sub-parts the question explicitly asks for:
- Stakeholders: name the actual roles in the room (engineering leadership who will feel the velocity cost, finance who controls budget, whoever owns risk/compliance who cares about exposure), since "senior leadership" is rarely one person with one objection.
- Objections: the two that come up in almost every real version of this story are cost (tooling licenses plus the engineering time to integrate and triage) and developer velocity (a new gate that could slow shipping); a credible answer names the specific one(s) that actually came up, not a generic list.
- Data and metrics used: this is the part interviewers probe hardest, and it should be data the candidate can actually explain the provenance of if pressed: findings from a prior external penetration test or audit, the count of vulnerabilities discovered in production versus caught pre-release over some period, or industry benchmark data on typical breach costs for context. A candidate should cite REAL numbers from their own experience here; if exact figures are not available or would be confidential, describe the direction and rough magnitude honestly ("a meaningful reduction," "roughly half," "went from weeks to days") rather than inventing a precise-sounding statistic to fill the gap.
- How concerns were addressed: the strongest version of this is a concrete concession that reduced the objection's force without abandoning the goal (a phased rollout starting with one team, a pilot period with a defined success bar, integrating the tool so it reports asynchronously at first rather than blocking merges immediately).
- Result: an outcome with SOME measurable signal (even a qualitative "critical findings caught before release" trend, if precise numbers cannot be shared) plus what changed structurally afterward (the tool became standard, a budget line became recurring, leadership asked for a similar business case again for the next initiative).
Why Software Composition Analysis (SCA) specifically is a strong version of this story
Software Composition Analysis, scanning dependencies for known-vulnerable versions, is a particularly persuasive initiative to pitch because its value is unusually easy to make concrete to a non-security audience: a specific named vulnerability in a specific widely-used library, with a public severity score, is a much more visceral argument than an abstract "we should improve our security posture." If a candidate's actual experience is with SCA/dependency-scanning adoption specifically, that is a strong, credible narrower version of this same story and should be told as such rather than generalized into a vaguer "application security tooling" pitch.
Worked example
An illustrative version of this story, built to show the shape rather than as a claim of a specific real outcome: at a mid-sized company where a recent external penetration test had flagged several vulnerable third-party libraries in production, an engineer proposed adopting Software Composition Analysis in the CI (continuous integration) pipeline. The stakeholders were the VP of Engineering (worried about the tool adding friction to every pull request) and the CISO or head of security (already convinced, but without budget authority alone). The main objection was velocity: engineering leadership did not want a new gate slowing releases. The candidate addressed it by proposing the tool run in report-only mode for the first month, using that period's data (how many findings surfaced, and roughly how many were actual actionable issues versus noise) to show the VP the real false-positive rate before asking for it to become a blocking gate on new dependencies only, not the whole existing dependency tree. The result: the pilot's low false-positive rate made the ask for a blocking gate an easy yes, the known-vulnerable dependency count trended down over the following quarter, and the initiative became the template the team used to pitch the next security tooling investment.
Trade-offs and pitfalls
- Being vague about objections and data. "I explained why security matters and they agreed" gives an interviewer nothing to probe and reads as either an invented story or one the candidate was not actually close to; the objections and the specific data used to counter them are the substance the question is testing for.
- Inventing precise-sounding metrics. A suspiciously exact improvement number with no explanation of how it was measured is a red flag to an interviewer, not a strength; if real numbers cannot be shared, say so and describe direction and rough magnitude honestly instead.
- Taking sole credit for a group outcome. Funding decisions almost always involve multiple stakeholders and some negotiation; a candidate who describes single-handedly convincing an entire leadership team with no pushback often reads as less credible than one who describes a real negotiation with a real concession made along the way.
- Skipping the "how you addressed the concerns" part. This is often the most differentiating part of the story: naming the specific compromise or phased approach that got a skeptical stakeholder to yes shows judgment the raw funding outcome alone does not demonstrate.
OWASP A09 highlights Security Logging and Monitoring Failures. Design a minimum telemetry model for a web application to detect exploitation attempts of common vulnerability classes such as credential stuffing, SQL injection, and SSRF: list the events you would log, the context fields to capture, correlation IDs, retention considerations, and PII-handling guidelines for each event.
Sample Answer
Direct answer
Open Worldwide Application Security Project (OWASP) category A09 (now named "Security Logging and Alerting Failures" in the current 2025 edition, still A09, reflecting that the failure is as much about nobody acting on a log as about the log not existing) is best addressed with a small, deliberately minimal set of high-signal events rather than "log everything": one or two events per vulnerability class, each carrying enough context to investigate without a follow-up query, a correlation identifier to reconstruct the full request across services, a retention window proportional to how long an investigation realistically takes, and default redaction of personally identifiable information (PII) so the logging system does not become its own data-exposure risk.
Structured elaboration
Minimum events per vulnerability class
| Vulnerability class | Event(s) to log | Key context fields | PII handling for this event |
|---|---|---|---|
| Credential stuffing | auth.failure (every failed login), auth.success (every successful login) | correlation_id, pseudonymized account_id, source IP, user agent, geo, count of recent failures for this account/IP | Never log the submitted password (correct or not); store account_id pseudonymized (a keyed hash), not the raw email/username, in the high-volume auth.failure stream. |
| SQL injection | query.rejected (parameterization layer or web application firewall, WAF, blocked a request), query.anomalous_shape (a query executed but with an unusual parameter pattern worth reviewing) | correlation_id, endpoint, a hash of the offending parameter value (not the raw value), matched rule/signature identifier, authenticated account_id if present | Log a hash or truncated/redacted form of the parameter, never the raw payload verbatim in the indexed, widely-accessible log stream; keep the raw payload only in a short-retention, access-restricted forensic store if needed for investigation. |
| Server-Side Request Forgery (SSRF) | outbound.request_blocked (an application-initiated outbound request was blocked by an egress allow-list) | correlation_id, requested destination host, the internal service that initiated the request, authenticated account_id if user-triggered | Destination URLs can themselves leak internal topology; restrict visibility of this event's raw destination field to the security team, not to a general-access dashboard. |
Correlation IDs
Every event carries a single request-scoped correlation_id generated at the edge and propagated through every downstream service call (as a header, for example X-Correlation-Id), so an investigator can pull one identifier and see the full path a request took, including which service ultimately triggered a database query or an outbound call. Without this, reconstructing a credential-stuffing attempt that touched an authentication service, a rate limiter, and an audit log means manually correlating three independent, loosely time-aligned event streams instead of one join.
Retention considerations
- High-fidelity security events (the three event types above): retain in indexed, searchable storage for a period long enough to cover a realistic detection-to-investigation gap, commonly a year or more, since breaches are frequently discovered months after the initial access.
- Raw request/response bodies needed for deep forensic replay: much shorter retention (days to a few weeks) in a separate, access-restricted store, since these are the highest PII-density artifacts and the ones least often actually needed.
- Aggregated metrics (counts, rates) derived from the events above: can be retained far longer at near-zero storage cost, since they carry no PII and are useful for long-term trend reporting.
PII-handling guidelines (general, on top of the per-event notes above)
- Pseudonymize user identifiers by default in high-volume streams (a keyed hash of the account ID, not the raw ID), reserving the reverse-lookup capability for a small, audited access path.
- Never log credentials, full card numbers, or full session tokens under any circumstance, successful attempt or not.
- Encrypt logs at rest and restrict access by role, with access itself logged, since a logging system holding pseudonymized identity and behavior data is a meaningful target in its own right.
Worked example
Trace one credential-stuffing attempt through this model: an attacker submits 40 login attempts against 40 different accounts from one IP in 60 seconds. Each attempt produces an auth.failure event with a shared source IP but distinct pseudonymized account_id values and a shared time window; a correlation rule watching for "many distinct accounts, one source IP, short window" fires once, referencing all 40 correlation_id values in one alert instead of 40 separate low-context alerts. The analyst pulls the 40 correlation IDs, confirms none succeeded (no auth.success event shares that IP in the window), and closes it as an unsuccessful stuffing attempt, all without ever needing to look up a real username, because the account identifiers stayed pseudonymized throughout.
Trade-offs and pitfalls
- Logging the raw offending payload directly into the main indexed stream "to be thorough." This is the single most common mistake: it turns a security log into a PII and secrets-exposure liability of its own, and is exactly the kind of gap a tester reviewing an engagement commonly flags: missing context on WHY a request was blocked, no retention plan distinguishing high-value from low-value events, and no alerting tied to the log at all (a log nobody looks at satisfies the letter of "logging" while still failing the point of A09).
- Building alerting with no false-positive budget. A rule that fires on every failed login (rather than a pattern across many accounts from one source) generates so much noise that real signal gets lost in it; the credential-stuffing example above escalates on the pattern, not the individual event.
- Treating retention as one global number. A single retention policy applied uniformly either keeps far too much low-value raw data (cost, and more PII sitting around than necessary) or discards high-value security events too early to support a realistic investigation timeline; tier retention by event type as shown above.
- Skipping the correlation ID because "our logs already have a timestamp." Timestamp-based correlation across independently-scaled services degrades badly under load and clock skew; a propagated identifier is the only reliable way to reconstruct a single request's path.
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.