aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/tests/test_security_regressions.py
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-node/tests/test_security_regressions.py')
-rw-r--r--packages/meshbay-node/tests/test_security_regressions.py90
1 files changed, 52 insertions, 38 deletions
diff --git a/packages/meshbay-node/tests/test_security_regressions.py b/packages/meshbay-node/tests/test_security_regressions.py
index 9db8ac1..1a318f7 100644
--- a/packages/meshbay-node/tests/test_security_regressions.py
+++ b/packages/meshbay-node/tests/test_security_regressions.py
@@ -133,14 +133,22 @@ def test_the_node_never_generates_a_name_it_would_refuse(tmp_path):
def _uploads_dir(session) -> Path:
"""
- Where this session's uploads land: uploads/ inside its first writable root.
+ Where an unaddressed upload lands: the first writable root itself.
+
+ There is no `uploads/` subdirectory any more. It was the last of v5's
+ quarantine — the per-user layer went on 2026-08-14 — and it went for the
+ same reason: a folder appearing beside the operator's library because
+ somebody sent a file is the node deciding how their disk is arranged. The
+ protections that made the quarantine worth having are the allowlist, the
+ size cap, the chunk ordering and the no-overwrite rule, and every one of
+ them is asserted below, unchanged.
Asked of the root set rather than assembled by hand, so a test cannot pass
while agreeing with a wrong answer the code also produced.
"""
writable = session._ctx["roots"].writable_roots
assert writable, "the fixture must give the group a writable root"
- return writable[0].path / "uploads"
+ return writable[0].path
def _session(tmp_path: Path, user_id: str) -> WebRTCPeerSession:
@@ -176,7 +184,6 @@ def test_upload_cannot_overwrite_another_members_file(tmp_path):
"""
victim = _session(tmp_path, "victim-user")
uploads = _uploads_dir(victim)
- uploads.mkdir()
original = uploads / "important.mp4"
original.write_bytes(b"operator's original content")
@@ -226,50 +233,57 @@ def test_dir_create_cannot_escape_the_shared_root(tmp_path, bad):
assert set(tmp_path.rglob("*")) == before, f"created something via {bad!r}"
-def test_upload_ignores_any_directory_the_client_asks_for(tmp_path):
+def test_the_client_names_a_folder_and_never_a_filesystem_path(tmp_path):
"""
- The destination inside a root is the node's decision, and stays so.
+ The destination is now the folder the sender is looking at, which means the
+ client does choose it — and the whole of what keeps that safe is that the
+ choice is *resolved against the group's own roots* rather than joined to
+ one.
- A client now names the *root* it is uploading into — it has to, once a group
- can have several writable ones — but that is a name looked up in the root
- table, never a path. Everything below the root is still chosen here, so the
- traversal surface a client-chosen destination would open does not exist.
+ `RootSet.resolve()` refuses `..`, absolute segments and anything whose
+ resolved form escapes its root, symlinks included. So "which of this
+ group's folders" is answerable by a member and "which path on the
+ operator's disk" is not.
"""
session = _session(tmp_path, "user-1")
+ (session._ctx["roots"].roots[0].path / "sub").mkdir()
+ before = set(tmp_path.rglob("*"))
- session._do_file_upload({
- "filename": "note.txt", "dir": "../../etc", "path": "/etc",
- "chunk_index": 0, "total_chunks": 1,
- "data": base64.b64encode(b"x").decode(),
- })
+ for bad in ("../../etc", "/etc", "shared/../..", "shared/../../etc",
+ "nope", "shared/missing"):
+ session.sent.clear()
+ session._do_file_upload({
+ "filename": "note.txt", "dir": bad,
+ "chunk_index": 0, "total_chunks": 1,
+ "data": base64.b64encode(b"x").decode(),
+ })
+ refusal = [m for m in session.sent if m.get("type") == "error"]
+ assert refusal, f"{bad!r} was accepted"
+ assert refusal[0].get("code") in ("no_such_root", "no_such_directory"), bad
- assert (_uploads_dir(session) / "note.txt").read_bytes() == b"x"
- assert not (tmp_path / "etc").exists()
+ assert set(tmp_path.rglob("*")) == before, "a refused upload still wrote"
-@pytest.mark.parametrize("named_root", [
- "../../etc", "/etc", "shared/../..", "Shared/uploads", "nope",
-])
-def test_a_root_name_is_looked_up_never_joined(tmp_path, named_root):
+def test_an_upload_lands_in_the_folder_it_names(tmp_path):
"""
- The name the client sends is matched against the group's root table and
- refused when it matches nothing. A version that joined it to a path — or
- that quietly fell back to the first writable root — would turn "which
- directory" into either a traversal or a file on a disk the operator did
- not intend, and the second is discovered weeks later.
+ And in that folder itself — the `uploads/` subdirectory the node used to
+ create is gone. Somebody dropping a file into the folder they are looking
+ at expects it to be in that folder.
"""
session = _session(tmp_path, "user-1")
- before = set(tmp_path.rglob("*"))
+ root = session._ctx["roots"].roots[0]
+ (root.path / "Albums").mkdir()
session._do_file_upload({
- "filename": "note.txt", "root": named_root,
+ "filename": "note.txt", "dir": f"{root.name}/Albums",
"chunk_index": 0, "total_chunks": 1,
"data": base64.b64encode(b"x").decode(),
})
- refusal = [m for m in session.sent if m.get("type") == "error"]
- assert refusal and refusal[0].get("code") == "no_such_root", named_root
- assert set(tmp_path.rglob("*")) == before, f"wrote something via {named_root!r}"
+ assert (root.path / "Albums" / "note.txt").read_bytes() == b"x"
+ assert not (root.path / "Albums" / "uploads").exists(), (
+ "the node invented a subdirectory in the operator's library")
+ assert not (root.path / "uploads").exists()
def test_an_upload_goes_to_the_root_it_names(tmp_path):
@@ -291,13 +305,13 @@ def test_an_upload_goes_to_the_root_it_names(tmp_path):
])
session._do_file_upload({
- "filename": "note.txt", "root": "Incoming",
+ "filename": "note.txt", "dir": "Incoming",
"chunk_index": 0, "total_chunks": 1,
"data": base64.b64encode(b"x").decode(),
})
- assert (incoming / "uploads" / "note.txt").read_bytes() == b"x"
- assert not (media / "uploads").exists(), "it went to the first root instead"
+ assert (incoming / "note.txt").read_bytes() == b"x"
+ assert not (media / "note.txt").exists(), "it went to the first root instead"
def test_a_read_only_root_refuses_an_upload(tmp_path):
@@ -314,14 +328,14 @@ def test_a_read_only_root_refuses_an_upload(tmp_path):
session._is_node_admin = lambda: True
session._do_file_upload({
- "filename": "note.txt", "root": "Published",
+ "filename": "note.txt", "dir": "Published",
"chunk_index": 0, "total_chunks": 1,
"data": base64.b64encode(b"x").decode(),
})
refusal = [m for m in session.sent if m.get("type") == "error"]
assert refusal and refusal[0].get("code") == "root_read_only"
- assert not (published / "uploads").exists()
+ assert not (published / "note.txt").exists()
def test_a_fully_read_only_group_refuses_an_unaddressed_upload(tmp_path):
@@ -343,7 +357,7 @@ def test_a_fully_read_only_group_refuses_an_unaddressed_upload(tmp_path):
refusal = [m for m in session.sent if m.get("type") == "error"]
assert refusal and refusal[0].get("code") == "no_writable_root"
- assert not (published / "uploads").exists()
+ assert not (published / "note.txt").exists()
def test_an_ejected_root_refuses_an_upload(tmp_path):
@@ -362,14 +376,14 @@ def test_an_ejected_root_refuses_an_upload(tmp_path):
session._ctx["roots"] = roots
session._do_file_upload({
- "filename": "note.txt", "root": "USB",
+ "filename": "note.txt", "dir": "USB",
"chunk_index": 0, "total_chunks": 1,
"data": base64.b64encode(b"x").decode(),
})
refusal = [m for m in session.sent if m.get("type") == "error"]
assert refusal and refusal[0].get("code") == "root_unavailable"
- assert not (usb / "uploads").exists()
+ assert not (usb / "note.txt").exists()
def test_two_members_can_send_the_same_filename(tmp_path):