CVE-2026-26187: escaping the Lake with a Path Traversal Two-Step
Feb 13, 2026·6 min read·4 visits
Executive Summary (TL;DR)
lakeFS failed to properly sanitize file paths in its Local Block Adapter. Due to a missing trailing slash in a prefix check and loose namespace validation, attackers can use `../` sequences to access files outside their repo. Fixed in v1.77.0.
A critical path traversal vulnerability in the lakeFS Local Block Adapter allows authenticated users to break out of their storage namespace boundaries. By exploiting a weak prefix validation check and a namespace logic error, attackers can read and write files in sibling repositories or unrelated directories on the host filesystem.
The Hook: Git for Data, Holes for Hackers
lakeFS bills itself as "Git for data." It’s a slick piece of engineering that brings branching, committing, and rolling back to your massive object storage buckets (S3, Azure Blob, GCS). But not everyone runs lakeFS on the cloud. For local development, testing, or just smaller deployments, lakeFS includes a Local Block Adapter. This component acts as a translation layer, pretending your local filesystem is an object store.
The Local Block Adapter is supposed to be a sandbox. It takes a root directory—say, /var/lib/lakefs/—and promises that all data operations will stay strictly within that box. It manages repositories as subdirectories, keeping repo-A separate from repo-B. Ideally, this creates a hard boundary, preventing a user in one repository from peeking at the data in another.
But boundaries in code are only as strong as the logic verifying them. CVE-2026-26187 is the story of what happens when you verify the general neighborhood but forget to check the specific house address. It turns out, lakeFS was checking if you were on the right street, but it didn't care if you were breaking into the neighbor's basement.
The Flaw: A Tale of Two Bugs
This vulnerability is actually a "buy one, get one free" deal. It stems from two distinct logic failures in pkg/block/local/adapter.go that, when combined, allow for a delightful escape. The first is a classic Prefix Bypass. When the adapter checked if a requested file path was valid, it used strings.HasPrefix.
Here is the catch: strings.HasPrefix("/data/lakefs_secrets", "/data/lakefs") returns true. Why? Because structurally, the string /data/lakefs is literally the start of /data/lakefs_secrets. By failing to append a directory separator (like /) to the check, the code allowed access to any sibling directory that started with the same characters. It’s like a bouncer letting you into "Club 33" because your ID says "Club 3"—close enough, right?
The second flaw is a Namespace Escape. lakeFS manages data in "Namespaces" (roughly mapping to repositories). The code correctly calculated where a namespace should be, but it didn't enforce that the final file path stayed inside that specific namespace. It only checked if the file was inside the global adapter root. This meant that if I'm in repo-A, I could issue a request for ../../repo-B/sensitive_file. The global check looks at the path, sees it resolves to /var/lib/lakefs/repo-B/sensitive_file, nods its head because that is indeed under /var/lib/lakefs, and serves up the stolen data.
The Code: The Missing Slash
Let's look at the smoking gun. This is the vulnerable verification function found in pkg/block/local/adapter.go. It's short, sweet, and insecure.
// Vulnerable Code: The Prefix Bypass
func (l *Adapter) verifyRelPath(p string) error {
// DANGER: No trailing separator on l.path
if !strings.HasPrefix(filepath.Clean(p), l.path) {
return fmt.Errorf("%s: %w", p, ErrBadPath)
}
return nil
}Because l.path (the adapter root) isn't terminated with a slash, an attacker can access /lakefs_backup if the root is /lakefs. But it gets worse. Here is how parameters were extracted and constructed:
// Vulnerable Code: The Namespace Escape
p := path.Join(l.path, ptr.StorageNamespace[len(DefaultNamespacePrefix):], ptr.Identifier)
if err := l.verifyRelPath(p); err != nil {
return "", err
}The code blindly joins the root, the namespace, and the user-controlled Identifier. It then passes the result to the flawed verifyRelPath. Even if verifyRelPath was perfect, this logic is still flawed because it only asserts that the file is in the Adapter Root, not the Namespace.
Here is the fix introduced in commit cbc106275357302a834280f133265dc39f1384ce. Notice the two-step verification: first verify the namespace is valid, then verify the final path is strictly inside that namespace.
// Patched Code
// 1. Validate the Namespace directory itself
namespacePath := filepath.Clean(filepath.Join(l.path, ptr.StorageNamespace[len(DefaultNamespacePrefix):]))
if err := l.verifyRelPath(namespacePath); err != nil {
return "", err
}
// 2. Validate the final path is INSIDE the namespace
p := filepath.Clean(filepath.Join(namespacePath, ptr.Identifier))
// Check 1: p != namespacePath (can't access root of namespace)
// Check 2: HasPrefix with Separator (prevents sibling access)
if p != namespacePath && !strings.HasPrefix(p, namespacePath+string(filepath.Separator)) {
return "", fmt.Errorf("%s: %w", p, ErrBadPath)
}The Exploit: Jumping the Fence
To exploit this, you need authenticated access to a lakeFS instance using the Local Block Adapter. The scenario is simple: you have access to Repo-A, but you want the juicy secrets in Repo-B. Both live under the same adapter root on the server's disk.
Step 1: Setup.
Imagine the server stores data at /var/lib/lakefs/block/.
Your repository (Repo-A) is at /var/lib/lakefs/block/repo-a/.
The target (Repo-B) is at /var/lib/lakefs/block/repo-b/.
Step 2: The Traversal.
You craft an API request to read an object. Instead of a normal filename like data.csv, you pass a malicious identifier:
identifier = "../../repo-b/config/secrets.yaml"
Step 3: Execution.
lakeFS constructs the path:
path.Join("/var/lib/lakefs/block", "repo-a", "../../repo-b/config/secrets.yaml")
This resolves to:
/var/lib/lakefs/block/repo-b/config/secrets.yaml
The vulnerable verifyRelPath checks: Does this path start with /var/lib/lakefs/block? Yes.
The server reads the file from Repo-B and returns it to you, the user of Repo-A. You have just violated the tenant isolation.
The Impact: Why This Matters
In a single-tenant environment, this might seem like a low-risk bug—you are stealing your own data. But lakeFS is often deployed in environments where data governance is key. The promise of lakeFS is that you can have different repositories for different teams (e.g., Finance vs. Engineering), potentially on the same infrastructure.
Data Leakage: An engineer with access to a sandbox repository could read production financial data if it is stored on the same local adapter volume.
Data Integrity: This isn't just a read vulnerability. The Local Block Adapter supports writes. An attacker could overwrite files in sibling repositories, corrupting the "source of truth" or injecting malicious data into a production pipeline. Imagine overwriting a machine learning model in a different repo with a poisoned version.
Host Compromise (Limited): While the prefix check prevents you from going strictly above the adapter root (you can't read /etc/passwd unless the adapter root is /), the "Prefix Bypass" bug means if the admin named folders poorly (e.g., /data/lakefs and /data/lakefs_ssh_keys), you might be able to read sensitive host configuration files adjacent to the storage directory.
The Fix: Remediation
The fix is straightforward: Upgrade to lakeFS v1.77.0. The treeverse team patched this by enforcing strict directory separators in their prefix checks and ensuring that object paths are anchored to their specific namespace, not just the global root.
If you are a developer writing similar file-handling code in Go (or any language), learn from this:
- Always append the separator: When checking if
Path Ais insideDir B, check ifPath Astarts withDir B+os.PathSeparator. - Scope correctly: If a user is authorized for a specific sub-folder, validate against that sub-folder, not the parent drive.
- Use
filepath.Clean: Always resolve..and.before performing security checks.
Official Patches
Fix Analysis (1)
Technical Appendix
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:NAffected Systems
Affected Versions Detail
| Product | Affected Versions | Fixed Version |
|---|---|---|
treeverse/lakeFS Treeverse | < 1.77.0 | 1.77.0 |
| Attribute | Detail |
|---|---|
| CVE ID | CVE-2026-26187 |
| CVSS Score | 8.1 (High) |
| CWE | CWE-22 (Path Traversal) |
| Vector | CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N |
| Affected Versions | < 1.77.0 |
| Fix Version | 1.77.0 |
MITRE ATT&CK Mapping
Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')