>From 666758d5e16f6129ed26c023b1d6abf6351d8975 Mon Sep 17 00:00:00 2001
From: Pavel Sanda <sanda () lyx org>
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<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);
 
