Threat Surfaces in Practice: Lessons from a Sandboxed Language Runtime
The rest of this section covers established attack classes — buffer overflows, SQL injection, firewall evasion — and how to defend against them. This page is a companion showing the same defensive principles from the builder's side: real access-control and sandboxing decisions made while building PatLang, a language designed to run the same program either as a trusted native binary or inside a browser's WebAssembly sandbox with no filesystem access at all.
A capability allowlist enforced by the compiler, not a runtime check
PatLang's compiler will not generate a call to a host-level capability — reading a file, opening a socket, anything that reaches outside the language's own value system — unless that capability's name appears on an explicit allowlist checked during compilation. A new capability that isn't on the list can't be lowered into a compiled program at all; the compiler refuses before a single instruction referencing it is ever emitted. This is the same idea as the principle of least privilege applied at the language-design level rather than the operating-system level: the default is "cannot," and every exception has to be named individually, in one place, rather than "can, unless something stops it." A gap in this list isn't a runtime vulnerability that a monitoring system might eventually notice — it's a compile error, discovered the first time anyone tries to use the missing capability, before the program exists at all.
Two trust boundaries, and exactly one deliberate gate between them
PatLang's virtual filesystem is, by default, purely in-memory — it exists identically whether the program is running natively or inside a browser's WASM sandbox, because in-memory data structures don't care which one they're in. That symmetry is itself a security property: most of the language's file-handling code never has to think about which trust boundary it's operating inside, because from its own point of view there isn't one.
The one place that symmetry deliberately breaks is the single operation that writes the virtual filesystem's contents out to a real directory on real disk — available natively only, since there's no real disk to write to inside a browser sandbox regardless. That one function is exactly where a real threat surface actually exists (a program could, in principle, try to write somewhere it shouldn't), and it's guarded accordingly: the destination directory is resolved to its real, canonical path and checked against an explicitly configured allowed root before anything is written, not trusted because the caller's own path string "looks fine." Canonicalizing before checking matters — checking a path string directly is exactly how path-traversal attacks (../../../etc/whatever, or a symlink pointing somewhere unexpected) get past a naive check; resolving to the real path first closes that gap structurally rather than by pattern-matching against known bad inputs.
The general shape worth taking away: don't scatter security checks across every function that touches the outside world. Find the single, narrow point where trust boundaries actually get crossed, and put the real gate exactly there — everything on the inside of that boundary can then be written without having to think about it, because the boundary itself is the whole defense.
Injection isn't one thing: a database built without SQL injection, that still has a different data-integrity gap
Building a real SQL interpreter for this language was a chance to build it "correctly" from day one: user-supplied values are tokenized into a structured representation and bound as typed data, never spliced into a query as raw text and re-parsed. That's the actual fix for SQL injection — not sanitizing or escaping user input before concatenating it into a query string (a defense that has to be applied perfectly, every time, forever, to actually work), but never constructing the query as a string built from untrusted text in the first place.
But building the storage layer underneath that same database surfaced a related, structurally different problem: the CSV format it uses to store table data has no quoting or escaping in its first version, which means a string value containing a comma silently corrupts the row around it — not a security exploit (nothing executes; nobody gains access to anything), but the same underlying category of mistake that causes real injection vulnerabilities elsewhere: treating a delimiter character as safe to embed in data without escaping it, because the common case doesn't happen to contain one. It's worth holding both of these in mind side by side — one gap deliberately avoided by construction (SQL injection, solved by never concatenating), and one gap knowingly accepted and documented rather than solved (CSV escaping, a genuinely different piece of work deferred on purpose) — because "we thought about this class of bug and made a decision" is a completely different risk posture from "we didn't think about it," even when the resulting code looks identical from the outside.
Deciding what's allowed to bring the whole program down
PatLang has no exception handling at all — a failing host call is fatal to the entire process, by design, not by omission. That's a real decision with a real security dimension: any interactive PatLang program (a console, a query tool) has to decide, explicitly, which inputs are allowed to reach that fatal path and which aren't, because "the process crashes" is itself an availability outcome an adversarial or just-careless input could otherwise trigger on demand. The systems built on this language treat a syntax error or a bad query as ordinary, recoverable data — reported back to whoever typed it, never allowed near the fatal path — precisely because letting untrusted input control whether your process stays up is a denial-of-service risk worth taking seriously even in a teaching demo, not just in production.
Lessons, generalized
- Prefer a default-deny allowlist enforced as early as possible (ideally before the program even runs) over a default-allow list with exceptions. A missing capability should be a build-time question, not a runtime surprise.
- Find the one real place a trust boundary gets crossed, and put your one real check exactly there. Scattering partial checks across many call sites is both more work and less reliable than a single narrow, well-tested gate.
- Canonicalize before you validate a path. Checking a path string directly is how traversal attacks get through; resolving to the real, canonical location first and checking that closes the gap structurally.
- The real fix for injection is never constructing untrusted input as executable syntax in the first place — parse and bind, don't concatenate and re-parse.
- A conscious, documented gap and an unconsidered one can look identical in the code — the difference is entirely in whether someone can point to the decision.
- Availability is a security property too. Decide on purpose which inputs are allowed to crash your process, rather than discovering the answer when someone else does.
See also
SQL Injection and Buffer Overflows & Fuzzing for the attacker's-side view of the classes of bug discussed above. Unix Permissions for another real access-control model to compare the compiler allowlist against. Risk Management: Lessons from Testing Real Systems for how these same design decisions map onto the Avoid/Reduce/Transfer/Accept framework. The SQL console demo is the actual system discussed above.