VNX-278: Insecure Preserved Inherited Permissions
Detects archive extraction that keeps the permissions (or owner) the archive carries: libarchive with ARCHIVE_EXTRACT_PERM/OWNER/ACL set unconditionally, Python tarfile extract()/extractall() without a filter and shutil.unpack_archive() without one, Go archive/tar and archive/zip extractors that reuse the entry's mode or Uid/Gid, and the Rust tar crate with set_preserve_permissions(true).
| Field | Value |
|---|---|
| Rule ID | VNX-278 |
| Severity | Low |
| SARIF level | note |
| Kind | sast |
| Languages | C, C++, Python, Go, Rust |
| CWE | CWE-278 |
| CAPEC | CAPEC-66 |
| ATT&CK | T1190 |
| CWSS vector | CWSS:1.0/TI:L/AP:RU/AL:S/IC:N/FC:LT/RP:RU/RL:S/AV:L/AS:N/IN:A/SC:A/BI:L/DI:H/EX:L/EC:N/P:L |
| Tags | preserved-inherited-permissions archive-extraction file-permissions cwe-278 |
| Source | internal/sast/rules/vnx-278.rego |
Overview
VNX-278 maps to CWE-278: Insecure Preserved Inherited Permissions. An archive records a mode, and often an owner, for every entry. An extractor that applies them as-is lets whoever built the archive decide that a file on your disk is world-writable, setuid, or owned by another account, without anyone running the extraction knowing.
Reported:
- C, C++ (libarchive):
ARCHIVE_EXTRACT_PERM,ARCHIVE_EXTRACT_OWNERorARCHIVE_EXTRACT_ACLin the flags passed toarchive_write_disk_set_optionsorarchive_read_extract, or in a#defined flag set, when they are set unconditionally. - Python:
extract()/extractall()on atarfileobject (a name or an attribute such asself.archivebound totarfile.open(), also through animport tarfile as tfalias, or a parameter annotatedtarfile.TarFile) with nofilter=argument,filter=Noneorfilter="fully_trusted", andshutil.unpack_archive()withoutfilter=. Before Python 3.14 the default keeps every mode bit, including setuid and group/other write. - Go:
os.OpenFile,os.Chmod,os.Mkdir,os.MkdirAllorFile.Chmodgiven the entry’s own mode (os.FileMode(hdr.Mode),hdr.FileInfo().Mode(),info.Mode()afterinfo := hdr.FileInfo(), a zip entry’sf.Mode(), directly or through a variable), andos.Chown/os.Lchowngiven the entry’sUid/Gid. - Rust (tar crate):
archive.set_preserve_permissions(true)andarchive.set_preserve_ownerships(true)beforeunpack().
Path traversal in the same calls (zip slip) is a separate weakness and is not judged here.
What Gets Flagged
int flags = ARCHIVE_EXTRACT_TIME | ARCHIVE_EXTRACT_PERM | ARCHIVE_EXTRACT_ACL; // FLAGGED
archive_write_disk_set_options(ext, flags);
with tarfile.open(path) as tar:
tar.extractall(dest) # FLAGGED
// FLAGGED: the file gets whatever mode the archive says
f, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR|os.O_TRUNC, os.FileMode(hdr.Mode))
info := header.FileInfo()
f, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR|os.O_TRUNC, info.Mode()) // FLAGGED
What Is Not Flagged
- libarchive flags set only when the user asked for them: under
if (preserve)(on the line above or around the block), acase 'p':option, orkeep ? ARCHIVE_EXTRACT_PERM : 0(astar -pdoes), and a flag that is only tested ((flags & ARCHIVE_EXTRACT_PERM) != 0). tar.extractall(dest, filter="data")orfilter="tar", a file that installs its own filter (archive.extraction_filter = my_filter,TarFile.extraction_filter = staticmethod(tarfile.data_filter)),zipfile(which does not restore modes), andshutil.unpack_archive()of a zip (format="zip"or a"....zip"path).- A Go mode masked with a constant (
os.FileMode(hdr.Mode)&0o755,hdr.FileInfo().Mode()&^0o7022), a fixed mode, and code outside an archive extractor. - Test code and the names in comments or strings.
Remediation
tar.extractall(dest, filter="data") # strips setuid/setgid and group/other write
mode := hdr.FileInfo().Mode().Perm() &^ 0o022
f, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR|os.O_TRUNC, mode)
With libarchive, leave out ARCHIVE_EXTRACT_PERM and ARCHIVE_EXTRACT_OWNER unless the user explicitly asks to preserve them.
References
- CWE-278: Insecure Preserved Inherited Permissions
- Python tarfile extraction filters
- PEP 706: Filter for tarfile.extractall
- archive_write_disk(3)
Suppressing this rule
Put a nosec comment on the line, or on line 1 to cover the whole file. Use the comment marker of the file’s language (#, //, -- or ;):
# nosec vnx-278
// nosec vnx-278
Or record an ignore rule: vulnetix ignore add --rule vnx-278 --reason "...". See Suppressing findings.