Skip to content

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).

Updated 02 Nov 2026Checked against cli@de9b52a
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_OWNER or ARCHIVE_EXTRACT_ACL in the flags passed to archive_write_disk_set_options or archive_read_extract, or in a #defined flag set, when they are set unconditionally.
  • Python: extract()/extractall() on a tarfile object (a name or an attribute such as self.archive bound to tarfile.open(), also through an import tarfile as tf alias, or a parameter annotated tarfile.TarFile) with no filter= argument, filter=None or filter="fully_trusted", and shutil.unpack_archive() without filter=. Before Python 3.14 the default keeps every mode bit, including setuid and group/other write.
  • Go: os.OpenFile, os.Chmod, os.Mkdir, os.MkdirAll or File.Chmod given the entry’s own mode (os.FileMode(hdr.Mode), hdr.FileInfo().Mode(), info.Mode() after info := hdr.FileInfo(), a zip entry’s f.Mode(), directly or through a variable), and os.Chown/os.Lchown given the entry’s Uid/Gid.
  • Rust (tar crate): archive.set_preserve_permissions(true) and archive.set_preserve_ownerships(true) before unpack().

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), a case 'p': option, or keep ? ARCHIVE_EXTRACT_PERM : 0 (as tar -p does), and a flag that is only tested ((flags & ARCHIVE_EXTRACT_PERM) != 0).
  • tar.extractall(dest, filter="data") or filter="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), and shutil.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

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.

Edit this pageSources: cli/internal/sast/rules/vnx-278.rego, cli/internal/sast/docs/vnx-278.md
esc
↑ ↓ move↵ openSearches in your browser. A key like autonomy.max or a code like threshold_required jumps straight to it.