← Back to Feed CACHED · 2026-07-12 08:09:28 · CACHE_KEY CVE-2026-39822
CVE-2026-39822 · CWE-61 · Disclosed 2026-07-08

On Unix systems

ASSESSED — NOISGATE V0.5
Vendor
Reassessed
Verdict:
Do you agree?
01 · The Real Story

A locked room where the exit sign flips on if you knock with a slash at the end

The Go standard library ships os.Root, a directory-sandbox primitive introduced in Go 1.24 to safely open files under a fixed root even when paths contain .. or symlinks. On Unix, the implementation delegates to openat(fd, path, O_NOFOLLOW). The kernel quirk: O_NOFOLLOW does *not* suppress symlink resolution when the path ends in / — it only refuses to follow the final component when there is no trailing slash. os.Root didn't strip that trailing /, so root.Open("symlink/") happily returned a handle to whatever the symlink pointed at, even outside the sandbox. Affected: Go 1.25.0 through 1.25.11 and Go 1.26.0 through 1.26.4; fixed in 1.25.12 and 1.26.5. Unix targets only (Linux/BSD/macOS); Windows is not affected because it uses a different resolver.

Vendor scored this HIGH / 7.8 with vector AV:L/AC:L/PR:L/UI:N/C:H/I:H/A:H — a generic local-privesc-style framing that assumes CIA impact on the host. That framing is too generous. This is a sandbox-escape in a library primitive: to weaponize it, an application must (a) use os.Root in the first place — a young, niche API — (b) accept attacker-controlled path strings, and (c) grant the process access to interesting files outside that root. Absent all three, the bug is inert. The realistic blast radius is scoped to the process's own filesystem view, not host takeover.

"Go stdlib sandbox-escape bug. Real-world impact requires attacker-controlled paths hitting os.Root — narrow, local, developer-facing."
02 · The Attack Path

4 steps from start to impact.

STEP 01

Locate a Go service using os.Root with attacker-influenced paths

Attacker enumerates a target Go application (server, CLI, container-init helper, artifact extractor) that constructs an os.Root and passes user-supplied path fragments into Root.Open, Root.Create, Root.Stat, or Root.OpenFile. Since os.Root is Go 1.24+ and marketed specifically as *the* untrusted-path handler, adopters are typically archive extractors, static file servers, and multi-tenant sandboxes.
Conditions required:
  • Target compiled with Go 1.25.0–1.25.11 or 1.26.0–1.26.4
  • Application accepts attacker-controlled path component
  • Runs on Unix (Linux/BSD/macOS)
Where this breaks in practice:
  • os.Root was only introduced in Go 1.24 (Feb 2025); adoption is thin
  • Most Go web frameworks still use filepath.Clean + http.FileServer, not os.Root
  • Static analyzers (govulncheck) flag both the CVE and unsafe usage patterns
Detection/coverage: govulncheck v1.1+ flags CVE-2026-39822 against binaries and modules; Semgrep rule go.lang.security.audit.os-root-trailing-slash (community).
STEP 02

Plant or coerce a symlink inside the sandbox root

Attacker either (a) uploads/writes a symlink into the sandboxed directory via a legitimate write endpoint or archive extraction, or (b) exploits a tenant that already contains a symlink they control. The symlink must point to a target outside the root — /etc/passwd, /proc/self/environ, another tenant's directory, secrets mount, etc.
Conditions required:
  • Write access to a path inside the sandbox root, OR pre-existing attacker-controlled symlink
  • Sandbox root sits on a filesystem where the process can also read the target
Where this breaks in practice:
  • Read-only sandboxes eliminate step 2a entirely
  • AppArmor/SELinux profiles restrict which paths the process may read
  • Container filesystem overlays often mask sensitive host paths
Detection/coverage: Auditd openat on symlink-through-root or file-integrity monitoring on tenant dirs; falco rule Read sensitive file untrusted fires on typical exfil targets.
STEP 03

Trigger open via path with trailing slash

Attacker submits a request whose path resolves to <symlinkname>/. The application calls root.Open("<symlinkname>/"). The underlying openat follows the symlink because the trailing slash forces symlink resolution regardless of O_NOFOLLOW. A file descriptor is returned pointing outside the root.
Conditions required:
  • Application does not strip trailing slashes before calling Root
  • Application returns file content / metadata to the attacker
Where this breaks in practice:
  • Many wrappers normalize paths (strings.TrimRight(p,"/")) upstream, killing the bug
  • Directory-only endpoints won't return file bytes even if fd is obtained
Detection/coverage: No signature-based network detection; artifact is a benign-looking HTTP path ending in /.
STEP 04

Read/write data outside the sandbox

With the escaped fd, the attacker reads sensitive files (credentials, other tenants' data, /proc for env vars), or writes if Root.Create/OpenFile was called. Impact is bounded by the OS uid the process runs under — not root, in most modern container deployments.
Conditions required:
  • Process uid can read/write the target file
  • Application surfaces file contents or accepts writes
Where this breaks in practice:
  • Non-root container uids see very little outside their own tenant dir
  • Read-only rootfs blocks write path entirely
Detection/coverage: EDR file-access telemetry; egress DLP on credential shapes.
03 · Intelligence Metadata

The supporting signals.

In-the-wild exploitationNone observed. Not in CISA KEV. No public campaigns reference this CVE as of 2026-07-12.
Public PoCTrivial one-liner in the Go issue tracker (go.dev/issue/79005); no weaponized toolkit.
EPSS0.00181 (roughly 45th percentile) — market signal says essentially no exploitation pressure.
KEV statusNot listed.
CVSS vectorAV:L/AC:L/PR:L/UI:N/C:H/I:H/A:H — local vector, low priv, no UI. Vendor treats it as local privesc-shaped, which overstates real reach.
Affected versionsGo 1.25.0–1.25.11 and 1.26.0–1.26.4 on Unix only. Windows unaffected. Anything pre-1.24 predates os.Root.
Fixed versionsGo 1.25.12, 1.26.5. Distro backports: RHEL via RHSA-2026:37435, Debian/Ubuntu golang-1.25 security updates, Alpine go 1.26.5-r0.
Exposure populationNo Shodan/Censys fingerprint (library-level bug, not a service). Realistic exposed population is the intersection of {Go 1.25+/1.26 binaries} ∩ {uses os.Root} ∩ {Unix} ∩ {attacker-controlled paths}. Small.
Disclosure date2026-07-08, coordinated release with Go 1.26.5 / 1.25.12.
ReporterMundur (GitHub M0nd0R), via Go security team.
04 · The Call

noisgate verdict.

Final Verdict
DOWNGRADED to LOW (3.4/10)

The single decisive factor is narrow reachable population: the bug only bites Go 1.25/1.26 Unix binaries that both use the young os.Root API and pipe attacker-controlled path strings into it — an intersection that is a rounding error across a 10,000-host fleet. Blast radius is bounded by the process uid and the sandbox's own tenant data, not host or domain compromise.

HIGH technical root cause and fix
HIGH no active exploitation
MEDIUM installed-base exposure estimate (no scanner telemetry for library-level bugs)

Why this verdict

  • Library-level sandbox bug, not a service RCE: the vulnerability lives inside a Go stdlib primitive. There is no listening port, no network vector, no automated exploitation surface.
  • Vector requires local + low priv + attacker-controlled path: AV:L/PR:L means the attacker already runs code or holds a session on the box, OR feeds path input into a specific vulnerable API. This is post-initial-access or narrowly application-scoped.
  • os.Root adoption is thin: the API shipped in Go 1.24 (Feb 2025). Sixteen months later, the overwhelming majority of Go code still uses filepath.Join + manual cleaning. The reachable code population is small.
  • Role multiplier — archive extractor / multi-tenant file server: on hosts where os.Root is used to sandbox untrusted archive extraction (e.g. artifact stores, CI runners, container image builders), a successful escape can reach adjacent tenant data or CI secrets. Blast radius = one process's fs view, not fleet.
  • Role multiplier — build server / CI runner: if the vulnerable code path is inside a self-hosted GitHub Actions runner or GitLab runner extracting attacker-supplied archives, escape ends at the runner's uid; not a supply-chain pivot unless the runner also has signing keys mounted — a config problem, not a CVE problem.
  • EPSS 0.00181 + no KEV + no in-the-wild signal: market pricing agrees with practitioner intuition — no exploitation pressure.

Why not higher?

MEDIUM would require broader reachable population or a plausible network-reachable trigger. Neither exists: this is a Unix-only, library-level bug in an API most Go code doesn't call yet, requiring attacker-controlled path input plus the ability to plant or find a symlink inside the sandbox. HIGH would require a canonical high-value-role component (IdP, hypervisor, DC, PAM, edge appliance) — Go stdlib is horizontal infrastructure but this specific API's blast radius is bounded to one process's filesystem view.

Why not lower?

IGNORE is inappropriate because the bug is real and does allow sandbox escape in vulnerable applications; archive-extraction and multi-tenant file-serving are legitimate os.Root use cases and the escape is trivial once the preconditions are met. Any team running a Go-based artifact extractor, CI runner, or multi-tenant SaaS needs to rebuild against a fixed toolchain — this is not backlog-only material.

05 · Compensating Control

What to do — in priority order.

  1. Rebuild Go binaries against Go 1.25.12 or 1.26.5 — Only a recompile removes the flaw; there is no runtime toggle. Rebuild all first-party Go services and re-release. Target: within the noisgate remediation SLA of 365 days for LOW, but prioritize any binary that touches attacker-controlled paths within 90 days.
  2. Run govulncheck ./... across your Go monorepo and CIgovulncheck reports both the CVE presence *and* whether your code actually calls the vulnerable symbols. Anything flagged as reachable jumps to the front of the remediation queue. Wire into CI as a blocking check within 30 days.
  3. Strip trailing slashes before passing user input to Root.Open/OpenFile/Stat — One-line application-layer mitigation: p = strings.TrimRight(p, "/") before the Root call. Ship as a defensive patch to any service you cannot recompile against fixed Go immediately.
  4. Audit archive-extraction and multi-tenant file paths for os.Root usage — Grep for os.OpenRoot, .OpenInRoot, Root.Open, Root.Create. These are the code paths where escape has real consequence. Prioritize hosts running CI runners, artifact stores, and SaaS tenant isolators.
  5. Enforce non-root uid and read-only rootfs on container workloads that use os.Root — Even if the escape succeeds, kernel-level access controls limit what the escaped fd can read/write. Cheap defense-in-depth via existing Pod Security Standards or systemd ProtectSystem=strict.
What doesn't work
  • WAF rules blocking trailing slashes — normal REST endpoints legitimately end in /; you'll break the app before you stop the exploit.
  • Chroot / namespace isolation alone — the escape happens inside the process's own view; if the target file is reachable via the process's normal fs view, chroot doesn't help.
  • SELinux/AppArmor without a tightly-scoped profile — default profiles for generic Go binaries won't distinguish sandbox-legal vs sandbox-escaping reads. Only a per-app profile pinned to the intended tenant directory helps.
06 · Verification

Crowdsourced verification payload.

Run on any Linux/macOS host that ships Go binaries or has the Go toolchain installed. Invoke as ./check-cve-2026-39822.sh [optional-binary-or-dir]; defaults to scanning $PATH and /opt. No root required, but root gives broader filesystem coverage. Emits VULNERABLE / PATCHED / UNKNOWN per binary and sets exit code accordingly.

noisgate-verify.sh
BASHREAD-ONLYSAFE
#!/usr/bin/env bash
# check-cve-2026-39822.sh
# Detects Go binaries built with a toolchain vulnerable to CVE-2026-39822.
# Vulnerable: Go 1.25.0-1.25.11, 1.26.0-1.26.4 on Unix.
# Patched:    Go 1.25.12+, 1.26.5+, or anything <1.25 (predates os.Root).

set -u
SCAN_TARGETS=("${1:-}")
if [[ -z "${SCAN_TARGETS[0]}" ]]; then
  SCAN_TARGETS=($(echo "$PATH" | tr ':' ' ') /opt)
fi

RC=0
SEEN=0
VULN=0

is_vuln_version() {
  # $1 = version string like go1.25.4
  local v="${1#go}"
  local major minor patch
  IFS='.' read -r major minor patch <<< "$v"
  [[ "$major" != "1" ]] && return 1
  case "$minor" in
    25) [[ "${patch:-0}" -lt 12 ]] && return 0 ;;
    26) [[ "${patch:-0}" -lt 5  ]] && return 0 ;;
  esac
  return 1
}

check_binary() {
  local bin="$1"
  local ver
  ver=$(go version "$bin" 2>/dev/null | awk '{print $2}')
  [[ -z "$ver" ]] && return
  SEEN=$((SEEN+1))
  if is_vuln_version "$ver"; then
    echo "VULNERABLE  $bin  ($ver)"
    VULN=$((VULN+1))
    RC=1
  else
    echo "PATCHED     $bin  ($ver)"
  fi
}

if ! command -v go >/dev/null 2>&1; then
  echo "UNKNOWN: 'go' toolchain not on PATH; cannot introspect binaries." >&2
  exit 2
fi

for tgt in "${SCAN_TARGETS[@]}"; do
  [[ -z "$tgt" || ! -e "$tgt" ]] && continue
  if [[ -f "$tgt" ]]; then
    check_binary "$tgt"
  else
    while IFS= read -r -d '' f; do
      [[ -x "$f" && -f "$f" ]] && check_binary "$f"
    done < <(find "$tgt" -maxdepth 3 -type f -perm -u+x -print0 2>/dev/null)
  fi
done

echo "---"
echo "Scanned Go binaries: $SEEN   Vulnerable: $VULN"
[[ $SEEN -eq 0 ]] && { echo "UNKNOWN: no Go binaries found in scan targets."; exit 2; }
exit $RC
07 · Bottom Line

If you remember one thing.

TL;DR
Monday morning: don't panic-patch this one. Under the noisgate mitigation SLA for LOW there is *no mitigation deadline* — go straight to the noisgate remediation SLA window of ≤ 365 days for fleet-wide rebuilds against Go 1.25.12 / 1.26.5. However, wire govulncheck into CI within the next 30 days so any newly-reachable call site trips a build. Any service you own that (a) is a Go binary, (b) uses os.Root, and (c) accepts attacker-controlled path strings — CI runners, archive extractors, multi-tenant file APIs — is the priority-90-day bucket: rebuild those first and, as a bridging patch, strip trailing slashes at the app layer. Windows-only Go services and anything on Go ≤ 1.24 are safe by construction — deprioritize.

Sources

  1. Go security release announcement (Go 1.26.5 / 1.25.12)
  2. oss-sec disclosure thread for CVE-2026-39822
  3. Red Hat CVE page (RHSA-2026:37435)
  4. Red Hat errata RHSA-2026:37435
  5. Go vulnerability database index
  6. Upstream Go issue #79005
  7. THREATINT CVE-2026-39822 record
  8. govulncheck documentation
Peer Review

What defenders are saying.

Submit a review attribution: handle + country only
0 flags selected · stored anonymously
Validation Results

Crowdsourced verification outputs.

Results submitted by users who ran the verification payload against their environment.