Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions 3rdparty/interface/common.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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:)).+");
Expand Down
7 changes: 6 additions & 1 deletion 3rdparty/interface/common.h
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -59,4 +59,9 @@ class Common: public QObject
*/
bool IsMtpFileOrDirectory(QString path) noexcept;

/**
* 解压安全:校验目标绝对路径在真实文件系统解析后仍位于解压根目录内(防止符号链接路径逃逸)。
*/
bool extractPathIsWithinTarget(const QString &extractRoot, const QString &absoluteDestPath);

#endif
2 changes: 2 additions & 0 deletions 3rdparty/libarchive/libarchive/libarchiveplugin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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() )
Expand Down
8 changes: 8 additions & 0 deletions 3rdparty/libminizipplugin/libminizipplugin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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()); // 是否为文件夹
Expand Down
6 changes: 6 additions & 0 deletions 3rdparty/libzipplugin/libzipplugin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading