← back

you didn't write a parser, you shipped an INTERPRETER

every bug you have ever found was a parser being wrong about what language it was reading. every single one. the buffer overflow, the sqli, the deserialization gadget chain, the smuggled request, the polyglot png that was also a jar that was also a shell script. you did not find a coding mistake, you found a machine that accepted a sentence nobody meant to make legal, and then FAITHFULLY EXECUTED IT. there is an entire field that starts from that sentence and builds outward. language-theoretic security, langsec to its friends, all of whom know each other. decades old, gorgeous, and if you say it out loud at a security conference you will get maybe four people nodding and one of them is the guy who wrote the paper.

so let's talk about it, because i think it's the closest thing infosec has to actual physics and everyone is out here selling dashboards instead.

the chomsky hierarchy is a threat model, babyyyyyyy

here is the trick that reorganized my whole brain. take your input format. not your code. your INPUT. and ask where it sits in the Chomsky hierarchy.

regular            finite automaton      decidable, cheap, boring, PERFECT
context-free       pushdown automaton    decidable, needs a stack
context-sensitive  linear bounded aut.   getting expensive
recursively enum.  turing machine        UNDECIDABLE. you're done. go home.

that last row is not a performance note. it's a proof. push your input language up to where it needs a Turing machine to recognize and two things break at once: recognizing it is only SEMI-decidable, so your parser can sit there forever on an input that was never in the language to begin with, and proving that two parsers accept the SAME language goes flatly UNDECIDABLE, the same brick wall Turing hit in 1936. no amount of code review, fuzzing budget, WAF, or senior engineer is going to fix that, because what you're actually asking for, a guarantee that your recognizer accepts EXACTLY the intended language and that everyone downstream agrees on it, is mathematically unavailable. you did not fail to validate your input. you PROVED you couldn't.

and now go look at what people ship. ASN.1 with its length fields that describe other length fields. XML with entity expansion, so the document can define its own vocabulary at parse time, which is just a macro system with a < in front of it. PostScript, which is Turing-complete because of course a page-description language is, and its heir PDF, which bolted on JavaScript and forms so it could keep the reputation honestly. HTTP, which is "regular-ish, unless there are two of a header, in which case ask your local reverse proxy what it feels like today". every one of these is somebody who wanted a data format and accidentally designed a programming language, then shipped an interpreter for it and called it a parser.

that's the whole langsec thesis in one line and it's why it made me feel insane: the input format IS the attack surface. not the code that handles it. the format. you can pick a format whose security question is decidable, or you can pick one where it isn't, and that choice happens in a design doc, months before anybody writes a memcpy.

shotgun parsing, or: how to be wrong in seventeen places at once

the specific crime has a name and the name is PERFECT. shotgun parsing: validation smeared through your processing code like buckshot, a little check here, a little check there, interleaved with the actions the input is asking for.

def handle(msg):
    header = msg[:8]
    if header[:2] != b"OK":
        raise Bad("bad magic")
    n = int.from_bytes(header[4:8], "big")
    body = msg[8:8+n]
    db.insert(body[:32])              # <- already happened
    if len(body) != n:
        raise Bad("truncated")        # <- too late lol
    return body

look at db.insert. the row is in the database. the length check arrives after the side effect, and even when it fires and you raise, you have left a partial write behind. you didn't reject the message. you executed most of it and then complained.

this is the shape of nearly every parser in production. the code interleaves recognition and action, so the program's real state machine is much bigger than the one in your head, and the extra states are reachable from the network. the langsec rule against this is boring and absolute: full recognition before ANY processing. the recognizer consumes the whole input, decides the input is in the language, has no side effects whatsoever, and hands you a structure. only then do you act. no exceptions, no "but we can stream it", no early-exit optimization you added because a profiler told you to.

which sounds like architecture-astronaut talk right up until you notice that every parser you can trust looks exactly like that, and every one you can't looks exactly like the block above.

two parsers walk into a bar, one of them orders SOMETHING ELSE

my favorite consequence, and the one you can actually go make money with: if two components in a pipeline parse the same bytes with two different grammars, the DIFFERENCE between their grammars is an exploit primitive. it doesn't matter that both parsers are individually bug-free by their own spec. the vuln lives in the gap.

POST /x HTTP/1.1
Host: h
Content-Length: 26
Transfer-Encoding: chunked

0

GET /admin HTTP/1.1

the front proxy reads Content-Length, takes the whole body, and forwards it as ONE request. the backend reads Transfer-Encoding, hits the 0 chunk, and decides the request ended right there. now there's a leftover GET /admin sitting in the socket wearing the NEXT PERSON'S credentials. that's request smuggling, and it is not a memory bug, not an injection, not a logic flaw. it's two automata disagreeing about where a sentence ends. HTTP allowed the ambiguity in the grammar and then a thousand engineers resolved it a thousand ways in good faith.

same pattern, different decade: X.509 certificates where the CA validates the CN as a length-prefixed ASN.1 string and the browser reads it as a C string, so bank.com\x00.evil.com is one name to the signer, who reads the WHOLE string, sees it sits under evil.com, the domain i actually own, and signs it without blinking, and another name to the browser, which reads the same bytes as a C string and stops dead at the NUL, seeing bank.com. one blob, two grammars, free cert for somebody else's bank.

once you have the lens you see it EVERYWHERE. json parsers disagreeing on duplicate keys, so your auth service checks {"admin":false,"admin":true} and reads the first while your backend reads the last. unicode normalization happening on one side of a comparison. a filename validated as a string and used as a path. every "the WAF said it was fine" bug is a parser differential where one of the parsers is a regex written by a guy who was in a hurry. differentials are not a bug class you patch, they are the geometry of putting two parsers in a row.

weird machines, or the part where it stops being an accident

here's where langsec goes fully unhinged and i love it so much i want to bite something.

when a parser accepts input outside its intended language, the code that runs afterward is no longer doing what it was written to do. it's still executing, deterministically, on state the input chose. the researchers call the result a WEIRD MACHINE: an emergent computer made out of your program's unintended states, whose instruction set is your bugs and whose program is the attacker's input, aka the WEIRD INSTRUCTIONS.

that's not a metaphor, it's the literal description of a ROP chain. the stack overflow isn't the exploit. the stack overflow is the LOADER. the chain of gadget addresses is a program, written in an instruction set you accidentally shipped, running on an interpreter you accidentally built. same story for a deserialization gadget chain, for a printf format string with %n in it, for spectre-flavored speculation gadgets, for the ELF loader tricks that get a binary to compute the whole thing in relocation entries before main runs.

exploitation is programming. the exploit dev is not "breaking" your program, they're writing code for a target you exposed without noticing. so the question "is my program exploitable" is really "did i ship a computer i didn't know about", and the answer is decided by whether your recognizer is exact.

a parser differential in a hospital integration engine is not a CTF flag, it's a dose that arrives twice, and the langsec papers are one of the very few places in this industry where somebody sat down and did the math on why that keeps happening instead of writing a checklist about it.

c-echo means "are you there". conquest says "yes, and here's a REPL"

everything above is a bug. this next one is a FEATURE, which is worse, and it's why i put the whole post together.

DICOM, the protocol every medical imaging device on earth speaks, has a verb called C-ECHO. that's it, that's the verb. it is PING. the entire semantic content of a C-ECHO is "are you alive", and the entire semantic content of the reply is "yes". conquest, a DICOM server that has been quietly running in radiology closets for two decades, ships Lua scripting in its listener as a documented capability. so:

(0000,0100) US  0x0030      C-ECHO-RQ            <- "are you alive"
(9999,0400) OB  "lua:local h=io.popen([[id]]); return h:read('*a')"
(9999,0401) OB  "uid=..."   in the ECHO RESPONSE <- the output comes BACK

a private element in the command set of a ping, carrying Lua, executed in-process, with the return value copied into the response buffer and shipped home inside the C-ECHO reply. that last part is what makes it obscene. the reply channel already exists, so you don't get a blind fire-and-forget, you get a synchronous read. request, response, request, response. i wrote maybe 150 lines of python around it and what came out the other end is an INTERACTIVE TERMINAL over one A-ASSOCIATE. arrow keys. history. a cd that tracks. output truncates at whatever fits in their response buffer, which is the most DICOM sentence i have ever typed.

this has been public since 2023 (code610, 1337DAY-ID-39183), it needs no authentication, because DICOM's idea of authentication is the CALLING AE TITLE, which is a STRING YOU PICK, and there is no memory corruption anywhere in it. nothing overflowed. nothing was smuggled. no recognizer was fooled. the grammar of a conquest C-ECHO genuinely, by design, on purpose, in the manual, CONTAINS LUA.

so there's no weird machine here. you don't need to build an emergent computer out of unintended states when the vendor has kindly provided a real one, correctly, as advertised, reachable from the verb whose only job is to say yes. the recognition problem for that listener isn't hard, it's not undecidable-in-an-interesting-way, it's just OVER. someone looked at a health-check and decided it should be extensible :)

so why is nobody talking about it

because langsec is UNSELLABLE. that's it. that's the reason.

it produces no product. you cannot buy a langsec. there's no agent to deploy, no dashboard, no MTTD to put on a slide, no ATT&CK technique ID, because the finding isn't "attacker did T1190", the finding is "your protocol's recognition problem is undecidable and has been since 2011, here is the reduction to halting, please recall the product." nobody's getting promoted for that. the remediation is redesign your input format, and the vendor pitch is fewer features, less flexibility, no extensions, be BORING on purpose. try selling boring to a product org.

and the culture points the other way too. Postel's robustness principle, "be conservative in what you send, liberal in what you accept", is the single most expensive sentence ever written about computers. liberal acceptance IS the vulnerability. every protocol that grew a "for compatibility, also accept..." clause grew an ambiguity, and every ambiguity is somebody's smuggling chain in five years. we took a politeness norm from 1980 and turned it into a permanent tax.

then there's the packaging. the foundational work is in langsec.org proceedings and USENIX WOOT papers, written in automata theory, with lemmas. an industry that mainlines threat intel PDFs and 90-second video takes bounced right off it. Sassaman and Patterson and Bratus and friends wrote down the physics and the field went "cool, anyway here's a new EDR." there IS a circle that gets it, mostly exploit devs, protocol people and formal methods folks, and it's small and it talks to itself, and the intersection with people who make shipping decisions is roughly EMPTY.

meanwhile the one place the ideas actually landed is compilers and parser generators, where "generate the recognizer from the grammar" is just tuesday. we had the answer, in a different building, the whole time.

what to actually do about it, in three moves

pick a decidable format. length-prefixed and regular if you can possibly get away with it. a length prefix is a finite automaton with a counter and it will never surprise you, while a sentinel-delimited format means SCANNING FOR BYTES that might legally appear inside the payload, which is how you get every framing bug in the history of the field.

then generate the recognizer, don't hand-roll it. if you wrote it by hand you wrote a grammar you cannot state, which means the language your service accepts is a mystery even to you, and mystery is where differentials live.

then have exactly ONE parser per format in the whole system. not one per service. one. if two things parse it, they will disagree, and their disagreement will be somebody's shell. serialize once at the boundary into an internal type and never re-parse the wire bytes downstream.

and please. PLEASE. DONT PUT AN INTERPRETER IN FRONT OF A C-ECHO OKAY? I'M LOOKING AT YOU CONQUEST.

that's it. it's not glamorous, no CVE has your name on it at the end, and you will spend the design review arguing with somebody who wants the config file to support a little bit of templating, just a small amount, how bad could it be :)

if it aint sellable... then make it mandatory.

as some people reading this blog know i'm the director of the red team diploma at a certain place. and since i can do whatever i want, i'll push langsec on every fucking side. EVERYBODY will speak langsec and i DO NOT CARE if you want "muhh tests, muhh complexity" FUCK YOU. YOU WILL LEARN LANGSEC AND YOU WILL LIKE IT.

the punch

you don't have a security posture. you have a GRAMMAR, and it is either exact or it is a programming language you're running for strangers, for free, at scale, with your database attached.

go read a langsec paper. it's the only thing in this industry that has ever told me why instead of what. it's a disgustingly beautiful frame of mind. and if you're reading this... you need it.

I THINK IM MOMENTS AWAY FROM EVERYTHING CRUMBLING ALL AROUND ME SO SHOW ME THE WAY