Parcourir la source

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.
Leslie P. Polzer il y a 2 semaines
Parent
commit
03f19aa4ea

+ 4 - 0
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,

+ 4 - 0
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.

+ 7 - 0
Help/release/dev/archive-path-traversal.rst

@@ -0,0 +1,7 @@
+archive-path-traversal
+----------------------
+
+* The :manual:`cmake(1)` :option:`-E tar <cmake-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.

+ 2 - 1
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;
     }

+ 2 - 1
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=$<TARGET_FILE:pseudo_llvm-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))

+ 6 - 0
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()

+ 7 - 0
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$

+ 57 - 0
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()

+ 6 - 0
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$

+ 57 - 0
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()

+ 6 - 0
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()

+ 1 - 0
Tests/RunCMake/File_Archive/path-absolute-result.txt

@@ -0,0 +1 @@
+1

+ 10 - 0
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$

+ 52 - 0
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()

+ 1 - 0
Tests/RunCMake/File_Archive/path-traversal-result.txt

@@ -0,0 +1 @@
+1

+ 9 - 0
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$

+ 52 - 0
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()