You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
docs(specs): update specs for post-v0.6.0 security and type-safety hardening (#563)
Documents the GHSA-wcmx-7f9h-5mv5 component-aware containment fix in the
security pipeline spec, and adds a retrospective spec for the SanitizedMode
newtype plus the six enums newly marked non_exhaustive. Also documents the
CLI Verbosity enum replacing the verbose/quiet bool pair, including the
split-level clap flag bug it fixes.
Claude-Session: https://claude.ai/code/session_01P7y9J9CVJ7NosUvwt86Src
@@ -157,13 +158,14 @@ THEN the written file has those bits cleared; ValidatedEntry.mode reflects the s
157
158
| FR-016 | WHEN opening a destination path for a file, symlink-target read, or 7z temp-file write, THE SYSTEM SHALL use `O_EXCL`/`O_NOFOLLOW` (Unix) or an equivalent atomic create-exclusive open rather than a prior `Path::exists()` check, since `exists()` follows symlinks and reports `false` for a dangling one — letting a pre-planted symlink at the destination bypass duplicate detection and redirect the write outside the extraction root | must |
158
159
| FR-017 | WHEN a file entry is validated, THE SYSTEM SHALL produce a `QuotaPermit` capability token from `QuotaTracker::reserve` that the eventual write path must consume by value, so a validated `File` entry with no quota charge is unrepresentable and a reservation cannot be spent twice (see [[013-quota-permit-capability-token/spec]]) | must |
159
160
| FR-018 | WHEN `SecurityConfig`/`CreationConfig` builder methods are used, THE SYSTEM SHALL make them available only on the `Unvalidated` typestate, and `validate()` SHALL consume `self` and return `Result<T<Validated>>`; every downstream security function SHALL require `&SecurityConfig<Validated>` (see [[014-config-typestate-validation/spec]]) | must |
161
+
| FR-019 | WHEN comparing a validated entry's canonicalized parent path against the destination root on macOS/Windows, THE SYSTEM SHALL compare `Path::components()` pairwise (case-folding each segment) rather than the raw lowercased path strings, so a sibling directory sharing the destination root's name as a string prefix (e.g. `/tmp/destevil` vs. `/tmp/dest`) is not treated as contained within it, closing GHSA-wcmx-7f9h-5mv5 | must |
160
162
161
163
## 4. Non-Functional Requirements
162
164
163
165
| ID | Category | Requirement |
164
166
|----|----------|-------------|
165
167
| NFR-001 | Security | All security checks run before any bytes are written to disk for each entry |
166
-
| NFR-002 | Security | Path component matching is case-insensitive to prevent bypass on case-insensitive filesystems |
168
+
| NFR-002 | Security | Path component matching is case-insensitive and component-wise (not a raw string-prefix comparison) to prevent bypass on case-insensitive filesystems (GHSA-wcmx-7f9h-5mv5)|
| Symlink or hardlink target is empty or contains a null byte |`ArchiveError::SecurityViolation` (raw target bytes never embedded in the error message) |
287
309
| Dangling symlink pre-planted at an entry's destination path | Rejected via `O_EXCL`/`O_NOFOLLOW` open, not silently followed; requires an attacker-writable destination directory |
310
+
| On macOS/Windows, a validated entry's canonicalized path resolves (e.g. via a symlink) into a sibling directory sharing the destination root's name as a string prefix (e.g. `/tmp/destevil` vs. `/tmp/dest`) | Rejected — `paths_start_with` compares `Path::components()` pairwise, not raw lowercased strings (GHSA-wcmx-7f9h-5mv5, unreleased) |
288
311
| Hardlink with `allowed.hardlinks = false`| Entry skipped |
289
312
| Hardlink to a path not previously seen |`ArchiveError::HardlinkEscape`|
290
313
| setuid/setgid bits on Unix | Stripped silently via `fchmod` on the open descriptor; `ValidatedEntry.mode()` reflects sanitized value |
@@ -348,4 +371,7 @@ THEN the written file has those bits cleared; ValidatedEntry.mode reflects the s
348
371
-[[013-quota-permit-capability-token/spec]] — `QuotaPermit` capability-token pattern in detail
349
372
-[[014-config-typestate-validation/spec]] — `SecurityConfig`/`CreationConfig``Unvalidated`/`Validated` typestate in detail
Copy file name to clipboardExpand all lines: specs/005-cli/spec.md
+4-1Lines changed: 4 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -8,7 +8,7 @@ tags:
8
8
- cli
9
9
- rust
10
10
created: 2026-05-20
11
-
updated: 2026-08-04
11
+
updated: 2026-09-02
12
12
status: draft
13
13
related:
14
14
- "[[constitution]]"
@@ -166,6 +166,7 @@ THEN stdout contains valid JSON with all report fields; stderr contains no progr
166
166
| FR-084 |`extract --atomic --force` SHALL reject a destination that is itself a symlink (or, on Windows, a junction/reparse point) on all platforms, and SHALL perform every rename/remove in its destination swap `*at`-relative to a file descriptor pinned on the destination's parent directory (Unix only), never by re-resolving a logical path mid-extraction. This is scoped to `--atomic --force` only — see [[015-atomic-force-destination-swap-hardening/spec]] for the full GHSA-x8wr-7ww2-c94x fix | must |
167
167
| FR-085 | WHEN `extract --atomic --force`'s best-effort cleanup on failure cannot locate its temp/backup directory at the expected logical path (e.g. because an intermediate path component was redirected mid-extraction), THE CLI SHALL disclose the directory's actual current path (resolved via an open file descriptor, not the possibly-stale logical path) in its error output rather than leaving surviving content undisclosed (v0.6.0, #530) | should |
168
168
| FR-086 |`verify`/`list` SHALL flag a TAR entry whose path is not valid UTF-8 as a `Medium`-severity `SuspiciousPath``VerificationIssue` (portability risk, not a security issue — `security_status` is unaffected), flipping `status` to `Warning` instead of reporting `Pass` for an entry that may fail to extract on filesystems requiring UTF-8 names (v0.6.0, #528) | should |
169
+
| FR-087 |`--verbose` and `--quiet` SHALL be resolved once into a single `output::Verbosity` (`Quiet`\|`Normal`\|`Verbose`) via `impl From<&cli::Cli> for Verbosity`, and threaded through `output::create_formatter`, `HumanFormatter::new`/`with_writers`, `commands::extract::execute`, and `commands::create::execute`, rather than each site independently interpreting the two raw booleans; `Verbosity::from_flags` SHALL resolve `verbose: true, quiet: true` to `Quiet` in every case, including when the two flags are parsed at different `clap``ArgMatches` levels (e.g. a global `--verbose` combined with a subcommand-level `--quiet`), which `clap`'s `conflicts_with` on `--quiet` does not reject (unreleased, post-v0.6.0, #550, behavior change — see Edge Cases) | must |
169
170
170
171
## 4. Non-Functional Requirements
171
172
@@ -191,6 +192,7 @@ THEN stdout contains valid JSON with all report fields; stderr contains no progr
|`JsonFormatter<W: Write = Stdout>`| JSON formatter (v0.6.0) |`JsonFormatter::stdout()` (replaces the old unit-struct constructor), `JsonFormatter::with_writer()`|
193
194
|`PinnedDir` (`commands::atomic_swap`) | Unix-only file-descriptor handle on the destination's parent directory, used by `--atomic --force` to perform `*at`-relative renames (v0.6.0, #526) | See [[015-atomic-force-destination-swap-hardening/spec]]|
195
+
|`Verbosity` (`output`) | Enum replacing independent `verbose: bool, quiet: bool` parameters across formatter/progress construction (unreleased, post-v0.6.0, #550) |`Quiet`\|`Normal`\|`Verbose`; `from_flags(verbose, quiet)` resolves `quiet` as the deterministic tie-break when both are `true`; `impl From<&cli::Cli> for Verbosity` is the single resolution point |
|`extract` pre-flight destination conflict with many pre-existing files | At most 10 conflicting paths listed (sorted), remainder collapsed into `... and N more` (v0.6.0, #500) |
243
245
| Human-readable size >= 1 TB (e.g. `u64::MAX` bytes) | Renders as `"... TB"`, not an inflated GB figure (v0.6.0, #451) |
244
246
|`verify`/`list` on a TAR archive with a non-UTF8 entry name |`status: Warning` with a `Medium`-severity `SuspiciousPath` issue, not `Pass` (v0.6.0, #528/#529) — portability risk only, `security_status` unaffected |
247
+
|`exarch --verbose extract archive.tar.gz out --quiet` (global `--verbose`, subcommand-level `--quiet`) | Resolves deterministically to `Quiet` (no progress output, no summary) via `Verbosity::from_flags` (unreleased, post-v0.6.0, #550, behavior change) — previously the progress reporter selected verbose output (verbose-wins) while the formatter suppressed the summary (quiet-wins), an inconsistency `clap`'s `conflicts_with` did not catch since the two flags parsed at different `ArgMatches` levels |
0 commit comments