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.

Diagram: Orchestrator, Worker, and Database before the rewrite, both scans passing clean

Before: two scans, two green checkmarks, one database that isn't in scope.

Diagram: Orchestrator and Worker after the rewrite, same scans passing, callout on the connecting request

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.

Diagram: Gateway and System of Records at the moment of the split, both scans passing clean

At the split: gateway checks, system of records trusts, because nothing else can reach it yet.

Diagram: A new reporting service bypassing the gateway with a direct, unverified line into the System of Records

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:

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:

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:

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.

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.