diff --git a/3rdparty/interface/common.cpp b/3rdparty/interface/common.cpp index b6c4fbcef..8f2582b07 100644 --- a/3rdparty/interface/common.cpp +++ b/3rdparty/interface/common.cpp @@ -537,6 +537,46 @@ bool Common::findLnfsPath(const QString &target, Compare func) } +bool extractPathIsWithinTarget(const QString &extractRoot, const QString &absoluteDestPath) +{ + const QFileInfo rootFi(extractRoot); + QString rootCanon = rootFi.canonicalFilePath(); + // 如果 canonical 失败,使用绝对路径作为后备 + if (rootCanon.isEmpty()) { + rootCanon = QDir(extractRoot).absolutePath(); + 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 IsMtpFileOrDirectory(QString path) noexcept { #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) const static QRegExp regexp("((/run/user/[0-9]+/gvfs/mtp:)|(/root/.gvfs/mtp:)).+"); diff --git a/3rdparty/interface/common.h b/3rdparty/interface/common.h index 017f769c6..167a9f09b 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 @@ -59,4 +59,9 @@ class Common: public QObject */ bool IsMtpFileOrDirectory(QString path) noexcept; +/** + * 解压安全:校验目标绝对路径在真实文件系统解析后仍位于解压根目录内(防止符号链接路径逃逸)。 + */ +bool extractPathIsWithinTarget(const QString &extractRoot, const QString &absoluteDestPath); + #endif diff --git a/3rdparty/libarchive/libarchive/libarchiveplugin.cpp b/3rdparty/libarchive/libarchive/libarchiveplugin.cpp index 55ee12553..eef505916 100644 --- a/3rdparty/libarchive/libarchive/libarchiveplugin.cpp +++ b/3rdparty/libarchive/libarchive/libarchiveplugin.cpp @@ -831,6 +831,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 e3e30778f..637d2ef98 100644 --- a/3rdparty/libminizipplugin/libminizipplugin.cpp +++ b/3rdparty/libminizipplugin/libminizipplugin.cpp @@ -293,6 +293,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()); // 是否为文件夹 diff --git a/3rdparty/libzipplugin/libzipplugin.cpp b/3rdparty/libzipplugin/libzipplugin.cpp index 472b05cb9..2db90488e 100644 --- a/3rdparty/libzipplugin/libzipplugin.cpp +++ b/3rdparty/libzipplugin/libzipplugin.cpp @@ -910,6 +910,12 @@ 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 detected):" << strDestFileName; + return ET_FileWriteError; + } + QFile file(strDestFileName); // Store parent mtime.