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
46 changes: 9 additions & 37 deletions 3rdparty/interface/common.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -521,60 +521,32 @@ 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();
QString rootCanon = rootFi.canonicalFilePath();
// 如果 canonical 失败,使用绝对路径作为后备
if (rootCanon.isEmpty()) {
return false;
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 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()) {
Expand All @@ -583,7 +555,7 @@ bool symlinkTargetIsWithinTarget(const QString &extractRoot, const QString &syml
path = parent;
}

return resolved.startsWith(rootCanon + QDir::separator()) || resolved == rootCanon;
return rootFi.exists();
}

bool IsMtpFileOrDirectory(QString path) noexcept {
Expand Down
5 changes: 0 additions & 5 deletions 3rdparty/interface/common.h
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,4 @@ 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
13 changes: 0 additions & 13 deletions 3rdparty/libminizipplugin/libminizipplugin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -301,19 +301,6 @@ 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) { // 文件夹
Expand Down
9 changes: 2 additions & 7 deletions 3rdparty/libzipplugin/libzipplugin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -895,8 +895,9 @@ 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;
qInfo() << "Rejected path (symlink escape detected):" << strDestFileName;
return ET_FileWriteError;
}

Expand Down Expand Up @@ -956,12 +957,6 @@ 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 {
Expand Down
Loading