Skip to content

security: fix path traversal (CVE-2026-4307) and SSRF (CVE-2026-4308) - #1785

Open
3baprinting wants to merge 6 commits into
agent0ai:mainfrom
3baprinting:security/path-traversal-and-ssrf-guards
Open

security: fix path traversal (CVE-2026-4307) and SSRF (CVE-2026-4308)#1785
3baprinting wants to merge 6 commits into
agent0ai:mainfrom
3baprinting:security/path-traversal-and-ssrf-guards

Conversation

@3baprinting

Copy link
Copy Markdown

Summary

Fixes the two publicly-disclosed vulnerabilities that are still reachable on main:

Both were verified against current main (not just the advisory's 0.9.7), and every anchor these patches touch exists there today.

CVE-2026-4307 — arbitrary file read

helpers/files.py: _resolve_path() returns a single absolute path verbatim and os.path.joins relative ones without normalising ..:

def _resolve_path(*relative_paths):
    if len(relative_paths) == 1 and os.path.isabs(relative_paths[0]):
        return relative_paths[0]                     # /etc/passwd -> /etc/passwd
    return os.path.join(_base_dir, *relative_paths)  # ../../etc/passwd escapes

api/api_files_get.py feeds request-supplied paths in, and its final branch skips resolution entirely before reading the file and returning it base64-encoded:

else:
    # Assume it's already an external/absolute path
    external_path = path
...
with open(external_path, "rb") as f:
    base64_content = base64.b64encode(f.read()).decode("utf-8")

The handler sets requires_auth = False and requires_csrf = False, so an API key alone yields POST {"paths": ["/etc/passwd"]} → file contents.

api/file_info.py has the same root cause with a smaller impact: it stats any absolute path, leaking existence, size, mtime and permission bits for arbitrary host files.

Fix. Adds files.get_abs_path_contained(), which realpaths the result (normalising .. and resolving symlinks) and refuses anything outside the base directory. api_files_get.py now accepts only /a0/... paths through that helper, and the arbitrary-absolute-path branch is removed. file_info.py returns a "refused" record instead of stat-ing outside the base dir.

get_abs_path() itself is deliberately unchanged — it is used throughout the codebase with trusted absolute paths, so tightening it would break internals. The rule this PR establishes is that handlers taking a path from a request use the contained variant.

CVE-2026-4308 — SSRF

plugins/_document_query/helpers/fetch.py performed:

async with session.get(uri, allow_redirects=True) as response:

with no destination validation anywhere in the codebase. A document URI — which an agent will readily take from a page it just read — could therefore reach http://169.254.169.254/latest/meta-data/ (cloud instance credentials), http://127.0.0.1:… (co-located services), or any RFC1918 address; and because redirects were delegated to the client, a public-looking URL could bounce into the private network on hop two.

Fix. Adds helpers/net_guard.py: deny-by-default validation requiring http/https, resolving the hostname, and refusing if any resolved address is loopback, private, link-local, reserved, multicast or unspecified — including IPv4-mapped IPv6 such as ::ffff:127.0.0.1. fetch.py validates before the first byte leaves and follows redirects manually, re-validating each hop (capped at 3). A blocked target raises immediately rather than being retried and masked as a generic Document fetch error.

Tests

tests/test_path_traversal_and_ssrf_guards.py — 26 tests covering absolute-path reads, .. traversal, symlink escape, non-HTTP schemes, loopback/link-local/RFC1918/IPv4-mapped targets, and redirect delegation.

They also assert the guards do not over-block: legitimate in-base paths still resolve and public addresses still pass. The public-address cases use IP literals rather than hostnames, because some sandboxed/proxied networks resolve every name into 198.18.0.0/15 (RFC 2544 benchmark space), which is correctly not public — a hostname assertion would fail there for environmental reasons rather than a real defect.

python -m pytest tests/test_path_traversal_and_ssrf_guards.py -q
26 passed

Notes

  • Standard library only; no new dependencies.
  • No behaviour change for legitimate input.
  • MAX_REDIRECTS and the scheme allowlist are module-level constants in net_guard.py if you would prefer them configurable.

Happy to adjust naming or split this into two PRs if you would rather review the fixes separately.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant