There's a particularly subtle kind of vulnerability in software that we, as practitioners, very rarely address. I have taken to calling those interstitial vulnerabilities, or interstitial risk. This essay sheds some light on those subtle vulnerability scenarios, and is my attempt at coining the terminology around them.
What I mean by "interstitial," and why clean scans miss it
My paraphrased definition of interstice here goes as follows: the space between things. The space between software components. The space between pieces of software we've scanned and fixed through traditional (SAST/DAST) and less traditional but growing (LLM) methods. I'll go through a couple examples below, but before I do that, let's go through the obvious question: why should you care?
As security leaders, and as the people who choose and stand behind commercial or open source security tooling, we almost all have this blind spot. We scan repositories. We scan containers. We scan servers. We scan networks. We scan cloud surfaces. And what we fail to scan and look for most of the time is their connecting tissue. A clean SAST/DAST report doesn't just fail to catch this class of issues, it actively produces a false sense of security. Two green scans and a dashboard full of checkmarks is precisely the conditions under which no one thinks to look for a seam. The tooling isn't neutral here, it's reassuring in exactly the wrong direction.
It's not just that the scanners miss this. It's that even a person looking directly at the system afterward sees two components behaving exactly as designed. There's no error, no crash, no obviously malformed output to pull on. The vulnerability wears the same clothes as correct behavior, which is what makes it insidious rather than merely hard to find.
What this looks like in practice
Example 1: The rewrite that passed every scan
Picture a fairly ordinary three-tier setup: a core service that owns product logic, a utility service that does a specific job on its behalf and talks to a database, and the database itself. Call them the orchestrator, the worker, and the database. The orchestrator asks the worker to read or write something in the database, and hands back whatever comes out. It's about as unglamorous an architecture pattern as exists in this industry, which is exactly why nobody thinks twice about it.
Of these three pieces, two are the ones anyone actually points a scanner at: the orchestrator and the worker. SAST passes on the orchestrator's repo. SAST passes on the worker's repo. DAST scans the worker's live container and finds nothing. Nobody's cutting corners, nobody's ignoring a report. The dashboards are green.
Then the worker gets rewritten. It was originally written in Python, which is fine for a lot of things, but under load, it wasn't fast enough. Someone rewrites it in Rust to fix that, and it works: throughput goes up, latency goes down, everyone's happy. The API the orchestrator talks to doesn't change on paper. Same endpoint, same method name, same rough shape of request and response. As far as anyone tracking "did the interface change" is concerned, the answer is no.
But the new Rust implementation doesn't handle types and bounds the same way the old Python one implicitly did. Python's dynamic typing and permissive runtime behavior papered over a category of input the new, strongly-typed implementation treats very differently. Nobody wrote that difference down anywhere, because nobody thought of it as a difference. It was merely "the same API, but faster."
At some point, a test, it doesn't really matter if it's an adversarial one or just a QA engineer poking around, sends a negative integer into a field that was always, implicitly, expected to hold a string. In the old worker, that input would have failed loudly or been coerced away harmlessly. In the new one, it underflows an array index. The worker doesn't crash. It returns data. Just not the data it was supposed to return. The orchestrator gets back information it was never scoped to see, and because the call succeeded and returned a well-formed response, nothing downstream notices anything went wrong.
Nobody lied. Nobody skipped a scan. Nobody was negligent. The orchestrator's code was correct for what it was asked to do. The worker's new code was correct, and faster, for what it was asked to do. The vulnerability didn't live in either of them. It lived in the gap between what one side assumed and the other side actually did, a gap neither team had any tooling pointed at, because neither team owned it.
Before: two scans, two green checkmarks, one database that isn't in scope.
After: same scans, same green checkmarks, one silent type mismatch.
Example 2: The auth check that once was
This one starts where a lot of systems start: as a monolith. Somewhere inside it, one code path checks who's allowed to do what, once, at the edge, before anything downstream runs. Everything past that check is trusted by construction, because there's no way to reach it except through the check. That's not a design decision anyone deliberated over. It's just what "one process" gets you for free.
Eventually the monolith gets split up, for the usual reasons: one part needs to scale differently than the rest, or a different team wants to own it, or someone's tired of a thirty-minute build. A system of records gets peeled off behind a gateway. The gateway still does the auth check. The system of records, now its own deployable thing with its own repo, doesn't check anything, because it never had to. Every request it's ever seen has already been authorized by the time it arrives. That's not a bug. It's an accurate description of reality, at the moment the split happens.
Both services pass their scans without complaint. The gateway's auth logic is intact and correctly tested. The system of records has no auth logic to test, so there's nothing for a scanner to flag, and nothing for a threat model to catch either, unless someone explicitly asks "what happens if this service is reachable from somewhere other than the gateway," which requires already suspecting the thing you'd need the exercise to surface in the first place.
Nine months later, someone on a different team needs to pull records for an internal reporting job. Going through the gateway adds latency and a dependency they don't want, and the system of records' API is right there, so they call it directly. It works. Of course it works, the system of records was never checking anything, it was only ever trusting that whoever was calling it had already been checked by something else. That assumption was true for exactly as long as the gateway was the only thing that could reach it, and nobody wrote that assumption down anywhere, because at the time it was written, it wasn't an assumption. It was just how the system was shaped.
Nobody lied. Nobody skipped a scan. (Yes, again!) The person who split the monolith made a reasonable call. The person who added the reporting job made a reasonable call too, they had no way to know the system of records' silence on auth was load-bearing rather than incidental. The vulnerability isn't in either decision. It's in the gap between what used to be structurally guaranteed and what was actually being verified, a gap that opened the moment the monolith split and stayed invisible until something new happened to walk through it.
At the split: gateway checks, system of records trusts, because nothing else can reach it yet.
Nine months later: same checks, same green checkmarks, one new path nobody re-verified.
These two examples are different mechanisms entirely, one's about types and bounds surviving a language rewrite, the other's about a trust guarantee that quietly stopped being structural, but they share the same shape. Something that was true by construction, or true by convention, stops being true, and nothing was watching that particular fact because nothing was ever assigned to watch it. That shape shows up again and again. Here's a handful of quick, high level examples so this reads as a pattern, and not merely two cherry picked stories I happen to have:
- Timeout/retry drift: Service X retries on 5xx assuming Service Y's operations are idempotent. Service Y's rewrite wraps a previously-safe read-then-write into a single non-idempotent transaction. Service X's unchanged retry logic double-executes under transient failure, only visible under real network jitter.
- Serialization/precision mismatch: Service X sends floats as JSON numbers, and the original Service Y is permissive. Service Y's reimplementation uses fixed-precision decimals for correctness. Values near a precision boundary round differently than Service X assumes, visible only statistically across many transactions.
- Error-code remapping: Service X treats a specific code from Service Y as "not found, safe to ignore." Service Y's rewrite reuses that code for a different failure class, like a permission denial. Service X silently suppresses an authorization failure instead of a benign miss.
- Message-ordering assumption: Service X publishes events assuming strict per-key ordering, true only because the original Service Y consumed single-threaded. Service Y's rewrite parallelizes consumption differently, producing valid-but-out-of-order delivery that breaks a state transition neither side's own tests would catch.
What do they all have in common? Each instance can look perfectly clean from a SAST/DAST, and LLM scan perspective. Each is a locally-correct implementation choice that becomes wrong or insecure only in combination with an assumption the other side never re-verified. The variety here, types, idempotency, precision, auth, error semantics, ordering, is the actual argument: this isn't one bug, it's a category.
Why this happens more than you'd think
Products evolve over time. And context gets lost as the people who built those products come and go. Small, seemingly innocent bits of tribal knowledge that were never couched in code, or were never documented, are lost to time. Some projects get qualified as "easy" or "intern-friendly," or the latest "have your LLM of choice rewrite this properly from existing code," and substance is lost.
Here's a few real world examples:
- Performance-driven rewrites, the kind in Example 1 above: throughput goes up, and everyone more or less assumes the security posture of the component came along for the ride.
- Team ownership changes: a brand new team inherits a component and makes assumptions on how it connects to, or is connected to from, another component. Absence of tribal knowledge.
- Type system mismatch in polyglot environments: type systems don't cross language boundaries, only the wire format does, and wire formats are permissive by nature (JSON doesn't enforce bounds; protobuf enforces shape but not range).
There's a quieter reason this persists, and it's not technical at all. Even when someone on a security team does start to suspect a seam of this nature exists, raising it runs headlong into "it's always worked this way," or worse, into the fact that a senior person in the org already reviewed and signed off on the design years ago. Questioning it means questioning them, and not everyone who spots the problem has the standing to do that. The gap doesn't just hide from tooling. It sometimes hides in plain sight, protected by the specter of past approvers.
And they sum to a simple but meaningful statement: "the API didn't change" gets treated as equivalent to "the contract didn't change." It isn't. Endpoint stability is not behavioral stability.
Where this fits next to what already exists
I have the hubris to try and coin a new term or discipline, and to point at how it is underused and underserved. But there are similar precedents:
- The aerospace world has ICDs (Interface Control Documents). This exact failure mode was learned the very expensive way, years ago.
- Contract testing tools exist, but they test specifically for functional drift between services, not adversarial type or boundary drift. Examples of those would be tools like Pact, Schemathesis, or Dredd.
- STRIDE-style threat modeling gets close in spirit. But it's typically used as a design-time exercise, not something continuously tested.
- Modern CSPM and graph-based analysis tooling (I won't name names here, but you know them) map which services can reach which. Not what happens once they do.
- Pentest engagements are the one practice that can catch an interstitial issue. Usually by accident. A good tester probing the right interface at the right depth would find it. But a pentest reflects one point in time, not a standing practice. If a rewrite happens immediately after the last engagement, the gap sits live until the next one, whenever that is. And finding it depends on a tester happening to go deep on exactly the right seam out of dozens of candidates, in a finite engagement window. It's less a systematic check than rolling the dice and hoping the right tester goes deep on the right interface.
- There's also a term already circulating in the industry: "API drift," used to describe undocumented changes that cause runtime behavior to diverge from a documented security model. It's the closest existing neighbor to what I'm describing. API drift is fundamentally about documentation hygiene: a contract stops matching its own docs. Interstitial risk is narrower and more adversarial: both sides can be fully in spec and still produce this failure, because the drift lives in runtime type and behavioral assumptions no spec was ever tracking. Fixing API drift gets your contract back in sync with its documentation. It doesn't get you a contract that's been adversarially tested at the boundary.
That said, as commonly practiced, none of these systematically and continuously fuzz the live contract at the seam, whether the trigger is a silent reimplementation or a structural change like a service getting split off on its own. Pentesting could stumble onto it. Nothing on this list is built to find it on purpose, every time.
What actually closes the gap
Talking about it like it's a real thing helps, since, as demonstrated, it is. But let's get more concrete than that.
- Treat any reimplementation or rewrite of a service with the same degree of security review and adversarial contract re-test. Don't stop at functional regression tests.
- Maintain explicit, deliberate interface specs that are diffed automatically upon changes of either side of a boundary. That includes a schema of types and bounds, not merely endpoints.
- Consider another category of tooling in your SSDLC: boundary-value and type fuzzing at the seam itself, distinct from fuzzing within a single component. As far as I can find, none exists, but hey, look down there in the conclusion.
- Treat "who owns the contract" as an explicit RACI question when a component gets rewritten or handed off, not an implicit assumption.
Why AI-assisted rewrites make this worse, not better
It's 2026, how could anyone write anything without dedicating at least some words opining on AI? Coding LLMs have increased the volume, and speed, of the first type of examples I went on about: code rewrites. Whether for performance or not. They make exactly this kind of rewrite-without-full-context more common, not less. The failure mode happens at both a higher frequency and faster.
Context and contracts may well matter now more than ever before. Our products get more and more vulnerable without anyone being the wiser.
Closing thoughts
Hopefully this was an interesting read. Evidence and experience tells me that this is a growing class of either vulnerabilities or security issues. But I am merely one person, so I'd like to ask of you: tell me your stories in this problem space. I want to hear more about it.
Finally, and in small letters: be on the lookout for a new tool I'm building to detect this exact class of security issues. If you come talk to me like a normal, reasonably well-meaning person, I'll talk about it a bit before it goes live.