From 30daf048b2c01dc4386b4bad18bd960ddf836991 Mon Sep 17 00:00:00 2001 From: Trial97 Date: Sun, 15 Feb 2026 10:22:37 +0200 Subject: [PATCH] fix zip path traversal this makes sure that the file is in the given root Signed-off-by: Trial97 (cherry picked from commit 56936cf48549b4f750bc20ccac824d00856d6699) --- launcher/MMCZip.cpp | 2 +- launcher/archive/ArchiveReader.cpp | 36 ++++++++++++++++++++ launcher/archive/ArchiveReader.h | 3 ++ launcher/archive/ExtractZipTask.cpp | 2 +- launcher/minecraft/launch/ExtractNatives.cpp | 2 +- 5 files changed, 42 insertions(+), 3 deletions(-) diff --git a/launcher/MMCZip.cpp b/launcher/MMCZip.cpp index 5e962d12a..64be89643 100644 --- a/launcher/MMCZip.cpp +++ b/launcher/MMCZip.cpp @@ -233,7 +233,7 @@ std::optional extractSubDir(ArchiveReader* zip, const QString& subd << target; return false; } - if (!f->writeFile(ext, target_file_path)) { + if (!f->writeFile(ext, target_file_path, target)) { qWarning() << "Failed to extract file" << original_name << "to" << target_file_path; return false; } diff --git a/launcher/archive/ArchiveReader.cpp b/launcher/archive/ArchiveReader.cpp index 1f87d8237..9b68e1cf3 100644 --- a/launcher/archive/ArchiveReader.cpp +++ b/launcher/archive/ArchiveReader.cpp @@ -23,7 +23,9 @@ #include #include #include +#include #include +#include namespace MMCZip { QStringList ArchiveReader::getFiles() @@ -128,7 +130,37 @@ static int copy_data(struct archive* ar, struct archive* aw, bool notBlock = fal } } +bool willEscapeRoot(const QDir& root, archive_entry* entry) +{ + const char* entryPathC = archive_entry_pathname(entry); + const char* linkTargetC = archive_entry_symlink(entry); + const char* hardlinkC = archive_entry_hardlink(entry); + + if (!entryPathC || (!linkTargetC && !hardlinkC)) + return false; + + QString entryPath = QString::fromUtf8(entryPathC); + QString linkTarget = linkTargetC ? QString::fromUtf8(linkTargetC) : QString::fromUtf8(hardlinkC); + + QString linkFullPath = root.filePath(entryPath); + auto rootDir = QUrl::fromLocalFile(root.absolutePath()); + + if (!rootDir.isParentOf(QUrl::fromLocalFile(linkFullPath))) + return true; + + QDir linkDir = QFileInfo(linkFullPath).dir(); + if (!QDir::isAbsolutePath(linkTarget)) { + linkTarget = (linkTargetC ? linkDir : root).filePath(linkTarget); + } + return !rootDir.isParentOf(QUrl::fromLocalFile(QDir::cleanPath(linkTarget))); +} + bool ArchiveReader::File::writeFile(archive* out, QString targetFileName, bool notBlock) +{ + return writeFile(out, targetFileName, {}, notBlock); +}; + +bool ArchiveReader::File::writeFile(archive* out, QString targetFileName, std::optional root, bool notBlock) { auto entry = m_entry; std::unique_ptr entryClone(nullptr, &archive_entry_free); @@ -138,6 +170,10 @@ bool ArchiveReader::File::writeFile(archive* out, QString targetFileName, bool n auto nameUtf8 = targetFileName.toUtf8(); archive_entry_set_pathname_utf8(entry, nameUtf8.constData()); } + if (root.has_value() && willEscapeRoot(root.value(), entry)) { + qCritical() << "Failed to write header to entry:" << filename() << "-" << "file outside root"; + return false; + } if (archive_write_header(out, entry) < ARCHIVE_OK) { qCritical() << "Failed to write header to entry:" << filename() << "-" << archive_error_string(out); return false; diff --git a/launcher/archive/ArchiveReader.h b/launcher/archive/ArchiveReader.h index aaeba6095..3a7fb51c6 100644 --- a/launcher/archive/ArchiveReader.h +++ b/launcher/archive/ArchiveReader.h @@ -19,8 +19,10 @@ #include #include +#include #include #include +#include struct archive; struct archive_entry; @@ -49,6 +51,7 @@ class ArchiveReader { QByteArray readAll(int* outStatus = nullptr); bool skip(); bool writeFile(archive* out, QString targetFileName = "", bool notBlock = false); + bool writeFile(archive* out, QString targetFileName, std::optional root, bool notBlock = false); private: int readNextHeader(); diff --git a/launcher/archive/ExtractZipTask.cpp b/launcher/archive/ExtractZipTask.cpp index acbcd39d5..35dc39d90 100644 --- a/launcher/archive/ExtractZipTask.cpp +++ b/launcher/archive/ExtractZipTask.cpp @@ -95,7 +95,7 @@ auto ExtractZipTask::extractZip() -> ZipResult return false; } - if (!f->writeFile(ext, target_file_path)) { + if (!f->writeFile(ext, target_file_path, target)) { result = ZipResult(tr("Failed to extract file %1 to %2").arg(original_name, target_file_path)); return false; } diff --git a/launcher/minecraft/launch/ExtractNatives.cpp b/launcher/minecraft/launch/ExtractNatives.cpp index f9c951135..17d1a8cda 100644 --- a/launcher/minecraft/launch/ExtractNatives.cpp +++ b/launcher/minecraft/launch/ExtractNatives.cpp @@ -53,7 +53,7 @@ static bool unzipNatives(QString source, QString targetFolder, bool applyJnilibH name = replaceSuffix(name, ".jnilib", ".dylib"); } QString absFilePath = directory.absoluteFilePath(name); - return f->writeFile(ext, absFilePath); + return f->writeFile(ext, absFilePath, directory); }); }