{"slug": "your-security-scanner-has-a-blind-spot-streaming", "title": "Your Security Scanner Has a Blind Spot: Streaming", "summary": "A developer building Cencurity, a local gateway that inspects LLM-generated code, discovered that security scanners fail on streaming output because patterns can be split across chunks. The fix involves a rolling buffer that holds back the last N characters to ensure detectors see continuous text while maintaining real-time streaming.", "body_md": "I spent an afternoon convinced my detector was broken.\n\nI was building [Cencurity](https://github.com/cencurity/cencurity), a local gateway that sits between an IDE and an LLM provider and inspects generated code before it lands in the editor. The rule was simple. If the model produces a SQL query built by concatenating user input, block it. I had a regex. I had a test string. The test passed.\n\nThen I pointed a real coding agent at it, asked for something that should have tripped the rule, and watched the unsafe code arrive in my editor untouched.\n\nThe rule was fine. My mental model of the input was wrong.\n\nEvery security tool I had used before this — linters, SAST scanners, pre-commit hooks — operates on a file. A complete, finished, sitting-still-on-disk file. You read it, you parse it, you walk it, you make a decision.\n\nLLM output is not a file. It is a stream.\n\nWhen an agent asks a provider for code, the response comes back over Server-Sent Events as a sequence of small chunks. Each chunk carries a fragment of text. The fragments are not split on anything meaningful — not on lines, not on tokens you would recognise, certainly not on syntactic boundaries. They are split on whatever the tokeniser and the transport happened to do.\n\nSo a query that looks like this when it is finished:\n\n```\ncursor.execute(\"SELECT * FROM users WHERE id = \" + request.args['id'])\n```\n\nmight arrive like this:\n\n```\ndata: {\"choices\":[{\"delta\":{\"content\":\"cursor.execute(\\\"SELECT * FROM us\"}}]}\ndata: {\"choices\":[{\"delta\":{\"content\":\"ers WHERE id = \\\" + reque\"}}]}\ndata: {\"choices\":[{\"delta\":{\"content\":\"st.args['id'])\"}}]}\n```\n\nMy regex ran against each chunk as it arrived. No single chunk contained the pattern. Every chunk passed. The dangerous line assembled itself in the editor, one safe-looking fragment at a time.\n\nThis is the part I want other people building in this space to have for free, because it cost me a day: **a detector that is correct on a complete string can be silently useless on a stream, and it will not fail loudly. It will just never fire.**\n\nThe obvious fix is to stop being clever. Collect the whole response, wait for the stream to end, then run the detector once against the finished text.\n\nThat works, and it is what I would do if the tool ran in CI. It does not work in an editor.\n\nThe entire value of streaming is that the developer sees output as it appears. If you buffer the full response before releasing anything, you have converted a responsive agent into a tool that stares blankly for eight seconds and then dumps a wall of text. Users notice this immediately and they turn your tool off. A security control that gets disabled protects nothing.\n\nSo the constraint is: inspect a stream, in real time, while still emitting it in real time, and catch patterns that no individual chunk contains.\n\nThe approach that held up is to stop treating chunks as the unit of analysis. Chunks are a transport artefact. They should not be visible to the detector at all.\n\nThe gateway keeps a rolling buffer of recently seen text. Each arriving chunk is appended, the detector runs against the tail of the accumulated buffer rather than the chunk alone, and then — this is the part that matters — the gateway releases only the portion of the buffer that can no longer participate in a match.\n\nConcretely: if your longest rule can match across N characters, you always hold back the last N characters. Everything before that is settled. Nothing arriving later can change whether it matched, so it is safe to forward.\n\n```\nchunk arrives\n  |\nappend to buffer\n  |\nrun detectors against buffer tail\n  |\nmatch?  --yes-->  allow / redact / block\n  |\n  no\n  |\nrelease buffer[0 : len-N]  -->  forward downstream\nhold buffer[len-N : ]\n```\n\nThe developer sees output flowing continuously. The delay is bounded by N, not by the length of the response. And the detector sees continuous text, which is what it was written for.\n\nThree details that were not obvious to me at the start:\n\n**The window has to be sized off your rules, not guessed.** N is a property of your ruleset. Add a rule that can span more characters and your hold-back has to grow with it, or that rule quietly becomes a chunk-boundary lottery.\n\n**You have to decide what happens to text you already released.** If a match completes at character 900 and you forwarded characters 0 to 850 twenty milliseconds ago, those 850 characters are already in the editor. Blocking now is a partial block. In Cencurity the enforcement actions are `allow`\n\n, `redact`\n\n, and `block`\n\n, and how each behaves mid-stream is a design decision you have to make explicitly rather than discover in production.\n\n**Not every chunk is text.** SSE frames carry structure, and a naive \"append the whole chunk\" will happily let JSON scaffolding contaminate the buffer and either create false matches or break real ones. Parsing the frame before appending is not optional.\n\nI want to be precise about what this is, because the AI security space is currently full of claims that do not survive contact with a real threat model.\n\nThis is a heuristic, pattern-based, stream-time guardrail. It is not a semantic analyser. It has no type information, no data-flow graph, no notion of whether the variable being concatenated is actually attacker-controlled. Cencurity's own README says this in as many words: these are stream-time guardrails, **not a full semantic SAST engine**.\n\nThat means it will produce false positives on code that is fine, and it will miss things a real analyser would catch. What it does that a real analyser cannot is act *during authoring* — before the code is in your file, before it is in your commit, before a scanner three steps downstream flags it and you have to reconstruct why you accepted it. When someone asked me how this differs from a code review tool, the distinction I landed on was: this is not static analysis of a repository after the fact, it is real-time control of the traffic between the agent and the model.\n\nBoth layers are worth having. Neither replaces the other. Anyone telling you a regex over a token stream is a substitute for a SAST pipeline is selling something.\n\nCencurity is a specific tool with specific choices — it runs as a local gateway on `127.0.0.1:38180`\n\n, your API key never leaves your IDE, and only policy violations get logged rather than the whole conversation. Those are my choices and you might make different ones.\n\nBut the streaming problem is not specific to me. It applies to anything that inspects model output in flight: prompt-injection filters reading tool-call arguments, PII redaction on chat responses, content policy enforcement on generated text. All of them are pattern-matching over a stream. All of them will silently under-fire if they treat the transport's chunk boundaries as real boundaries.\n\nThe failure mode is the dangerous part. It does not throw. It does not log an error. Your dashboard shows zero violations and you conclude the traffic is clean.\n\nIf you are building anything in this shape, the test to write first is not \"does my rule match the bad string.\" It is \"does my rule still match when I split the bad string at every possible offset and feed it through in pieces.\"\n\nMine didn't.\n\n*Sangyeon Park builds Cencurity, an open-source policy-driven security gateway for LLM coding agents (Apache-2.0). Writing about LLM security, guardrails, and the gap between generated code and reviewed code.*", "url": "https://wpnews.pro/news/your-security-scanner-has-a-blind-spot-streaming", "canonical_source": "https://dev.to/sangyeonpark/your-security-scanner-has-a-blind-spot-streaming-1636", "published_at": "2026-08-27 17:37:00+00:00", "updated_at": "2026-08-27 17:48:07.675614+00:00", "lang": "en", "topics": ["ai-safety", "developer-tools", "ai-infrastructure"], "entities": ["Cencurity"], "alternates": {"html": "https://wpnews.pro/news/your-security-scanner-has-a-blind-spot-streaming", "markdown": "https://wpnews.pro/news/your-security-scanner-has-a-blind-spot-streaming.md", "text": "https://wpnews.pro/news/your-security-scanner-has-a-blind-spot-streaming.txt", "jsonld": "https://wpnews.pro/news/your-security-scanner-has-a-blind-spot-streaming.jsonld"}}