skillctl: argument injection, path traversal in --dest, FIFO/device DoS, hardlink exfiltration, and commit-trailer forgeryGHSA-74p7-6h78-gw8p
Fix: umanio-agency/skillctl@28dfce3GHSA-74p7-6h78-gw8p is a remote code execution vulnerability in skillctl. A fix is available for skillctl — see the affected versions and patch details below.
Real-World Exposure
skillctlReal-time download stats are indexed for npm and PyPI packages. This vulnerability affects crates.io packages — download data is not available via public APIs for these ecosystems.
Description
Impact
Following the path-safety patches in GHSA-wx3m-whqv-xv47 (v0.1.2), a comprehensive multi-angle audit surfaced five further vulnerabilities, now patched in v0.1.3:
source_shaargument injection ingit ls-tree(CRITICAL).InstalledSkill.source_shadeserialized from.skills.toml(committed, PR-mergeable) flowed unvalidated intogit ls-tree -r -z <refspec> -- <path>. Because the refspec sits before--, an attacker who slipped a malicious.skills.tomlinto a PR could setsource_sha = "--name-only"/--abbrev=0/--output=…and corrupt the diff classifier (which drivespull/pushdestructive decisions), or forge a divergence state to trickpush --on-divergence overwriteinto clobbering the wrong content.- FIFO / device / socket denial-of-service in
copy_dir_all(CRITICAL). The file-type branch only checkedis_dir()/is_symlink(); a FIFO inside a skill folder fell through tofs::copy, which blocks indefinitely waiting for a writer. A character device like/dev/zerowould read until OOM. Reachable onskillctl addagainst any adversarial library. add --destarbitrary-directory wipe in agent mode (HIGH).--destaccepted absolute paths and..traversal without validation, so a single invocationskillctl add --dest /Users/victim/.ssh --on-conflict overwrite --skill <maliciously-named>wouldremove_dir_allarbitrary directories — no.skills.tomlround-trip required. Reachable in any non-interactive / agent-driven workflow where flag values may be attacker-supplied.- Commit-message trailer forgery via skill names (HIGH). Skill names were spliced verbatim into
git commit -m "update skill: <name>"and into thecommit.messagefield of--jsonoutput. A skill namedfoo\nCo-Authored-By: evil@xproduced a forged commit trailer that downstream tooling (Linear, GitHub commit-bot, release-notes scrapers) treats as real authorship metadata. - Hardlink exfiltration via the round-trip (HIGH).
fs::symlink_metadatareports a regular file for hardlinks (shared inode), andfs::copyreads the target content. An untrusted agent writing<project>/my-skill/dataas a hardlink to~/.ssh/id_rsawould have shipped the SSH key content to the (possibly public) library on the nextskillctl pushordetect.
Patches
Fixed in v0.1.3:
InstalledSkill::validaterejects anysource_shathat isn't 40–64 hex characters.fs_util::copy_dir_allonly allows regular files and directories; FIFO / socket / device / other special files are rejected withAppError::Config.commands::add::resolve_destinationrejects..unconditionally and rejects absolute paths in non-interactive /--jsonmode.- New
src/sanitize.rsmodule:validate_identifier(strict, no control bytes / newlines / ESC, used for skillname+ individualtags) andvalidate_message_safe(lenient, allows\n+\t, rejects\r+ DEL + C0/C1 controls, used fordescriptionand--message). Wired at theskill::discoverandread_tagsboundaries so poisoned skills are dropped silently and poisoned descriptions/tags are stripped from otherwise-valid skills. fs_util::copy_dir_allchecksmetadata.nlink() > 1on regular files (Unix) and refuses hardlinked content.
All checks are lexical or single-syscall (symlink_metadata, metadata). No canonicalize, no TOCTOU windows. 23 new unit + integration tests cover each rejection class; cargo test: 95 pass; clippy clean; cargo audit clean.
Workarounds
Upgrade to v0.1.3. Pre-patch mitigations are awkward but possible:
- Audit every
.skills.tomlsource_shafield before runningskillctl pull/push/detect. - Audit library content for FIFO / device files and hardlinks before running
skillctl add. - Never invoke
skillctl addwith attacker-controllable--destvalues in agent / CI contexts. - Never use
--messagewith attacker-controlled content.
Credit
The findings were surfaced by a maintainer-led multi-angle audit (6 parallel sub-agents, one per threat-model dimension) following the firebaguette audit that motivated v0.1.2. The methodology (cross-agent convergence to identify the most exploitable items) is documented in the project's internal decisions log; the strongest signal was the four-of-six independent convergence on the source_sha vector.
Resources
- Fix commit: 28dfce3
- Release: https://github.com/umanio-agency/skillctl/releases/tag/v0.1.3
- Prior advisory (path-safety + symlinks): GHSA-wx3m-whqv-xv47
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 🦀crates.io | skillctl | all versions | 0.1.3cargo update -p skillctl --precise 0.1.3 |
Detection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for skillctl, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.
Fix
Update skillctl to 0.1.3 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-74p7-6h78-gw8p is resolved across your whole dependency graph.
Workarounds
Do not deserialise data from untrusted sources: where the format allows it, restrict deserialisation to an explicit allowlist of expected types, and prefer a data-only format (JSON, Protobuf) over one that can reconstruct arbitrary objects until you can upgrade.
Frequently Asked Questions
Is GHSA-74p7-6h78-gw8p in your dependencies?
Find it across crates.io, including transitive dependencies.