Fixed arbitrary file write vulnerability via symlinks. #936

This commit is contained in:
Nathan Moinvaziri 2026-01-21 13:26:14 -08:00
parent 3e5213bcde
commit bb282ace22
No known key found for this signature in database
4 changed files with 139 additions and 1 deletions

View file

@ -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
View file

@ -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;

View file

@ -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 */

View file

@ -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)