Normalize path before writing file when extracting tar.gz (#4752)
diff --git a/lib/src/io.dart b/lib/src/io.dart index 04e3b05..8c85ca1 100644 --- a/lib/src/io.dart +++ b/lib/src/io.dart
@@ -1159,11 +1159,13 @@ while (await reader.moveNext()) { final entry = reader.current; - final filePath = p.joinAll([ - destination, - // Tar file names always use forward slashes - ...p.posix.split(entry.name), - ]); + final filePath = p.normalize( + p.joinAll([ + destination, + // Tar file names always use forward slashes + ...p.posix.split(entry.name), + ]), + ); if (!paths.add(filePath)) { // The tar file contained the same entry twice. Assume it is broken. await reader.cancel();
diff --git a/test/io_test.dart b/test/io_test.dart index bc6275a..7551763 100644 --- a/test/io_test.dart +++ b/test/io_test.dart
@@ -552,6 +552,41 @@ }); }); + test('avoid zip slip using combined symlink and ../', () { + return withTempDir((tempDir) async { + final entry = Stream<TarEntry>.fromIterable([ + TarEntry.data( + TarHeader( + name: 'nested/bad_link', + typeFlag: TypeFlag.symlink, + linkName: '../nested', + mode: _defaultMode, + ), + const [], + ), + TarEntry.data( + TarHeader( + name: 'nested/bad_link/../../payload.txt', + typeFlag: TypeFlag.reg, + mode: _defaultMode, + ), + utf8.encode('text content'), + ), + ]); + + await extractTarGz( + entry.transform(tarWriter).transform(gzip.encoder), + tempDir, + ); + // Make sure that the payload did not slip outside the destination via + // the symlink. + expect( + Directory(tempDir).listSync().map((x) => x.path), + contains(endsWith('payload.txt')), + ); + }); + }); + test('skips hardlinks escaping the tar file', () { return withTempDir((tempDir) async { final entry = Stream<TarEntry>.value(