Skip to content

Commit 5d2fc16

Browse files
committed
Fixed the Zip Slip vulnerability in JlCompress
When extracting a file with a dangerous path like "../evil.exe" from a ZIP archive with JlCompress::extractDir(), the target file would be created outside of the target directory, potentially even overwriting an existing file there.
1 parent ad509e8 commit 5d2fc16

File tree

3 files changed

+30
-14
lines changed

3 files changed

+30
-14
lines changed

NEWS.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
QuaZIP changes
22

3+
* Current
4+
* Fixed the Zip Slip vulnerability in JlCompress
5+
36
* 0.7.5
47
* Fixed target_link_libraries call in CMakeLists
58
* Worked around a Qt 4.6 bug (QTBUG-15421) screwing up hidden

quazip/JlCompress.cpp

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -364,15 +364,19 @@ QStringList JlCompress::extractDir(QuaZip &zip, const QString &dir)
364364
if(!zip.open(QuaZip::mdUnzip)) {
365365
return QStringList();
366366
}
367-
368-
QDir directory(dir);
367+
QString cleanDir = QDir::cleanPath(dir);
368+
QDir directory(cleanDir);
369+
QString absCleanDir = directory.absolutePath();
369370
QStringList extracted;
370371
if (!zip.goToFirstFile()) {
371372
return QStringList();
372373
}
373374
do {
374375
QString name = zip.getCurrentFileName();
375376
QString absFilePath = directory.absoluteFilePath(name);
377+
QString absCleanPath = QDir::cleanPath(absFilePath);
378+
if (!absCleanPath.startsWith(absCleanDir + "/"))
379+
continue;
376380
if (!extractFile(&zip, "", absFilePath)) {
377381
removeFile(extracted);
378382
return QStringList();

qztest/testjlcompress.cpp

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -302,17 +302,25 @@ void TestJlCompress::extractDir_data()
302302
{
303303
QTest::addColumn<QString>("zipName");
304304
QTest::addColumn<QStringList>("fileNames");
305-
QTest::newRow("simple") << "jlextdir.zip" << (
306-
QStringList() << "test0.txt" << "testdir1/test1.txt"
305+
QTest::addColumn<QStringList>("expectedExtracted");
306+
QTest::newRow("simple") << "jlextdir.zip"
307+
<< (QStringList() << "test0.txt" << "testdir1/test1.txt"
308+
<< "testdir2/test2.txt" << "testdir2/subdir/test2sub.txt")
309+
<< (QStringList() << "test0.txt" << "testdir1/test1.txt"
307310
<< "testdir2/test2.txt" << "testdir2/subdir/test2sub.txt");
308-
QTest::newRow("separate dir") << "sepdir.zip" << (
309-
QStringList() << "laj/" << "laj/lajfile.txt");
311+
QTest::newRow("separate dir") << "sepdir.zip"
312+
<< (QStringList() << "laj/" << "laj/lajfile.txt")
313+
<< (QStringList() << "laj/" << "laj/lajfile.txt");
314+
QTest::newRow("Zip Slip") << "zipslip.zip"
315+
<< (QStringList() << "test0.txt" << "../zipslip.txt")
316+
<< (QStringList() << "test0.txt");
310317
}
311318

312319
void TestJlCompress::extractDir()
313320
{
314321
QFETCH(QString, zipName);
315322
QFETCH(QStringList, fileNames);
323+
QFETCH(QStringList, expectedExtracted);
316324
QDir curDir;
317325
if (!curDir.mkpath("jlext/jldir")) {
318326
QFAIL("Couldn't mkpath jlext/jldir");
@@ -325,17 +333,18 @@ void TestJlCompress::extractDir()
325333
}
326334
QStringList extracted;
327335
QCOMPARE((extracted = JlCompress::extractDir(zipName, "jlext/jldir"))
328-
.count(), fileNames.count());
329-
foreach (QString fileName, fileNames) {
330-
QString fullName = "jlext/jldir/" + fileName;
336+
.count(), expectedExtracted.count());
337+
const QString dir = "jlext/jldir/";
338+
foreach (QString fileName, expectedExtracted) {
339+
QString fullName = dir + fileName;
331340
QFileInfo fileInfo(fullName);
332341
QFileInfo extInfo("tmp/" + fileName);
333342
if (!fileInfo.isDir())
334343
QCOMPARE(fileInfo.size(), extInfo.size());
335344
QCOMPARE(fileInfo.permissions(), extInfo.permissions());
336345
curDir.remove(fullName);
337346
curDir.rmpath(fileInfo.dir().path());
338-
QString absolutePath = fileInfo.absoluteFilePath();
347+
QString absolutePath = QDir(dir).absoluteFilePath(fileName);
339348
if (fileInfo.isDir() && !absolutePath.endsWith('/'))
340349
absolutePath += '/';
341350
QVERIFY(extracted.contains(absolutePath));
@@ -344,17 +353,17 @@ void TestJlCompress::extractDir()
344353
QFile zipFile(zipName);
345354
QVERIFY(zipFile.open(QIODevice::ReadOnly));
346355
QCOMPARE((extracted = JlCompress::extractDir(&zipFile, "jlext/jldir"))
347-
.count(), fileNames.count());
348-
foreach (QString fileName, fileNames) {
349-
QString fullName = "jlext/jldir/" + fileName;
356+
.count(), expectedExtracted.count());
357+
foreach (QString fileName, expectedExtracted) {
358+
QString fullName = dir + fileName;
350359
QFileInfo fileInfo(fullName);
351360
QFileInfo extInfo("tmp/" + fileName);
352361
if (!fileInfo.isDir())
353362
QCOMPARE(fileInfo.size(), extInfo.size());
354363
QCOMPARE(fileInfo.permissions(), extInfo.permissions());
355364
curDir.remove(fullName);
356365
curDir.rmpath(fileInfo.dir().path());
357-
QString absolutePath = fileInfo.absoluteFilePath();
366+
QString absolutePath = QDir(dir).absoluteFilePath(fileName);
358367
if (fileInfo.isDir() && !absolutePath.endsWith('/'))
359368
absolutePath += '/';
360369
QVERIFY(extracted.contains(absolutePath));

0 commit comments

Comments
 (0)