Application Security Deep Dive Interview Questions | JiQuest

add

#

Application Security Deep Dive

Deep dive · application security

Application security deep dive: OWASP Top 10, STRIDE, and secure SDLC for backend engineers.

JWT and RBAC solve authentication and authorization for one system. This guide covers the broader discipline interviewers probe independently of any specific auth mechanism — injection, broken access control, insecure deserialization, and SSRF with Java/Spring examples; STRIDE threat modeling walked through on a real transfer endpoint; a secure SDLC pipeline; secrets management; and dependency/SBOM hygiene.

10OWASP categories
6STRIDE threats
20Interview Q&A
Client request WAF / Gatewayedge filtering AuthN & AuthZJWT + ownership check Input validationBean Validation, DTOs Business logicparameterized queries Output encodingcontext-aware escaping Secrets Managershort-lived creds Databasebind params, not concat External APISSRF: allow-list only every layer assumes the layer before it can fail or be bypassed

Why application security is its own interview topic

A JWT filter and a role check answer one narrow question: is this caller who they claim to be, and are they allowed to hit this endpoint at all. Application security is the much broader discipline that decides whether the code behind that endpoint can be tricked into doing something it was never supposed to do — running a query it didn't intend to run, deserializing bytes it shouldn't trust, fetching a URL an attacker chose instead of the user. Interviewers ask about OWASP categories, threat modeling, and secure SDLC placement specifically because they're generalist signals: they work regardless of whether a candidate's last project used JWT, sessions, OAuth, or something else entirely, and they separate someone who wrote code that happens to work from someone who reasons about what an attacker would try differently.

OWASP reasoningNot memorizing the list — explaining why each category's specific fix works.
Threat modelingA repeatable method (STRIDE) for finding threats before code is written, not after.
Secure SDLCKnowing which security activity belongs at which pipeline stage, and why.
Operational hygieneSecrets, dependencies, and encoding — the unglamorous controls that prevent the big breaches.

OWASP Top 10 at a glance

The current (2021) OWASP Top 10 is a ranked list of the application-security risk categories that show up most often in real audits. Not every category gets equal depth below — the ones most distinctive to Java/Spring backends (injection, access control, cryptographic failures, deserialization, SSRF) get a full treatment; the rest get a tight, concrete summary.

RankCategoryWhere it typically shows up in a Java/Spring service
A01Broken Access ControlAn endpoint checks authentication but not resource ownership — the IDOR pattern.
A02Cryptographic FailuresFast hashes for passwords, hand-rolled encryption, secrets in plaintext at rest.
A03InjectionString-concatenated SQL/JPQL, unvalidated NoSQL query operators, OS command building.
A04Insecure DesignA missing threat model — the vulnerability was never going to be caught by testing because the design itself had no control for it.
A05Security MisconfigurationActuator endpoints exposed publicly, verbose error pages, default credentials left in place.
A06Vulnerable and Outdated ComponentsA transitive dependency with a known CVE nobody on the team explicitly chose.
A07Identification and Authentication FailuresNo rate limiting on login, tokens that never expire, weak session fixation handling.
A08Software and Data Integrity FailuresInsecure deserialization lives here in the 2021 list — unsigned artifacts, unverified CI plugins.
A09Security Logging and Monitoring FailuresAn auth bypass or a data exfiltration that ran for months with nothing alerting on it.
A10Server-Side Request Forgery (SSRF)A "fetch this URL" feature lets the server reach internal IPs or the cloud metadata endpoint.

Jump to a section

Injection: SQL, NoSQL, and why parameterization actually works

Injection happens whenever attacker-controlled data is interpreted as part of a command's structure instead of purely as its data. SQL injection is the canonical case: if a query is built by concatenating a string, a crafted value can close a quote early and append new clauses, turning a lookup into an arbitrary read, write, or authentication bypass.

// Vulnerable -- string-concatenated query; user input becomes part of the SQL grammar
String sql = "SELECT * FROM users WHERE email = '" + email + "'";
Statement stmt = connection.createStatement();
ResultSet rs = stmt.executeQuery(sql);
// email = "x' OR '1'='1" turns the WHERE clause into an always-true condition
// Fixed -- Spring Data derived query, or an explicit JPQL query with a named parameter
public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByEmail(String email);           // parameter bound, never concatenated

    @Query("SELECT u FROM User u WHERE u.email = :email")
    Optional<User> findByEmailExplicit(@Param("email") String email);
}

JPA and Spring Data prevent this by construction, not by convention: a derived query method or a @Query with a named parameter sends the SQL text and the parameter value to the driver as two separate channels. The driver binds the value as a literal of a fixed type at execution time, so no matter what characters the value contains, the database can never reinterpret it as SQL syntax. Escaping quotes by hand is the fragile alternative — it requires remembering every dangerous character for every specific database dialect, and it's exactly one forgotten case away from a bypass.

NoSQL injection is the same idea with a different mechanism. MongoDB queries are JSON documents, and if a client-supplied field is deserialized straight into a query object instead of a typed DTO, a value like {"$gt": ""} is interpreted as a query operator instead of a literal string, potentially matching every document in the collection. The fix mirrors the SQL case: bind incoming fields to a strongly typed request DTO with Bean Validation constraints first, so a field declared as String can never be reinterpreted as an operator object.

Common mistake Building a dynamic ORDER BY or table/column name from user input can't be parameterized the normal way, since identifiers aren't values — the fix there is a strict allow-list of known-safe column names, never direct interpolation of user input into that position.

Broken access control: the IDOR pattern

This is OWASP's #1-ranked category, and for good reason: it's the easiest class of bug to introduce and the hardest to catch with automated tooling, because the code runs successfully and returns a plausible-looking response — it's just the wrong response for who's asking.

// Vulnerable -- checks that the caller is logged in, never that the order is theirs
@GetMapping("/orders/{id}")
public OrderDto getOrder(@PathVariable Long id) {
    return orderService.findById(id);  // any authenticated customer can read ANY order
}
// Fixed -- ownership is checked before the data is ever returned
@GetMapping("/orders/{id}")
public OrderDto getOrder(@PathVariable Long id, @AuthenticationPrincipal UserPrincipal caller) {
    Order order = orderService.findById(id);
    boolean owns = order.getUserId().equals(caller.getId());
    if (!owns && !caller.hasRole("ADMIN")) {
        throw new NotFoundException();   // 404, not 403 -- don't confirm the id even exists
    }
    return OrderDto.from(order);
}

This is an Insecure Direct Object Reference (IDOR): the endpoint takes an identifier straight from the URL and uses it to fetch a record without checking that the identifier belongs to the caller. It's dangerous precisely because it's invisible in normal testing — a developer testing as themselves only ever requests their own orders, so the bug never surfaces until someone deliberately tries a different id, which is exactly what an attacker does first. The fix is cheap once you know to look for it: either scope the repository query itself to the caller (findByIdAndUserId), or fetch the record and explicitly compare its owner to the caller before returning anything.

Interview-depth follow-up Notice the fixed version returns 404 instead of 403 for someone else's order. Returning 403 confirms the id exists and belongs to someone; 404 gives an attacker no signal either way, which matters when ids are sequential and enumerable.

Cryptographic failures

This category covers everything from weak password hashing to home-grown encryption to secrets sitting in plaintext where a breach of any one system exposes them directly. The common thread is using cryptography incorrectly rather than not at all — teams usually do encrypt something, they just pick the wrong primitive or the wrong mode for the job.

Password hashingUse BCrypt or Argon2, never a fast general-purpose hash (SHA-256/MD5) — speed is the attacker's friend here, not yours.
Randomnessjava.util.Random is predictable from its seed; use SecureRandom for tokens, reset codes, and session identifiers.
Key managementHardcoded encryption keys defeat the point of encryption; keys belong in a KMS with rotation, not a config file next to the ciphertext.
TransportTLS everywhere — including internal service-to-service calls, not just the public-facing edge.
What "good" looks like PasswordEncoder encoder = new BCryptPasswordEncoder(12); for storage, SecureRandom.getInstanceStrong() for anything unpredictability-sensitive, and encryption keys fetched from a KMS at runtime rather than committed alongside the data they protect.

Insecure deserialization: a classic Java-specific vulnerability class

In the 2017 OWASP Top 10 this was its own category (A8: Insecure Deserialization); the 2021 revision folds it into the broader Software and Data Integrity Failures category, but it's common and distinctive enough as a Java problem specifically that it's worth understanding on its own.

ObjectInputStream.readObject() doesn't just parse bytes into data — it reconstructs live objects, invoking constructors, readObject, and readResolve methods as part of rebuilding the object graph. If the byte stream comes from an untrusted source, the attacker isn't limited to whatever fields your own classes expose: they can chain together "gadget" classes already sitting on the classpath (the Apache Commons Collections gadget chain is the famous example) into a sequence of side effects that ends in arbitrary code execution, without exploiting any bug you wrote yourself. The vulnerability is in the mechanism, not in any specific application logic.

// Vulnerable -- deserializing an untrusted byte stream with native Java serialization
try (ObjectInputStream in = new ObjectInputStream(request.getInputStream())) {
    Object payload = in.readObject();   // gadget-chain code can run before your code ever sees "payload"
}

The most reliable fix is architectural: don't deserialize untrusted input with native Java serialization at all. Use a data-only format like JSON via Jackson, bound to explicit DTO classes with polymorphic typing disabled, so the deserializer can only ever populate known fields on known, inert classes — there's no mechanism for it to instantiate an arbitrary gadget class, because the target type is fixed by the code, not chosen by the input.

// Mitigated -- if native serialization can't be avoided, allow-list which classes may ever be built
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
    "com.jiquest.model.*;java.util.*;!*");  // allow known classes, reject everything else by default
in.setObjectInputFilter(filter);            // JEP 290 -- rejects disallowed classes before construction starts
What "filtering" actually means An ObjectInputFilter is a deny-by-default allow-list checked against the class being constructed, before any of its code runs — it's a safety net for cases where native serialization is unavoidable (legacy protocols, RMI), not a reason to treat native deserialization of untrusted data as generally safe. The default posture should still be: don't do it.

SSRF: when the server becomes the attacker's proxy

Server-Side Request Forgery happens whenever the application makes an outbound request to a destination that's fully or partially controlled by the caller — a "fetch this image," "validate this webhook URL," or "import from this link" feature. The danger is that the request originates from inside your network, with your server's credentials and network access, not the attacker's.

// Vulnerable -- fetches whatever URL the client supplies, no destination check at all
byte[] image = restTemplate.getForObject(request.getImageUrl(), byte[].class);
// an attacker-supplied URL of http://169.254.169.254/latest/meta-data/iam/security-credentials/
// can leak the instance's IAM role credentials on AWS
// Fixed -- resolve and validate the destination before the request is made, and re-check on redirect
URI target = URI.create(request.getImageUrl());
InetAddress resolved = InetAddress.getByName(target.getHost());
boolean privateRange = resolved.isLoopbackAddress() || resolved.isLinkLocalAddress() || resolved.isSiteLocalAddress();
if (privateRange || !allowedHosts.contains(target.getHost())) {
    throw new BadRequestException("destination not allowed");
}
byte[] image = restTemplate.getForObject(target, byte[].class);  // redirects disabled by client config

Two details separate a real fix from a false sense of safety. First, validating the hostname string isn't enough — you have to resolve it and check the actual IP, because a validated public hostname can still point at (or later be changed via DNS to point at) an internal address, a technique called DNS rebinding. Second, an HTTP client that automatically follows redirects can be handed a public URL that 302s to an internal one, silently defeating a check that only ran against the original address; redirects should either be disabled or re-validated at every hop.

More OWASP Top 10 categories worth knowing

These don't need a dedicated code example each to be interview-relevant — knowing the concrete failure mode in a Spring context is usually what's being tested.

A04: Insecure DesignA vulnerability that no amount of careful coding would have caught, because the design itself never accounted for the threat — this is exactly what threat modeling (below) exists to catch earlier.
A05: Security MisconfigurationSpring Boot Actuator's /env or /heapdump exposed publicly, verbose Whitelabel error pages left on in production, default credentials never rotated.
A06: Vulnerable and Outdated ComponentsA known CVE in a transitive dependency — covered in depth in the dependency scanning section below.
A07: Identification and Authentication FailuresNo lockout or rate limit on /login, session tokens with no expiry, password reset tokens that don't invalidate after use.
A09: Security Logging and Monitoring FailuresAuthentication failures and access-control denials that are never logged mean a slow, low-and-slow attack looks identical to normal traffic until it's too late to matter.
A08 (integrity): Unsigned artifactsA CI/CD pipeline that deploys whatever a build produces without verifying the artifact's signature or provenance is trusting the build system as much as the code itself.

OWASP category → defense mapping

No single control fixes every category — the point of walking through the Top 10 individually is that each one has a specific, different mechanism that actually closes it. This map is the condensed version of everything above, side by side.

Injection (SQL / NoSQL) Broken access control Cryptographic failures Insecure deserialization SSRF Vulnerable components Parameterized queries (JPA) Ownership check + deny by default BCrypt/Argon2 + KMS-managed keys Avoid native serialization / filter Resolve + allow-list destinations SBOM + automated CVE scanning each category maps to a mechanism, not a general "be more careful"

STRIDE threat modeling, applied to a real endpoint

Threat modeling is the practice of systematically asking "what could go wrong here" about a design before it's built, rather than discovering the answer later via a penetration test or, worse, an incident. STRIDE is Microsoft's mnemonic for six threat categories to check against every trust boundary in a data flow: Spoofing, Tampering, Repudiation, Information disclosure, Denial of service, and Elevation of privilege. It's not a checklist you fill in once — it's a lens you apply to each component and each hop in a request flow, asking the same six questions every time.

SpoofingCan an attacker successfully pretend to be someone or something they're not?
TamperingCan data be modified in transit or at rest without detection?
RepudiationCould a user or service convincingly deny having performed an action?
Information disclosureCan data be exposed to someone who shouldn't see it?
Denial of serviceCan availability be degraded or knocked out for legitimate users?
Elevation of privilegeCan a caller gain capabilities beyond what they were granted?

Applying it works best against a concrete example rather than the abstract system: take the internal transfer endpoint from a wallet service like the one in this site's payment mini project — POST /internal/transfers, called only by an upstream Payment Service, which debits one wallet and credits another as a single local transaction.

Client Payment Service Wallet Service Database 1. POST /transfers S: is the JWT forgeable? 2. POST /internal/transfers T: body modified without mTLS? 3. lock + debit/credit write R: is there an immutable audit row? 4. commit result I: does a failure leak internals? 5. 200 OK D: can retries flood the row lock? E: reachable directly, bypassing Payment Service?

Each threat above resolves to a concrete control: service-to-service authentication so the internal endpoint can't be called by anything except Payment Service (closes spoofing and elevation of privilege at once, since they're really the same trust boundary viewed two ways); request signing or mTLS so tampering in transit is detectable (tampering); an append-only ledger table alongside the mutable balance so every transfer has a permanent, attributable record (repudiation); a generic error response with internal detail only in server-side logs (information disclosure); and lock timeouts plus per-caller rate limiting on the transfer endpoint (denial of service). None of these controls are exotic — the value STRIDE adds isn't the fixes, which are mostly standard, it's making sure all six questions actually get asked against every hop instead of stopping once the obvious one (spoofing, usually) is handled.

When to do this Threat modeling belongs at design time, before code is written — ideally sketched against the same sequence diagram used to explain the feature to the team, since the diagram already shows every hop and trust boundary STRIDE needs to be applied to.

Secure SDLC: a pipeline, not a checklist

Bolting a single "security review" step before release catches what's left over after everything upstream already missed it, and catches it at the most expensive possible point — after the feature is otherwise done. A secure SDLC instead places a specific activity at the stage where it's cheapest to fix what it finds: design-time threat modeling on paper, static analysis at commit time, dependency and secrets scanning at build time, dynamic testing against a running staging environment, and monitoring once it's live.

Designthreat model (STRIDE) CodeSAST + secrets scan BuildSCA + SBOM generation TestDAST on staging Releasepen test, sign artifact Monitorlogging + alerting a finding in production feeds back into the next design-time threat model
StageActivityRepresentative tool
DesignThreat modeling against the data flow diagram before code exists.STRIDE, whiteboard or a tool like OWASP Threat Dragon.
CodeStatic analysis on every pull request; secrets scanning before commit.Semgrep, SpotBugs, SonarQube; gitleaks/truffleHog.
BuildSoftware composition analysis of every dependency; generate an SBOM.OWASP Dependency-Check, Snyk; CycloneDX/SPDX.
TestDynamic testing against a running staging deployment.OWASP ZAP, Burp Suite.
ReleaseManual or scheduled penetration testing before high-risk releases; signed artifacts.In-house or third-party pen test; artifact signing (Sigstore/cosign).
MonitorRuntime logging and alerting on authz denials and anomalous access patterns.Centralized logging, SIEM alert rules.
# .github/workflows/security.yml (excerpt) -- separate gates, each blocking at the stage it belongs to
jobs:
  sast:
    steps: [ run: semgrep --config=auto . ]          # every pull request
  secrets-scan:
    steps: [ run: gitleaks detect --source=. ]        # every pull request
  dependency-scan:
    steps: [ run: mvn org.owasp:dependency-check-maven:check ]   # every build, generates the SBOM too
  dast:
    needs: [deploy-staging]
    steps: [ run: zap-baseline.py -t https://staging.internal ]  # after deploy to staging, before prod
Why order matters Running DAST before the app is deployed anywhere is impossible by definition — it needs something running to attack. Running SAST only right before release, instead of on every pull request, means a developer gets feedback weeks after writing the vulnerable line instead of minutes after, when the context is still fresh and the fix is a one-line change instead of an unpicking exercise.

Secrets management: beyond environment variables

Hardcoding a database password in source control is an obvious mistake everyone avoids by now; moving it into an environment variable feels like the fix, and it's a real improvement, but it's not sufficient once a system has more than a handful of services.

# application.yml -- vulnerable: a secret checked into source control, visible to anyone with repo access
spring:
  datasource:
    password: Sup3rSecretProd123

An environment variable is still a single, static, long-lived value, and that's the actual problem, not where it happens to be stored. The same value gets copied into every container definition, every CI job that needs it, and it shows up in crash dumps and in CI logs the moment someone adds a debug step that prints the environment. Rotating it means finding and redeploying every service holding a copy, which in practice happens rarely, so a leaked value tends to stay valid for a long time. And because it's usually one shared value per environment rather than one per service instance, a single leak exposes everything that value can access, not just the one component that leaked it.

// Fixed -- a short-lived credential fetched at startup, never stored in source or a static env var
@Bean
DataSource dataSource(SecretsManagerClient secretsClient) {
    GetSecretValueResponse secret = secretsClient.getSecretValue(r -> r.secretId("prod/order-db"));
    DbCredential cred = objectMapper.readValue(secret.secretString(), DbCredential.class);
    return DataSourceBuilder.create().username(cred.username()).password(cred.password()).build();
}

A secrets manager like HashiCorp Vault or AWS Secrets Manager changes the shape of the problem rather than just relocating the value: credentials can be generated dynamically per consumer and expire automatically in minutes, so a leaked credential has a narrow window of usefulness instead of an indefinite one. Rotation becomes automatic instead of a manual, easy-to-forget task, and every access is logged, so "who fetched this secret and when" is an audit query instead of an unanswerable question. The cost is real complexity — every service now needs logic to fetch and refresh a credential instead of reading one static value — which is exactly why teams typically adopt this first for the highest-value secrets (database and payment-provider credentials) and expand from there rather than migrating everything on day one.

Dependency vulnerability scanning and SBOM

Your own code being free of the vulnerabilities above isn't the same as your application being secure — a typical Spring Boot service pulls in dozens of direct dependencies and hundreds of transitive ones nobody on the team explicitly chose or even necessarily knows are there.

Log4Shell is the reference example: a remote-code-execution vulnerability in Log4j, a logging library present transitively in an enormous fraction of Java applications worldwide, most of which had never directly listed it as a dependency — it arrived through some other library's own dependency tree. When a disclosure like that happens, the question every team has to answer immediately is "are we affected, and where," and without automated tracking that means someone manually running a dependency tree across every repository and hoping nothing is missed.

Software composition analysis (SCA) tools address the day-to-day version of this by scanning declared and transitive dependencies against a vulnerability database on every build, failing the build (or at least raising a flag) when a known-vulnerable version is pulled in. A Software Bill of Materials (SBOM) is the artifact that makes the emergency version of the same question fast: a machine-readable, standardized inventory (CycloneDX or SPDX format) of every component and exact version that actually shipped in a given build, generated automatically rather than reconstructed after the fact. When the next Log4Shell-shaped disclosure happens, "are we affected" becomes a search across existing SBOMs instead of a scramble.

OWASP Dependency-CheckSnykGitHub Dependabotmvn dependency:treeCycloneDX / SPDX

Input validation and output encoding: complementary, not interchangeable

These get conflated constantly, but they answer different questions at different points in a request's life, and skipping either one leaves a real gap the other can't cover.

public record CreateCustomerRequest(
    @NotBlank @Size(max = 120) String companyName,   // structural validation, right at the boundary
    @Email String contactEmail
) {}
<!-- Output encoding at the point of use -- context-specific, not applied at input time -->
<span th:text="${customer.companyName}"></span>  <!-- Thymeleaf auto-HTML-escapes th:text by default -->

// or explicitly, when a string is built by hand instead of through a templating engine:
String safe = Encode.forHtml(customer.getCompanyName());  // OWASP Java Encoder, HTML context

Input validation runs once, at the moment data enters the system: is this the right type, within the right length and range, in the right format. Its job is to reject malformed or wildly out-of-range data early and cheaply, before it reaches any business logic. Output encoding runs later and can run many times for the same piece of data, once for every different place it's written to: into an HTML page, into a SQL string (where parameterization is really output encoding for the query-syntax context), into a shell command, into a log line, into a JSON response. Each of those destinations has its own syntax and its own dangerous characters, so encoding has to be specific to the sink it's writing into, not generic.

The reason one can't substitute for the other is that legitimately valid input can still be dangerous output. A company name like O'Brien & Sons should pass validation without complaint — apostrophes and ampersands are normal in real names, and rejecting them just breaks the product for real customers. But if that exact string is written into an HTML page without encoding, the ampersand can start an unintended character reference and, combined with other unescaped input elsewhere on the page, contribute to a stored XSS vulnerability. Validation correctly let the value through because it was structurally fine; encoding is the separate step that neutralizes it for the specific place it ends up.

The rule of thumb Validate on input to reject garbage early and keep bad data out of your system entirely. Encode on output, at the specific sink, every single time data crosses into a new syntax — because the same stored value might legitimately need to be written into HTML in one place and a CSV export in another, and each needs its own encoding.

Key design decisions and interview talking points

These are the questions most likely to come up when an interviewer wants to know whether application security is something a candidate actually reasons about, or just a list of terms they can recite.

Why do parameterized queries (JPA/PreparedStatement) prevent SQL injection when string concatenation doesn't?

String concatenation lets attacker-supplied bytes become part of the SQL grammar itself — a value like ' OR '1'='1 changes the structure of the query, not just its data. A parameterized query sends the SQL text and the parameter values to the database as two separate channels; the driver binds the value as a literal of a fixed type, so no matter what characters it contains, it can never be reinterpreted as SQL syntax. This is why "just escape the quotes" is a weaker, error-prone substitute for parameterization — there is always another encoding you forgot.

What's an example of an endpoint that's authenticated but still vulnerable to IDOR, and how do you fix it?

GET /orders/{id} that checks isAuthenticated() but never checks that the order belongs to the caller lets any logged-in customer view any other customer's order just by incrementing the id. The fix is to scope the query itself to the caller (findByIdAndUserId(id, callerId)) or explicitly compare order.getUserId() to the caller's id, with an ADMIN bypass, before returning data — and to return 404 rather than 403 for someone else's resource so the endpoint doesn't even confirm the id exists.

Why is BCrypt or Argon2 preferred over SHA-256 for password hashing?

SHA-256 is a fast, general-purpose hash designed to verify integrity quickly, exactly the wrong property for password storage — a fast hash lets an attacker with a stolen hash dump try billions of guesses a second on commodity GPUs. BCrypt and Argon2 are deliberately slow and tunable (a cost factor you can raise as hardware gets faster), and Argon2 additionally makes large amounts of memory a required part of computing the hash, which specifically blunts GPU and ASIC cracking rigs that are memory-constrained relative to raw compute.

Why is deserializing untrusted data with native Java serialization dangerous, and what's the safer alternative?

ObjectInputStream.readObject() doesn't just reconstruct data, it reconstructs objects, running constructors and readObject/readResolve methods as it goes; an attacker who controls the byte stream can chain together gadget classes already present on the classpath into a sequence that executes arbitrary code, without any bug in your own code at all. The safer approach for untrusted input is to not use native Java serialization at all — use JSON via Jackson with explicit DTOs and polymorphic typing disabled. If Java serialization is unavoidable, an ObjectInputFilter (JEP 290) can allow-list the exact classes permitted to deserialize, rejecting everything else before construction even starts.

How would you prevent SSRF in a "fetch image from URL" feature?

Validate more than syntax — resolve the hostname and reject anything that resolves to a private, loopback, or link-local range (including the cloud metadata endpoint attackers specifically target for IAM credentials), and re-check after any redirect rather than validating only the original URL. Pair that with an explicit allow-list of permitted destination domains where the use case allows it, and route the outbound fetch through an egress proxy that enforces the same rules server-side as a second layer, since application-level checks alone are one missed code path away from being bypassed.

What's the difference between authentication failures and broken access control in OWASP terms?

Authentication failures are about proving who you are going wrong — weak password policies, session tokens that don't expire, credential stuffing with no rate limiting. Broken access control assumes authentication succeeded correctly and asks a different question: now that we know who you are, should you be allowed to do this specific thing to this specific resource. An endpoint can have flawless authentication and still be completely broken on access control — the system correctly knows you're customer 42, it just never checks whether order 99 belongs to customer 42.

Why isn't storing secrets in environment variables sufficient at scale?

Environment variables are a real improvement over hardcoding a secret in source, but they're still a static, long-lived value copied everywhere the process runs — into container definitions, CI logs that print env for debugging, crash dumps, and any process that can read another process's environment on the same host. Rotating one means redeploying every service that holds a copy, which in practice means teams rotate rarely, so a leaked value stays valid for months. At scale the real requirements are automatic rotation, per-service scoping, and an audit trail of exactly which service fetched which secret and when — none of which a plain env var gives you.

How does a secrets manager with short-lived credentials reduce blast radius compared to static API keys?

A static database password that leaks is valid until someone notices and manually rotates it, potentially indefinitely. A secrets manager like Vault or AWS Secrets Manager can instead issue a credential that's dynamically generated per request and expires in minutes, so even a successfully exfiltrated credential is only useful for a narrow window, and each service instance gets its own credential rather than everyone sharing one, so a compromised instance only exposes its own access, not the whole fleet's. The trade-off is added complexity, which is why this is usually adopted for the highest-value secrets first — database and payment-provider credentials — rather than everything at once.

Why do you need an SBOM if your own code has no vulnerabilities?

A typical Spring Boot service pulls in dozens of direct dependencies and hundreds of transitive ones you never explicitly chose — Log4Shell was exactly this shape of problem, a vulnerability three or four levels deep in a logging library nobody on the team had directly added. Without a software bill of materials, answering "are we affected?" during a live disclosure means someone manually running dependency trees across every service and hoping they didn't miss one. An SBOM is a queryable inventory of every component and version actually shipped, so the same question becomes a search instead of an afternoon of archaeology across every repo.

What's the difference between SAST and DAST, and where does each belong in the pipeline?

SAST (static analysis) reads source code without running it, catching patterns like string-concatenated SQL or a missing authorization check before the code is ever deployed — it belongs in CI, on every pull request, because it's fast and gives a developer feedback in minutes. DAST (dynamic analysis) attacks a running instance of the application from the outside, the way a real attacker would, catching issues SAST structurally can't see — misconfigured headers, an endpoint reachable when it shouldn't be, actual exploitability rather than a suspicious pattern. DAST needs a deployed environment and takes longer, so it runs later, typically against staging before release, not on every commit.

Walk through applying STRIDE to the wallet service's transfer endpoint — what threats do you find?

Spoofing: can the caller's identity on the internal POST /internal/transfers call be forged, given it's meant to be called only by Payment Service? Tampering: is the request body protected from modification in transit, or does it rely only on network trust? Repudiation: if a transfer is disputed later, is there an immutable audit trail of who initiated it, separate from the mutable balance itself? Information disclosure: does a failed transfer leak internal details in its error response? Denial of service: can the endpoint be flooded with transfer attempts against the same wallet to hold its row lock and starve legitimate transfers? Elevation of privilege: if the internal endpoint were reachable directly, could a caller move money between wallets it doesn't own? Each answer points at a specific control — service-to-service auth, request signing, an append-only ledger, generic error messages, lock timeouts, and network-level restriction — which is the actual output STRIDE is for.

Why are input validation and output encoding both necessary — isn't one enough?

They defend against different things at different points. Input validation checks that data has the shape you expect the moment it enters the system and rejects what doesn't. Output encoding is applied later, at the specific point data is written into a specific destination, transforming characters that are dangerous in that destination's syntax. A perfectly valid input can still be dangerous output: a company name like O'Brien & Sons is legitimate and should pass validation, but if it's written into an HTML page without encoding, the ampersand and apostrophe can still break the page's structure. Validation narrows what data is allowed to exist; encoding neutralizes it for wherever it's about to be written.

Why should authorization checks live in the service layer instead of only in the controller?

A controller-level authorization annotation is easy to forget on the next endpoint a different developer adds six months later, and easy to accidentally bypass if the same service method later gets called from a second, internal code path that skips the controller entirely — a scheduled job, an event listener, a new admin tool. Putting the ownership and role check inside the service method itself means every caller, present and future, goes through the same gate, instead of the gate being a per-endpoint decision someone has to remember to reapply.

What's a real-world example of insecure deserialization causing a major breach?

The Apache Commons Collections gadget chain, disclosed in 2015, is the canonical one — a library present on the classpath of huge numbers of Java applications could be chained through readObject calls to achieve remote code execution anywhere an application deserialized untrusted bytes with native Java serialization, even though Commons Collections itself has nothing to do with security and nobody had written an exploit-specific bug — the vulnerability was in the combination, not any one library. It's the reason "don't deserialize untrusted data with ObjectInputStream" became a blanket rule rather than a case-by-case judgment call.

How do you prevent NoSQL injection in a MongoDB-backed Spring Data application?

The classic NoSQL injection isn't a syntax-escaping problem the way SQL injection is — it's an operator injection problem, where a client sends a JSON body with an operator like $gt for a field expected to be a plain string, and if that value is deserialized straight into a query without a strict DTO, it's interpreted as a query operator instead of a literal, potentially matching every document. The fix is the same instinct as parameterized SQL applied to a different mechanism: bind incoming request fields to a typed DTO with Bean Validation constraints first, never pass a raw, attacker-controlled JSON map straight into a query, so a field that should be a String can never be reinterpreted as a query operator.

Why doesn't TLS termination at a load balancer eliminate the need for HTTPS between internal services?

Terminating TLS at the edge secures the leg the public internet can see, but traffic between the load balancer and internal services, and between internal services themselves, often then travels in plaintext inside the "trusted" network — which is only trusted until it isn't, whether through a misconfiguration, a compromised container, or an insider with network access. Encrypting internal traffic too, via a service mesh or TLS on internal listeners, is the same defense-in-depth argument as verifying a JWT in every service instead of only at the gateway.

What's the risk of returning a raw exception stack trace to the client on error?

A stack trace reveals internal structure an attacker would otherwise have to guess at for free — class and package names, the ORM and its version, sometimes a fragment of a SQL query or a file path, all of which narrows down exactly which known vulnerabilities to try next. The fix is a global exception handler (@ControllerAdvice) that logs the full detail server-side for debugging and returns a generic, safe message and a correlation id to the client, so a developer can still find the real error in the logs without ever putting it on the wire.

How would you decide whether to patch a vulnerable dependency yourself versus wait for an upstream fix?

Start from actual exploitability in your context, not just the CVE's severity score — a critical-rated bug in a class your application never instantiates is lower real risk than a medium-rated one in a code path that handles unauthenticated user input directly. If a fixed version already exists, bumping it is almost always faster and safer than a local patch, since you inherit the maintainer's fix and their future updates. If no fix exists yet and the risk is live, a temporary compensating control — disabling the vulnerable feature, filtering the specific input pattern at a gateway layer — buys time without forking a dependency you'd then be responsible for maintaining yourself.

What's the single most common mistake in how developers implement access control checks?

Checking that a user is authenticated and stopping there, treating "logged in" as equivalent to "authorized for this specific resource" — which is exactly the IDOR pattern, and it's common because authentication is the part every framework sets up for you by default, while resource-level ownership checks have to be written deliberately, every time, for every endpoint that takes an id. The mistake compounds when authorization logic is duplicated across many controller methods instead of centralized, because then fixing it means finding and fixing every instance instead of one shared gate.

Related guides

Update these hrefs to your published Blogger post URLs once each page is live.

No comments
Leave a Comment