I've just finished watching a wonderful Weird Machines: Exploiting Turing-Completeness talk by Pedro Castilho. He was talking about accidental Turing-completeness and how computer technology is weird machines all the way down. This talk had security and program design takeaways that every programmer should remember. I'll cite just the two of these:
These sound pretty obvious, and yet every day there are dozens of RCEs revealed
in different software products. Why?
Because it's not rewarding to follow low-priority security department commandments,
and it's just too easy to copy the code from StackOverflow
or use JS eval() To read a number from user input.
How does one fight it, then, if not with more security department commandments? With mindsets and tools. Pedro Castilho says using least power possible and considering input to be untrusted code? I say:
But wait, this is like the two recommendations above, but inverted to make them meaninglessly dangerous, right? Yes... And no. Buckle up, I'm going to explain why designing for exploitation leads to a safer code.
Disclaimer: the meaning of meta-programming used in this post is not a typical meaning of meta-programming, as seen in communities like Rust and C++—a way to abstract some repetitive/generic code using a fancy built-in preprocessor,—and not even the one commonly used in Lisp community—a way to generate/alter the code running in the image, using the code itself
The meaning of meta-programming that you have to bear with for the duration of this post is: the ability of the programming language—and a program written in it—to access the parsing, evaluation, compilation, and other infrastructural functions that the programming language itself uses internally.
First things first: you should use meta-programming abilities of your technology as much as possible.
There's a mantra in JavaScript community:
never use eval()
especially so for reading untrusted input.
But many developers still do it,
because
I mean, using JS
I'll exemplify this with Common Lisp and the way we use its meta-programming facilities in Nyxt code.
Disclaimer: I'm one of Nyxt browser maintainers working almost full-time on its maintenance. Be aware of that when I mention Nyxt.
We have a way to define your own pages integrated with Nyxt, called Internal Pages API. The way it works is:
For example, here's a (redacted for readability) definition
of
While it may be non-obvious, we actually are passing raw Lisp symbols to this function
and use
But before you get to filing dozens of CVE-s
for every internal page in Nyxt, I'll spice it up:
we use Lisp compiler-native code parsing facilities
(namely,
But you also hold yourself together and refrain from screaming out loud,
because you'll remember that there's a small code snippet
(and a
This CL developer thought train is an example of effect
that using meta-programming has on a person:
you are aware of the power your language has, the dangers it poses,
and the ways to mitigate that.
It's not the opaque JS
Meta-programming, in this perspective,
is a tool quite like loggers and database daemons,
and it's as integratable in your design documents, as loggers and databases are.
And that's what I meant by using meta-programming abilities of your technology:
use the things provided by your language,
if it's at least a bit more self-aware than JavaScript.
You'll have lots of problems solved by the compiler / interpreter / transpiler
without the need to prove Greenspun's Tenth Rule yet again,
and your code will be safe from accidental
I am aware that there are many languages
that don't have meta-programming facilities exposed to the programmer.
And I have no particular suggestions to the programmers in those.
Except maybe moving to Lisp and enjoying the programmer-friendly
and secure meta-languages you can build in less than an hour:
Now that we established that language implementations are our best friends
providing us with power and structure,
how about making things Turing-complete by design? I mean it.
You should allow your users to cange your program by exposing programming languages to them.
This point may sound uneasy to you,
because the immediate thought would be
"so I have to write a parser, analyzer, compiler and an environment for it..."
No, you don't have to!
Remember the previous point:
use the tools provided by your programming language and expose those to your users.
Here, by exposing programming languages,
I don't necessarily mean making users write code.
I rather mean exposing a Turing-complete way to customize your system.
Be it scratch-like code blocks, Notion Relations,
or HTML templates from MySpace—all of them are united
by exposing an (almost) Turing-complete language
to their users and to allow users shape their experience
in the allocated boundaries and compiled to the programming language-ish form.
Again, example from Nyxt.
When I've stumbled across the problem of managing per-website user configuration,
my initial idea was inspired by ad-blocking host lists:
simply store a match pattern for a website,
and store all the settings associated with it on the same line:
Then, when I implemented the first prototype of this feature
(called auto-mode and destined to be renamed to auto-rules
in Nyxt 3.* after a major refactoring we've done),
I realized that the
With this design decisions, one can now write rules like:
This overpowered feature is used by a reasonable fraction of the community
and there were
mentions
and
suggestions
on how to improve it further on our forum,
which may be the best marker of success for such a niche feature.
Auto-mode, being the restricted, yet still quite a turing-complete programming sub-system that Nyxt has,is not a vulnerability. It's consciously designed as a restricted (in this case—by the filesystem and the rule structure) meta-programming tool. If you consciously design a feature to be turing-complete, you make yourself aware of the abilities it has and restrict them.
Designing for Exploitation, you avoid discovering tool turing-completeness being more that you anticipated, after thousands of your users are already pwned...
If you only have one thing to take away from this post, take this one:
using meta-programming tools for everyday tasks
makes these tasks both easier to complete and more secure,
because of the psychological and technical restrictions
one puts on the code and themselves when being aware of
and using a sufficiently good meta-programming system.
Designing for Exploitation, you stay aware of the huge security risks
that accidental Turing-completeness possesses.
Designing for Exploitation,
you shape your software in such a way as to leverage
the most powerful yet domain-specific parts of your programming language.
You don't simply put
Now that you understand how embracing the meta-programming
can actually make your code safer,
go out there and make some software that's Designed for Exploitation;)
This post wouldn't be as good as it (arguably) is without the help of
I am the only person to blame for mistakes and inconsistencies you may find in this post, though.
I hope there are none left, but one can only hope :)
(define-internal-page-command-global describe-variable (&key universal variable) (buffer (concat "*Help-" (symbol-name variable) "*"))
"Inspect a variable and show it in a help buffer."
(let ((*print-case* :downcase))
(with-html-string
(some-code)
(:raw (resolve-backtick-quote-links (documentation variable 'variable) variable))
(:h2 "Current Value:")
(some-more-code)
(:p (:raw (value->html (symbol-value variable))))))))
#.(this '(sharp-period syntax)
(that allows evaluating
#|arbitrary code|# #(at read time)))
(let ((*read-eval* nil))
(read-from-string
"#.(this '(sharp-period syntax)
(that allows evaluating
#|arbitrary code|# #(at read time)))"))
;; Evaluation aborted on #
eval() injections 😃
Embeddable Weird Machines
((match-domain "paulgraham.com") :excluded (:force-https-mode :reading-line-mode))
Using raw function calls allows to arbitrarily
fine-grain the website settings,
even to the point we couldn't have had anticipated it in Nyxt core.
((lambda (url) (string= (quri.uri:uri-path url) "/index")) :excluded (:dark-mode))
Takeaways
By using the meta-programming facilities your programming language has,
you're both using and restricting it's most exploitable part—the
meta-programming facilities themselves.
Acknowledge
Pierre Neidhardt, who helped me polish the phrasing and pointed at some of my assertions about Lisp that were somewhat exaggerated.
And do watch
his talk on GambiConf too,
it a nice showcase of what we've done in Nyxt :)