Files
openswarm/backend/tests/test_resolve_upload_path.py
T

91 lines
3.1 KiB
Python

"""ENG-47: the upload tool is the first browser tool that can move bytes off the machine, and the
browser sub-agent takes its instructions from the page it is driving. These pin the containment
guard, including the case that matters most: a symlink planted INSIDE an allowed root."""
import os
import pytest
from backend.apps.agents.browser.resolve_upload_path import (
resolve_upload_path,
allowed_upload_roots,
UploadPathRefused,
MAX_UPLOAD_BYTES,
)
@pytest.fixture
def uploads_dir():
root = allowed_upload_roots()[0]
os.makedirs(root, exist_ok=True)
return root
def test_a_file_the_user_attached_is_uploadable(uploads_dir):
f = os.path.join(uploads_dir, "resume.pdf")
with open(f, "w", encoding="utf-8") as fh:
fh.write("cv")
assert resolve_upload_path(f) == os.path.realpath(f)
@pytest.mark.parametrize("hostile", ["~/.ssh/id_rsa", "/etc/passwd", "/etc/hosts", ""])
def test_paths_outside_every_allowed_root_are_refused(hostile):
with pytest.raises(UploadPathRefused):
resolve_upload_path(hostile)
def test_a_symlink_inside_an_allowed_root_cannot_smuggle_a_file_out(uploads_dir):
# String math on the path would pass this: it really does live under the uploads root.
link = os.path.join(uploads_dir, "innocent.txt")
if os.path.lexists(link):
os.remove(link)
os.symlink("/etc/hosts", link)
try:
with pytest.raises(UploadPathRefused):
resolve_upload_path(link)
finally:
os.remove(link)
def test_a_sibling_root_with_a_shared_prefix_is_not_inside_it(uploads_dir):
# Without the trailing separator, root `self-swarm-uploads` would own `self-swarm-uploads-evil`.
evil = uploads_dir + "-evil"
os.makedirs(evil, exist_ok=True)
f = os.path.join(evil, "x.txt")
with open(f, "w", encoding="utf-8") as fh:
fh.write("x")
with pytest.raises(UploadPathRefused):
resolve_upload_path(f)
def test_a_directory_is_not_a_file(uploads_dir):
with pytest.raises(UploadPathRefused):
resolve_upload_path(uploads_dir)
def test_an_oversized_file_is_refused(uploads_dir, monkeypatch):
f = os.path.join(uploads_dir, "huge.bin")
with open(f, "w", encoding="utf-8") as fh:
fh.write("x")
monkeypatch.setattr(os.path, "getsize", lambda p: MAX_UPLOAD_BYTES + 1)
with pytest.raises(UploadPathRefused):
resolve_upload_path(f)
def test_the_dispatcher_refuses_before_the_command_leaves_the_backend(monkeypatch):
"""The guard has to sit in front of the WS hop, not inside the renderer."""
import asyncio
import backend.apps.agents.core.ws_manager as ws_mod
from backend.apps.agents.browser import browser_agent
sent = []
async def spy(*args, **kwargs):
sent.append(args)
return {"text": "should never happen"}
monkeypatch.setattr(ws_mod.ws_manager, "send_browser_command", spy, raising=True)
out = asyncio.run(browser_agent.execute_browser_tool(
"BrowserUploadFile", {"path": "/etc/passwd"}, "browser-1"))
assert "Refused" in str(out.get("error", ""))
assert sent == [], "a refused path must never reach the renderer"