[LyX/2.2.x] Security hardening backport for LyX 2.2.x

Pavel Sanda <[email protected]>
Newsgroups gmane.editors.lyx.cvs
Message-ID <[email protected]>
commit 7fbc5ab3cf475d778d64ca92aedbd6cde73ba0e8
Author: Pavel Sanda <[email protected]>
Date:   Wed Jul 15 14:10:29 2026 +0200

    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<string>(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 <sstream>
+#include <algorithm>
+#include <cctype>
 #include <fstream>
 
 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);
 
-- 
lyx-cvs mailing list
[email protected]
https://lists.lyx.org/mailman/listinfo/lyx-cvs
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.