diff --git a/3rdparty/interface/common.cpp b/3rdparty/interface/common.cpp index a8bc3299f..f77e30f78 100644 --- a/3rdparty/interface/common.cpp +++ b/3rdparty/interface/common.cpp @@ -518,6 +518,74 @@ bool Common::findLnfsPath(const QString &target, Compare func) } +bool extractPathIsWithinTarget(const QString &extractRoot, const QString &absoluteDestPath) +{ + const QFileInfo rootFi(extractRoot); + const QString rootCanon = rootFi.canonicalFilePath(); + if (rootCanon.isEmpty()) { + return false; + } + + const QString destAbs = QFileInfo(absoluteDestPath).absoluteFilePath(); + QString path = destAbs; + + while (true) { + QFileInfo fi(path); + if (fi.exists()) { + const QString canon = fi.canonicalFilePath(); + if (canon.isEmpty()) { + return false; + } + if (!canon.startsWith(rootCanon + QDir::separator()) && canon != rootCanon) { + return false; + } + return true; + } + const QString parent = fi.path(); + if (parent == path || parent.isEmpty()) { + break; + } + path = parent; + } + return rootFi.exists(); +} + +bool symlinkTargetIsWithinTarget(const QString &extractRoot, const QString &symlinkFilePath, const QString &symlinkTarget) +{ + const QFileInfo rootFi(extractRoot); + const QString rootCanon = rootFi.canonicalFilePath(); + if (rootCanon.isEmpty()) { + return false; + } + + if (symlinkTarget.isEmpty()) { + return false; + } + + const QString linkParent = QFileInfo(symlinkFilePath).path(); + const QString resolved = QDir::cleanPath(QDir(linkParent).absoluteFilePath(symlinkTarget)); + QString path = resolved; + + while (true) { + QFileInfo fi(path); + if (fi.exists()) { + const QString canon = fi.canonicalFilePath(); + if (canon.isEmpty()) { + return false; + } + return canon.startsWith(rootCanon + QDir::separator()) || canon == rootCanon; + } + + const QString parent = fi.path(); + if (parent == path || parent.isEmpty()) { + break; + } + path = parent; + } + + return resolved.startsWith(rootCanon + QDir::separator()) || resolved == rootCanon; +} + bool IsMtpFileOrDirectory(QString path) noexcept { const static QRegExp regexp("((/run/user/[0-9]+/gvfs/mtp:)|(/root/.gvfs/mtp:)).+"); return regexp.exactMatch(path); diff --git a/3rdparty/interface/common.h b/3rdparty/interface/common.h index 1df9b95f3..d7c2f590f 100644 --- a/3rdparty/interface/common.h +++ b/3rdparty/interface/common.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2022 UnionTech Software Technology Co., Ltd. +// SPDX-FileCopyrightText: 2022 - 2026 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: GPL-3.0-or-later @@ -58,4 +58,14 @@ class Common: public QObject */ bool IsMtpFileOrDirectory(QString path) noexcept; +/** + * 解压安全:校验目标绝对路径在真实文件系统解析后仍位于解压根目录内(防止符号链接路径逃逸)。 + */ +bool extractPathIsWithinTarget(const QString &extractRoot, const QString &absoluteDestPath); + +/** + * 解压安全:ZIP 内符号链接目标解析后须位于解压根目录内。 + */ +bool symlinkTargetIsWithinTarget(const QString &extractRoot, const QString &symlinkFilePath, const QString &symlinkTarget); + #endif diff --git a/3rdparty/libarchive/libarchive/libarchiveplugin.cpp b/3rdparty/libarchive/libarchive/libarchiveplugin.cpp index 4dd59d19c..fbe4d1a52 100644 --- a/3rdparty/libarchive/libarchive/libarchiveplugin.cpp +++ b/3rdparty/libarchive/libarchive/libarchiveplugin.cpp @@ -826,6 +826,8 @@ int LibarchivePlugin::extractionFlags() const { int result = ARCHIVE_EXTRACT_TIME; result |= ARCHIVE_EXTRACT_SECURE_NODOTDOT; + result |= ARCHIVE_EXTRACT_SECURE_SYMLINKS; + result |= ARCHIVE_EXTRACT_SECURE_NOABSOLUTEPATHS; // TODO: Don't use arksettings here /*if ( ArkSettings::preservePerms() ) diff --git a/3rdparty/libminizipplugin/libminizipplugin.cpp b/3rdparty/libminizipplugin/libminizipplugin.cpp index 2049c475a..79f2293a2 100644 --- a/3rdparty/libminizipplugin/libminizipplugin.cpp +++ b/3rdparty/libminizipplugin/libminizipplugin.cpp @@ -283,6 +283,14 @@ ErrorType LibminizipPlugin::extractEntry(unzFile zipfile, unz_file_info file_inf strFileName = strFileName.remove(0, options.strDestination.size()); } + while (strFileName.contains(QStringLiteral("../"))) { + qInfo() << "skipped ../ path component(s) in " << strFileName; + strFileName = strFileName.replace(QStringLiteral("../"), QString()); + } + if (strFileName.contains(QLatin1Char('\\'))) { + strFileName = strFileName.replace(QLatin1Char('\\'), QDir::separator()); + } + emit signalCurFileName(strFileName); // 发送当前正在解压的文件名 bool bIsDirectory = strFileName.endsWith(QDir::separator()); // 是否为文件夹 @@ -293,6 +301,19 @@ ErrorType LibminizipPlugin::extractEntry(unzFile zipfile, unz_file_info file_inf // 解压完整文件名(含路径) QString strDestFileName = options.strTargetPath + QDir::separator() + strFileName; + + const QString cleanTargetPath = QDir::cleanPath(QDir(options.strTargetPath).absolutePath()); + const QString cleanDestPath = QDir::cleanPath(QDir(strDestFileName).absolutePath()); + if (!cleanDestPath.startsWith(cleanTargetPath + QDir::separator()) && + cleanDestPath != cleanTargetPath) { + qInfo() << "Path traversal detected! Rejected path: " << strFileName; + return ET_FileWriteError; + } + if (!extractPathIsWithinTarget(options.strTargetPath, strDestFileName)) { + qInfo() << "Rejected path (symlink escape or out of root):" << strDestFileName; + return ET_FileWriteError; + } + QFile file(strDestFileName); if (bIsDirectory) { // 文件夹 diff --git a/3rdparty/libzipplugin/libzipplugin.cpp b/3rdparty/libzipplugin/libzipplugin.cpp index 52ace67b7..23f107aed 100644 --- a/3rdparty/libzipplugin/libzipplugin.cpp +++ b/3rdparty/libzipplugin/libzipplugin.cpp @@ -895,6 +895,11 @@ ErrorType LibzipPlugin::extractEntry(zip_t *archive, zip_int64_t index, const Ex return ET_FileWriteError; } + if (!extractPathIsWithinTarget(options.strTargetPath, strDestFileName)) { + qInfo() << "Rejected path (symlink escape or out of root):" << strDestFileName; + return ET_FileWriteError; + } + QFile file(strDestFileName); // Store parent mtime. @@ -951,6 +956,12 @@ ErrorType LibzipPlugin::extractEntry(zip_t *archive, zip_int64_t index, const Ex const auto readBytes = zip_fread(zipFile, buf, zip_uint64_t(READBYTES)); if (readBytes > 0) { QString strBuf = QString(buf).toLocal8Bit(); + if (!symlinkTargetIsWithinTarget(options.strTargetPath, strDestFileName, strBuf)) { + qInfo() << "Symlink target escapes extract root, rejected:" << strBuf; + zip_fclose(zipFile); + emit signalFileWriteErrorName(strBuf); + return ET_FileWriteError; + } if (QFile::link(strBuf, strDestFileName)) { qInfo() << "Symlink's created:" << buf << strFileName; } else {