>From 666758d5e16f6129ed26c023b1d6abf6351d8975 Mon Sep 17 00:00:00 2001 From: Pavel Sanda Date: Wed, 15 Jul 2026 14:10:29 +0200 Subject: [PATCH] Security hardening backport for LyX 2.2.x Accumulated backport of the coordinated LyX security release, encompassing Tier 00 variants. Folded hardening cases (per-case detail in the advisory): 00a kpsewhich filename -> shell command open/export -> exec 00b lyx2lyx invocation filename open -> exec 00c graphics filename extension -> os.system() open -> exec 00g mangled graphics filename extension export -> exec 00h document basename -> conversion helpers open/import -> exec [DiD] 00i document basename backtick in "..." export -> exec 00k \paperwidth/\paperheight -> parsecmd redirect export -> file write 00h ships as defence-in-depth on 2.2: reviewed and applied, but the 2.4 PoC does not reproduce here (a similar PoC variant is nonetheless likely). Assisted-by: Claude Opus 4.8 --- src/Buffer.cpp | 18 +++++++++++++++--- src/BufferParams.cpp | 10 ++++++++++ src/graphics/GraphicsConverter.cpp | 9 ++++++++- src/support/FileName.cpp | 12 +++++++++--- src/support/filetools.cpp | 19 +++++++++++++++++-- 5 files changed, 59 insertions(+), 9 deletions(-) diff --git a/src/Buffer.cpp b/src/Buffer.cpp index ce345bce38..f13a71fabc 100644 --- a/src/Buffer.cpp +++ b/src/Buffer.cpp @@ -1286,12 +1286,22 @@ Buffer::ReadStatus Buffer::convertLyXFormat(FileName const & fn, // Run lyx2lyx: // $python$ "$lyx2lyx$" -t $LYX_FORMAT$ -o "$tempfile$" "$filetoread$" + + // guard against command expansion in filename strings on linux, + // keep " on windows + auto sh_quote = [](string const & s) -> string { +#ifdef _WIN32 + return quoteName(s); +#else + return '\'' + subst(s, "'", "'\\''") + '\''; +#endif + }; ostringstream command; command << os::python() - << ' ' << quoteName(lyx2lyx.toFilesystemEncoding()) + << ' ' << sh_quote(lyx2lyx.toFilesystemEncoding()) << " -t " << convert(LYX_FORMAT) - << " -o " << quoteName(tmpfile.toFilesystemEncoding()) - << ' ' << quoteName(fn.toSafeFilesystemEncoding()); + << " -o " << sh_quote(tmpfile.toFilesystemEncoding()) + << ' ' << sh_quote(fn.toSafeFilesystemEncoding()); string const command_str = command.str(); LYXERR(Debug::INFO, "Running '" << command_str << '\''); @@ -4182,6 +4192,8 @@ Buffer::ExportStatus Buffer::doExport(string const & target, bool put_in_tempdir formats.extension(backend_format)); LYXERR(Debug::FILES, "filename=" << filename); + // (00i-wide) makeLatexName keep-set is now shell-safe; no re-sanitize here. + // Plain text backend if (backend_format == "text") { runparams.flavor = OutputParams::TEXT; diff --git a/src/BufferParams.cpp b/src/BufferParams.cpp index a379e2740d..6b869fb7ab 100644 --- a/src/BufferParams.cpp +++ b/src/BufferParams.cpp @@ -951,8 +951,18 @@ string BufferParams::readToken(Lexer & lex, string const & token, lcolor.setColor("boxbgcolor", color); } else if (token == "\\paperwidth") { lex >> paperwidth; + if (!paperwidth.empty() && !isValidLength(paperwidth)) { + lyxerr << "Rejecting non-Length \\paperwidth value: " + << paperwidth << endl; + paperwidth.clear(); + } } else if (token == "\\paperheight") { lex >> paperheight; + if (!paperheight.empty() && !isValidLength(paperheight)) { + lyxerr << "Rejecting non-Length \\paperheight value: " + << paperheight << endl; + paperheight.clear(); + } } else if (token == "\\leftmargin") { lex >> leftmargin; } else if (token == "\\topmargin") { diff --git a/src/graphics/GraphicsConverter.cpp b/src/graphics/GraphicsConverter.cpp index 4bd5cf1ef4..0ffe4840f3 100644 --- a/src/graphics/GraphicsConverter.cpp +++ b/src/graphics/GraphicsConverter.cpp @@ -28,6 +28,8 @@ #include "support/TempFile.h" #include +#include +#include #include using namespace std; @@ -288,7 +290,12 @@ static void build_script(string const & from_file, theConverters().getPath(from_format, to_format); // Create a temporary base file-name for all intermediate steps. - string const from_ext = getExtension(from_file); + // The extension string is user-controlled. Avoid metacharacters + // to prevent havoc down the pipeline. + string from_ext = getExtension(from_file); + from_ext.erase(remove_if(from_ext.begin(), from_ext.end(), + [](unsigned char c){ return !(isalnum(c) || c == '_' || c == '-'); }), + from_ext.end()); TempFile tempfile(addExtension("gconvertXXXXXX", from_ext)); tempfile.setAutoRemove(false); string outfile = tempfile.name().toFilesystemEncoding(); diff --git a/src/support/FileName.cpp b/src/support/FileName.cpp index 6d40687a25..9b3620e643 100644 --- a/src/support/FileName.cpp +++ b/src/support/FileName.cpp @@ -944,14 +944,20 @@ string DocFileName::mangledFileName(string const & dir) const // are forbidden: '/', '.', ' ', and ':'. // On windows it is not possible to create files with '<', '>' or '?' // in the name. + // We forbid ';', '=' as they coudl become active in shell. static string const keep = "abcdefghijklmnopqrstuvwxyz" "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - "+-0123456789;="; + "+-0123456789"; string::size_type pos = 0; while ((pos = mname.find_first_not_of(keep, pos)) != string::npos) mname[pos++] = '_'; - // Add the extension back on - mname = support::changeExtension(mname, getExtension(name)); + // Add the extension back on, but sanitize from metachars, + // it's user-controlled string. + string ext = getExtension(name); + pos = 0; + while ((pos = ext.find_first_not_of(keep, pos)) != string::npos) + ext[pos++] = '_'; + mname = support::changeExtension(mname, ext); // Prepend a counter to the filename. This is necessary to make // the mangled name unique. diff --git a/src/support/filetools.cpp b/src/support/filetools.cpp index ff9ea8110a..044b3e77a2 100644 --- a/src/support/filetools.cpp +++ b/src/support/filetools.cpp @@ -202,7 +202,7 @@ FileName const makeLatexName(FileName const & file) // a non-latin world out there... string const keep = "abcdefghijklmnopqrstuvwxyz" "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - "@!'()*+,-./0123456789:;<=>?[]`|"; + "0123456789+-._,@"; string::size_type pos = 0; while ((pos = name.find_first_not_of(keep, pos)) != string::npos) @@ -1153,7 +1153,22 @@ FileName const findtexfile(string const & fil, string const & /*format*/) // tfm - TFMFONTS, TEXFONTS // This means that to use kpsewhich in the best possible way we // should help it by setting additional path in the approp. envir.var. - string const kpsecmd = "kpsewhich " + fil; + + if (fil.empty()) + return FileName(); + + // Wrap fil in the shell's quoting form that disables the relevant + // metacharacter set. +#ifdef _WIN32 + // Reject '"' in filename, can't be backslashed & forbidden by NTFS anyway + if (fil.find('"') != string::npos) + return FileName(); + // disable metacharacters + string const kpsecmd = "kpsewhich -- \"" + fil + "\""; +#else + // disable metacharacters & escape existing ' + string const kpsecmd = "kpsewhich -- '" + subst(fil, "'", "'\\''") + "'"; +#endif cmd_ret const c = runCommand(kpsecmd);