Emacs Arbitrary Code Execution and How to Avoid It
Details and advice about a long standing arbitrary code execution vulnerability in Emacs
This is a security advisory about CVE-2024-53920, an Emacs vulnerability that I (re-)discovered a few months ago.
TL;DR
Viewing or editing Emacs Lisp code in Emacs can run arbitrary code. The vulnerability stems from unsafe Lisp macro-expansion, which runs unrestricted Emacs Lisp code. Most common configurations are vulnerable (see details below). The best security measures are:
- Avoid visiting untrusted
.el
files in Emacs - Disable automatic error checking (with Flymake or Flycheck) in
untrusted
.el
files - Disable auto-completion features in untrusted
.el
files
This is a long-standing vulnerability which has been known for several years, but has not been addressed thus far. Emacs maintainers are working on countermeasures that will hopefully make their way into future Emacs versions. This advisory is intended to help users of existing Emacs versions protect themselves.
Background
Macros are a staple feature across Lisp dialects. They are often cited as one of the superpowers of Lisp. They are essentially a meta-programming facility: a macro is just a Lisp function that outputs Lisp code. Since Lisp is homoiconic (code and data are represented using the same data structures), manipulating Lisp code in Lisp is as simple as processing any other program input. This makes such meta-programming fun and easy, especially in comparison to the experience of writing elaborate C preprocessor macros, for example, which often feels a bit hackish.
However, as is often the case with great powers, Lisp macros are double-edged swords—wielding them safely requires special care.
Normally, macros are executed, or “expanded”, during so-called macro-expansion time: after parsing (“reading”) text into a Lisp form, macro calls that occur in the form are expanded by executing the macro, which produces new (sub-)forms. The macro-free form obtained by expanding all macro calls can then be compiled and executed. Thus macro-expansion time comes after “read time” and before compile time and runtime.
Emacs Lisp is the programming language used implement most of
Emacs’s core features and extensions, as well to configure it. It is
not the most powerful Lisp dialect out there, but it does boast a
full-blown meta-programming facility in the form of macros. The
problem is that macros in Emacs Lisp come with no safety
measures—they can execute arbitrary, unrestricted, Emacs Lisp code.
The basic macro-expansion primitive in Emacs is the Lisp function
macroexpand
, defined in C code in src/eval.c
in the Emacs sources.
It repeatedly replaces macro names with their definitions as
functions, and applies those functions to the provided code:
while (1) { /* Come back here each time we expand a macro call, in case it expands into another macro call. */ ... { Lisp_Object newform = apply1 (expander, XCDR (form)); if (EQ (form, newform)) break; else form = newform; } } return form;
That apply1
call up there can do, well, literally anything,
depending on the expander
function (the definition of the macro) and
the given input form
.
The Emacs Lisp library macroexp.el
provides higher-level routines on
top of this macroexpand
primitive, such as macroexpand-all
which
the Lisp byte-compiler in bytecomp.el
uses to preprocess Lisp forms.
In addition, Emacs ships with several built-in macros that actually do
execute arbitrary code by evaluating some of their arguments, no
questions asked. These macros are static-if
, rx
, cl-eval-when
,
eval-when-compile
, eval-and-compile
, and perhaps others.
Therefore, if we can nudge Emacs to expand one of these macros, we get arbitrary code execution. That’s the crux of this vulnerability. Expanding macros in Emacs Lisp is unsafe by design.
Exploitation
But could an attacker really coerce Emacs to expand macros without an
explicit user request? When you open (or “visit”, in Emacs parlance)
an Emacs Lisp file, Emacs enables “ELisp mode”, a dedicated editor
mode defined in elisp-mode.el
, which provides various useful
features for exploring and editing Emacs Lisp code.
One of the features that ELisp mode provides is code completion.
Completion is implemented in the function elisp-completion-at-point
,
which tries to examine the code around your cursor and come up with
relevant completions. Among other things, it invokes a subroutine
elisp--local-variables
that looks for local variable names in the
current scope. Since macros can completely change the meaning of the
code they apply to, elisp--local-variables
expands macros in the
surrounding code to uncover local variables that may be created or
obscured by such macros. Hence invoking code completion runs
arbitrary code. In vanilla Emacs, by default, code completion is
only triggered when you issue a completion command. However, since
macros run arbitrary code in a Turing complete language (Emacs Lisp),
there’s no way to know for sure whether invoking completion will get
you pwned. More importantly, almost no one uses the default Emacs
configuration. Emacs users tweak various knobs, and in many common
configurations folks enable auto-completion features which then
trigger code completion without an explicit completion command. Such
auto-completion is performed by the popular Emacs packages Corfu and
Company, as well as the newly built-in Completion Preview mode.
But the most common flow that involves automatic macro-expansion is
probably on-the-fly code diagnosis. There are two widespread Emacs
packages that check your code and warn about potential errors
automatically. One is Flymake, which is built into Emacs, and the
other is a popular extension package called Flycheck. Both of them,
when enabled in an ELisp mode buffer, check for code issues by
byte-compiling the code. As mentioned earlier, this involves
macro-expansion, and thus arbitrary code execution. For Flymake, this
byte-compilation happens in the function elisp-flymake-byte-compile
.
Like auto-completion, on-the-fly diagnosis is not enabled by default
in vanilla Emacs, but it is extremely common for users to enable it.
In some Emacs “distributions”, such as the popular Doom Emacs and
Prelude, either Flymake or Flycheck are enabled by default in ELisp
mode.
So the idea is simple: to exploit this vulnerability, an attacker crafts an Emacs Lisp file that includes a malicious macro invocation, and sends that file to an unsuspecting Emacs user. When that user opens the file in Emacs, code diagnosis is triggered automatically, which expands macros and executes arbitrary code.
Here’s the content of the POC “malicious” file that I shared with the Emacs maintainers when reporting this vulnerability:
(rx (eval (call-process "touch" nil nil nil "/tmp/owned")))
If you have Flymake or Flycheck hooked to ELisp mode (again, such a
setting is often the default in Emacs starter kits, and generally very
common among Emacs users), then just putting the above line of code
anywhere in a .el
file and opening that file in Emacs will create a
new file /tmp/owned
on your system. Such a setup usually looks
something like the following in the Emacs initialization file,
~/.emacs.d/init.el
:
;; Unsafe common configuration. (add-hook 'emacs-lisp-mode-hook #'flymake-mode)
This is reproducible at least since Emacs version from 26.1 and all the way up to the development version of the upcoming Emacs 30.
So this is a long-standing vulnerability, and the gist of it is very simple: macros are unsafe, and in common setups Emacs expands them automatically. I’ve come to discover this issue while working on an enhancement for ELisp mode, which employed macro-expansion to provide semantic code highlighting. I quickly realized that doing so naively is a security risk, and soon afterwards it hit me that Emacs suffered from such a vulnerability already without my custom hacks.
The very same day, 17/08/2024, I reported my findings to the Emacs maintainers via private email. The maintainers informed me that variants of this issue have been surfaced in the past, but the issue, sadly, still stands. AFAICT the earliest public discussion about the security implications of Emacs Lisp macros started in August 2018, when Wilfred Hughes noted that code completion can lead to arbitrary code execution via macro-expansion. In October 2019, Adam Plaice reported that Flymake specifically can be used in a similar exploit. Some solutions have been floated in the discussions following these reports, but unfortunately, Emacs remains vulnerable to this very day.
Following my report, the maintainers requested 90 days to work on a
fix before public disclosure. That non-disclosure period have since
expired, hence this advisory. They continue to work on a fix, which I
hope will be available soon, and now we at least have a CVE to track
this vulnerability. Until new guardrails are put in place to mitigate
this risk, it is important to realize that macro-expansion of
untrusted Emacs Lisp code is unsafe, and to be vigilant about .el
files that you open in Emacs. Crucially, do not enable
Flymake/Flycheck in ELisp mode automatically. Only allow automatic
macro-expansion in .el
files that trust and you control, and protect
those files from tampering.