mirror of
https://github.com/zlib-ng/minizip-ng
synced 2026-08-25 14:26:08 -04:00
Fixed arbitrary file write vulnerability via symlinks. #936
This commit is contained in:
parent
3e5213bcde
commit
bb282ace22
4 changed files with 139 additions and 1 deletions
26
doc/mz_os.md
26
doc/mz_os.md
|
|
@ -14,6 +14,7 @@ These functions provide support for handling common file system operations.
|
|||
- [mz_path_remove_extension](#mz_path_remove_extension)
|
||||
- [mz_path_get_filename](#mz_path_get_filename)
|
||||
- [Directory](#directory)
|
||||
- [mz_dir_has_unsafe_symlink](#mz_dir_has_unsafe_symlink)
|
||||
- [mz_dir_make](#mz_dir_make)
|
||||
- [File](#file)
|
||||
- [mz_file_get_crc](#mz_file_get_crc)
|
||||
|
|
@ -284,6 +285,31 @@ else
|
|||
|
||||
## Directory
|
||||
|
||||
### mz_dir_has_unsafe_symlink
|
||||
|
||||
Checks if any existing component of a path is a symbolic link that escapes a base path. This function is used to prevent symlink-based path traversal attacks during archive extraction.
|
||||
|
||||
**Arguments**
|
||||
|Type|Name|Description|
|
||||
|-|-|-|
|
||||
|const char *|path|Path to check|
|
||||
|const char *|base_path|Base path that symlinks must not escape|
|
||||
|
||||
**Return**
|
||||
|Type|Description|
|
||||
|-|-|
|
||||
|int32_t|[MZ_ERROR](mz_error.md) code, MZ_OK if path is safe, MZ_EXIST_ERROR if an unsafe symlink is found.|
|
||||
|
||||
**Example**
|
||||
```
|
||||
const char *base_path = "/tmp/extract/";
|
||||
const char *file_path = "/tmp/extract/subdir/file.txt";
|
||||
if (mz_dir_has_unsafe_symlink(file_path, base_path) == MZ_OK)
|
||||
printf("Path is safe to write\n");
|
||||
else
|
||||
printf("Path contains unsafe symlink\n");
|
||||
```
|
||||
|
||||
### mz_dir_make
|
||||
|
||||
Creates a directory recursively.
|
||||
|
|
|
|||
100
mz_os.c
100
mz_os.c
|
|
@ -277,6 +277,106 @@ int32_t mz_path_get_filename(const char *path, const char **filename) {
|
|||
return MZ_OK;
|
||||
}
|
||||
|
||||
int32_t mz_dir_has_unsafe_symlink(const char *path, const char *base_path) {
|
||||
char *check_path = NULL;
|
||||
char *symlink_target = NULL;
|
||||
char *combined = NULL;
|
||||
char *resolved = NULL;
|
||||
size_t path_len = 0;
|
||||
size_t base_len = 0;
|
||||
size_t alloc_size = 0;
|
||||
size_t parent_len = 0;
|
||||
size_t pos = 0;
|
||||
int32_t err = MZ_OK;
|
||||
|
||||
if (!path || *path == 0 || !base_path)
|
||||
return MZ_PARAM_ERROR;
|
||||
|
||||
path_len = strlen(path);
|
||||
base_len = strlen(base_path);
|
||||
|
||||
/* Remove trailing slash from base_path for comparison */
|
||||
while (base_len > 0 && mz_os_is_dir_separator(base_path[base_len - 1]))
|
||||
base_len--;
|
||||
|
||||
/* Allocate buffers with max size needed: path + symlink target + separator */
|
||||
alloc_size = path_len * 2 + 2;
|
||||
|
||||
check_path = (char *)calloc(1, path_len + 1);
|
||||
symlink_target = (char *)calloc(1, path_len + 1);
|
||||
combined = (char *)calloc(1, alloc_size);
|
||||
resolved = (char *)calloc(1, alloc_size);
|
||||
|
||||
if (!check_path || !symlink_target || !combined || !resolved) {
|
||||
err = MZ_MEM_ERROR;
|
||||
}
|
||||
|
||||
/* Walk through each path component */
|
||||
while (err == MZ_OK && pos < path_len) {
|
||||
/* Copy separator if present */
|
||||
if (mz_os_is_dir_separator(path[pos])) {
|
||||
check_path[pos] = path[pos];
|
||||
pos++;
|
||||
}
|
||||
|
||||
/* Copy next path component */
|
||||
while (pos < path_len && !mz_os_is_dir_separator(path[pos])) {
|
||||
check_path[pos] = path[pos];
|
||||
pos++;
|
||||
}
|
||||
check_path[pos] = 0;
|
||||
|
||||
/* Check if this existing path component is a symlink */
|
||||
if (mz_os_is_symlink(check_path) != MZ_OK)
|
||||
continue;
|
||||
if (mz_os_read_symlink(check_path, symlink_target, (int32_t)(path_len + 1)) != MZ_OK)
|
||||
continue;
|
||||
|
||||
/* Absolute symlink targets are not allowed */
|
||||
if (mz_os_is_dir_separator(symlink_target[0])) {
|
||||
err = MZ_EXIST_ERROR;
|
||||
break;
|
||||
}
|
||||
|
||||
/* Find parent directory length by scanning backwards past filename and trailing slashes */
|
||||
parent_len = pos;
|
||||
while (parent_len > 0 && !mz_os_is_dir_separator(check_path[parent_len - 1]))
|
||||
parent_len--;
|
||||
while (parent_len > 0 && mz_os_is_dir_separator(check_path[parent_len - 1]))
|
||||
parent_len--;
|
||||
|
||||
/* Combine parent + symlink_target */
|
||||
combined[0] = 0;
|
||||
if (parent_len > 0) {
|
||||
strncpy(combined, check_path, parent_len);
|
||||
combined[parent_len] = 0;
|
||||
mz_path_append_slash(combined, (int32_t)alloc_size, MZ_PATH_SLASH_PLATFORM);
|
||||
}
|
||||
strncat(combined, symlink_target, alloc_size - strlen(combined) - 1);
|
||||
|
||||
/* Resolve the combined path to eliminate .. */
|
||||
if (mz_path_resolve(combined, resolved, (int32_t)alloc_size) != MZ_OK) {
|
||||
err = MZ_EXIST_ERROR;
|
||||
break;
|
||||
}
|
||||
|
||||
/* Check that resolved path starts with base_path */
|
||||
if (strlen(resolved) < base_len ||
|
||||
strncmp(resolved, base_path, base_len) != 0 ||
|
||||
(resolved[base_len] != 0 && !mz_os_is_dir_separator(resolved[base_len]))) {
|
||||
err = MZ_EXIST_ERROR;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
free(check_path);
|
||||
free(symlink_target);
|
||||
free(combined);
|
||||
free(resolved);
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
int32_t mz_dir_make(const char *path) {
|
||||
int32_t err = MZ_OK;
|
||||
char *current_dir = NULL;
|
||||
|
|
|
|||
3
mz_os.h
3
mz_os.h
|
|
@ -91,6 +91,9 @@ int32_t mz_path_remove_extension(char *path);
|
|||
int32_t mz_path_get_filename(const char *path, const char **filename);
|
||||
/* Get the filename from a path */
|
||||
|
||||
int32_t mz_dir_has_unsafe_symlink(const char *path, const char *base_path);
|
||||
/* Checks if any existing component of path is a symlink that escapes base path. */
|
||||
|
||||
int32_t mz_dir_make(const char *path);
|
||||
/* Creates a directory recursively */
|
||||
|
||||
|
|
|
|||
11
mz_zip_rw.c
11
mz_zip_rw.c
|
|
@ -59,6 +59,7 @@ typedef struct mz_zip_reader_s {
|
|||
uint8_t cd_zipped;
|
||||
uint8_t entry_verified;
|
||||
uint8_t recover;
|
||||
const char *destination_dir;
|
||||
} mz_zip_reader;
|
||||
|
||||
/***************************************************************************/
|
||||
|
|
@ -711,6 +712,13 @@ int32_t mz_zip_reader_entry_save_file(void *handle, const char *path) {
|
|||
mz_path_remove_filename(directory);
|
||||
}
|
||||
|
||||
/* Check if path traverses through an existing symlink that escapes destination */
|
||||
if (reader->destination_dir &&
|
||||
mz_dir_has_unsafe_symlink(directory, reader->destination_dir) != MZ_OK) {
|
||||
err = MZ_EXIST_ERROR;
|
||||
goto save_cleanup;
|
||||
}
|
||||
|
||||
/* Create the output directory if it doesn't already exist */
|
||||
if (mz_os_is_dir(directory) != MZ_OK) {
|
||||
err = mz_dir_make(directory);
|
||||
|
|
@ -749,7 +757,6 @@ int32_t mz_zip_reader_entry_save_file(void *handle, const char *path) {
|
|||
mz_stream_mem_delete(&stream);
|
||||
}
|
||||
|
||||
/* Don't check return value because we aren't validating symbolic link target */
|
||||
goto save_cleanup;
|
||||
}
|
||||
|
||||
|
|
@ -846,6 +853,8 @@ int32_t mz_zip_reader_save_all(void *handle, const char *destination_dir) {
|
|||
|
||||
if (!reader)
|
||||
return MZ_PARAM_ERROR;
|
||||
|
||||
reader->destination_dir = destination_dir;
|
||||
err = mz_zip_reader_goto_first_entry(reader);
|
||||
|
||||
if (err == MZ_END_OF_LIST)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue