Skip to content

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

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

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.

@3clyp50

3clyp50 commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Hi @3baprinting, we are finally looking at all the PRs, thanks for the report and for covering both advisories.

We reviewed the two separately:
CVE-2026-4308 / document-query SSRF: this is a meaningful security boundary, and we have addressed it. Rather than adding a second net_guard implementation, the plugin now uses the existing shared helpers.network.fetch_public_http_resource guard.

CVE-2026-4307 / download paths: the /a0 restriction was inconsistent with the existing authenticated File Browser and editor, which are intentionally rooted at /. We have aligned the single and bulk download APIs with that same filesystem scope while retaining authentication, CSRF protection, and canonical path resolution.
Your audit did identify an additional concern in api_files_get: it is API-key protected rather than an authenticated WebUI route, so its filesystem authority should be evaluated independently. That part should not inherit the WebUI’s root-level policy automatically but we want to think through it.

We are implementing the fixes through the existing shared owners, so we won’t merge this PR, but thank you for surfacing the paths and again for the review.

Under our current security posture, File Browser and editor already expose the container filesystem, while the agent itself runs as root inside that container. The impact of CVE-2026-4307 is therefore narrower than what the advisory describes. CVE-2026-4308 instead suffered a regression after Document Query Tool was extracted into a plugin.

Thank you,
Alessandro

@3clyp50 3clyp50 closed this Aug 12, 2026
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.

2 participants