commit f7e05eb13aa9513ba3a9ddb49e65dffe12419dd4
Author: Pavel Sanda <[email protected]>
Date:   Sun Oct 11 21:55:36 2026 +0200

    VCS: block argument smuggling
    
    Insert " -- " between every git, svn and cvs subcommand and the filename
    arguments that may originate from attacker-controlled paths.
    
    Excluded:
      * git describe       - no filename argument
      * git show <rev>:./<file>  - the argv element starts with the
        revision, not '-'; '--' is not supported in this syntax
      * RCS - has no functional --, handled separately
    
    Tier 01 DiD / vcs-args: -- separator for VCS filename arguments
    
    https://www.mail-archive.com/[email protected]/msg227023.html
    
    Assisted-by: Claude Opus 5
---
 src/VCBackend.cpp | 86 +++++++++++++++++++++++++++----------------------------
 1 file changed, 43 insertions(+), 43 deletions(-)

diff --git a/src/VCBackend.cpp b/src/VCBackend.cpp
index 751529c9be..897c35489f 100644
--- a/src/VCBackend.cpp
+++ b/src/VCBackend.cpp
@@ -612,7 +612,7 @@ bool CVS::retrieve(FileName const & file)
 {
        LYXERR(Debug::LYXVC, "LyXVC::CVS: retrieve.\n\t" << file);
        // The caller ensures that file does not exist, so no need to check 
that.
-       return doVCCommandCall("cvs -q update " + 
quoteName(file.toFilesystemEncoding()),
+       return doVCCommandCall("cvs -q update -- " + 
quoteName(file.toFilesystemEncoding()),
                               file.onlyPath()) == 0;
 }
 
@@ -679,7 +679,7 @@ CVS::CvsStatus CVS::getStatus()
                return StatusError;
        }
 
-       if (doVCCommandCallWithOutput("cvs status " + getTarget(File),
+       if (doVCCommandCallWithOutput("cvs status -- " + getTarget(File),
                FileName(owner_->filePath()), tmpf)) {
                return StatusError;
        }
@@ -720,7 +720,7 @@ void CVS::getRevisionInfo()
        }
 
        int rc = doVCCommandCallWithOutput("cvs log -r" + version_
-               + ' ' + getTarget(File),
+               + " -- " + getTarget(File),
                FileName(owner_->filePath()), tmpf);
        if (rc) {
                LYXERR(Debug::LYXVC, "cvs log failed with exit code " << rc);
@@ -756,7 +756,7 @@ void CVS::getRevisionInfo()
 
 void CVS::registrer(string const & msg)
 {
-       doVCCommand("cvs -q add -m \"" + msg + "\" "
+       doVCCommand("cvs -q add -m \"" + msg + "\" -- "
                + getTarget(File),
                FileName(owner_->filePath()));
 }
@@ -775,7 +775,7 @@ string CVS::rename(support::FileName const & newFile, 
string const & msg)
        string ret = copy(newFile, msg);
        if (ret.empty())
                return ret;
-       string cmd = "cvs -q remove -m \"" + msg + "\" " +
+       string cmd = "cvs -q remove -m \"" + msg + "\" -- " +
                quoteName(oldFile.onlyFileName());
        FileName path(oldFile.onlyPath());
        return doVCCommand(cmd, path) ? string() : ret;
@@ -796,14 +796,14 @@ string CVS::copy(support::FileName const & newFile, 
string const & msg)
                return string();
        FileName path(oldFile.onlyPath());
        string relFile(to_utf8(newFile.relPath(path.absFileName())));
-       string cmd("cvs -q add -m \"" + msg + "\" " + quoteName(relFile));
+       string cmd("cvs -q add -m \"" + msg + "\" -- " + quoteName(relFile));
        return doVCCommand(cmd, path) ? string() : "CVS: Proceeded";
 }
 
 
 void CVS::getDiff(OperationMode opmode, FileName const & tmpf)
 {
-       doVCCommandWithOutput("cvs diff " + getTarget(opmode),
+       doVCCommandWithOutput("cvs diff -- " + getTarget(opmode),
                FileName(owner_->filePath()), tmpf, false);
 }
 
@@ -811,7 +811,7 @@ void CVS::getDiff(OperationMode opmode, FileName const & 
tmpf)
 int CVS::edit()
 {
        vcstatus_ = LOCKED;
-       return doVCCommand("cvs -q edit " + getTarget(File),
+       return doVCCommand("cvs -q edit -- " + getTarget(File),
                FileName(owner_->filePath()));
 }
 
@@ -819,14 +819,14 @@ int CVS::edit()
 int CVS::unedit()
 {
        vcstatus_ = UNLOCKED;
-       return doVCCommand("cvs -q unedit " + getTarget(File),
+       return doVCCommand("cvs -q unedit -- " + getTarget(File),
                FileName(owner_->filePath()));
 }
 
 
 int CVS::update(OperationMode opmode, FileName const & tmpf)
 {
-       return doVCCommandWithOutput("cvs -q update "
+       return doVCCommandWithOutput("cvs -q update -- "
                + getTarget(opmode),
                FileName(owner_->filePath()), tmpf, false);
 }
@@ -864,7 +864,7 @@ LyXVC::CommandResult CVS::checkIn(string const & msg, 
string & log)
                return LyXVC::VCSuccess;
        case LocallyModified:
        case LocallyAdded: {
-               int rc = doVCCommand("cvs -q commit -m \"" + msg + "\" "
+               int rc = doVCCommand("cvs -q commit -m \"" + msg + "\" -- "
                        + getTarget(File),
                    FileName(owner_->filePath()));
                if (rc)
@@ -1082,7 +1082,7 @@ bool CVS::undoLastEnabled()
 
 void CVS::getLog(FileName const & tmpf)
 {
-       doVCCommandWithOutput("cvs log " + getTarget(File),
+       doVCCommandWithOutput("cvs log -- " + getTarget(File),
                FileName(owner_->filePath()),
                tmpf);
 }
@@ -1129,7 +1129,7 @@ bool CVS::prepareFileRevision(string const & revis, 
string & f)
                return false;
        }
 
-       doVCCommandWithOutput("cvs update -p -r" + rev + ' '
+       doVCCommandWithOutput("cvs update -p -r" + rev + " -- "
                + getTarget(File),
                FileName(owner_->filePath()), tmpf);
        tmpf.refresh();
@@ -1173,7 +1173,7 @@ bool SVN::findFile(FileName const & file)
        // Now we check the status of the file.
        string const fname = onlyFileName(file.absFileName());
        LYXERR(Debug::LYXVC, "LyXVC: Checking if file is under svn control for 
`" << fname << '\'');
-       bool found = 0 == doVCCommandCall("svn info " + quoteName(fname),
+       bool found = 0 == doVCCommandCall("svn info -- " + quoteName(fname),
                                                file.onlyPath());
        LYXERR(Debug::LYXVC, "SVN control: " << (found ? "enabled" : 
"disabled"));
        return found;
@@ -1200,7 +1200,7 @@ bool SVN::checkLockMode()
        }
 
        LYXERR(Debug::LYXVC, "Detecting locking mode...");
-       if (doVCCommandCall("svn proplist " + 
quoteName(onlyFileName(owner_->absFileName()))
+       if (doVCCommandCall("svn proplist -- " + 
quoteName(onlyFileName(owner_->absFileName()))
                    + " > " + quoteName(tmpf.toFilesystemEncoding()),
                    FileName(owner_->filePath())))
                return false;
@@ -1235,14 +1235,14 @@ bool SVN::retrieve(FileName const & file)
 {
        LYXERR(Debug::LYXVC, "LyXVC::SVN: retrieve.\n\t" << file);
        // The caller ensures that file does not exist, so no need to check 
that.
-       return doVCCommandCall("svn update -q --non-interactive " + 
quoteName(file.onlyFileName()),
+       return doVCCommandCall("svn update -q --non-interactive -- " + 
quoteName(file.onlyFileName()),
                               file.onlyPath()) == 0;
 }
 
 
 void SVN::registrer(string const & /*msg*/)
 {
-       doVCCommand("svn add -q --parents " + 
quoteName(onlyFileName(owner_->absFileName())),
+       doVCCommand("svn add -q --parents -- " + 
quoteName(onlyFileName(owner_->absFileName())),
                    FileName(owner_->filePath()));
 }
 
@@ -1260,10 +1260,10 @@ string SVN::rename(support::FileName const & newFile, 
string const & msg)
        // confusing to the user to have two uncommitted files.
        FileName path(owner_->filePath());
        string relFile(to_utf8(newFile.relPath(path.absFileName())));
-       string cmd("svn move -q " + 
quoteName(onlyFileName(owner_->absFileName())) +
+       string cmd("svn move -q -- " + 
quoteName(onlyFileName(owner_->absFileName())) +
                   ' ' + quoteName(relFile));
        if (doVCCommand(cmd, path)) {
-               cmd = "svn revert -q " +
+               cmd = "svn revert -q -- " +
                        quoteName(onlyFileName(owner_->absFileName())) + ' ' +
                        quoteName(relFile);
                doVCCommand(cmd, path);
@@ -1276,7 +1276,7 @@ string SVN::rename(support::FileName const & newFile, 
string const & msg)
        f.push_back(newFile);
        string log;
        if (checkIn(f, msg, log) != LyXVC::VCSuccess) {
-               cmd = "svn revert -q " +
+               cmd = "svn revert -q -- " +
                        quoteName(onlyFileName(owner_->absFileName())) + ' ' +
                        quoteName(relFile);
                doVCCommand(cmd, path);
@@ -1301,7 +1301,7 @@ string SVN::copy(support::FileName const & newFile, 
string const & msg)
        // confusing to the user to have an uncommitted file.
        FileName path(owner_->filePath());
        string relFile(to_utf8(newFile.relPath(path.absFileName())));
-       string cmd("svn copy -q " + 
quoteName(onlyFileName(owner_->absFileName())) +
+       string cmd("svn copy -q -- " + 
quoteName(onlyFileName(owner_->absFileName())) +
                   ' ' + quoteName(relFile));
        if (doVCCommand(cmd, path))
                return string();
@@ -1380,7 +1380,7 @@ bool SVN::isCheckInWithConfirmation()
                return true;
        }
 
-       doVCCommandCall("svn diff " + quoteName(owner_->absFileName())
+       doVCCommandCall("svn diff -- " + quoteName(owner_->absFileName())
                    + " > " + quoteName(tmpf.toFilesystemEncoding()),
                FileName(owner_->filePath()));
 
@@ -1426,7 +1426,7 @@ bool SVN::fileLock(bool lock, FileName const & tmpf, 
string &status)
        if (!locked_mode_ || (isLocked() == lock))
                return true;
 
-       string const arg = lock ? "lock " : "unlock ";
+       string const arg = lock ? "lock -- " : "unlock -- ";
        doVCCommand("svn "+ arg + quoteName(onlyFileName(owner_->absFileName()))
                    + " > " + quoteName(tmpf.toFilesystemEncoding()),
                    FileName(owner_->filePath()));
@@ -1466,7 +1466,7 @@ string SVN::checkOut()
                return N_("Error: Could not generate logfile.");
        }
 
-       doVCCommand("svn update --non-interactive " + 
quoteName(onlyFileName(owner_->absFileName()))
+       doVCCommand("svn update --non-interactive -- " + 
quoteName(onlyFileName(owner_->absFileName()))
                    + " > " + quoteName(tmpf.toFilesystemEncoding()),
                    FileName(owner_->filePath()));
 
@@ -1503,7 +1503,7 @@ string SVN::repoUpdate()
                return N_("Error: Could not generate logfile.");
        }
 
-       doVCCommand("svn diff " + quoteName(owner_->filePath())
+       doVCCommand("svn diff -- " + quoteName(owner_->filePath())
                    + " > " + quoteName(tmpf.toFilesystemEncoding()),
                FileName(owner_->filePath()));
        docstring res = tmpf.fileContents("UTF-8");
@@ -1532,7 +1532,7 @@ string SVN::repoUpdate()
        // + " > " + quoteName(tmpf.toFilesystemEncoding()),
        // FileName(owner_->filePath()));
        // res = "Revert log:\n" + tmpf.fileContents("UTF-8");
-       doVCCommand("svn update --accept mine-full " + 
quoteName(owner_->filePath())
+       doVCCommand("svn update --accept mine-full -- " + 
quoteName(owner_->filePath())
                + " > " + quoteName(tmpf.toFilesystemEncoding()),
                FileName(owner_->filePath()));
        res += "Update log:\n" + tmpf.fileContents("UTF-8");
@@ -1557,7 +1557,7 @@ string SVN::lockingToggle()
                return N_("Error: Could not generate logfile.");
        }
 
-       int ret = doVCCommand("svn proplist " + 
quoteName(onlyFileName(owner_->absFileName()))
+       int ret = doVCCommand("svn proplist -- " + 
quoteName(onlyFileName(owner_->absFileName()))
                    + " > " + quoteName(tmpf.toFilesystemEncoding()),
                    FileName(owner_->filePath()));
        if (ret)
@@ -1567,12 +1567,12 @@ string SVN::lockingToggle()
        string res = scanLogFile(tmpf, log);
        bool locking = contains(res, "svn:needs-lock");
        if (!locking)
-               ret = doVCCommand("svn propset svn:needs-lock ON "
+               ret = doVCCommand("svn propset svn:needs-lock ON -- "
                    + quoteName(onlyFileName(owner_->absFileName()))
                    + " > " + quoteName(tmpf.toFilesystemEncoding()),
                    FileName(owner_->filePath()));
        else
-               ret = doVCCommand("svn propdel svn:needs-lock "
+               ret = doVCCommand("svn propdel svn:needs-lock -- "
                    + quoteName(onlyFileName(owner_->absFileName()))
                    + " > " + quoteName(tmpf.toFilesystemEncoding()),
                    FileName(owner_->filePath()));
@@ -1601,7 +1601,7 @@ bool SVN::revert()
        // gets the updated version from the repository.
        string const fil = quoteName(onlyFileName(owner_->absFileName()));
 
-       if (doVCCommand("svn revert -q " + fil,
+       if (doVCCommand("svn revert -q -- " + fil,
                    FileName(owner_->filePath())))
                return false;
        owner_->markClean();
@@ -1676,7 +1676,7 @@ bool SVN::getFileRevisionInfo()
                return false;
        }
 
-       doVCCommand("svn info --xml " + 
quoteName(onlyFileName(owner_->absFileName()))
+       doVCCommand("svn info --xml -- " + 
quoteName(onlyFileName(owner_->absFileName()))
                    + " > " + quoteName(tmpf.toFilesystemEncoding()),
                    FileName(owner_->filePath()));
 
@@ -1748,7 +1748,7 @@ bool SVN::getTreeRevisionInfo()
 
 void SVN::getLog(FileName const & tmpf)
 {
-       doVCCommand("svn log " + quoteName(onlyFileName(owner_->absFileName()))
+       doVCCommand("svn log -- " + 
quoteName(onlyFileName(owner_->absFileName()))
                    + " > " + quoteName(tmpf.toFilesystemEncoding()),
                    FileName(owner_->filePath()));
 }
@@ -1781,7 +1781,7 @@ bool SVN::prepareFileRevision(string const & revis, 
string & f)
                return false;
        }
 
-       doVCCommand("svn cat -r " + revname + ' '
+       doVCCommand("svn cat -r " + revname + " -- "
                      + quoteName(onlyFileName(owner_->absFileName()))
                      + " > " + quoteName(tmpf.toFilesystemEncoding()),
                FileName(owner_->filePath()));
@@ -1840,7 +1840,7 @@ bool GIT::findFile(FileName const & file)
        string const fname = onlyFileName(file.absFileName());
        LYXERR(Debug::LYXVC, "LyXVC: Checking if file is under git control for 
`"
                        << fname << '\'');
-       doVCCommandCall("git ls-files " +
+       doVCCommandCall("git ls-files -- " +
                        quoteName(fname) + " > " +
                        quoteName(tmpf.toFilesystemEncoding()),
                        file.onlyPath());
@@ -1863,14 +1863,14 @@ bool GIT::retrieve(FileName const & file)
 {
        LYXERR(Debug::LYXVC, "LyXVC::GIT: retrieve.\n\t" << file);
        // The caller ensures that file does not exist, so no need to check 
that.
-       return doVCCommandCall("git checkout -q " + 
quoteName(file.onlyFileName()),
+       return doVCCommandCall("git checkout -q -- " + 
quoteName(file.onlyFileName()),
                               file.onlyPath()) == 0;
 }
 
 
 void GIT::registrer(string const & /*msg*/)
 {
-       doVCCommand("git add " + quoteName(onlyFileName(owner_->absFileName())),
+       doVCCommand("git add -- " + 
quoteName(onlyFileName(owner_->absFileName())),
                    FileName(owner_->filePath()));
 }
 
@@ -1888,10 +1888,10 @@ string GIT::rename(support::FileName const & newFile, 
string const & msg)
        // confusing to the user to have two uncommitted files.
        FileName path(owner_->filePath());
        string relFile(to_utf8(newFile.relPath(path.absFileName())));
-       string cmd("git mv " + quoteName(onlyFileName(owner_->absFileName())) +
+       string cmd("git mv -- " + 
quoteName(onlyFileName(owner_->absFileName())) +
                   ' ' + quoteName(relFile));
        if (doVCCommand(cmd, path)) {
-               cmd = "git checkout -q " +
+               cmd = "git checkout -q -- " +
                        quoteName(onlyFileName(owner_->absFileName())) + ' ' +
                        quoteName(relFile);
                doVCCommand(cmd, path);
@@ -1904,7 +1904,7 @@ string GIT::rename(support::FileName const & newFile, 
string const & msg)
        f.push_back(newFile);
        string log;
        if (checkIn(f, msg, log) != LyXVC::VCSuccess) {
-               cmd = "git checkout -q " +
+               cmd = "git checkout -q -- " +
                        quoteName(onlyFileName(owner_->absFileName())) + ' ' +
                        quoteName(relFile);
                doVCCommand(cmd, path);
@@ -1990,7 +1990,7 @@ bool GIT::isCheckInWithConfirmation()
                return true;
        }
 
-       doVCCommandCall("git diff " + quoteName(owner_->absFileName())
+       doVCCommandCall("git diff -- " + quoteName(owner_->absFileName())
                    + " > " + quoteName(tmpf.toFilesystemEncoding()),
                FileName(owner_->filePath()));
 
@@ -2069,7 +2069,7 @@ bool GIT::revert()
        // gets the updated version from the repository.
        string const fil = quoteName(onlyFileName(owner_->absFileName()));
 
-       if (doVCCommand("git checkout -q " + fil,
+       if (doVCCommand("git checkout -q -- " + fil,
                    FileName(owner_->filePath())))
                return false;
        owner_->markClean();
@@ -2150,7 +2150,7 @@ bool GIT::getFileRevisionInfo()
                return false;
        }
 
-       doVCCommand("git log -n 1 --pretty=format:%H%n%h%n%an%n%ai " + 
quoteName(onlyFileName(owner_->absFileName()))
+       doVCCommand("git log -n 1 --pretty=format:%H%n%h%n%an%n%ai -- " + 
quoteName(onlyFileName(owner_->absFileName()))
                    + " > " + quoteName(tmpf.toFilesystemEncoding()),
                    FileName(owner_->filePath()));
 
@@ -2203,7 +2203,7 @@ bool GIT::getTreeRevisionInfo()
 
 void GIT::getLog(FileName const & tmpf)
 {
-       doVCCommand("git log " + quoteName(onlyFileName(owner_->absFileName()))
+       doVCCommand("git log -- " + 
quoteName(onlyFileName(owner_->absFileName()))
                    + " > " + quoteName(tmpf.toFilesystemEncoding()),
                    FileName(owner_->filePath()));
 }
-- 
lyx-cvs mailing list
[email protected]
https://lists.lyx.org/mailman/listinfo/lyx-cvs

Reply via email to