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
robustness: polish parsers, cap reads, hide $HOME (Phase 8.4)
Phase 8.4 of the post-firebaguette audit closes 9 of the 17 remaining LOW
findings, plus adds a Trust model section to SECURITY.md.
Why: the LOW tier doesn't include single-shot exploits but it is a steady
trickle of bad-UX-becoming-bad-security if left untouched (cleartext HTTP
clones, orphan post-push commits, $HOME paths in CI logs, unbounded
SKILL.md reads). Knocking them out now keeps the audit closure rate ahead
of the next audit cycle.
Code changes:
* config::slug_for_url rejects http:// with a clear "use HTTPS" message
(L1). MITM-on-clone surface gone.
* skill::parse_frontmatter strips a leading UTF-8 BOM before checking
the opening --- fence; files with a BOM are no longer treated as
"no frontmatter" (L2).
* skill::clean_value only strips quotes when they balance ("foo' is
left as-is rather than silently coerced to foo) (L4).
* git::reset_hard_to_parent helper; push.rs and detect.rs roll back
the orphan commit when git push fails after git commit succeeded (L7).
* skill::read_skill_md_bounded caps SKILL.md reads at 1 MiB and surfaces
a per-skill warning instead of slurping a 5 GiB file (L8).
* git::clone passes --no-recurse-submodules; cargo-dist's release
workflow now uses submodules: false on actions/checkout (L12 + L13).
* add.rs apply loop wrapped in per-skill IIFE: a single failure logs +
continues + saves partial state (L15). Matches pull.rs (v0.1.4) and
push.rs (v0.1.5).
* fs_util::display_path renders a $HOME-rooted path as ~/<rest>;
applied at every "library cache not found" site + the init JSON's
cache_path field so /Users/<operator>/ no longer leaks to CI logs
or agent-mode JSON (L17).
* list.rs replaces a bare eprintln! with ui::log_warning (which is
JSON-aware) (L18).
Docs:
* SECURITY.md gains a "Trust model" section that explicitly names the
three trust boundaries (Trusted / Semi-trusted / Adversarial) plus
an Out-of-scope list (compromised git binary, side-channel attacks).
External auditors can now know where to look without reverse-engineering
the code.
Deferred items (recorded with reasons in the audit notes):
* L3 (homograph warning) — needs unicode-confusables dep
* L5 (NFC normalisation) — needs unicode-normalization dep
* L6 (APFS case-insensitive collision warn) — UX-focused release
* L9 (Cargo.toml caret-semantics doc) — contributor-docs pass
* L10 (SLSA provenance) — release-workflow PR with its own dry-run
* L11 (cache-slug uniqueness) — pre-v1 single-library, revisit later
* L14 (fork destination prompt) — UX question for a broader fork review
11 new unit tests; cargo test: 147 pass; clippy clean; cargo audit clean.
Copy file name to clipboardExpand all lines: CHANGELOG.md
+29Lines changed: 29 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -6,6 +6,35 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
7
7
## [Unreleased]
8
8
9
+
## [0.1.6] - 2026-05-25
10
+
11
+
### Robustness & hygiene
12
+
13
+
Close the audit's Phase 8.4 "low-impact polish" batch: 9 of the 17 remaining LOW findings, plus a new "Trust model" section in SECURITY.md that documents the boundaries underlying all the v0.1.2 → v0.1.6 hardening work. The 8 deferred LOW items either need a new runtime dependency (homograph detection, Unicode normalization), require a release-workflow change (SLSA provenance), or interact with pre-v1 design questions (slug-collision uniqueness, fork-destination UX).
14
+
15
+
-**Force HTTPS in library URLs** (L1). `skillctl init http://github.com/owner/repo` was previously accepted and silently downgraded to cleartext for the initial clone. A network attacker on the operator's link could MITM the response and serve modified content. Now `slug_for_url` rejects `http://` with a clear "use HTTPS instead" message. SSH (`git@host:`, `ssh://`) is unchanged.
16
+
-**UTF-8 BOM stripped before frontmatter parse** (L2). Some editors (Notepad on Windows, occasionally VS Code) prepend a `\u{feff}` BOM to UTF-8 files. The frontmatter parser saw `\u{feff}---` instead of `---` and treated the whole SKILL.md as "no frontmatter." Now the parser strips a leading BOM before checking the opening fence.
17
+
-**Balanced quotes enforced in `clean_value`** (L4). `clean_value` was using `trim_matches(|c| c == '"' || c == '\'')` which silently stripped mismatched quotes — `"foo'` became `foo`. Mismatched quotes now pass through unchanged so the operator sees the malformed value and can fix it.
18
+
-**`git push` failure rolls back the just-created commit** (L7). When `git commit` succeeds but `git push` fails (network blip, auth expiry), the local commit sat orphaned in the cache, ahead of upstream. The next `fetch_and_fast_forward` would silently `reset --hard @{upstream}` it away — or, post-M10, refuse to refresh because the working tree happened to get dirty in between. New `git::reset_hard_to_parent` helper, wired into both `push` and `detect`, restores the cache to a clean state on push failure.
19
+
-**SKILL.md read capped at 1 MiB** (L8). `std::fs::read_to_string` for SKILL.md was unbounded — a 5 GiB file would be slurped silently into RAM during `discover`. New `read_skill_md_bounded` helper refuses to load more than 1 MiB and surfaces a per-skill warning instead.
20
+
-**Submodule recursion disabled** (L12 + L13). `git clone` now passes `--no-recurse-submodules` explicitly so a malicious library with a `.gitmodules` pointing at attacker-controlled repos cannot pull-through during `skillctl init`. The cargo-dist release workflow's `actions/checkout` steps switched from `submodules: recursive` to `submodules: false` (we have no submodules; this is defense-in-depth that survives the next `cargo dist init` regeneration). Skills do not use submodules; if a legitimate use case appears, it can be opt-in via an explicit flag later.
21
+
-**`add` continues on per-skill failure** (L15). The apply loop in `add` used `?` for `fs::remove_dir_all`, `copy_dir_all`, and the `source_path` strip-prefix — a single per-skill failure aborted the whole batch, and `.skills.toml` was only saved at the end, so partial successes were untracked. Now each skill is wrapped in an IIFE that logs a warning + continues on failure, and `project_config::save` always runs (capturing partial state). Same pattern as `pull` (v0.1.4) and `push` (v0.1.5).
22
+
-**`$HOME` rendered as `~/` in displayed paths** (L17). Absolute paths in error messages and JSON output (`library cache not found at /Users/<operator>/Library/Caches/...`) leaked the operator's Unix username into CI logs and agent-mode JSON. New `fs_util::display_path(&path)` swaps a leading `$HOME` with `~/` and is applied at every "library cache not found" / cache-path-display site.
23
+
-**`list`'s `eprintln!` routed through `ui::log_warning`** (L18). A single bare `eprintln!("warning: could not refresh library cache (...)")` in `list` bypassed the `--json` gating, polluting JSON consumers' stderr with non-JSON text. Now routed through the shared `ui::log_warning` helper, which is JSON-aware.
24
+
-**SECURITY.md trust-model section**. New section that explicitly names the three trust boundaries — Trusted (operator's machine, interactive flags, the binary itself), Semi-trusted (library URL and cache), Adversarial (frontmatter, `.skills.toml`, git working tree, non-interactive flag values) — plus an explicit Out-of-scope list (compromised git binary, side-channel attacks). External auditors and contributors can now know where to look without reverse-engineering the code.
**Deferred to a future release** (with reasons, since v0.1.6 explicitly chose to keep the scope minimal):
29
+
30
+
-**L3** (homograph warning, e.g. Cyrillic `а` vs Latin `a` in skill names). Needs a `unicode-confusables` (or similar) dep; warrants its own decision before adding a runtime crate.
31
+
-**L5** (NFC normalisation of paths/names). Needs `unicode-normalization`; same reasoning.
32
+
-**L6** (case-insensitive FS collision warning on APFS-CI). No new dep but ~30 lines of runtime logic; deferred to a UX-focused release.
33
+
-**L9** (Cargo.toml caret-semantics doc). Documentation-only; will land alongside a broader contributor-docs pass.
34
+
-**L10** (SLSA provenance / cosign attestations on release binaries). Release-workflow change; deserves its own PR + dry-run on a tag.
35
+
-**L11** (cache-slug collision via hash suffix). Pre-v1 with one-library-at-a-time, slug collisions are theoretical only; revisit if multi-library support lands.
36
+
-**L14** (prompt operator on fork destination instead of inheriting the source's parent). UX question best decided alongside a broader `fork` flow review.
Copy file name to clipboardExpand all lines: SECURITY.md
+29Lines changed: 29 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -7,6 +7,35 @@
7
7
- It reads YAML-like frontmatter from `SKILL.md` files and writes TOML to `.skills.toml` and the global config file.
8
8
- There is no network surface beyond what `git` does, no privileged operations, and no telemetry.
9
9
10
+
## Trust model
11
+
12
+
`skillctl`'s internal validation and sanitisation is shaped by the following trust boundaries. External auditors and contributors should focus probes on the **adversarial** category — anything in the **trusted** category is taken at face value.
- Flags typed by the operator on an interactive TTY. When skillctl runs interactively, `--dest <absolute-path>` and other path-accepting flags are accepted as-is.
18
+
- The skillctl binary itself, its dependencies (audited via `cargo audit` on every release), and the configuration written to the per-user config file (which only skillctl writes).
19
+
20
+
**Semi-trusted (the operator chose the source but its content is treated as adversarial):**
21
+
22
+
- The library repository URL passed to `skillctl init`. The host (GitHub) is trusted; the *content* it serves is not.
23
+
- The library cache (`~/Library/Caches/dev.umanio-agency.skills-cli/<slug>/`). Skillctl owns the directory but treats every file under it as adversarial after the initial clone.
24
+
25
+
**Adversarial (treated as untrusted in every code path that touches them):**
26
+
27
+
-`SKILL.md` frontmatter (`name`, `description`, `tags`) from any source — library cache, project tree, fork-locally targets. Sanitised at the discovery boundary; control bytes / ANSI / NUL / CRLF are rejected; oversize files (> 1 MiB) are refused.
28
+
-`.skills.toml` entries, especially those that arrive via PR. `name`, `source_path`, `source_sha`, `destination` are validated at load: identifier-class for `name`, hex regex for `source_sha`, lexical subpath check (no `..`, no absolute, no Windows prefix) for the two `PathBuf` fields. Duplicates and unknown fields are rejected.
29
+
- The library cache's git working tree and submodules. `git clone --no-recurse-submodules` blocks submodule pull-through; every git invocation runs with `-c core.hooksPath=/dev/null` so a malicious library cannot ship hook scripts.
30
+
- Skill folder contents: symlinks, hardlinks (Unix), FIFOs, devices, and sockets are refused at copy time. File modes are masked to `0o644 | (src_mode & 0o100)` on Unix — only the user-execute bit propagates.
31
+
- Non-interactive flag values (`--dest <path>` in `--json` / `--no-interaction` mode, where the "operator" may be an LLM running on attacker-supplied input). Absolute paths are rejected; `..` is always rejected.
32
+
33
+
**Out of scope (not defended against):**
34
+
35
+
- A compromised git binary or local credential helper. Skillctl uses whatever git you point it at; if `which git` returns a trojan, all bets are off.
36
+
- A compromised library repository owned by a third party that the operator explicitly trusts (e.g. corporate skills repo). Skillctl reduces blast radius via the controls above, but cannot make a malicious-but-trusted library safe.
37
+
- Side-channel attacks via filesystem timing, memory analysis, or OS-level surveillance. The threat model assumes a normal single-user developer machine.
38
+
10
39
## Reporting a vulnerability
11
40
12
41
If you find a security issue (e.g. a way to make `skillctl` write outside the destinations it's supposed to, or to leak credentials in error messages), please report it privately:
0 commit comments