mirror of
https://github.com/Kitware/CMake.git
synced 2026-07-02 12:47:48 +00:00
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.
This commit is contained in:
committed by
Brad King
parent
be2ac223b0
commit
03f19aa4ea
@@ -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,
|
||||
|
||||
@@ -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
Help/release/dev/archive-path-traversal.rst
Normal file
7
Help/release/dev/archive-path-traversal.rst
Normal file
@@ -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.
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
Tests/RunCMake/CommandLineTar/path-absolute-stderr.txt
Normal file
7
Tests/RunCMake/CommandLineTar/path-absolute-stderr.txt
Normal file
@@ -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
Tests/RunCMake/CommandLineTar/path-absolute.cmake
Normal file
57
Tests/RunCMake/CommandLineTar/path-absolute.cmake
Normal file
@@ -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
Tests/RunCMake/CommandLineTar/path-traversal-stderr.txt
Normal file
6
Tests/RunCMake/CommandLineTar/path-traversal-stderr.txt
Normal file
@@ -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
Tests/RunCMake/CommandLineTar/path-traversal.cmake
Normal file
57
Tests/RunCMake/CommandLineTar/path-traversal.cmake
Normal file
@@ -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()
|
||||
@@ -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
Tests/RunCMake/File_Archive/path-absolute-result.txt
Normal file
1
Tests/RunCMake/File_Archive/path-absolute-result.txt
Normal file
@@ -0,0 +1 @@
|
||||
1
|
||||
10
Tests/RunCMake/File_Archive/path-absolute-stderr.txt
Normal file
10
Tests/RunCMake/File_Archive/path-absolute-stderr.txt
Normal file
@@ -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
Tests/RunCMake/File_Archive/path-absolute.cmake
Normal file
52
Tests/RunCMake/File_Archive/path-absolute.cmake
Normal file
@@ -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
Tests/RunCMake/File_Archive/path-traversal-result.txt
Normal file
1
Tests/RunCMake/File_Archive/path-traversal-result.txt
Normal file
@@ -0,0 +1 @@
|
||||
1
|
||||
9
Tests/RunCMake/File_Archive/path-traversal-stderr.txt
Normal file
9
Tests/RunCMake/File_Archive/path-traversal-stderr.txt
Normal file
@@ -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
Tests/RunCMake/File_Archive/path-traversal.cmake
Normal file
52
Tests/RunCMake/File_Archive/path-traversal.cmake
Normal file
@@ -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()
|
||||
Reference in New Issue
Block a user