Research / Attack paths

Two moderate assumptions, one serious path

The highest-impact findings are rarely a single dramatic bug. They are two or three reasonable local decisions that meet at a seam nobody owned.

Kahu Labs ResearchJuly 9, 2026Attack paths9 min read
The short version

Ask which single bug caused a serious compromise and you often get the wrong picture. In our work the most damaging results are compositions: a piece of information that was only mildly sensitive, plus an endpoint that was only slightly too trusting, plus a control that was reasonable in isolation. Each was defensible on its own. Together they formed a path. Severity was an emergent property of the system, not a property of any one finding.

The chain below is synthetic, assembled to illustrate the shape rather than to describe any one assessment. The shape is what we keep seeing.

The system assumption

Organizations reason about risk one finding at a time. A scanner reports issues individually; a triage queue scores them individually; a sprint fixes them individually. Each is rated on its own merit, and a "low" or a "medium" is filed as something to get to later. The assumption is that risk is additive and local: fix the criticals, manage the rest. But attackers do not experience findings one at a time. They experience a system, and they compose whatever it offers. The interesting question is never "how bad is this bug" but "what does this bug make reachable when combined with the next one."

Where the assumption breaks

It breaks at the seams between components, which are exactly the places no single team owns. One team decides that an identifier is not sensitive, because on its own it is not. Another team decides an endpoint can accept that identifier, because who would have it. A third leaves a default in place because changing it seemed unnecessary. Every decision is locally reasonable and reviewed by people looking at their own component. The composition is reviewed by no one, because it is not anyone's component. That gap is where the path forms.

Reconstructing it

A synthetic composite, three modest issues chained into account takeover. Reserved hosts and placeholder values throughout.

  Issue A (low):    an internal identifier is exposed in a public
                    response. On its own: "just an id."

  Issue B (medium): an endpoint accepts that identifier and returns
                    a record without checking the caller owns it.
                    On its own: "needs a valid id to exploit."

  Issue C (low):    a state-changing action trusts an id in the
                    request body rather than the session.
                    On its own: "you'd need to know the target."

  Compose:
     A gives the attacker a valid target id
        -> B turns that id into the target's data (incl. more ids)
           -> C uses those ids to act as the target  ==> takeover

  three findings rated low/medium/low  ->  one critical path
No single step is dramatic. The exposure feeds the authorization gap, which feeds the state-changing trust. The severity is in the arrows.

Read the steps in isolation and each triage note is defensible. "It is only an id." "You would need a valid id." "You would need to know the target." The chain answers every one of those objections with the previous step. Issue A supplies the id that made B exploitable; B supplies the ids that made C exploitable. The objections were true individually and irrelevant in sequence.

Why the obvious defenses didn't solve it

Severity-based triage. Fixing criticals first is sensible until a set of non-criticals composes into one. A queue that sorts by individual severity will rank all three of these below the "real" criticals and leave the path open indefinitely. The scoring was accurate per finding and wrong about the system.

Scanners. Automated tools find components, not compositions. Each of the three issues might be reported, each correctly rated low or medium, with no signal that together they are a takeover. The tool has no model of "and then." Chaining is where automated coverage ends and a tester who holds state begins.

Component-scoped review. Reviewing each service in isolation confirms each is locally fine. The seam between them is out of scope for every review that is scoped to a component. Nobody signed off on the composition because it did not belong to anyone.

Root cause

The root cause is that risk was assessed locally while attacks are composed globally. A finding's real severity depends on what else is true about the system, and that context is invisible from inside any one component or any one severity score. The corrective model is to reason about paths, not just findings: to ask, for each modest issue, what it enables when combined with the others, and to treat a chain that reaches a serious outcome as itself a serious finding, regardless of how its individual links are rated. Defense in depth is the same idea run in reverse. It works precisely because it forces the attacker to chain, and it fails silently when the links are all slightly weaker than assumed.

How to test for this class of failure

  • Do not stop at cataloguing findings. After enumerating issues, ask of each: what does this give me, and what would it unlock if combined with any other issue here.
  • Follow the data. An exposed identifier is a lead: find every endpoint that accepts it. A verbose error is a lead: find what it reveals about the next step. Chains are built by treating each finding's output as the next finding's input.
  • Hold state across the whole assessment. The links are usually in different components tested at different times; only a tester carrying context across them will see the arrow between two notes written an hour apart.
  • Re-rate by reachable outcome. When a set of low and medium issues composes to a serious result, report the path at the severity of the outcome, not the average of the links.

How to design the control correctly

  • Close links independently and deliberately. You do not need to fix every issue to break a chain; breaking any one link stops the path. But do not rely on a chain being "too complex to exploit," because complexity is not a control.
  • Give the seams an owner. Trust boundaries between services need explicit review. Decide, on purpose, what each service assumes about its callers and its inputs, and check that the service on the other side actually guarantees it.
  • Assume every identifier leaks and every internal endpoint is reachable. Design each component to be safe even when the modest assumptions of its neighbors fail, so a single leaked id or a single exposed endpoint does not complete a path.
  • Fund defense in depth honestly. Layered controls only help if each layer is real. Treat "the other layer will catch it" as a claim to verify, since chains are built exactly out of layers that each assumed another was holding.

What we took away from it

The trophy framing of security, the single critical bug, undersells how real compromises usually happen and oversells how they are found. Most serious paths we walk are built from findings that a reasonable person rated as minor, connected across seams that a reasonable process left unowned. That is why we test with state and identity and follow the data from one finding into the next, and why we report the path, not just its parts. The lesson for builders is the same one in reverse: your system's security is not the list of your worst bugs. It is the shortest path an attacker can compose from all of them.

References

  1. OWASP Web Security Testing Guide
  2. MITRE ATT&CK: Enterprise tactics and techniques
  3. CWE-1173: Improper Use of Validation Framework (composition)
  4. NIST SP 800-53 Rev. 5: Security and Privacy Controls

Internal evidence: Kahu Labs Research, anonymized authorized assessments (2025). The chain below is a synthetic composite, not a reconstruction of any single engagement. Client, target and identifying details are withheld; see our research policy.