Skip to content
Shell & TerminalDeep Dive Published Updated 9 min readViews unavailable

GNU tar in Shell Workflows: Safe Extraction and Reproducible Archives

Extract untrusted tar files in isolation, prevent path and symlink escapes, preserve safe metadata defaults, and create archives with controlled metadata.

Shell scripts frequently download, unpack, and publish tar archives. The dangerous assumption is that an archive is only a bag of files. Each member carries a pathname and metadata, and extraction writes those names into a live filesystem. A trusted backup and an untrusted upload need different treatment. GNU tar includes safeguards for absolute and parent-relative names, but links, repeated archives, existing files, permissions, and races still require a deliberate extraction environment.

The operational pattern is straightforward: inspect provenance, extract into a newly created empty directory, disable owner and permission restoration when they are not required, keep extraction away from privileged or live paths, validate the resulting tree, and only then install approved files. For archives produced by builds, normalize ordering and metadata if byte-for-byte reproducibility matters. These examples are GNU tar-specific; check the target version and options before claiming portability.

An archive member name is a filesystem operation

Tar records names such as package/bin/tool and recreates those paths under the extraction directory. An archive can also contain absolute paths, names with .. components, symbolic links, hard links, device nodes, and permission metadata. A member that appears harmless in a listing can interact with another member or a pre-existing filesystem object during extraction.

GNU tar normally strips leading slashes and leading ../ components when extracting, rather than allowing those names to escape the current directory. Do not use --absolute-names (-P) for an untrusted archive: it tells tar to honor absolute names and .. components and can overwrite any path writable by the extracting process. This option is appropriate only for a trusted archive when absolute restoration is an explicit requirement.

Even without -P, extraction should happen in a new, empty directory. GNU tar documents that symbolic links whose targets contain .. or begin with / can cause problems during extraction, so it normally extracts those links last and may create empty placeholders first. It separately warns that two untrusted archives must not share an extraction directory: the first archive can create a symbolic link that the second archive follows. Isolation narrows the impact of unexpected paths and makes post-extraction review manageable.

Create a private scratch directory for each archive

Use a unique directory and avoid extracting directly over the application tree:

#!/usr/bin/env bash
set -euo pipefail

archive=${1:?usage: inspect-archive ARCHIVE}
work=$(mktemp -d "${TMPDIR:-/tmp}/archive-review.XXXXXX") || exit 1
cleanup() { rm -rf -- "$work"; }
trap cleanup EXIT
trap 'exit 129' HUP
trap 'exit 130' INT
trap 'exit 143' TERM

mkdir -- "$work/unpacked"
tar --extract --file="$archive" --directory="$work/unpacked" \
  --no-same-owner --no-same-permissions --one-top-level

find "$work/unpacked" -mindepth 1 -maxdepth 4 -print

--one-top-level creates a directory beneath the extraction directory and places members under it, which helps contain tarbombs containing many top-level names. It is a GNU tar feature; verify the installed version. --no-same-owner avoids restoring archived numeric ownership, and --no-same-permissions prevents the archive from overriding the process umask in the way --same-permissions would. The process still needs sufficient filesystem permissions, and the contents remain untrusted after extraction.

The find command is only an inventory. Do not parse its newline-delimited output into a shell loop if filenames may contain newlines. Use a NUL-delimited interface or a language API that can inspect pathnames as byte strings. In production, capture inventory in a structured format and validate allowed paths, file types, size limits, executable bits, symlink targets, and expected contents before installation.

Never run the extraction step as root merely because the final files will eventually be installed as root. Extract as an unprivileged account into an owned scratch directory, verify the tree, then use a narrow installation step to copy only approved files. If the process must run privileged, use a container or filesystem namespace with no sensitive host paths mounted and enforce resource limits against archive bombs.

Listing helps review but does not prove safe extraction

Before unpacking, tar --list --file=archive.tar can reveal suspicious member names and unexpected file types. Listing does not fully validate the effects of extraction. The result still depends on link ordering, filesystem state, existing symlinks, path normalization, tar implementation, and options. Treat a listing as triage, not as a security sandbox.

Inspect archived links and metadata in a disposable environment. Reject entries outside an allowed root, absolute paths, path traversal, setuid/setgid bits, device nodes, FIFOs, and link targets that resolve outside the extraction root unless the archive’s trusted format explicitly requires them. Avoid restoring ownership or special permissions from untrusted archives. A release bundle should usually contain regular files and directories plus a small, reviewed set of symlinks.

There are legitimate cases where a backup requires ownership, ACLs, extended attributes, sparse files, or hard links. Those are restoration semantics, not defaults to enable for arbitrary downloads. Record the creating tool, tar format, platform, and metadata options in the backup manifest. Test restoration into an isolated environment with the intended privilege and filesystem before declaring the archive recoverable.

Install from the validated tree, not by extraction into production

After validation, promote the contents into a new versioned directory and switch the active reference only after a health check. For example, unpack to /srv/app/releases/2026-10-04T120000Z, verify checksums and required files, then atomically update a symlink or deployment pointer. Keep the previous release for rollback. A rename of one symlink or directory entry can be atomic on one filesystem, but a series of copied files is not a transaction over the whole tree.

Do not use tar -x against / or the live application root just because the archive came from a nominally internal build. A build artifact can be corrupted, a publisher credential can be compromised, or a pipeline can accidentally package an unexpected path. Least privilege and staging reduce the impact of those failures. Use --keep-old-files or --skip-old-files only when their behavior matches the intended policy; neither replaces a review of existing symlinks or ownership.

If extraction happens in a directory writable by multiple users, another process may swap paths between validation and installation. Create the scratch tree with restrictive permissions, keep it owned by the extracting identity, and do not expose it to untrusted writers. For high-assurance package installation, rely on a package manager or deployment tool that already provides signature verification, path validation, and transaction semantics.

Shell safety: paths, options, and cleanup

Quote every variable used as a pathname and pass option terminators where supported. A filename beginning with - can be mistaken for an option if it appears in an option position. Prefer --file="$archive" and --directory="$work/unpacked" rather than relying on positional parsing. Validate that an input is a regular file in an expected directory if the workflow must not follow a symlink to an arbitrary location.

The trap in the example removes the private workspace on exit and common signals, but a signal can arrive during cleanup. Keep cleanup idempotent, preserve the original command status when reporting errors, and ensure the target of rm -rf is a value created by mktemp rather than an unvalidated environment string. Never replace the generated path with an empty variable or a general directory such as /tmp.

Apply resource limits when archive members are untrusted. A tiny compressed file may expand to enormous data, contain millions of entries, or use sparse-file metadata to consume resources unexpectedly. Set a maximum archive size, bound extracted bytes and file count, enforce disk quotas, and run extraction with CPU and memory limits. GNU tar is not a policy engine for all of these constraints; use a purpose-built validator when limits must be enforced precisely.

Reproducible archive creation requires normalizing metadata

Two archives made from files with identical contents can differ because directory entries arrive in a different order, modification times differ, ownership metadata varies, or extended headers encode timestamps. GNU tar’s reproducibility guidance recommends running in the C locale and using options such as --sort=name and --format=posix; additional normalization may be needed based on the archive’s required metadata and environment.

For a controlled source tree, a starting point is:

export LC_ALL=C
source_date_epoch=${SOURCE_DATE_EPOCH:?set a reproducible timestamp}
tar --create --file=release.tar \
  --format=posix --sort=name \
  --mtime="@$source_date_epoch" \
  --owner=0 --group=0 --numeric-owner \
  --pax-option=delete=atime,delete=ctime \
  -C build-output .

The fixed timestamp should come from the build’s declared source revision or release metadata, not the current wall clock. --sort=name stabilizes directory traversal ordering. Fixed ownership prevents the build account’s UID and GID from leaking into the artifact. Deleting atime and ctime from PAX metadata can remove host-dependent values. Confirm the exact requirements in GNU tar’s reproducibility section and test archive hashes across clean builds.

This command alone does not make every archive reproducible. File contents, symlink targets, permission bits, xattrs, ACLs, sparse representation, compression utility version, compression flags, and archive naming can still differ. Normalize only metadata that the product contract permits changing; preserving executable bits may be essential, while preserving build-user ownership usually is not. Compare tar --list --verbose output and use a binary diff or checksums to identify remaining sources of nondeterminism.

Reproducibility and authenticity are separate properties. A reproducible archive can still contain malicious files; a signed archive can faithfully authenticate a non-reproducible build. Use signatures and provenance attestations to identify who built and published an artifact, then use deterministic inputs and normalized metadata to make independent rebuild comparisons meaningful.

Validate before distributing or restoring

For a newly created archive, list its members and verify the expected top-level path, file count, total size, and metadata. Avoid archiving the archive into itself by writing the output outside the source tree or excluding it explicitly. Use a manifest of file paths and checksums generated from the intended source tree, and verify that manifest after extraction. If the archive is compressed, test the relevant decompressor and verify the full stream rather than trusting a truncated listing.

Test restoration from a clean environment. Confirm that the backup contains required data, that symlink and ownership policy matches the restore procedure, and that all application-level consistency checks pass. A backup is not proven by successfully running tar -tf; restore drills expose missing files, wrong paths, incompatible metadata, and hidden dependencies.

When reporting an extraction error, preserve tar’s exit status and stderr, but redact private filenames or customer data where necessary. Do not ignore warnings with --ignore-failed-read unless omitted files are an explicit acceptable result. For a backup, a partial archive can be worse than an obvious failure because it may appear valid until a restore is needed.

Finally, keep the source of each archive and its trust level explicit. A signed internal release artifact, an operator-created backup, and an arbitrary user upload should not share the same extraction script and privilege context. Isolate first, validate what the application actually needs, and promote only approved content. Those boundaries make a shell tar workflow safer without pretending that a command-line option can turn every archive into trusted input.

Related:

Sources:


Comments