The Security Gap Between SAST, DAST, and Microservice Rewrites | Raphaël Théberge
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,...