CVE-2026-26187

CVE-2026-26187: escaping the Lake with a Path Traversal Two-Step

Alon Barad
Alon Barad
Software Engineer

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:

  1. Always append the separator: When checking if Path A is inside Dir B, check if Path A starts with Dir B + os.PathSeparator.
  2. Scope correctly: If a user is authorized for a specific sub-folder, validate against that sub-folder, not the parent drive.
  3. Use filepath.Clean: Always resolve .. and . before performing security checks.

Fix Analysis (1)

Technical Appendix

CVSS Score
8.1/ 10
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N

Affected Systems

lakeFS (Local Block Adapter)

Affected Versions Detail

Product
Affected Versions
Fixed Version
treeverse/lakeFS
Treeverse
< 1.77.01.77.0
AttributeDetail
CVE IDCVE-2026-26187
CVSS Score8.1 (High)
CWECWE-22 (Path Traversal)
VectorCVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N
Affected Versions< 1.77.0
Fix Version1.77.0
CWE-22
Path Traversal

Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')

Vulnerability Timeline

Patch committed to main branch
2026-02-12
CVE-2026-26187 Published
2026-02-13
lakeFS v1.77.0 Released
2026-02-13