From 03f19aa4ea851f8aa07736dbc75e9e63a0e421a4 Mon Sep 17 00:00:00 2001 From: "Leslie P. Polzer" Date: Tue, 6 Jan 2026 02:13:07 +0000 Subject: [PATCH] cmSystemTools: Fix path traversal vulnerability in archive extraction Add security flags to libarchive extraction to prevent path traversal (Zip Slip) and absolute path attacks: - ARCHIVE_EXTRACT_SECURE_NODOTDOT: Block ".." path components - ARCHIVE_EXTRACT_SECURE_NOABSOLUTEPATHS: Block absolute paths - ARCHIVE_EXTRACT_SECURE_SYMLINKS: Block symlinks escaping extract dir This hardens both `cmake -E tar` and `file(ARCHIVE_EXTRACT)` against malicious archives that attempt to write files outside the intended extraction directory. --- Help/command/file.rst | 4 ++ Help/manual/cmake.1.rst | 4 ++ Help/release/dev/archive-path-traversal.rst | 7 +++ Source/cmSystemTools.cxx | 3 +- Tests/RunCMake/CMakeLists.txt | 3 +- .../CommandLineTar/RunCMakeTest.cmake | 6 ++ .../CommandLineTar/path-absolute-stderr.txt | 7 +++ .../CommandLineTar/path-absolute.cmake | 57 +++++++++++++++++++ .../CommandLineTar/path-traversal-stderr.txt | 6 ++ .../CommandLineTar/path-traversal.cmake | 57 +++++++++++++++++++ .../RunCMake/File_Archive/RunCMakeTest.cmake | 6 ++ .../File_Archive/path-absolute-result.txt | 1 + .../File_Archive/path-absolute-stderr.txt | 10 ++++ .../RunCMake/File_Archive/path-absolute.cmake | 52 +++++++++++++++++ .../File_Archive/path-traversal-result.txt | 1 + .../File_Archive/path-traversal-stderr.txt | 9 +++ .../File_Archive/path-traversal.cmake | 52 +++++++++++++++++ 17 files changed, 283 insertions(+), 2 deletions(-) create mode 100644 Help/release/dev/archive-path-traversal.rst create mode 100644 Tests/RunCMake/CommandLineTar/path-absolute-stderr.txt create mode 100644 Tests/RunCMake/CommandLineTar/path-absolute.cmake create mode 100644 Tests/RunCMake/CommandLineTar/path-traversal-stderr.txt create mode 100644 Tests/RunCMake/CommandLineTar/path-traversal.cmake create mode 100644 Tests/RunCMake/File_Archive/path-absolute-result.txt create mode 100644 Tests/RunCMake/File_Archive/path-absolute-stderr.txt create mode 100644 Tests/RunCMake/File_Archive/path-absolute.cmake create mode 100644 Tests/RunCMake/File_Archive/path-traversal-result.txt create mode 100644 Tests/RunCMake/File_Archive/path-traversal-stderr.txt create mode 100644 Tests/RunCMake/File_Archive/path-traversal.cmake diff --git a/Help/command/file.rst b/Help/command/file.rst index 8aa485d3bd..b01b200315 100644 --- a/Help/command/file.rst +++ b/Help/command/file.rst @@ -1065,6 +1065,10 @@ Archiving ``VERBOSE`` Enable verbose output from the extraction operation. + .. versionchanged:: 4.3 + Archive entries containing path traversal sequences (``..``), or + absolute paths, are rejected for security. + .. note:: The working directory for this subcommand is the ``DESTINATION`` directory (provided or computed) except when ``LIST_ONLY`` is specified. Therefore, diff --git a/Help/manual/cmake.1.rst b/Help/manual/cmake.1.rst index 0276abd351..e67891ae7e 100644 --- a/Help/manual/cmake.1.rst +++ b/Help/manual/cmake.1.rst @@ -1454,6 +1454,10 @@ Available commands are: When extracting selected files or directories, you must provide their exact names including the path, as printed by list (``-t``). + .. versionchanged:: 4.3 + Archive entries containing path traversal sequences (``..``), or + absolute paths, are rejected for security. + .. option:: t List archive contents. diff --git a/Help/release/dev/archive-path-traversal.rst b/Help/release/dev/archive-path-traversal.rst new file mode 100644 index 0000000000..57f8b69ca0 --- /dev/null +++ b/Help/release/dev/archive-path-traversal.rst @@ -0,0 +1,7 @@ +archive-path-traversal +---------------------- + +* The :manual:`cmake(1)` :option:`-E tar ` command-line tool, + and the :command:`file(ARCHIVE_EXTRACT)` command, now reject archive + entries whose paths are absolute or contain ``..`` path traversal + components. diff --git a/Source/cmSystemTools.cxx b/Source/cmSystemTools.cxx index 330bea8ccd..950dc94dcd 100644 --- a/Source/cmSystemTools.cxx +++ b/Source/cmSystemTools.cxx @@ -2637,7 +2637,8 @@ bool extract_tar(std::string const& outFileName, struct archive* a = archive_read_new(); struct archive* ext = archive_write_disk_new(); if (extract) { - int flags = 0; + int flags = ARCHIVE_EXTRACT_SECURE_NODOTDOT | + ARCHIVE_EXTRACT_SECURE_NOABSOLUTEPATHS | ARCHIVE_EXTRACT_SECURE_SYMLINKS; if (extractTimestamps == cmSystemTools::cmTarExtractTimestamps::Yes) { flags |= ARCHIVE_EXTRACT_TIME; } diff --git a/Tests/RunCMake/CMakeLists.txt b/Tests/RunCMake/CMakeLists.txt index 3893598db2..8e4c7459a2 100644 --- a/Tests/RunCMake/CMakeLists.txt +++ b/Tests/RunCMake/CMakeLists.txt @@ -979,7 +979,7 @@ endif() add_RunCMake_test(LinkLibrariesProcessing) add_RunCMake_test(LinkLibrariesStrategy) -add_RunCMake_test(File_Archive) +add_RunCMake_test(File_Archive -DPython_EXECUTABLE=${Python_EXECUTABLE}) add_RunCMake_test(File_Configure) add_RunCMake_test(File_Generate) add_RunCMake_test(ExportWithoutLanguage) @@ -1075,6 +1075,7 @@ add_RunCMake_test(CommandLine -DLLVM_RC=$ -DCMAKE_SY if(CMake_TEST_LibArchive_VERSION) list(APPEND CommandLineTar_ARGS -DCMake_TEST_LibArchive_VERSION=${CMake_TEST_LibArchive_VERSION}) endif() +list(APPEND CommandLineTar_ARGS -DPython_EXECUTABLE=${Python_EXECUTABLE}) add_RunCMake_test(CommandLineTar) if(CMAKE_PLATFORM_NO_VERSIONED_SONAME OR (NOT CMAKE_SHARED_LIBRARY_SONAME_FLAG AND NOT CMAKE_SHARED_LIBRARY_SONAME_C_FLAG)) diff --git a/Tests/RunCMake/CommandLineTar/RunCMakeTest.cmake b/Tests/RunCMake/CommandLineTar/RunCMakeTest.cmake index 9669fb609d..e2b0ca72f3 100644 --- a/Tests/RunCMake/CommandLineTar/RunCMakeTest.cmake +++ b/Tests/RunCMake/CommandLineTar/RunCMakeTest.cmake @@ -127,3 +127,9 @@ run_cmake(set-mtime) # Use the --touch option to avoid extracting the mtime run_cmake(touch-mtime) + +# Security: Test path traversal protection +if(Python_EXECUTABLE) + run_cmake_script(path-absolute -DPython_EXECUTABLE=${Python_EXECUTABLE}) + run_cmake_script(path-traversal -DPython_EXECUTABLE=${Python_EXECUTABLE}) +endif() diff --git a/Tests/RunCMake/CommandLineTar/path-absolute-stderr.txt b/Tests/RunCMake/CommandLineTar/path-absolute-stderr.txt new file mode 100644 index 0000000000..b9abace428 --- /dev/null +++ b/Tests/RunCMake/CommandLineTar/path-absolute-stderr.txt @@ -0,0 +1,7 @@ +^CMake Error: Problem with archive_write_header\(\): Path is absolute +CMake Error: Current file: + [^ +]*/Tests/RunCMake/CommandLineTar/path-absolute-build/SHOULD_NOT_EXIST_ABS\.txt +CMake Error: Problem extracting tar: + [^ +]*/Tests/RunCMake/CommandLineTar/path-absolute-build/malicious_abs\.tar$ diff --git a/Tests/RunCMake/CommandLineTar/path-absolute.cmake b/Tests/RunCMake/CommandLineTar/path-absolute.cmake new file mode 100644 index 0000000000..eb0cffeeb5 --- /dev/null +++ b/Tests/RunCMake/CommandLineTar/path-absolute.cmake @@ -0,0 +1,57 @@ +# Test that absolute path attacks are blocked during extraction + +set(EXTRACT_DIR "${CMAKE_CURRENT_BINARY_DIR}/extract_dir_abs") +# Use an absolute path within the build tree (but outside EXTRACT_DIR) +set(MALICIOUS_FILE "${CMAKE_CURRENT_BINARY_DIR}/SHOULD_NOT_EXIST_ABS.txt") + +# Clean up +file(REMOVE_RECURSE "${EXTRACT_DIR}") +file(REMOVE "${MALICIOUS_FILE}") +file(MAKE_DIRECTORY "${EXTRACT_DIR}") + +# Create a malicious tar archive using Python +# The archive contains a file with an absolute path +set(MALICIOUS_TAR "${CMAKE_CURRENT_BINARY_DIR}/malicious_abs.tar") +file(REMOVE "${MALICIOUS_TAR}") + +execute_process( + COMMAND "${Python_EXECUTABLE}" -c [==[ +import sys +import tarfile +import io + +# Create a tar archive in memory +tar_data = io.BytesIO() +with tarfile.open(fileobj=tar_data, mode='w') as tar: + # Add a file with absolute path + data = b'malicious content' + info = tarfile.TarInfo(name=sys.argv[2]) + info.size = len(data) + tar.addfile(info, io.BytesIO(data)) + +# Write to file +with open(sys.argv[1], 'wb') as f: + f.write(tar_data.getvalue()) +]==] "${MALICIOUS_TAR}" "${MALICIOUS_FILE}" + RESULT_VARIABLE result +) + +if(NOT result EQUAL 0) + message(FATAL_ERROR "Failed to create malicious tar archive") +endif() + +# Try to extract the malicious archive +execute_process( + COMMAND "${CMAKE_COMMAND}" -E tar xf "${MALICIOUS_TAR}" + WORKING_DIRECTORY "${EXTRACT_DIR}" + RESULT_VARIABLE extract_result +) + +# The file should not exist at the absolute path +if(EXISTS "${MALICIOUS_FILE}") + message(FATAL_ERROR "PATH TRAVERSAL VULNERABILITY: File was created outside extraction directory!") +endif() + +if(extract_result EQUAL 0) + message(FATAL_ERROR "Extraction of malicious path did not fail!") +endif() diff --git a/Tests/RunCMake/CommandLineTar/path-traversal-stderr.txt b/Tests/RunCMake/CommandLineTar/path-traversal-stderr.txt new file mode 100644 index 0000000000..02739a3633 --- /dev/null +++ b/Tests/RunCMake/CommandLineTar/path-traversal-stderr.txt @@ -0,0 +1,6 @@ +^CMake Error: Problem with archive_write_header\(\): Path contains '\.\.' +CMake Error: Current file: + \.\./SHOULD_NOT_EXIST.txt +CMake Error: Problem extracting tar: + [^ +]*/Tests/RunCMake/CommandLineTar/path-traversal-build/malicious\.tar$ diff --git a/Tests/RunCMake/CommandLineTar/path-traversal.cmake b/Tests/RunCMake/CommandLineTar/path-traversal.cmake new file mode 100644 index 0000000000..d8b60bd2cb --- /dev/null +++ b/Tests/RunCMake/CommandLineTar/path-traversal.cmake @@ -0,0 +1,57 @@ +# Test that path traversal attacks are blocked during extraction + +set(EXTRACT_DIR "${CMAKE_CURRENT_BINARY_DIR}/extract_dir") +set(PARENT_DIR "${CMAKE_CURRENT_BINARY_DIR}") +set(MALICIOUS_FILE "${PARENT_DIR}/SHOULD_NOT_EXIST.txt") + +# Clean up +file(REMOVE_RECURSE "${EXTRACT_DIR}") +file(REMOVE "${MALICIOUS_FILE}") +file(MAKE_DIRECTORY "${EXTRACT_DIR}") + +# Create a malicious tar archive using Python +# The archive contains a file with path "../SHOULD_NOT_EXIST.txt" +set(MALICIOUS_TAR "${CMAKE_CURRENT_BINARY_DIR}/malicious.tar") +file(REMOVE "${MALICIOUS_TAR}") + +execute_process( + COMMAND "${Python_EXECUTABLE}" -c [==[ +import sys +import tarfile +import io + +# Create a tar archive in memory +tar_data = io.BytesIO() +with tarfile.open(fileobj=tar_data, mode='w') as tar: + # Add a file with path traversal + data = b'malicious content' + info = tarfile.TarInfo(name='../SHOULD_NOT_EXIST.txt') + info.size = len(data) + tar.addfile(info, io.BytesIO(data)) + +# Write to file +with open(sys.argv[1], 'wb') as f: + f.write(tar_data.getvalue()) +]==] "${MALICIOUS_TAR}" + RESULT_VARIABLE result +) + +if(NOT result EQUAL 0) + message(FATAL_ERROR "Failed to create malicious tar archive") +endif() + +# Try to extract the malicious archive +execute_process( + COMMAND "${CMAKE_COMMAND}" -E tar xf "${MALICIOUS_TAR}" + WORKING_DIRECTORY "${EXTRACT_DIR}" + RESULT_VARIABLE extract_result +) + +# The extraction should fail or the file should not exist outside extract dir +if(EXISTS "${MALICIOUS_FILE}") + message(FATAL_ERROR "PATH TRAVERSAL VULNERABILITY: File was created outside extraction directory!") +endif() + +if(extract_result EQUAL 0) + message(FATAL_ERROR "Extraction of malicious path did not fail!") +endif() diff --git a/Tests/RunCMake/File_Archive/RunCMakeTest.cmake b/Tests/RunCMake/File_Archive/RunCMakeTest.cmake index 03eb153e9a..0602db1a01 100644 --- a/Tests/RunCMake/File_Archive/RunCMakeTest.cmake +++ b/Tests/RunCMake/File_Archive/RunCMakeTest.cmake @@ -52,3 +52,9 @@ run_cmake(pax-xz-compression-level) run_cmake(pax-zstd-compression-level) run_cmake(paxr-bz2-compression-level) run_cmake(zip-deflate-compression-level) + +# Security: Test path traversal protection +if(Python_EXECUTABLE) + run_cmake_script(path-absolute -DPython_EXECUTABLE=${Python_EXECUTABLE}) + run_cmake_script(path-traversal -DPython_EXECUTABLE=${Python_EXECUTABLE}) +endif() diff --git a/Tests/RunCMake/File_Archive/path-absolute-result.txt b/Tests/RunCMake/File_Archive/path-absolute-result.txt new file mode 100644 index 0000000000..d00491fd7e --- /dev/null +++ b/Tests/RunCMake/File_Archive/path-absolute-result.txt @@ -0,0 +1 @@ +1 diff --git a/Tests/RunCMake/File_Archive/path-absolute-stderr.txt b/Tests/RunCMake/File_Archive/path-absolute-stderr.txt new file mode 100644 index 0000000000..ddd4afbf47 --- /dev/null +++ b/Tests/RunCMake/File_Archive/path-absolute-stderr.txt @@ -0,0 +1,10 @@ +^CMake Error: Problem with archive_write_header\(\): Path is absolute +CMake Error: Current file: + [^ +]*/Tests/RunCMake/File_Archive/path-absolute-build/SHOULD_NOT_EXIST_ABS\.txt +CMake Error at [^ +]*/Tests/RunCMake/File_Archive/path-absolute\.cmake:[0-9]+ \(file\): + file failed to extract: + + [^ +]*/Tests/RunCMake/File_Archive/path-absolute-build/malicious_abs\.tar$ diff --git a/Tests/RunCMake/File_Archive/path-absolute.cmake b/Tests/RunCMake/File_Archive/path-absolute.cmake new file mode 100644 index 0000000000..8886946c65 --- /dev/null +++ b/Tests/RunCMake/File_Archive/path-absolute.cmake @@ -0,0 +1,52 @@ +# Test that path traversal attacks are blocked during file(ARCHIVE_EXTRACT) + +set(EXTRACT_DIR "${CMAKE_CURRENT_BINARY_DIR}/extract_dir_abs") +# Use an absolute path within the build tree (but outside EXTRACT_DIR) +set(MALICIOUS_FILE "${CMAKE_CURRENT_BINARY_DIR}/SHOULD_NOT_EXIST_ABS.txt") + +# Clean up +file(REMOVE_RECURSE "${EXTRACT_DIR}") +file(REMOVE "${MALICIOUS_FILE}") +file(MAKE_DIRECTORY "${EXTRACT_DIR}") + +# Create a malicious tar archive using Python +# The archive contains a file with an absolute path +set(MALICIOUS_TAR "${CMAKE_CURRENT_BINARY_DIR}/malicious_abs.tar") +file(REMOVE "${MALICIOUS_TAR}") + +execute_process( + COMMAND "${Python_EXECUTABLE}" -c [==[ +import sys +import tarfile +import io + +# Create a tar archive in memory +tar_data = io.BytesIO() +with tarfile.open(fileobj=tar_data, mode='w') as tar: + # Add a file with absolute path + data = b'malicious content' + info = tarfile.TarInfo(name=sys.argv[2]) + info.size = len(data) + tar.addfile(info, io.BytesIO(data)) + +# Write to file +with open(sys.argv[1], 'wb') as f: + f.write(tar_data.getvalue()) +]==] "${MALICIOUS_TAR}" "${MALICIOUS_FILE}" + RESULT_VARIABLE result +) + +if(NOT result EQUAL 0) + message(FATAL_ERROR "Failed to create malicious tar archive") +endif() + +# Try to extract the malicious archive using file(ARCHIVE_EXTRACT) +file(ARCHIVE_EXTRACT + INPUT "${MALICIOUS_TAR}" + DESTINATION "${EXTRACT_DIR}" +) + +# The file should not exist outside the extraction directory +if(EXISTS "${MALICIOUS_FILE}") + message(FATAL_ERROR "PATH TRAVERSAL VULNERABILITY: File was created outside extraction directory!") +endif() diff --git a/Tests/RunCMake/File_Archive/path-traversal-result.txt b/Tests/RunCMake/File_Archive/path-traversal-result.txt new file mode 100644 index 0000000000..d00491fd7e --- /dev/null +++ b/Tests/RunCMake/File_Archive/path-traversal-result.txt @@ -0,0 +1 @@ +1 diff --git a/Tests/RunCMake/File_Archive/path-traversal-stderr.txt b/Tests/RunCMake/File_Archive/path-traversal-stderr.txt new file mode 100644 index 0000000000..ebbe84ac53 --- /dev/null +++ b/Tests/RunCMake/File_Archive/path-traversal-stderr.txt @@ -0,0 +1,9 @@ +^CMake Error: Problem with archive_write_header\(\): Path contains '\.\.' +CMake Error: Current file: + \.\./SHOULD_NOT_EXIST\.txt +CMake Error at [^ +]*/Tests/RunCMake/File_Archive/path-traversal\.cmake:[0-9]+ \(file\): + file failed to extract: + + [^ +]*/Tests/RunCMake/File_Archive/path-traversal-build/malicious\.tar$ diff --git a/Tests/RunCMake/File_Archive/path-traversal.cmake b/Tests/RunCMake/File_Archive/path-traversal.cmake new file mode 100644 index 0000000000..9b43bb7e10 --- /dev/null +++ b/Tests/RunCMake/File_Archive/path-traversal.cmake @@ -0,0 +1,52 @@ +# Test that path traversal attacks are blocked during file(ARCHIVE_EXTRACT) + +set(EXTRACT_DIR "${CMAKE_CURRENT_BINARY_DIR}/extract_dir") +set(PARENT_DIR "${CMAKE_CURRENT_BINARY_DIR}") +set(MALICIOUS_FILE "${PARENT_DIR}/SHOULD_NOT_EXIST.txt") + +# Clean up +file(REMOVE_RECURSE "${EXTRACT_DIR}") +file(REMOVE "${MALICIOUS_FILE}") +file(MAKE_DIRECTORY "${EXTRACT_DIR}") + +# Create a malicious tar archive using Python +# The archive contains a file with path "../SHOULD_NOT_EXIST.txt" +set(MALICIOUS_TAR "${CMAKE_CURRENT_BINARY_DIR}/malicious.tar") +file(REMOVE "${MALICIOUS_TAR}") + +execute_process( + COMMAND "${Python_EXECUTABLE}" -c [==[ +import sys +import tarfile +import io + +# Create a tar archive in memory +tar_data = io.BytesIO() +with tarfile.open(fileobj=tar_data, mode='w') as tar: + # Add a file with path traversal + data = b'malicious content' + info = tarfile.TarInfo(name='../SHOULD_NOT_EXIST.txt') + info.size = len(data) + tar.addfile(info, io.BytesIO(data)) + +# Write to file +with open(sys.argv[1], 'wb') as f: + f.write(tar_data.getvalue()) +]==] "${MALICIOUS_TAR}" + RESULT_VARIABLE result +) + +if(NOT result EQUAL 0) + message(FATAL_ERROR "Failed to create malicious tar archive") +endif() + +# Try to extract the malicious archive using file(ARCHIVE_EXTRACT) +file(ARCHIVE_EXTRACT + INPUT "${MALICIOUS_TAR}" + DESTINATION "${EXTRACT_DIR}" +) + +# The file should not exist outside the extraction directory +if(EXISTS "${MALICIOUS_FILE}") + message(FATAL_ERROR "PATH TRAVERSAL VULNERABILITY: File was created outside extraction directory!") +endif()