hashicorp/nomad · critical · ErrSandboxEscape
artifact includes symlink that resolves outside of sandbox
Error message
artifact includes symlink that resolves outside of sandbox
What it means
Nomad's artifact getter runs downloads inside a sandbox rooted at the task's allocation directory. After extraction, it verifies every symlink target stays inside that sandbox root; if any symlink resolves outside it, the getter aborts and returns the exported sentinel ErrSandboxEscape ('artifact includes symlink that resolves outside of sandbox'). This prevents a malicious artifact from escaping the chroot-like sandbox and reading or overwriting host files.
Source
Thrown at client/allocrunner/taskrunner/getter/util.go:33
"path/filepath"
"runtime"
"sort"
"strings"
"unicode"
"github.com/hashicorp/go-getter"
"github.com/hashicorp/nomad/client/interfaces"
"github.com/hashicorp/nomad/helper/subproc"
"github.com/hashicorp/nomad/helper/users"
"github.com/hashicorp/nomad/nomad/structs"
)
const (
// githubPrefixSSH is the prefix for downloading via git using ssh from GitHub.
githubPrefixSSH = "git@github.com:"
)
var ErrSandboxEscape = errors.New("artifact includes symlink that resolves outside of sandbox")
func getURL(taskEnv interfaces.EnvReplacer, artifact *structs.TaskArtifact) (string, error) {
source := taskEnv.ReplaceEnv(artifact.GetterSource)
// fixup GitHub SSH URL such as git@github.com:hashicorp/nomad.git
gitSSH := false
if strings.HasPrefix(source, githubPrefixSSH) {
gitSSH = true
source = source[len(githubPrefixSSH):]
}
u, err := url.Parse(source)
if err != nil {
return "", &Error{
URL: artifact.GetterSource,
Err: fmt.Errorf("failed to parse source URL %q: %v", artifact.GetterSource, err),
Recoverable: false,
}View on GitHub (pinned to 482b49bf1a)
Solutions
- Treat the artifact source as untrusted/compromised: remove or rebuild the artifact so it contains no symlinks resolving outside its own root
- Use retries: false plus the artifact's optional/chroot settings appropriately, and re-download from a trusted mirror
- If the symlink is legitimate and points outside, restructure the artifact (relative in-sandbox links) or split it into multiple artifact stanzas
- Upgrade Nomad if you believe a safe archive is being flagged — sandbox link validation has had hardening fixes across versions
Example fix
// before: artifact references a file outside the sandbox via symlink source = "https://example.com/bad.tar.gz" // contains symlink /etc/passwd -> ../../etc/passwd // after: rebuild artifact with in-root relative symlinks only tar -czf good.tar.gz --transform 's|^/etc/passwd|./passwd|' app/ // and verify before publishing: tar -tvzf good.tar.gz | grep -E '^l' # inspect every link target
Defensive patterns
Strategy: try-catch
Validate before calling
// Go: pre-scan an untrusted archive before handing it to the getter
func archiveHasSuspiciousLinks(tarPath string) error {
r, _ := os.Open(tarPath); defer r.Close()
return tar.NewReader(r).Iterate(func(h *tar.Header) error {
if h.Typeflag == tar.TypeSymlink {
target := filepath.Join(filepath.Dir(h.Name), h.Linkname)
if !strings.HasPrefix(filepath.Clean(target), "./") {
return fmt.Errorf("symlink %s -> %s escapes artifact root", h.Name, h.Linkname)
}
}
return nil
})
} Type guard
// Sentinel check helper
func IsSandboxEscape(err error) bool {
return errors.Is(err, getter.ErrSandboxEscape)
} Try / catch
// Go has no try/catch; use errors.Is on the exported sentinel
if err := sbox.Get(env, artifact, "nobody"); err != nil {
if errors.Is(err, ErrSandboxEscape) {
// quarantine artifact, alert, do not retry blindly
return fmt.Errorf("artifact %q rejected: %w", artifact.GetterSource, err)
}
return err
} Prevention
- Build artifacts with relative in-root symlinks only; never ship absolute links
- Inspect archives (tar -tvf, unzip -l) for symlinks before publishing
- Pin and verify artifact sources (checksums/signed releases) so tampered archives are caught
- Run untrusted-artifact clients with minimal host privileges as defense in depth
When it happens
Trigger: Calling sbox.Get(env, artifact, user) (or the getter pipeline it backs) where the downloaded archive (tar/zip/git repo) contains a symlink or hardlink whose resolved target is not within the sandbox directory — e.g. a link to /etc/passwd, ../.., or an absolute path outside the alloc dir. The check fires in util.go:474 when isWithin is false during post-extraction link verification.
Common situations: Consuming a third-party artifact (tarball, git repo, GitHub release) that contains malicious or careless symlinks — e.g. build outputs linking to absolute paths on the build machine, or a deliberately weaponized archive (symlink-traversal attack). Also seen when an artifact was repackaged preserving absolute symlinks.
Related errors
- error evaluating symlink: %w
- archive contains symlink that escapes alloc dir
- running container as ContainerAdmin is unsafe; change the co
- ACL token not found
- error reading symlink: %v
AI-assisted analysis of hashicorp/nomad@482b49bf1a (2026-09-04).
Data as JSON: /api/errors/64aca86005c1b4a2.
Report an issue: GitHub.