skip to content

Why can a front end refuse an attacker's expensive-to-parse request but not an expensive-to-answer one, and what does that check cost?

level: middleimportance: nice to knowfreq 30%

answer

  1. the cost is in the bytes, or in the data
  2. shape can be capped, meaning cannot
  3. abort while decoding, not after
  4. small and well formed is the dangerous one
  5. every limit grows an exception list

basics

~20 s

Parse cost is a property of the request's own bytes - size, nesting depth, expansion ratio - so a front end can cap it by shape. Answer cost lives in data the front end has never seen.

solid answer

~50 s

Two attacks look identical on the wire and are not the same problem. An expensive-to-parse request carries its cost in its own bytes: a deeply nested document, a body that decompresses enormously, thousands of repeated fields, a pattern that makes the parser backtrack. All of that is a property of the input, so a front end can cap it - maximum body size, maximum nesting depth, maximum element count, a decompressed-size limit with a hard abort - and refuse before the service is involved. An expensive-to-answer request is small and well formed; its cost lives in the data it will touch, and only the datastore and its statistics know that. The check is not free: enforcing shape limits means buffering and inspecting enough of every legitimate request to measure it, and every limit becomes an exception list the day a real customer needs a bigger export.

go deeper

for a junior

Be able to name the two kinds of expense - costly to parse and costly to answer - and give one example of each rather than treating all heavy requests as one thing.

for a middle

Explain why shape limits work on one and not the other, and why the limit must abort during decoding rather than after the parse has completed.

for a senior

Show how you bound what can be asked for in the API contract, and be honest that the resulting exception list is a standing review burden somebody must carry.

for a principal

Be ready to defend narrowing a public contract against the customers it inconveniences, and to say who funds and reviews the override list over time.

## Two costs that arrive in the same envelope When people say a request is expensive they are usually collapsing two different things, and the distinction decides where the control can live. **Cost of parse** is incurred while turning bytes into a structure. Its causes are visible in the bytes themselves: a body that expands hugely when decompressed, a document nested thousands of levels deep, a structure with an enormous number of elements, an input crafted so the parser backtracks catastrophically. The defining property is that the *input alone* determines the cost. Nothing about your data, your tenants or your indexes changes it. **Cost of answer** is incurred after the request is understood, while producing the response. Its causes live in your data: a date range that covers the whole table, a filter with no usable index, an export of every row a tenant owns, an aggregation over a shared analytics store. The request that triggers it is short, well formed and indistinguishable in shape from a cheap one. Two requests differing by a single parameter can differ by four orders of magnitude in cost. ## Why one is stoppable in front and the other is not A front end can enforce anything that is a function of the request. Shape limits are exactly that, which is why they are cheap, durable and worth having: | Limit | Bounds | | --- | --- | | Maximum body size | Memory and parse time per request | | Maximum decompressed size, aborted early | Compression-expansion attacks | | Maximum nesting depth | Recursive-descent blowups | | Maximum element or field count | Structure-size blowups | | Timeout on the parse itself | Backtracking pathologies | None of these needs to know anything about your database, so they can be enforced by a component that has never seen it. Crucially, they are enforced *while* decoding, aborting early rather than parsing to completion and then judging - a limit checked after the parse has finished has already paid the cost it was meant to prevent. Answer cost has no such handle. The property that makes the request expensive - the cardinality behind that filter, the size of that tenant's table, whether an index covers that predicate - is not in the request and is not in the front end. You can only learn it by consulting the resource you are trying to protect, or by pre-declaring what a class of request is allowed to touch. ## The control that does work on answer cost, and its bill Since the cost cannot be measured from the request, the durable move is to bound what can be *asked for* at all: a maximum date range, a mandatory tenant filter, a maximum result-set size, a fixed set of report shapes rather than an open query language, exports pushed onto an asynchronous path with bounded concurrency instead of being served inline. These are refusals stated in the API contract rather than judgments made per request. That is where the price shows up, and an interviewer is listening for it: - **Latency and complexity on every ordinary request**, because the bounding check runs on all of them, not just the hostile ones. - **An exception list.** The first legitimate customer whose report exceeds the cap files a ticket. Either you raise the limit for everyone, undoing the control, or you build per-caller overrides that somebody must review, approve and revisit. That recurring review burden, not the CPU, is the real recurring bill - and an unattended exception list drifts back toward no limit at all. - **A contract change**, because narrowing what can be asked is a breaking change for someone. ## The adversary's view of the same picture An attacker probing a public API is doing exactly this classification, from the other side. He is looking for the input where the ask is small and the answer is enormous, and he will find it faster than your team will, because he only needs one and you must defend all of them. Once he has it he does not need volume: a few hundred a minute is enough, they are well formed, and they will pass every shape limit you set because there is nothing wrong with their shape. ## Getting the direction of the claim right A request being well formed proves it parsed successfully, not that it is safe to answer. A request passing every input-validation rule proves its shape is acceptable, not that its cost is. Input validation and cost control are different controls answering different questions, and treating the first as though it covers the second is the single most common wrong answer here - it is what leaves a team confident that a hardened front end protects a service it cannot possibly price.

  • Give a concrete parse-cost example and the limit that bounds it.
    A compressed body that expands from a few kilobytes to gigabytes, or a structure nested deeply enough to blow the parser's stack. Both are bounded by input-shape limits enforced during decoding: a cap on decompressed size with a hard abort, a maximum nesting depth, a maximum element count. The limits are crude, which is exactly what makes them cheap and durable.
  • Your team wants a maximum export size. What does that limit actually cost you?
    It costs an exception path. The first legitimate customer whose report exceeds the cap files a ticket, and either you raise the limit for everyone or you build per-caller overrides someone must review and re-approve. That review burden is the recurring bill, and an unattended exception list drifts back toward having no limit at all.
  • Does requiring authentication change which of the two costs you can control?
    It changes attribution, not cost. Authentication tells you whom to bill, suspend or call afterwards and gives you an appeals path; it does not make an expensive query cheaper, and a hostile or compromised legitimate account sends exactly the same request. It narrows who can ask, which is why an unauthenticated surface is where this bites hardest.

saying these in an interview costs you the question

  • Treats every cheap-to-send request as stoppable at the edge
  • Claims input validation solves expensive queries
  • Assumes small and well formed means harmless
  • Checks the size limit only after parsing completes
  • Sets a shape limit and plans no exception path

context