Does Lisp's treatment of code as data make it more vulnerable to security exploits than a language that doesn't treat code as data?

Viewed 218

I know that this might be a stupid question but I was curious.
Since Lisp treats code and data the same, does this mean that it's easier to write a payload and pass it as "innocent" data that can be used to exploit programs? In comparison to languages that don't do so?

For e.g. In python you can do something like this.

malicious_str = "print('this is a malicious string')"
user_in = eval(malicious_str)

>>> this is a malicious string

P.S I have just started learning Lisp.

2 Answers

Any language can be exploited if you are not careful.

A well-known attack against Lisp is via the #. reader macro:

(read-from-string "#.(start-the-war)")

will start the war if *read-eval* is non-nil - this is why one should always bind it when reading from an un-trusted stream.

However, this is not directly related to "code is data" doctrine...

No, I don't think it does. In fact because of what is normally meant by 'code is data' in Lisp, it is potentially less vulnerable than some other languages.

[Note: this answer is really about Common Lisp: see the end for a note about that.]

There are two senses in which 'code can be data' in a language.

Turning objects into executable code: eval & friends

This is the first sense. What this means is that you can, say, take a string or some other object (not all types of object, obviously) and say 'turn this into something I can execute, and do that'.

Any language that can do this has either

  • to be extremely careful about doing this on unconstrained data;
  • or to be able to be certain that a given program does not actually do this.

Plenty of languages have equivalents of eval and its relations, so plenty of languages have this problem. You give an example of Python for instance, which is a good one, and there are probably other examples in Python (I've written programs even in Python 2 which supported dynamic loading of modules at runtime, which executes potentially arbitrary code, and I think this stuff is much better integrated in Python 3).

This is also not just a property of a language: it's a property of a system. C can't do this, right? Well, yes it can if you're on any kind of reasonable *nixy platform. Not only can you use an exec-family function, but you can probably dynamically load a shared library and execute code in it.

So one solution to this problem is to, somehow, be able to be certain that a given program doesn't do this. One thing that helps is if there are a finite, known number of ways of doing it. In Common Lisp I think those are probably

  • eval of course;
  • unconstrained read (because of *read-eval*);
  • load;
  • compile;
  • compile-file;
  • and probably some others that I have forgotten.

Well, can you detect calls to those, statically, in a program? Not really: consider this:

(funcall (symbol-function (find-symbol s)) ...)

and now you're in trouble unless you have very good control over what s is: it might be "EVAL" for instance.

So that's frightening, but I don't think it's more frightening than what Python can do, for instance (almost certainly you can poke around in the namespace to find eval or something?). And something like that in a program ought to be a really big hint that bad things might happen.

I think there are probably two approaches to this, neither of which CL adopts but which implementations could (and perhaps even programs written in CL could).

  • One would be to be able to run programs in such a way that the finite set of bad functions above simply are disallowed: they'd signal errors if you tried to call them. An implementation could clearly do that (see below).
  • The other would be to have something like Perl's 'tainting' where data which came from a user needs to be explicitly looked-at by the program somehow before it's used. That doesn't guarantee safety of course, but it does make it harder to make silly mistakes: if s above came from user input and was thus tainted you'd have to explicitly say 'it's OK to use it' and, well, then it's up to you.

So this is a problem, but I don't think it's worse than the problems that very many other languages (and language-families) have.

An example of an implementation that can address the first approach is LispWorks: if you're building an application with LW, you typically create the binary with a function called deliver, which has options which allow you to remove the definitions of functions from the resulting binary whether or not the delivery process would otherwise leave them there. So, for instance

(deliver 'foo "x" 5
         :functions-to-remove '(eval load compile compile-file read))

would result in an executable x which, whatever else it did, couldn't call those functions, because they're not present, at all.

Other implementations probably have similar features: I just don't know them as well.

But there's another sense in which 'code is data' in Lisp.

Program source code is available as structured data

This is the sense that people probably really mean when they say 'code is data' in Lisp, even if they don't know that. It's worth looking at your Python example again:

>>> eval("exit('exploded')")
exploded
$

So what eval eats is a string: a completely unstructured vector of characters. If you want to know whether that string contains something nasty, well, you've got a lot of work ahead of you (disclaimer: see below).

Compare this with CL:

> (let ((trying-to-be-bad "(end-the-world :now t)"))
    (eval trying-to-be-bad))
"(end-the-world :now t)"

OK, so that clearly didn't end the world. And it didn't end the world because eval evaluates a bit of Lisp source code, and the value of a string, as source code, is the string.

If I want to do something nasty I have to hand it an actual interesting structure:

> (let ((actually-bad '(eval (progn
                               (format *query-io* "? ")
                               (finish-output *query-io*)
                               (read *query-io*)))))
    (eval actually-bad))
? (defun foo () (foo))
foo

Now that's potentially quite nasty in at least several ways. But wait: in order to do this nasty thing, I had to hand eval a chunk of source code represented as an s-expression. And the structure of that s-expression is completely open to inspection by me. I can write a program which inspects this s-expression in any arbitrary way I like, and decides whether or not it is acceptable to me. That's just hugely easier than 'given this string, interpret it as a piece of source text for the language and tell me if it is dangerous':

  • the process of turning the sequence of characters into an s-expression has happened already;
  • the structure of s-expressions is both simple and standard.

So in this sense of 'code is data', Lisp is potentially much safer than other languages which have versions of eval which eat strings, like Python, say, because code is structured, standard, simple data. Lisp has an answer to the terrible 'language in a string' problem.


I am fairly sure that Python does in fact have some approach to making the parse tree available in a standard way which can be inspected. But eval still happily eats strings.

As I said above, this answer is about Common Lisp. But there are many other Lisps of course, which will have varying versions of this problem. Racket for instance probably can really fairly tightly constrain things, using sandboxed execution and modules, although I haven't explored this.

Related