Archive path traversal

Archive path traversal

Description

Entry pathnames and link targets returned by libarchive functions such as archive_entry_pathname(), archive_entry_hardlink(), and archive_entry_symlink() come from the archive. Passing them directly to filesystem APIs can select a location outside the intended extraction directory through absolute paths, .. components, or link redirection.

archive_entry_sourcepath() has a different meaning. It returns the local filesystem source path used by archive_read_disk when creating an archive; that value itself is not stored in the archive.

Potential impact

  • Files may be read, or files and directories created, replaced, renamed, or deleted with the process's permissions.
  • Configuration files, executables, deployment paths, or another user's data may be altered.

Remediation

Choose a fixed, trusted extraction directory and, where possible, use all three libarchive protections together:

  • ARCHIVE_EXTRACT_SECURE_NODOTDOT: reject paths containing .. components.
  • ARCHIVE_EXTRACT_SECURE_NOABSOLUTEPATHS: reject absolute paths.
  • ARCHIVE_EXTRACT_SECURE_SYMLINKS: reject entries whose destination would be redirected by symlinks on disk.

The libarchive 3.8.9 documentation states that these checks are not enabled by default. Pass the flags to archive_read_extract(), or set them with archive_write_disk_set_options() on the archive_write_disk object used by archive_read_extract2().

ARCHIVE_EXTRACT_SAFE_WRITES avoids partial writes by creating a temporary file and renaming it; it does not confine paths. Preventing existing files from being overwritten is also a separate protection.

For custom extraction, reject platform-specific absolute paths and .. components in both entry names and stored hard-link or symlink targets. Use operating-system APIs that enforce confinement during path lookup relative to a directory opened in advance. Reject link entries if they do not need to be restored. Normalizing a string and comparing a prefix, or calling basename() alone, does not safely handle links, platform-specific separators, and roots. C++ operations such as std::filesystem::path::filename(), lexically_normal(), canonical(), and weakly_canonical() likewise do not by themselves enforce confinement or prevent link races when a later file API uses the result. If directory structure is unnecessary, allowing only a single filename without separators is simpler, as shown below.

Examples

C

Before

c
#include <archive_entry.h>
#include <stdio.h>

void extract_unsafe(struct archive_entry *entry) {
    const char *name = archive_entry_pathname(entry);
    FILE *out = fopen(name, "wb");
    if (out != NULL) {
        fclose(out);
    }
}

After

c
#include <archive.h>
#include <archive_entry.h>

int extract_entry(struct archive *reader, struct archive_entry *entry) {
    const int flags = ARCHIVE_EXTRACT_SECURE_NODOTDOT |
                      ARCHIVE_EXTRACT_SECURE_NOABSOLUTEPATHS |
                      ARCHIVE_EXTRACT_SECURE_SYMLINKS;

    return archive_read_extract(reader, entry, flags);
}

This enables libarchive's path checks, assuming the process's current working directory is a trusted extraction root. Treat an error return as a failure and stop extracting that entry.

A manual extraction policy that does not support directory entries can use the following approach:

c
#if defined(__APPLE__)
#define _DARWIN_C_SOURCE
#else
#define _POSIX_C_SOURCE 200809L
#endif

#include <archive_entry.h>
#include <fcntl.h>
#include <string.h>

int open_flat_entry(int output_dir_fd, struct archive_entry *entry) {
    const char *name = archive_entry_pathname(entry);
    if (name == NULL || name[0] == '\0' ||
        strchr(name, '/') != NULL || strchr(name, '\\') != NULL ||
        strcmp(name, ".") == 0 || strcmp(name, "..") == 0) {
        return -1;
    }

    return openat(output_dir_fd, name,
                  O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW, 0600);
}

Explanation:

  • Before: The name stored in the archive is used directly as a file creation path.
  • After: The first example explicitly enables all three absolute-path, parent-directory, and symlink protections. The second rejects separators and ./.., then calls openat() relative to a safely opened output_dir_fd, rejecting existing files and a symlink in the final path component.

C++

Before

cpp
#include <archive_entry.h>
#include <filesystem>
#include <fstream>

void extract_unsafe_cpp(struct archive_entry *entry) {
    const auto leaf = std::filesystem::path(
        archive_entry_pathname(entry)).filename();
    std::ofstream output{leaf};
}

filename() only selects the last path component. It neither establishes a fixed extraction root nor opens the file without following existing symlinks.

After

cpp
#include <archive.h>
#include <archive_entry.h>

int extract_entry_cpp(struct archive *reader, struct archive_entry *entry) {
    constexpr int flags = ARCHIVE_EXTRACT_SECURE_NODOTDOT |
                          ARCHIVE_EXTRACT_SECURE_NOABSOLUTEPATHS |
                          ARCHIVE_EXTRACT_SECURE_SYMLINKS;

    return archive_read_extract(reader, entry, flags);
}

This example also assumes that the current working directory is a trusted extraction root. Handle errors returned by libarchive as failures.

References