skip to content

Two services run the same source code and the same call that rebuilds application objects from attacker-influenced serialized bytes, yet a security team rates one critical and the other low. Describe what an attacker actually achieves at such a decode point, and explain why the set of types the decoding process can construct — not the code — is what separates the two ratings.

level: seniorimportance: should knowfreq 38%

answer

  1. capability surface = type graph the process can construct
  2. exhaustion band needs no gadget, survives allow-lists
  3. decoded object is still untrusted input
  4. stored now, decoded later by a bigger principal
  5. adding a dependency arms a dormant sink

basics

~20 s

Outcomes split into exhaustion, state tampering, disclosure, code execution, and second-order pivot. Identical code differs in risk because of the capability surface: which types that process can construct. Adding a dependency can arm a sink that was inert for years.

solid answer

~60 s

**Impact bands** (they are separate, not degrees of one thing): - **Resource exhaustion** — deep nesting, huge declared collection sizes, decompression bombs, colliding keys that degrade hash containers. Needs no interesting type at all, so it survives a perfect type allow-list. - **State tampering** — a decoded object carrying a role, tenant id, expiry or price is trusted downstream because "it's our own type". No execution involved; the most commonly missed band. - **Disclosure** — decode errors and accept/reject differences turn the decoder into an oracle for internal types and versions. - **Code execution** — reconstruction-time behaviour of types already present is composed into a chain. - **Second-order pivot** — the blob is stored in a cache, queue, column or client-held cookie and decoded later by a more privileged consumer. Rank sites with the standard reachability × privilege × data-sensitivity rubric, then multiply by the axis unique to this class: the **capability surface** — the type graph loadable in that process. It is why identical code differs between services, why a dependency upgrade arms a dormant sink, and why "we tested it, no chain worked" is a statement about today's classpath.

go deeper

for a junior

Recall that the outcomes are broader than code execution and that a decoded object is still untrusted input — its fields must not be trusted for authorisation.

for a middle

Separate the bands and know which defence covers which: budgets for exhaustion, no-trust-in-decoded-fields for tampering, surface reduction for execution and disclosure.

for a senior

Lead with the capability surface: explain why the same code carries different risk per deployment, why a dependency change alters exploitability, and why you inventory decode sites and second-order storage rather than endpoints.

for a principal

Frame it as an assurance problem — exploitability drifts with the dependency graph, so point-in-time testing does not hold. Argue for deleting the requirement (opaque identifiers, data-only formats) and for isolating decoding into a minimal process, and be explicit about which bands each move does and does not close.

## Two questions in one The first is a taxonomy question: given that untrusted bytes reach a decoder able to instantiate application types, what does the attacker get? The second is the discriminating one: why does that same defect carry wildly different risk in two deployments of the same code? The answer to the second is a factor that exists for this bug class and almost no other — the **capability surface**. ## The five impact bands They are genuinely different failure modes with different defences, so listing them in order matters less than keeping them separate. **1. Resource exhaustion.** A payload can declare an enormous collection before supplying any elements, nest structures a thousand levels deep, describe a heavily shared or cyclic graph that expands on reconstruction, expand from a tiny compressed blob, or fill a hash-based container with keys chosen to collide so lookups degrade toward quadratic. None of this requires constructing an interesting type. That is the load-bearing point: this band is untouched by type filtering, so budgets — encoded size, decompressed size and ratio, nesting depth, element count, declared-size caps, a decode timeout, and a cap on concurrent decodes — are an independent, mandatory control rather than defence in depth. **2. State tampering.** If a decoded object carries a role flag, tenant identifier, user id, expiry timestamp or price, and downstream code reads authorisation or business decisions out of those fields, the attacker just edits them. Nothing executes. Teams under-rate this band because it looks mundane next to code execution, but it is the one most often actually exploited. The invariant: **a decoded object is still untrusted input**. Authority must be re-derived from server-side state keyed by an identifier, never read from the payload. **3. Disclosure.** Verbose decode errors, type-confusion behaviour, and timing or message differences between accepted and rejected types make the decoder an oracle for which types and versions are present. That is reconnaissance that feeds band 4. **4. Code execution.** The attacker composes behaviour that already exists in the process and runs while the graph is rebuilt or shortly after. They ship no code of their own, which is why "we never load user code" is not a defence. This band depends entirely on the capability surface. **5. Second-order pivot.** The most dangerous shape in practice: a low-privilege path accepts and stores the blob — cache entry, queue message, database column, client-held cookie or saved-state token — and something else decodes it later: a batch job, a cache warmer, an admin console, a different service. **The trust boundary is not where the bytes entered; it is where they are decoded**, and the decoding principal is frequently far larger than the accepting one. When the blob is held by the client, the storage is on the attacker's own machine and mutation is free. ## The capability surface Define it precisely: the set of types the decoding process can resolve and construct, together with whatever behaviour those types run during reconstruction. It is a property of the deployed process — its dependency graph, its plugin set, its runtime configuration — not of the decode statement. Three consequences follow, and they are what the interviewer is listening for. **Identical source, different risk.** Service A is a thin worker with a handful of libraries and no reconstruction-time behaviour worth chaining; service B carries a large framework, an object-relational layer, a scripting engine and a template library. The same decode call is a low finding in A and critical in B. Risk assessment therefore has to be per deployment, not per code pattern. **Exploitability is not monotonic and not stable.** Adding or upgrading a library can arm a sink that has been inert for years, with no change to the code that decodes. Removing one can disarm it. This makes "we tested known payloads and none worked" a statement about today's dependency graph and today's public research, not a property of the system. It is also why the class resists point-in-time assurance and needs a structural answer. **The surface is an engineering lever.** It can be shrunk deliberately: perform decoding in a minimal, isolated process; refuse dynamic type resolution so the encoding cannot name a type; keep the decoder's dependency set separate from the application's; and prefer a format that carries data rather than type identity. Shrinking the surface reduces bands 3 and 4 directly — though not band 1, and not band 2, which is defeated only by refusing to trust decoded fields. ## Ranking across a codebase Use the ordinary prioritisation rubric — reachability by an untrusted principal, privilege and blast radius of the executing process, sensitivity of reachable data — and then multiply by capability surface, which is the term the generic rubric does not have. Inventory **decode sites, not endpoints**, and for each record the producer's trust level, any storage sitting between producer and decoder, the privilege of the decoding process, and the surface loaded there. Two practical rules fall out: chase the second-order paths, because the dangerous decode is usually downstream of the endpoint that accepted the bytes; and apply budgets everywhere at once, because they are cheap and they cover the one band that shrinking the surface cannot.

  • Which impact band survives a strict type allow-list, and what control covers it instead?
    Resource exhaustion. An allow-list constrains which types may be constructed, but depth, declared collection sizes, compression ratio and hash collisions need no interesting type at all. The control is independent budgets — maximum encoded and decompressed size plus ratio, nesting depth, element and declared-size caps, and a decode timeout — with the decode isolated so a budget breach fails one request rather than the process, and concurrent decodes capped so a few expensive payloads cannot occupy every worker.
  • A pentest report says the decode point is not exploitable. How much assurance does that give you?
    Very little over time. Non-exploitability is a fact about the current capability surface and the currently published chains, both of which move. A dependency added or upgraded next quarter can make the identical code exploitable with no diff at the decode site. Treat the result as a snapshot, and prefer structural changes — refusing untrusted decoding, or refusing type identity in the encoding — over relying on the absence of a chain.
  • Where in a system would you look first for the highest-impact instance of this class?
    Not at the endpoint that accepts the bytes, but at the most privileged process that later decodes something a low-privilege actor could have written: a cache warmer, a batch job, an admin tool or a queue consumer. That inverts the usual attacker/defender view — the accepting path may be well reviewed while the consuming path assumes the data is internal and trusted.

The decode call is a keyhole; the capability surface is the ring of keys hanging inside the room. The same keyhole is harmless next to an empty hook and catastrophic next to a full ring — and someone adds a key every time you take a dependency.

saying these in an interview costs you the question

  • Treating the class as remote-code-execution-only and dismissing tampering findings as informational.
  • Assessing risk at the endpoint that received the bytes instead of the process that decodes them.
  • Claiming safety because a test found no working chain — that is a statement about today's dependency graph.
  • Trusting a role, tenant id or price on a decoded object because it is "our own type".
  • Assuming a strict type allow-list makes the decode point safe, leaving the exhaustion band wide open.

context