This document lists the threats pin is designed to defend against,
where each defence lives in the code, and the threats that are
out of scope. The structured adversary-by-asset threat model lives in
THREAT_MODEL.md. Lockfile schema is in SPEC.md.
A package author, a registry, a CDN, and the network path between them
are all potentially adversarial. The lockfile is the user's contract
with the version they once approved. pin exists to make that contract
verifiable: every fetched byte is anchored to a registry-published hash
or a Trust-On-First-Use hash; no code from a fetched package ever runs;
the manifest contains the full bill of materials with no transitive
resolution. The threat model is small because the feature set is small.
A malicious manifest entry could in principle smuggle .. into the
output path. Three layers stop it:
manifest.validateFilePathrejectsfiles:entries that are absolute, that begin with.., or thatpath.Cleanto...Entry.Slugreplaces/with__in package names and forge owners, so slugs cannot contain path separators.safeOut(sync.go) recomputes the joined destination path, takes itsfilepath.Relagainst the project'soutroot, and refuses to write if the relative path begins with... Defence in depth on the last step before bytes hit the disk.
- The compressed tarball is capped at
npm.DefaultMaxTarballBytes(100 MiB) viaSource.fetchTarball. Larger fetches abort before extraction. github.com/git-pkgs/archivesenforces its own per-entry and per-archive caps during extraction, and rejects symlink and hardlink entries.- The
package/prefix stripping isOpenBytesWithPrefix, which strips exactly one path component; entries outsidepackage/in the tarball are not exposed.
There is no code path that writes a file before computing and
recording its SHA-384. For npm sources, the tarball's outer hash is
verified against the registry's dist.integrity before extraction;
files extracted from a tarball whose outer hash didn't match never
reach disk. For forge sources, the commit SHA is the anchor; the
per-file hash is recorded on first fetch and verified on every
subsequent fetch. For url sources, the per-file hash is recorded on
first fetch (TOFU) and verified thereafter.
lock.Readcaps input atMaxLockfileBytes(16 MiB) viaio.LimitReader, so a malicious or truncated lockfile cannot trigger arbitrary memory growth.lock.ReadrequiresbomFormat: "CycloneDX"and refuses anypin:lockfile_versionit does not understand.- JSON parsing uses the standard library decoder; no extension or alias mechanism that could blow up.
manifest.Readparses withyaml.v3, which does not follow YAML anchors / aliases unbounded (no billion-laughs).- Unknown fields at the asset-entry level are rejected so a typo doesn't silently disable a validation rule. Unknown top-level keys are tolerated for forward compatibility with future versions of the spec.
- HTTP fetches go through
github.com/git-pkgs/registries/client, which sets a 30-second timeout, retries on 429 and 5xx with exponential backoff and jitter, and (viaregistries/fetch) caches DNS for 5 minutes and implements a per-host circuit breaker. - TLS verification is on by default; there is no flag to disable it.
- The HTTP client passes through
internal/safehttp, which rejects connections to loopback (127.0.0.0/8, ::1), RFC1918 private ranges, ULA (fc00::/7), CGNAT (100.64.0.0/10), link-local, and multicast addresses. DNS is resolved at dial time and the connection is made directly to the resolved IP, so a rebind between resolution and connect can't escape the gate. The defence applies to both the initial URL and every redirect target. - HTTP redirects are capped at 10 and re-validated on each hop
(
internal/safehttpCheckRedirect). Non-http(s)schemes (file://,gopher://,data://) are rejected on redirect so a registry that returns aLocation: file:///etc/passwdcannot exfiltrate local files.
pin never runs code from a fetched package. There are no install
scripts, no lifecycle hooks, no plugin loaders. The tool operates only
in stages 1–4 of the package-installation model (fetch metadata,
resolve, download, unpack); stages 5 (build) and 6 (post-install) are
absent by design. This eliminates an entire class of supply-chain
attacks that target stages 5–6, including the post-install download
leak where a package's real payload arrives via a postinstall hook.
- The release binary is built with
-trimpathandCGO_ENABLED=0, withmod_timestampset to the commit timestamp. - Goreleaser signs the checksum file with cosign keyless on every
release; consumers verify with
cosign verify-bloborgh attestation verify. - The released archives include syft-generated SBOMs.
- Sandboxing.
pinwrites files to disk that the consuming web server later serves to browsers. The defences above ensure the bytes match what was published; they do not ensure the bytes are free of bugs or backdoors that the publisher introduced. A vendored htmx with a backdoor in v2.0.6 will still backdoor your users. - Private registries.
pindoes not yet support authenticated registries. Thepkg:npm/foo?repository_url=...purl qualifier is reserved for when this lands. - Resource exhaustion via huge file counts. A manifest with ten thousand entries will issue ten thousand resolves. The shape is unusual enough that we don't defend against it.