SECURITY.md — Threat Model and Security Rationale
Why This Document Exists
Archive tools are a common attack vector. They process untrusted input (archive files received from external sources), run with user-level permissions, and are installed on virtually every workstation. The choice of which tool to trust is a security decision, not just a UX preference.
This document explains the threat model behind Windows Archiver Wrapper and why the design choices were made.
Threat Model
Assets Being Protected
- Files on the host filesystem (documents, credentials, configs)
- Integrity of the extraction destination
- User session and process context
Threat Actors Considered
| Actor | Capability | Vector |
|---|---|---|
| Malicious archive file | Crafted ZIP sent to user | Extraction path traversal, ZIP bomb |
| Compromised tool binary | Supply chain attack | Backdoored .exe distributed via official channel |
| Compromised update | Supply chain attack | Automatic update delivers malicious version |
| Vulnerable parser | CVE exploitation | Malformed archive triggers memory corruption |
Out of Scope (v1.0)
- Attacks requiring physical access to the machine
- OS-level privilege escalation
- Attacks against the Microsoft Store distribution infrastructure
Supply Chain Risk: 7-Zip, WinRAR, and Windows tar.exe
This section documents the rationale for excluding these tools as a dependency or reference implementation.
7-Zip
| Property | Status |
|---|---|
| Developer | Igor Pavlov — Russian Federation |
| Source code | Published (LGPL) |
| Reproducible builds | ❌ No — distributed binary cannot be verified against source |
| Independent security audit | ❌ None publicly documented |
| Governance | Single developer, no independent review process |
| CVE history | Multiple: CVE-2016-9296, CVE-2017-17969, CVE-2018-10115, CVE-2022-29072, and others |
The absence of reproducible builds is a critical gap: anyone with access to the build infrastructure can distribute a modified binary while the published source remains clean. This is a known and documented attack pattern (see XZ Utils backdoor, 2024).
WinRAR
| Property | Status |
|---|---|
| Developer | Eugene Roshal — Russian Federation |
| Source code | ❌ Closed source |
| Reproducible builds | ❌ Not applicable — source not available |
| Independent security audit | ❌ Impossible without source |
| CVE history | CVE-2018-20250 (critical, path traversal, 500M+ installations affected), and others |
Closed-source compression tools that process untrusted files are not appropriate for environments handling sensitive data. There is no technical means to verify the absence of intentional backdoors or unintentional vulnerabilities.
Windows tar.exe
C:\Windows\System32\tar.exe is an acceptable dependency for extracting non-ZIP formats. It is Microsoft-signed, open source (bsdtar/libarchive on GitHub), and delivered through the Windows Update chain. See the tar.exe Trust Model section below for details.
Risk Classification for Regulated Environments
For organizations operating under security requirements (government, defense, critical infrastructure, financial sector):
- Using software from developers in adversarial jurisdictions without source auditability is a supply chain risk
- Unverifiable binaries processing sensitive files fail basic security hygiene standards
- Neither 7-Zip nor WinRAR meets reproducible build requirements
This Project's Security Properties
What We Rely On
| Component | Trust Basis |
|---|---|
System.IO.Compression |
Part of .NET BCL — open source at dotnet/runtime, CVE process via Microsoft MSRC, reproducible builds available |
| Windows App SDK / WinUI 3 | Open source at microsoft/WindowsAppSDK, Microsoft security response process |
| CommunityToolkit.Mvvm | Open source at CommunityToolkit/dotnet, UI-only, no file processing |
Architectural Decisions with Security Impact
No shell extensions in v1.0 — added in v1.2 with a narrowed surface
v1.0 excluded context menu integration to minimize attack surface, since it requires elevated
trust and COM registration. v1.2 adds a shell extension (IExplorerCommand, T-F61), registered
as a com:SurrogateServer — the COM DLL runs inside an isolated dllhost.exe process, not
in-process inside explorer.exe, so a crash in the extension cannot bring down Explorer itself.
IContextMenu (the legacy, in-process-only shell extension API) is not used, by hard constraint.
No network access
The application makes no network requests of its own: no telemetry, no update checks, no cloud
storage. Pakko registers no URI scheme; it opens only paths handed to it by Explorer (context menu,
file association) or picked by the user in Pakko. A network path (UNC) handed over this way is
accessed by Windows with the user's credentials, as for any other application. See "Advisory:
pakko:// Links in Earlier Versions" below.
No background services The app runs only when the user explicitly opens it. No persistent processes.
No format parsers beyond ZIP
Each supported format adds parser attack surface. RAR and 7z are read-only, via the Windows
built-in tar.exe process — not an in-process parser — since libarchive has no writer for
either. TAR-family formats (read and create) use the same tar.exe process.
Two bounded metadata reads run in-process, neither extracting anything: the RAR5 encryption check
(see "Encrypted-Archive Diagnostics" below) and, for an uncompressed tar only, a walk of its
512-byte header blocks (T-F305) that decides whether the names are UTF-8 or the OEM code page when
tar.exe's own error text cannot tell. It reads names only, verifies every header checksum, skips
file content by offset, and on anything it cannot read keeps the refusal; it only chooses between
two sandboxed tar.exe readings, and the security pre-scan still runs on tar.exe's own listing.
For a gzip-compressed tar the same walk reads through .NET's own GZipStream (T-F310) — the
Deflate decoder Pakko already uses for ZIP, no new parser. The decompressed bytes are read forward
and dropped, never kept or written, so memory stays flat. The walk can be cancelled and stops
undecided at 1000 times the archive's size; Deflate itself expands at most about 1000 times, so in
practice a gzip that reaches this check is decompressed once in full, outside the sandbox's memory
and processor limits (7 GiB of zeros behind a 32 MB archive: 0.9 s). bzip2, xz and zstd have no decoder in .NET, so such a tar is not
read in-process and keeps the refusal.
ZIP extraction: staged, checked per entry, verified (v1.5.0)
- Integrity. Every extracted entry's content CRC-32 is checked against its header (AE-2 encrypted entries carry no CRC — their HMAC authenticates them instead), and no entry may produce more than its declared uncompressed size; a failing entry is reported and never reaches the destination (T-F246, T-F231). .NET does not check CRC-32 on read by itself.
- Unsafe names. An entry name with a
..segment (either separator) or a rooted/drive-relative path is rejected per entry and reported as an error, never normalized (T-F228) — the tar path rejects such archives whole. - Deep names. A name deeper than 256 folder levels counts as an unsafe path too, with the same per-entry (ZIP) and whole-archive (tar) rejection before any file system call (T-F237, v1.6.0). Windows parses the whole path again for every new level inside one call that cannot be cancelled, so a deep chain from a small archive costs minutes of CPU (a 60 KB ZIP with 4,000 levels: 91 s).
- Owned staging. Extraction stages into a fresh, uniquely named folder the run creates and
removes itself, never a fixed
<dest>_tmpname that could be a user's own folder (T-F227). - Local headers that disagree with the central directory. Pakko reads names, sizes and CRC-32 from the central directory only, so its own result is consistent; another program may take them from the local headers and extract different names or data from the same archive. "Test archive" reports such an archive as an error, and extraction warns about it while extracting by the central directory (T-F280; before v1.7.1 extraction said nothing).
Minimal MSIX capabilities
Package.appxmanifest declares only runFullTrust. No broadFileSystemAccess, no internetClient, no device capabilities.
Known Limitations and Residual Risks
| Risk | Severity | Mitigation |
|---|---|---|
ZIP path traversal (e.g., ../../etc/passwd style entries) |
High | System.IO.Compression with .NET 10 validates entry paths — covered in ZipArchiveService tests |
| ZIP bomb (highly compressed entries) | Medium | Whole-archive ratio check (T-F94, v1.3; supersedes T-F28's per-entry version) — an archive whose declared uncompressed size exceeds 1000:1 against the archive file's on-disk size is blocked unless the destination has free space for the declared size AND the user explicitly confirms extraction; see DECISIONS.md's T-F94 entry. Explorer's extract commands ask the same question since T-F217 (operation window, or a Win32 dialog as fallback): declining is the default button and Esc, closing the window or a failed window all count as no, never as yes, and the question is per archive with no "apply to all" |
tar-family decompression bomb (.tar.gz/.bz2/.xz/.zst/.lzma) |
Medium | Mitigated (T-F94, v1.3; supersedes T-F90's auto-reject-only version) — same whole-archive ratio check and confirm-if-it-fits model as ZIP, run before -xf ever executes; see DECISIONS.md's T-F94 entry |
| Symlink/reparse point attacks in ZIP entries | Medium | Mitigated (T-F37, v1.2) — reparse point check after file creation; path traversal via reparse point rejected |
Alternate Data Stream entries (: in filename) |
Medium | Mitigated (T-F38, v1.2) — ADS entries rejected |
Reserved Windows filenames in entries (CON, NUL, etc.) |
Low-Medium | Mitigated (T-F39, v1.2) — reserved names and control characters filtered |
| MOTW not propagated to extracted files | — | Resolved (T-F45, v1.2) — MOTW propagated to every extracted file by default |
| tar.exe runs at Medium IL | Medium | Resolved (T-F52, v1.4) — extraction now runs inside an AppContainer via P/Invoke (empty capability list, ACL'd quarantine directory, Job Object process/resource limits); archive creation stays unsandboxed by design, since it only reads trusted local files — see the "Why Archive Creation Is NOT Sandboxed" note below |
| tar.exe symlink entries escape a naive quarantine (confirmed exploit, T-F49) | High | Mitigated (T-F49, v1.3) — whole-archive pre-scan via tar -tf/-tvf rejects any archive containing a symlink/hardlink/device entry or a traversal/ADS/reserved name before -xf ever runs; see DECISIONS.md's T-F49 entry |
| Recursive decompression bomb via nested archives (an archive inside an archive inside an archive, multiplying expansion per level) | Medium | Mitigated (T-F98, v1.4) — Archive Browser drill-down into a nested archive is capped at 4 levels deep, and every level independently re-runs the same whole-archive pre-scan (T-F49) and compression-ratio + disk-space check (T-F90/T-F94) a normal extraction would — no shortcut or inherited "already checked" state from an outer level; see DECISIONS.md's T-F98 entry |
Native decompression 0-day in the ZIP path (a memory-corruption bug in the native zlib-derived code System.IO.Compression's DeflateStream calls across its managed→native boundary, triggered by a maliciously malformed compression stream — e.g. corrupted Huffman tables) |
Low (theoretical) | Accepted risk, not sandboxed. Unlike tar-family extraction (AppContainer, T-F52), ZIP handling runs unsandboxed in-process by design — see "No format parsers beyond ZIP" above. Successful exploitation would execute with the app's own user-level privileges; no isolation boundary catches it. The only mitigation is indirect: Microsoft's MSRC CVE process on dotnet/runtime, the same trust basis this project already extends to System.IO.Compression generally. Sandboxing the ZIP path the same way as tar.exe is a real, undone option — not pursued, since it would add real overhead (cross-process marshaling for the common case) against a threat class with no track record against this specific code path so far. Revisit if that changes. |
| Opening a tar-family archive changed that file's permissions (versions before v1.5.0) | High | Fixed in v1.5.0 (T-F233) — tar.exe now reads the archive only as a handle Pakko opens itself; the sandbox gets no permission entry on the user's file. Older versions added an entry for the sandbox and replaced the entries the file inherited from its folder, which could remove other users' access on a shared folder. See "Advisory: Permissions Changed by Earlier Versions" below. |
Command-line option injection into tar.exe through a crafted file name ("WorstFit" best-fit mapping, e.g. U+FF02 becoming ") |
High | Fixed in v1.5.0 (T-F266) — every string passed to tar.exe must convert to the ANSI code page exactly (WC_NO_BEST_FIT_CHARS, no default character, exact round trip); a name that does not is refused before tar.exe runs, with a message pointing to ZIP. Archive creation is unsandboxed, so before this fix a selected file named this way could add tar options. |
| tar.exe reading a selected name that looks like one of its options as an option (versions before v1.6.0) | High | Fixed in v1.6.0 (T-F283) — archive creation gives tar.exe every name as a line of a -T - list on its standard input, never as an argument; in that list only an exact -C line is special, and a source named exactly -C is written as ./-C. Extracting selected entries puts -- before the entry names. Before this fix a crafted file or folder name could change what the unsandboxed creation run did, up to starting another program as the user. |
A web page, e-mail or document link launching Pakko on an arbitrary (UNC) path through the pakko:// scheme (versions before v1.5.0) |
High | Fixed in v1.5.0 (T-F232) — the scheme is removed; Explorer's commands reach the app through a Launch activation only a process already on the machine can start. See "Advisory: pakko:// Links in Earlier Versions" below. |
| Microsoft as trust anchor | Low-Medium | Accepted tradeoff for the target audience; .NET is open source and auditable |
Mark of the Web — Security Rationale
What Zone.Identifier Is
Windows NTFS Alternate Data Stream Zone.Identifier records the security zone of a file's origin (e.g., ZoneId=3 = Internet). This is the Mark of the Web (MOTW).
Why MOTW Propagation Prevents Attacks
When a user downloads a ZIP archive from the internet, the archive receives MOTW. If extracted files do not inherit MOTW:
- Microsoft Office opens the extracted
.docxor.xlsmin full edit mode — macros execute without Protected View - Windows SmartScreen does not warn before running extracted
.exefiles
This is a documented exploitation technique: deliver a macro-containing document inside a ZIP, knowing the extractor will strip MOTW on extraction.
What Other Extractors Do
- Windows Explorer's own ZIP folder does propagate MOTW: tested 2026-10-05 on Windows 11
build 26300, a ZIP marked
ZoneId=3gaveZoneId=3on every extracted file. Earlier versions of this document said it does not. Explorer's handling of 7z, RAR and tar was not tested - 7-Zip does not propagate MOTW by default (added as an option in 7-Zip 23.01, off by default)
- NanaZip 6.0 (Feb 2026) propagates MOTW by default
Pakko's Behavior (v1.2+, implemented)
Pakko propagates MOTW on all extracted files by default:
- Read
Zone.IdentifierADS from the source archive - Write identical
Zone.IdentifierADS to each extracted file - Default: on. Since T-F360 (2026-10-08) the user may leave the mark off for one extraction of
an archive they trust: an "apply the download mark" checkbox in the App's extract options
(also what Explorer's "Extract..." opens), shown only for an archive that carries the mark and
checked again for every new list, and
pakko x -snz0. Unchecked means no mark at all, not "unsafe types only": that list has no Office, PDF or ISO types, the very files MOTW protects. TheEnforceMOTWGroup Policy wins either way and locks the checkbox. Archive Browser previews, opening a nested archive and Explorer's "Extract Here"/"Extract to folder" always apply the mark — their files are opened by other programs, and they have no options to show. The checkbox exists because the mark costs time: about 1.5 ms per file (T-F358).
Implementation: FileStream with ADS path "extractedfile.txt:Zone.Identifier", no P/Invoke required.
For system administrators: the Group Policy surface (EnforceMOTW,
AllowedFormats/BlockedFormats, DisableTarExtraction under HKLM\Software\Policies\Pakko\,
shipped 2026-07-18, T-F51) is documented in full, with deployment instructions, in
POLICIES.md.
Archive Browser Preview — Safe-Type Allowlist (T-F97)
Double-clicking a file inside the Archive Browser (T-F05) extracts just that entry to a throwaway temp cache and opens it with the OS's default handler, instead of requiring a manual Extract first. Two constraints keep this from becoming a new attack surface:
- Safe-type allowlist only —
Archiver.Core.Services.PreviewPolicy.IsPreviewablerestricts auto-open to images (.jpg/.jpeg/.png/.gif/.bmp/.webp), plain text (.txt/.md/.log/.ini/.csv/.json/.xml/.yaml/.yml), common video containers (.mp4/.m4v/.mkv/.avi/.mov/.wmv/.webm, added T-F109), and audio (.mp3/.wav/.flac/.ogg/.m4a/.aac, added T-F109) — no executable, script,.lnk, macro-capable document, or PDF (PDF is deliberately excluded despite looking "safe" — some readers execute embedded JavaScript, unlike every other allowlisted type here). Limit (T-F257): Pakko opens a preview with whatever handler Windows has for the extension. On a machine with Office,.csvopens in Excel, which evaluates formulas. The allowlist restricts file types, not the handlers a machine has registered for them.ShellExecute-ing an arbitrary archive entry with one click, no "Extract to..." friction first, would itself be an attack surface (a malicious file inside an archive, opened automatically). This is deliberately stricter than 7-Zip/NanaZip, confirmed by reading NanaZip's realPanelItemOpen.cpp/OpenItemInArchive: neither has any type allowlist at all — a double-click always extracts to temp and unconditionallyShellExecuteExs the result, including.exe(with special handling to extract every sibling file first, so a portable app's DLL dependencies resolve). The only check either performs isIsVirus_Message— a Unicode right-to-left-override filename-spoofing check, not a file-type restriction. Pakko's narrower allowlist is a deliberate choice for its government/defense audience, not something forced by archiver convention — seeDECISIONS.md's T-F109 entry. - Anything outside the allowlist is warned, not silently extracted to the user's chosen
Destination. T-F109: double-clicking a non-allowlisted entry shows a confirm dialog
(
IDialogService.ShowConfirmAsync) explaining it can't be safely opened directly; on confirmation, only that one entry is extracted — into a dedicated subfolder named after the archive, created next to the archive itself on disk (ArchiveNaming.GetBaseName), never the general Destination field (which is for deliberate bulk Extract Selected/All operations). No auto-open follows extraction — the friction is the point. - No shortcut around existing extraction security — both the preview flow and the
warn-then-extract flow reuse the real
IExtractionRouter.ExtractAsyncpipeline (ExtractOptions.SelectedEntryPathsrestricted to the one entry), the same mechanism T-F05's "Extract Selected" already uses. This means T-F49's whole-archive pre-scan for tar-family formats always runs first, unconditionally, before any bytes are extracted — neither path is ever a "validate this one entry only" shortcut — and MOTW propagation (above) applies automatically, since it happens insideZipArchiveService/TarSandboxedServiceas part of normal extraction, not something the App layer has to remember to call separately.
Preview files are staged under %TEMP%\PakkoPreview\<pid>-<start ticks>\<random>\ (one
subfolder per Pakko process, a fresh subfolder per preview). A window deletes only its own
process's folder on close, best-effort; the next start removes folders whose process is gone
(T-F252) — see DECISIONS.md's T-F97 entry.
tar.exe Trust Model
Why tar.exe Is Acceptable
C:\Windows\System32\tar.exe is:
- Microsoft-signed — binary integrity verified by Windows Authenticode chain
- Open source — based on bsdtar/libarchive, source on GitHub (
microsoft/bsdtar) - Part of Windows Update — patched through normal OS update cycle, MSRC process applies
- No SHA-256 verification needed — unlike third-party binaries, the Authenticode signature provides equivalent or stronger integrity guarantee
- T-F52 (v1.4) additionally verifies the Authenticode subject is Microsoft before every launch — cheap defense-in-depth, not a primary control (see "Why Sandbox tar.exe at All" below for why)
Why Sandbox tar.exe at All, Given It's Microsoft-Signed?
Two distinct threat vectors, only the second of which the v1.4 sandbox (T-F52) defends against —
see DECISIONS.md's T-F52 entry for the full design session:
- The binary itself is swapped/tampered with. Reaching
C:\Windows\System32\tar.exe's ACLs requires SYSTEM-level access — the host is already fully compromised at that point, and no sandbox around Pakko's own invocation of it changes that. The realistic version of this vector (PATH hijacking from a lower privilege level) is already covered by the existing hard constraint thattar.exeis always invoked by absolute path, never via PATH search. A signature check (above) adds a cheap additional check but is not the reason to sandbox. - The legitimate, unmodified tar.exe is driven by a hostile archive into misbehaving.
libarchive is a native parser with a real CVE history, processing attacker-controlled bytes —
this is the standard "sandbox the untrusted-input parser" pattern (the same reason browsers
sandbox image/PDF decoders). This is what T-F52's AppContainer confinement, Job Object limits,
and network isolation actually defend against, and why the sandbox is proportionate despite
tar.exebeing a trusted OS component.
Why Archive Creation (T-F105, v1.4) Is NOT Sandboxed
TarSandboxedService.CompressAsync deliberately runs tar.exe unsandboxed — no
TarSandboxScope, no AppContainer, no Job Object — even though it shares a class name and a
process (tar.exe) with the extraction path above. This is not an oversight; it follows directly
from the "Why Sandbox tar.exe at All" reasoning: vector 2 above ("the legitimate, unmodified
tar.exe is driven by a hostile archive into misbehaving") requires an attacker-controlled
archive feeding libarchive's parser. Archive creation has no such input — tar.exe reads
ArchiveOptions.SourcePaths, files the user themselves selected via Pakko's own UI/shell
integration, and writes a brand-new archive. There is no untrusted-input parser in this data flow
for the sandbox to isolate; the threat model this task exists to close simply does not apply in
the reverse direction.
This is consistent with ZipArchiveService.ArchiveAsync, which has never been sandboxed either,
for the identical reason — ZIP creation via System.IO.Compression.ZipFile also only ever reads
trusted local files. The Authenticode signature check (above) still runs before every tar.exe
launch regardless of direction — cheap, and not specific to the extraction threat model — but
CompressAsync gets no TarSandboxScope/quarantine/AppContainer/Job-Object machinery.
The "maliciously-named source file" case this section once called hypothetical was real, but
through the command line, not libarchive's writer: tar.exe converts its command line with
best-fit mapping, and a file name with fullwidth quotes could add options (T-F266). It is closed by
validating every argument (see the risk table), not by sandboxing creation; running creation in
the sandbox stays a separate, open decision. A second case of the same class was a name that
simply starts with - (T-F283, fixed in v1.6.0): since then the selected names never reach
tar.exe's command line at all — they go to its standard input as a name list, where only an exact
-C line has a meaning of its own.
Trust Chain
Windows Update → Microsoft signing infrastructure → tar.exe
This is the same trust chain as System.IO.Compression via the .NET runtime — both are Microsoft-signed, open source, and maintained by MSRC.
Process Isolation Levels
| Version | Isolation | Method |
|---|---|---|
| v1.3 | Medium IL | tar.exe inherits Pakko process token |
| v1.4 | AppContainer | P/Invoke CreateAppContainerProfile + PROC_THREAD_ATTRIBUTE_SECURITY_CAPABILITIES (empty capability list — no network) + PROC_THREAD_ATTRIBUTE_HANDLE_LIST (the child inherits only its own stdin and pipes); the archive is passed as an inherited read-only handle, held open for the whole operation so it cannot change between the pre-scan and extraction; only Pakko's own quarantine folder is ACL'd to the AppContainer SID; Job Object (ActiveProcessLimit = 1, memory = half the machine's memory within 1–4 GiB, CPU time = at least 60 minutes plus one per 10 MB of archive). Chosen over a Low-IL restricted token because network isolation falls out of the empty capability list for free — no global firewall rule needed (see T-F52 in TASKS_DONE.md/DECISIONS.md) |
In both cases: extraction goes to a staging directory, all output files are validated (ADS, reserved names, reparse points), then atomically moved to final destination. The staging-directory walk alone is not sufficient — a symlink entry can cause tar.exe to write outside the staging directory before any C# code inspects it, confirmed empirically in T-F49 (see DECISIONS.md). The primary defense is a whole-archive pre-scan (tar -tf/-tvf) that rejects any archive containing a symlink/hardlink/device entry or a traversal/rooted/ADS/reserved name before extraction ever runs. The same pre-scan also sums each entry's declared uncompressed size (from -tvf's size column); if that total exceeds 1000x the compressed file's size on disk, extraction is blocked unless the destination has free space for the declared size AND the user explicitly confirms — the same shared evaluator (ArchiveEntrySecurity.EvaluateCompressionBombAsync) and confirm-if-it-fits model ZIP uses, computed once for the whole archive since tar-family compression wraps the entire stream rather than each entry independently (T-F94, v1.3; see DECISIONS.md's T-F94 entry — supersedes T-F90's original auto-reject-only version).
Advisory: Permissions Changed by Earlier Versions (T-F233)
Pakko versions before v1.5.0 staged a tar-family archive (.tar, .gz, .7z, .rar, ...) for the
sandbox by hardlinking it into %TEMP%, then granted the sandbox access to that link. A hardlink is
the same file, so each list, browse, extract, test or scan of such an archive:
- added an entry for Pakko's sandbox (an
S-1-15-2-...AppContainer SID) to the archive itself, and - replaced the permissions the archive inherited from its folder with those of the temporary folder — on a shared folder, other people could lose access to that file.
Nothing is repaired automatically. scripts/Find-PakkoSandboxAce.ps1 lists affected files
(read-only); scripts/Repair-PakkoSandboxAce.ps1 -Path <file> saves the current permissions with
icacls /save, removes only Pakko's entries and makes the file inherit from its real folder again.
Entries set explicitly for anyone else are kept.
Advisory: pakko:// Links in Earlier Versions (T-F232)
Pakko versions before v1.5.0 registered a pakko:// URI scheme. A web page, e-mail or
document could use a crafted link to make Pakko open an arbitrary path, including a network (UNC)
path on a remote host, which makes Windows send the user's NTLM credentials to that host. Browsers
ask before opening such a link but offer "always allow". The scheme is removed; update to the
current version. Nothing needs cleaning up afterwards.
Encrypted-Archive Diagnostics (7z/RAR, T-F113)
For 7z/RAR, Pakko does not decrypt anything — this is diagnostics-only, so a password-protected archive fails with a clear message instead of raw libarchive stderr. (Password-protected ZIP is different — Pakko reads it natively since T-F188–T-F192; see the next section.) Detection is asymmetric between the two formats, and deliberately so:
- RAR is checked proactively, before tar.exe ever runs, by walking RAR5's own block/extra-area
structure directly (
ArchiveFormatDetector.IsEncryptedRar) — RAR headers are never compressed, only file data is, so reading a block's type/flags is real, bounded metadata parsing, not decryption. A block of type 4 ("Archive encryption header") as the very first block means the whole archive, including filenames, is unreadable without a password; otherwise, the first File Header block's extra area is checked for an "Encryption" record (type 1) — same first-entry-only fidelityZipArchiveService.IsEncryptedZipalready accepts for ZIP, not a weaker standard. Legacy RAR4 (7-byte signature, no version byte) is not parsed — an accepted scope cut, since it falls through to the reactive check below instead. - 7z cannot be checked the same way: an AES-256 coder ID would appear inside the folder/coder
metadata, which 7z itself typically stores LZMA-compressed as an "Encoded Header" — inspecting it
would require decompressing 7z's own header stream, i.e. writing a partial 7z reader, which is
disproportionate hand-rolled-format-parsing effort for a diagnostics-only task. Instead, 7z (and
RAR's own rarer header-encrypted case) is classified reactively: the same
tar -tf/-tvf/-xfcalls that already run unconditionally are inspected for a stderr message containing "encrypt" (case-insensitive) — confirmed empirically to catch every encryption-related libarchive failure message across both formats and both encryption modes. Exact byte offsets and stderr strings are recorded inDECISIONS.md's T-F113 entry.
Password-Protected ZIP (T-F188–T-F194, T-F193)
Pakko reads password-protected ZIP entries — both legacy PKWARE ZipCrypto and WinZip AES
(AE-1/AE-2, 128/192/256-bit) — for Extract, Test, the Archive Browser, and "Scan for threats",
with a password prompt in every frontend (WinUI dialog, native Explorer dialog, pakko x/t -p).
This reverses the earlier "encrypted archives are out of scope" position, a deliberate
user-confirmed scope change. Pakko also creates encrypted ZIPs (T-F193: the app's "Encrypt with
password" option, pakko a -p), AES-256 only (WinZip AE-2) — ZipCrypto is cryptographically
broken and Pakko never writes it, nor AES-128/192. The Explorer "Add to X.zip" command stays
one-click and unencrypted.
- No second extraction path. Decryption plugs in exactly where
ZipArchiveEntry.Open()was called; entry names are never encrypted by the ZIP format, so every existing traversal/ADS/ reserved-name/reparse-point/bomb/MOTW check runs unchanged before any ciphertext is touched. - Cryptography is .NET's own, not hand-rolled: PBKDF2-HMAC-SHA1 (
Rfc2898DeriveBytes), AES, and HMAC-SHA1 fromSystem.Security.Cryptography— SHA-1 and 1000 iterations are fixed by the WinZip AE specification, not chosen. Only the ZIP container parsing and the (spec-mandated, broken-by-design) ZipCrypto stream cipher are Pakko code. - Authenticate before release. A WinZip AES entry is read in two passes: the first streams the ciphertext from disk through HMAC-SHA1 only, and decryption starts only after the tag matches, so no plaintext of a tampered entry is ever produced. The cost is reading the entry twice, not holding it in memory — entry size is not limited. ZipCrypto has no authentication — its one-byte password check accepts ~1 in 256 wrong passwords — so its content CRC-32 is always checked (as for every unencrypted entry, T-F246), and a mismatch fails the entry. Every decrypted entry must also be exactly its declared size: longer fails (T-F231), and so does shorter (T-F333; v1.7.0 and earlier accepted shorter) — AE-2 has no CRC, so this is its only length check.
- What WinZip AES authenticates, and what it does not. The HMAC covers each entry's ciphertext and nothing else. An entry's real compression method, its declared sizes, its name and date, and the list of entries are outside it. Without the password someone can therefore rename, reorder or remove entries, or change an entry's method and size together so that a file extracts as its own compressed bytes; 7-Zip accepts such an archive too. They cannot read a file or put chosen content into one. This is a limit of the format, not of Pakko: a password-protected ZIP keeps content secret and detects changed content, but "Test archive" passing does not prove the archive is the one its author made.
- Hostile headers fail closed. Sizes and extra fields read from local headers are attacker-controlled; the parser bounds every allocation by the archive file's real size and rejects malformed WinZip AES extra records as a normal per-archive error, never an unhandled exception. Zip64 fields (entry sizes, offsets, the Zip64 end-of-central-directory) are range-checked against the file the same way.
- What encryption does not hide. The ZIP format encrypts file contents only: file and folder names, sizes and timestamps stay readable without the password (the app's inline password panel says so). Folder entries are never encrypted. AE-2 zeroes the CRC-32 in the headers, so it does not leak a checksum of the plaintext.
- Fresh key material per entry. Every encrypted entry gets its own random salt and PBKDF2 derivation, so no two entries share a key or an AES-CTR keystream.
- Only passwords every reader can use. A new archive's password must be printable ASCII and
at most 99 characters. 7-Zip decodes ZIP passwords through the ANSI code page and rejects
longer AES passwords, so anything else would produce an archive 7-Zip cannot open. A cancelled
or refused password creates nothing, never an unencrypted archive; a tar-family format with a
password is refused, since tar has no encryption. The App and
pakko a's masked prompt do not take a disallowed key at all; one typed key blocks until the field is emptied, so a password typed in a Cyrillic layout cannot shrink to its ASCII leftovers (T-F199). - Pakko never logs or persists a password. A password lives only for the one operation that
asked for it ("apply to remaining" spans one multi-archive selection), with one exception: the
App's Archive Browser keeps a password that worked for the archive being browsed, in memory
only, so previews and extracts from it do not ask again (T-F200). It is forgotten when a
password is rejected, when another archive is opened and when the window closes. Caveat:
pakko x|t|a -p{pwd}puts the password on the command line, visible in shell history and the process list exactly as with 7-Zip's own-p— use a bare-p(or, forx/t, omit it) to get the masked interactive prompt instead. The App's new-archive password is typed inline in the window (T-F199): it is not stored or logged, and the window drops its references after every operation, on unticking encryption, on a format change and on window close. A managed string cannot be wiped, so this means the text is no longer kept, not that its memory is erased. - The Explorer prompt crosses a process boundary (T-F268). Explorer commands ask for the
password in a separate window process (
Archiver.OperationUi.exe, same package, same user), which sends it back toArchiver.Shellover one of two unnamed anonymous pipes created for that one operation — never on a command line, in a file, or through a named object another process could open. Shell closes its copies of the helper's pipe ends as soon as the helper starts, and every sandboxedtar.exeis started with an explicit inheritable-handle list (PROC_THREAD_ATTRIBUTE_HANDLE_LIST), so the untrusted-archive sandbox never holds a pipe end. The window empties its password box as soon as the text is read, and the message type prints the password as***. Shell does not trust the window with the answer's meaning: an unknown or malformed conflict answer is Skip, never Overwrite. If the window dies mid-prompt, the prompt is asked again in Shell's own native dialog.
Absolute Path Requirement
Always invoked as C:\Windows\System32\tar.exe — never as tar via PATH search. Prevents:
- EXE hijacking via PATH manipulation
- DLL side-loading from working directory
- User-placed
tar.exetaking precedence over system binary
PAR2 Recovery Data (T-F275, v1.8, in progress)
- What it protects against: accidental damage — bad sectors, a truncated copy. It is not authentication: whoever writes a PAR2 set chooses both the recovery data and the MD5s a repair is checked against, so repairing from a set of unknown origin trusts its author. An encrypted ZIP's AES authentication still applies after a repair.
- No secrets in it: recovery data is computed over the finished archive's bytes — an encrypted ZIP's ciphertext, never plaintext — and repair needs no password.
- New untrusted input: a PAR2 file is parsed like an archive: bounded memory and sizes, counts in 64 bits with a 32768-slice cap (the class of par2cmdline's GHSA-3c2j-rccw-j2vj), every packet's MD5 checked.
- No paths from the file: the repaired copy is a new file next to the archive (or in a folder the user picks); a name inside the set is only compared, never used as a path (par2cmdline's GHSA-j5pc-g362-c5xp). The original is never written, and the copy keeps its Zone.Identifier.
- MD5 is the format's checksum, not a security primitive (a won't-fix entry in
docs/CONVENTIONS.mdcomes with the code). - Test oracles (par2cmdline, par2cmdline-turbo, MultiPar; GPL-2.0) are downloaded with pinned
SHA-256s for tests only, never shipped or committed (
scripts/Get-Par2Oracles.ps1).
Design and research: docs/DECISIONS.md "T-F275 — PAR2 recovery data".
Vendored 7-Zip: Test-Only, Sandboxed, Never Shipped (T-F114)
tests/Archiver.Core.PerformanceTests/Tools/7-Zip/{x64,arm64}/7za.exe is a pinned,
hash-verified, LGPL-attributed copy of 7-Zip's standalone console binary, used purely as a
speed-comparison reference in automated performance-regression tests (T-F114). It exists only
in the test tree and is never referenced by, built into, or shipped inside
Archiver.Core/Archiver.App/Archiver.Shell or the MSIX package — this is not an exception to
this project's "no 7-Zip, no WinRAR, no third-party compression code" rule (see SPEC.md's
Security Rationale), it is entirely outside the shipped product's dependency surface. See
tests/Archiver.Core.PerformanceTests/Tools/7-Zip/NOTICE.md for provenance (exact version,
source URL, SHA-256, vendoring date) and CONVENTIONS.md for the packages-allowed note.
Why it's sandboxed anyway, even though it's test-only: the binary is hash-verified at
vendoring time, but a third-party executable checked into the repo is still worth containing on
the assumption that a hash check only proves the file matches what was downloaded, not that it's
safe to run unconditionally forever. Every 7za.exe launch (SevenZipRunner.cs) runs under a
Job Object (SandboxJobObject, reused directly from the tar.exe sandbox subsystem below —
ActiveProcessLimit = 1 so it cannot spawn further processes, plus RAM/CPU caps) via the same
SandboxedProcessLauncher tar.exe uses. This deliberately stops short of tar.exe's full
AppContainer + ACL'd-quarantine treatment: that layer exists to contain a hostile archive
feeding an untrusted-input parser (see "Why Sandbox tar.exe at All" above) — 7za.exe's input
here is Pakko's own freshly-generated fixture data, not attacker-controlled, so that specific
threat doesn't apply, and adding AppContainer's staging/ACL overhead would risk biasing the very
timing the tests exist to measure. The Job Object alone still meaningfully bounds the damage a
compromised copy of the binary itself could do (no process spawning, capped resource use)
without touching filesystem access or measured performance. See DECISIONS.md's T-F114 entry for
the full design rationale.
CI Signing Secret: New Supply-Chain Surface (T-F122)
.github/workflows/build.yml signs the MSIX with the same local self-signed CN=Pakko Dev
development certificate Deploy.ps1 uses, exported once as a PFX and stored as two GitHub
Actions repo secrets (PAKKO_DEV_CERT_PFX_BASE64, PAKKO_DEV_CERT_PASSWORD). This is a new
supply-chain surface worth naming explicitly: a compromised repository or organization secret
could be used to sign a malicious build that carries this project's dev-cert identity.
Why the residual risk is limited today: the cert is the same self-signed, sideload-only
certificate already described above — it carries no elevated trust of its own. It is not in the
Windows Trusted Root chain and is not recognized by SmartScreen; a package signed with it still
cannot install on a machine that hasn't separately been given PakkoDev.cer and had it placed in
Cert:\LocalMachine\TrustedPeople (see "Self-signed" in TASKS.md's T-F10 cert-options table).
In practice this means a compromised secret could produce a signed-looking package, but it could
not silently install anywhere Pakko isn't already explicitly trusted — the blast radius is
bounded by the same manual-trust step that already limits this cert's legitimate use.
This changes once T-F10 (SignPath Foundation) lands: a SignPath-issued certificate carries real, broadly-trusted Authenticode reputation, so the same secret-compromise scenario would then let an attacker produce a package that installs and runs without any of the manual-trust friction above. At that point this section should be revisited — likely tightening the workflow to require signing approval via SignPath's own managed CI integration rather than a bare PFX-in-secrets model, since SignPath's whole design point is to avoid handing the raw private key to CI at all. Tracked as a T-F10 Phase 1 follow-up, not solved by this task.
Component List (SBOM) and Build Attestations (T-F125, T-F365)
Every GitHub Release carries, next to each MSIX and CLI zip, a CycloneDX 1.6 SBOM
(pakko-msix-<arch>.cdx.json, pakko-win-<arch>.cdx.json) listing the third-party components
that artifact ships or needs at run time: the .NET runtime linked into each Native AOT exe
(runtime.win-<arch>.Microsoft.DotNet.ILCompiler), the managed packages compiled into the exes,
WebView2, and the Windows App SDK family behind the Microsoft.WindowsAppRuntime.2 framework the
MSIX depends on. Build-only packages and Windows ML (whose DLLs are kept out of the package) are
not listed. Both the SBOM and an SLSA build-provenance statement are attested by this repository's
CI (Sigstore-backed GitHub artifact attestations) against the artifact's own digest:
gh attestation verify pakko-win-x64.zip -R pakkoapp-oss/pakko --predicate-type https://cyclonedx.org/bom
gh attestation verify pakko-win-x64.zip -R pakkoapp-oss/pakko # SLSA provenance
The SBOM generator (the CycloneDX .NET tool, pinned) is third-party code, so CI runs it in a job of its own with read-only repository access, no secrets, no OIDC token and no shared NuGet cache; the jobs that hold the signing certificate only run GitHub's own attestation action on its output. It describes the exact package graph the build used, which the committed lock files and locked restore fix (T-F364).
Not covered: the MSVC runtime that Archiver.ShellExtension.dll (C++, no package
dependencies) links is not listed as a component; the Microsoft Store package is re-signed by
Microsoft and has no attestation from this repository.
Recommended Usage Context
This tool is appropriate for:
- Government and public sector organizations on Windows
- Defense and military administrative workstations
- Businesses with supply chain security policies
- Organizations that have banned or restricted 7-Zip / WinRAR on security grounds
- Any user who prefers an auditable, dependency-minimal archive tool
This tool is not a replacement for:
- Full-featured archivers where RAR/7z writing, encrypted 7z/RAR, encrypted file names, or encryption other than AES-256 ZIP is required
- Environments requiring FIPS 140-2 validated cryptography (password-protected ZIP is read and written via .NET's own AES/HMAC/PBKDF2, but the WinZip AES format itself mandates SHA-1 and Pakko makes no FIPS-validation claim; a password-protected 7z/RAR is detected and refused with a clear error, not silently mishandled — see "Encrypted-Archive Diagnostics" above)
Symlink Detection — Filesystem Compatibility Notes
T-F23 added symlink and junction detection to ZipArchiveService using
File.GetAttributes(path).HasFlag(FileAttributes.ReparsePoint). This section
documents how that detection behaves on every Windows-mountable filesystem.
How the Detection Works
IsReparsePoint calls GetFileAttributesW (via .NET's File.GetAttributes).
If the attribute FILE_ATTRIBUTE_REPARSE_POINT is set, the path is treated as
a symlink or junction and added to SkippedFiles. All exceptions are swallowed
and return false (conservative: if attributes are unreadable, let the
subsequent file-open produce an ArchiveError rather than silently skipping).
Non-NTFS Filesystem Considerations
This table covers all Windows-mountable filesystems. It applies to every place
in ZipArchiveService that touches the filesystem: IsReparsePoint,
AddDirectoryToArchiveAsync, ComputeDirectoryBytes, TryPropagateMotw.
| Filesystem | Reparse Points | IsReparsePoint |
Notes |
|---|---|---|---|
| NTFS | Yes | Correctly true/false | Symlinks, junctions, cloud stubs all detected |
| ReFS | Yes | Correctly true/false | Behavior identical to NTFS; all reparse point types supported |
| FAT32 | No | Always false |
No reparse points; all files enumerated and archived normally |
| exFAT | No | Always false |
Same as FAT32; FILE_ATTRIBUTE_REPARSE_POINT never returned |
| SMB/UNC (Windows server, NTFS/ReFS backend) | Possible | Usually correct | DFS junctions are followed transparently by the SMB redirector and appear as normal directories — they are not detected as reparse points. True NTFS symlinks may be blocked by server policy; access denial produces ArchiveError |
| SMB/UNC (Linux/Samba backend) | No NTFS equivalent | Always false |
Linux symlinks are resolved server-side; they appear as their targets (normal files/dirs) or as inaccessible entries. Symlink cycles are handled server-side — no infinite-loop risk. No reparse-point detection applies |
| ISO 9660 / UDF | No | Always false |
Read-only optical media; no reparse points; all files archived normally. UDF 2.5+ extended attributes are not exposed as FILE_ATTRIBUTE_REPARSE_POINT by the Windows driver |
| MOTW (ADS, Zone.Identifier) | NTFS/ReFS only | N/A | TryPropagateMotw writes Zone.Identifier ADS to extracted files. ADS is not supported on FAT32, exFAT, or network shares backed by non-NTFS servers — the write silently fails (best-effort, never fatal) |
Security Assumptions That Break on Non-NTFS Volumes
| Assumption | NTFS | FAT32/exFAT | SMB/Linux | Notes |
|---|---|---|---|---|
| Symlinks detected and skipped | ✅ | ✅ (none exist) | ⚠️ Partial | Linux symlinks not detected; see above |
| MOTW propagated to extracted files | ✅ | ❌ | ❌ (non-NTFS) | ADS not supported; best-effort silently no-ops |
| No infinite loop on circular symlinks | ✅ | ✅ | ✅ (server-side) | Safe on all filesystems |
All exceptions produce ArchiveError |
✅ | ✅ | ✅ | IOException propagates to caller catch blocks |
Known Limitation: Cloud Storage Stubs
Files that are cloud-only (OneDrive, not locally downloaded) carry
FILE_ATTRIBUTE_REPARSE_POINT | FILE_ATTRIBUTE_OFFLINE. IsReparsePoint
returns true, causing these files to be added to SkippedFiles rather than
being downloaded and archived. The user sees a skipped-file entry but no
indication that it was a cloud stub rather than a symlink.
This is safe (no data corruption or security issue) but is a usability
limitation. Distinguishing cloud stubs from true symlinks requires reading the
raw reparse tag (IO_REPARSE_TAG_CLOUD_* vs. IO_REPARSE_TAG_SYMLINK /
IO_REPARSE_TAG_MOUNT_POINT), which needs P/Invoke or FileSystemInfo.LinkTarget.
AMSI-Based Threat Scanning (T-F146)
"Scan for threats" (Explorer context menu, Archive Browser) calls the Windows Antimalware Scan
Interface (amsi.dll), the same non-elevated API PowerShell, browsers, and Office VBA/JScript
hosts use to ask the OS's registered AV/EDR "is this content safe" before acting on it. Pakko
never talks to a specific antivirus product directly — AMSI dispatches to
whichever provider is actually registered, which resolved a real design worry for free: a
third-party EDR shadowing Defender-only tooling is a non-issue here, since AMSI is provider-
agnostic by construction.
Provider dependency. If no AMSI provider is registered (AV disabled, uninstalled, or one that
never registers as a provider), a scan can't produce a real verdict. AmsiScanBuffer alone can't
tell this apart from "a provider is registered and says clean" — both return
AMSI_RESULT_NOT_DETECTED. Pakko checks HKLM\SOFTWARE\Microsoft\AMSI\Providers before scanning
and forces every finding to Inconclusive when it's empty, rather than risk rendering an
unscanned archive as Clean. Inconclusive is a first-class, distinctly-labeled result — never
silently collapsed into Clean anywhere in the UI.
Password-protected ZIP entries (T-F194). Password-protecting a payload is a well-known way to
slip malware past automated AV scanning, since the scanner can't see inside. Before T-F194 Pakko's
scan had the same blind spot: an encrypted ZIP entry was reported Inconclusive ("could not
read"), never actually scanned. Now "Scan for threats" asks for the password (same prompt as
Extract) and hands AMSI the decrypted plaintext, entirely in memory — no plaintext touches disk.
Without a password (declined, wrong, or none given) every encrypted entry stays Inconclusive
("password-protected, not scanned") — never Clean. A decrypted entry is only reported Clean
once its content integrity check also passes, so a wrong-but-accepted ZipCrypto password
(garbage plaintext) can never masquerade as a clean scan. Encrypted 7z/RAR remain unscannable
(they can't be decrypted at all) and stay Inconclusive.
Report-only — with a caveat. Pakko's own code never deletes, quarantines, or otherwise acts on
a detection; it reports and stops, the same "verify, don't act" posture as Test Archive. This is
not a guarantee that nothing on the machine reacts, though: when Defender is the registered AMSI
provider, it can and does act independently of Pakko's call — confirmed empirically during design
(a spike where Defender's own real-time on-access scanner intercepted a plain on-disk EICAR file
being read by tar.exe, logged as a separate detection from the AMSI call itself). Whatever
antivirus is installed governs its own behavior; Pakko's contribution is asking the question and
reporting the answer.
Scan depth and target. Deep/quarantine-expanded scanning only — no shallow "hash the archive
file as one opaque blob" mode, since Windows Explorer already ships a native "Scan with Microsoft
Defender" verb for any file/folder/drive that covers that case. Pakko's differentiated value is
scanning what that OS verb cannot reliably reach: an archive's expanded contents, especially for
tar-family formats it doesn't natively understand. ZIP entries are read and scanned entirely
in-process, straight from System.IO.Compression — no bytes ever touch disk. tar-family archives
reuse the exact same TarSandboxScope AppContainer quarantine T-F49/T-F52 already established for
extraction, extracting into the quarantine "out" directory and stopping there — no
move-to-destination phase ever runs, and the quarantine is deleted (using/Dispose()) whether
the scan finds a threat, comes back clean, or fails partway through.
Size limit. Entries above 256 MiB (AntivirusScanService.MaxScannableEntryBytes) are reported
Inconclusive rather than buffered whole into memory — AmsiScanBuffer's contract is an
in-memory buffer with no documented size limit. T-F151's Phase 0 spike tried the alternative,
IAmsiStream/IAntimalware::Scan (a real streaming COM interface the caller implements), and
found it actually fails above ~16-20 MiB against the real registered Defender provider on this
machine, while the simpler existing AmsiScanBuffer call scanned real content up to 256 MiB
without error — see docs/DECISIONS.md's T-F151 entry. 256 MiB is the empirically-verified
ceiling, not a documented AMSI limit; raising it further would need re-verifying against a real
provider rather than assumed safe by extrapolation.
Not recursive. A scan does not look inside a nested archive found within the archive being scanned (unlike the Archive Browser's T-F98 drill-down, which is a separate, unrelated feature). This is why every "no threats found" message says exactly that — never "this archive is safe" — since a nested archive's own contents were never examined.
Reporting Vulnerabilities
Report security issues via GitHub Security Advisories (private disclosure). Do not open public issues for security vulnerabilities.