Problem/Motivation

The module's custom tar extractor, TarArchiveReader, is used by the Restore
feature to unpack an uploaded .tar backup onto disk. Its only path-traversal
defense, maliciousFilename(), checks entry filenames for the literal
substrings "/../" or a leading "../". It never validates the link target of
a symlink entry (tar typeflag == '2').

An authenticated user holding the "restore from backup" permission can
upload a crafted .tar archive containing a symlink entry with an innocuous
name (e.g. "evilsym") whose target is an arbitrary absolute path on the
filesystem, followed by a regular file entry named e.g. "evilsym/payload.php".
That filename contains neither "/../" nor a leading "../", so it passes
maliciousFilename() untouched — but during extraction, fopen() follows the
symlink and writes the file OUTSIDE the archive's own extraction directory,
anywhere the web server process can write. This is the well-documented "Tar
Slip" vulnerability class applied to a bespoke extractor that was never
hardened against it.

Impact: arbitrary file write as the web server user, trivially escalating
to full remote code execution wherever the write lands in a
web-server-writable, script-execution-permitted location (common on many
hosting setups, or via any custom "File Directory" source pointed outside
sites/*/files).

This was previously reported to the Drupal Security Team as a confidential
issue. The team determined it's out of scope for a formal security advisory
since the required permission ("restore from backup") is already marked
"restrict access" in the module's permissions, and cleared it to be
reported publicly here.

Steps to reproduce

1. As a user with "restore from backup" permission, ensure a
directory-based backup source is configured and selectable in the
Restore form.
2. Craft a .tar archive with exactly two entries: a symlink (e.g.
"evilsym") pointing at an arbitrary absolute directory, and a regular
file "evilsym/payload.php" containing a PHP payload.
3. Go to /admin/config/development/backup_migrate/restore, select the
source, upload the .tar via "Restore now".
4. The extractor reports "Restore completed" — payload.php now exists
outside the source's own configured directory, at the attacker-chosen
symlink target.
5. Where that target directory is script-execution-permitted, requesting
payload.php over HTTP executes the payload as the web server user.

Root cause — src/Core/Service/TarArchiveReader.php, maliciousFilename():

private function maliciousFilename($file) {
if (strpos($file, '/../') !== FALSE) {
return TRUE;
}
if (strpos($file, '../') === 0) {
return TRUE;
}
return FALSE;
}

This is only checked against $header['filename']. The symlink branch of
extractAllToDirectory() never validates $header['link'] before calling
symlink($header['link'], $header['filename']).

A working PoC (build malicious tar, authenticate, submit via the Restore
form, execute arbitrary commands via the planted file) exists and is
available on request.

Proposed resolution

maliciousFilename() should reject filenames that are exactly ".." or end
in "/..", and — the actual root cause — must validate symlink link
targets: reject absolute targets, and reject any target that, resolved
relative to the extraction directory, escapes it. Simplest robust fix:
refuse to extract symlink entries from untrusted archives entirely, since
there is no legitimate backup/restore use case that needs them preserved.

Remaining tasks

Write a patch, add test coverage for symlink-based traversal attempts,
review whether to replace the bespoke tar reader with a maintained library.

User interface changes

None.

API changes

None expected, aside from symlink entries in restored archives no longer
being extracted (no legitimate backup/restore workflow relies on this).

Data model changes

None.

Comments

0xbabar0ka created an issue.