If you discover a security vulnerability, please report it responsibly by emailing matthewcgetz@gmail.com. Do not open a public issue.
You should receive a response within 72 hours. If accepted, a fix will be developed privately and released as a patch version.
fs is a filesystem-helpers library. Its security posture is shaped around three classes of risk:
- Untrusted input as a path. A user-controlled filename, an archive entry's name, or a glob pattern reaching a privileged context.
- Untrusted input as bytes. Reading a file that may be attacker-controlled (or pointed at
/dev/zero); extracting an archive from an untrusted source. - Concurrent / racing actors. TOCTOU windows between a check (
Stat) and a use (Open), or between resolving a path and acting on it.
The package's job is to make the safe path the easy path. The defenses below are the ones it bakes in by default.
Every entry point that consumes external bytes is bounded by default. Disabling the bound is opt-in and documented.
- Bounded reads.
ReadFile,ReadLines,ReadFirstLine, andOpenLineshonorWithMaxSize(defaultDefaultMaxReadSize= 100 MiB). A file exceeding the cap returnsErrFileTooLarge. The cap defends against/dev/zero-class reads, runaway logs, and similarly pathological inputs. Set the cap to0only when the source is a trusted regular file of known size. - Bounded archive extraction.
ExtractArchive/ExtractArchiveFilehonorWithArchiveMaxBytes(default 10 GiB). The cumulative extracted byte count is tracked across every entry; exceeding it returnsErrArchiveTooLarge. Defends against zip-bomb / tar-bomb attacks. Zip stream extraction additionally caps the temp-file buffer atmaxBytes+1so a crafted compressed payload cannot exhaust disk before the cap trips. - Bounded find-up walks.
FindUp,FindUpAll, andProjectRoothonorWithMaxAncestors(default 32). Defends against pathological symlink loops and deeply-nested mount points that would otherwise drive the walk indefinitely.
- Zip-slip / tar-slip. Every archive entry resolves through
MustBeChildOf(dst, ...)before any filesystem write. A crafted entry named../../../etc/passwd(or an absolute path) errors withErrEscapesRootrather than writing outside the extraction root. Hand-rolled extraction usingarchive/ziporarchive/tardirectly is the classic vulnerability; always useExtractArchive. - Symlink-escape inside archives. Tar symlink entries whose target resolves outside
dstare refused withErrEscapesRootat extraction time (seearchive_extract_tar.go:validateTarSymlinkTarget). - Mode masking. Archive entries are masked to
0o644for files and0o755for directories by default. Setuid, setgid, sticky, and other mode bits in the archive are stripped unlessWithPreserveMode(true)is set explicitly. Don't enable mode preservation for archives from untrusted sources. MustBeChildOf/IsSubpath. Public predicates for callers writing their own confined-path logic. Both performfilepath.Abs+filepath.Cleanbefore comparison.EvalSymlinksWithin. Resolves all symlinks in a path while verifying the resolved target stays inside a parent root. Use when a caller-supplied path must be a real on-disk location AND must not escape its sandbox.
Many filesystem APIs have time-of-check-to-time-of-use windows. The package provides primitives that close those windows where the platform allows.
OpenNoFollow. Opens a path with POSIXO_NOFOLLOW. If the final component is a symlink, returnsErrSymlinkLoop. Defends against link-replace attacks where an attacker swaps the target between aStatand anOpen. Intermediate components are still resolved normally; only the final component is protected.OpenAt. Resolves a relative path through a held directory file descriptor via POSIXopenat(2). Defends against directory-replace races where a directory is swapped for a symlink mid-walk.- Known limitation on Windows.
OpenAtfalls back tofilepath.Join+os.OpenFile; the fallback is not race-safe and is documented as such. Callers needing TOCTOU resistance on Windows must use other hardening (transactional NTFS, locked parent directories, etc.).OpenNoFollowopens the reparse point itself rather than following it; callers needing strict "refuse if final component is a symlink" semantics on Windows should inspect the returned file's mode. - The
Existsasymmetry.Exists(path)returnsfalseon permission errors as well as missing paths. This is deliberate for ergonomics but it meansif !Exists(p) { create(p) }is unsafe in privileged contexts; a privilege-restricted attacker can hide an existing file. UseStatdirectly and inspect the error when correctness depends on the distinction.
SanitizeFilenamestrips ASCII control bytes, the Windows-illegal characters (< > : " | ? * / \), and trailing dots and spaces. When the cleaned stem matches a Windows reserved device name (CON, PRN, AUX, NUL, COM1–COM9, LPT1–LPT9), an underscore is inserted before the extension;CON.txtbecomesCON_.txt. Suffixing the whole filename (CON.txt_) would leave Windows still treating the file as the CON device.IsReservedNameis portability-safe: it returnstruefor reserved names regardless of the host OS so callers writing files for cross-platform consumption (archive extraction, scaffolding) catch them before they hit a Windows reader.
HashCompareusescrypto/subtle.ConstantTimeCompareso integrity check call sites are not timing-oracles.- MD5 and SHA-1 are exposed for non-security uses (legacy compatibility, content-addressed caches, file-integrity checks against published digests). They are documented as broken for security purposes; do not use them for anything an attacker can influence.
- Default algorithm.
HashAlgo's zero value isHashSHA256. Pick SHA-256 or SHA-512 for new code unless you have a specific legacy reason.
- No shell execution. The package never invokes a shell or
execs a subprocess. Glob patterns are evaluated viapath/filepath.Match, notsh -c. - No silent env mutation. The package does not call
os.Setenvor otherwise mutate process-global env state. (Expandreads env vars viaos.LookupEnvbut never writes.) - No automatic privilege escalation. Symlink helpers on Windows surface the privilege error clearly when the calling user lacks the
SeCreateSymbolicLinkPrivilege; the package does not attempt to invokerunasor otherwise elevate. - No telemetry. The package emits no logs of its own from library entry points.
Watcheraccepts a*slog.LoggerviaWithLogger; the default is a discard logger.
Symlink's idempotency is best-effort under concurrent callers. Betweenos.Readlinkandos.Symlinkanother process can create the link; POSIXsymlink(2)is atomic for create-if-not-exists but Go's stdlib does not expose the flags needed to thread that atomicity through.ProjectRootcaches results process-globally with no invalidation. Long-lived processes that change project layouts on disk will see stale answers. Designed for CLI tools; consider it best-effort in daemons.- The watcher's polling backend reads mtimes from
os.Lstat. On filesystems that round mtime to second resolution (FAT, some SMB mounts), back-to-back writes within one second can be missed. UseWithPolling-with-a-finer-interval where this matters; the platform-native backend (post-v0.1) will close this gap on supported filesystems.
For the full list of caveats including TOCTOU, the Exists permission asymmetry, watcher debounce latency, and macOS /var -> /private/var resolution, see the "Pitfalls" section of the package documentation in doc.go.