CVE Patch Review

CVE-2026-11769 Root-Cause Fix Review for Grafana Operator Jsonnet Import Traversal

CVE-2026-11769 · Updated 2026-06-20 Root-cause

Summary

The patch replaces Grafana Operator's direct use of jsonnet.FileImporter with a custom ScopedImporter backed by os.Root, constraining Jsonnet imports to the extracted project root and blocking traversal-based file reads from the manager pod. Based on the provided diff and test coverage, this addresses the documented root cause of arbitrary file access during Jsonnet evaluation rather than merely filtering specific payloads.

Analysis

Vulnerability

CVE-2026-11769 describes a local file read in Grafana Operator's Jsonnet evaluation path that can be leveraged by namespace-level users to access arbitrary files from the manager pod and potentially escalate privileges. The vulnerable implementation configured the Jsonnet VM with the upstream file importer:

vm.Importer(&jsonnet.FileImporter{JPaths: jPath})

The patch summary and CVE context indicate the issue is directory traversal during import resolution. The upstream importer behavior, as quoted in the patch comments, does not treat JPaths as a confinement boundary: absolute paths and parent-directory traversal remain valid. That means a Jsonnet payload such as an importstr of an absolute path could read host-visible files from the operator container. The added regression fixture demonstrates this exact class of abuse with /etc/passwd in the embedded test project. See the official patch reference at GitHub commit 5bb71ae and the CVE records at CVE.org and NVD.

Patch

The patch removes direct reliance on jsonnet.FileImporter and introduces a custom ScopedImporter that reads files through os.Root opened on the extracted project directory. This is the key security change because os.Root provides path resolution scoped to a root directory, preventing escape via absolute paths or traversal sequences when reading files.

type ScopedImporter struct {
	Root    *os.Root
	fsCache map[string]*fsCacheEntry
	JPaths []string
}

func (importer *ScopedImporter) Import(importedFrom, importedPath string) (contents jsonnet.Contents, foundAt string, err error) {
	found, content, foundHere, err := importer.tryPath(importedPath)
	if err != nil {
		return jsonnet.Contents{}, "", err
	}

	for i := len(importer.JPaths) - 1; !found && i >= 0; i-- {
		found, content, foundHere, err = importer.tryPath(filepath.Join(importer.JPaths[i], importedPath))
		if err != nil {
			return jsonnet.Contents{}, "", err
		}
	}

	if !found {
		return jsonnet.Contents{}, "", fmt.Errorf("couldn't open import %#v: no match locally or in the Jsonnet library paths", importedPath)
	}

	return content, foundHere, nil
}

fsRoot, err := os.OpenRoot(extractTo)
if err != nil {
	return nil, fmt.Errorf("error creating os.Root: %w", err)
}

vm.Importer(&ScopedImporter{
	Root:   fsRoot,
	JPaths: jPath,
})

The patch also adds a regression test that builds a malicious Jsonnet project and asserts an error containing path escapes from parent. The embedded fixture includes:

local token = importstr '/etc/passwd';
{
  out: token,
}

This test is important because it validates the security property directly against an absolute-path import, which is central to the reported exploit path. Source: official patch commit.

Review

Pros

The patch appears to address the root cause rather than sanitizing a few dangerous strings. The vulnerable design flaw was trusting Jsonnet's standard file importer in a context where imports must be confined to extracted dashboard content. Replacing it with a root-scoped importer changes the trust boundary at the file access layer.

Using os.OpenRoot(extractTo) is a strong implementation choice because confinement is delegated to a filesystem primitive instead of ad hoc path normalization. That is materially better than checking for .. or rejecting leading slashes, both of which are easier to bypass or misapply.

The patch preserves expected Jsonnet library path behavior through JPaths while making those lookups relative to the scoped root. The inline comment explicitly documents why the upstream importer was unsafe, which improves maintainability and reduces the chance of regression.

The regression test is security-relevant and source-grounded: it uses an embedded project that attempts to import /etc/passwd and expects failure. This directly exercises the exploit primitive described by the CVE.

Cons

The provided diff shows no explicit resource cleanup for the opened os.Root. If the surrounding function does not close it elsewhere, that could introduce a file descriptor leak. This is not a security bypass in itself, but it is worth verifying in the full function context.

The test coverage shown is focused on absolute-path traversal. It does not demonstrate additional cases such as nested relative traversal through ../, symlink behavior inside the extracted tree, or edge cases involving crafted JPath entries. The use of os.Root suggests these should be handled correctly, but the evidence in the supplied snippets is narrower than the full attack surface.

The importer ignores the importedFrom parameter in the shown code. If Grafana Operator intentionally supports relative imports based on the importing file's directory, this may differ from upstream resolution semantics. That is primarily a compatibility concern, but it should be regression-tested for legitimate projects.

Verdict

Root-cause.

Based on the supplied patch, this is a root-cause fix. The vulnerable behavior came from using an importer that allowed filesystem access outside the extracted Jsonnet project, and the patch replaces that mechanism with a root-scoped importer backed by os.Root. The added test demonstrates that an absolute import of /etc/passwd now fails with a path-escape error, which is directly aligned with the CVE's arbitrary file read vector. While broader regression coverage would strengthen confidence, the security control introduced here is architectural and appropriately placed at the file access boundary. References: official patch, NVD, CVE.org.

Sources