summaryrefslogtreecommitdiffstats
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.py84
1 files changed, 47 insertions, 37 deletions
diff --git a/packages/meshbay-node/tests/test_security_regressions.py b/packages/meshbay-node/tests/test_security_regressions.py
index 7627926..77d544b 100644
--- a/packages/meshbay-node/tests/test_security_regressions.py
+++ b/packages/meshbay-node/tests/test_security_regressions.py
@@ -132,7 +132,9 @@ def test_upload_cannot_overwrite_another_members_file(tmp_path):
victim = _session(tmp_path, "victim-user")
shared_root = victim._ctx["shared_root"]
- original = shared_root / "important.mp4"
+ uploads = shared_root / "uploads"
+ uploads.mkdir()
+ original = uploads / "important.mp4"
original.write_bytes(b"operator's original content")
attacker = _session(tmp_path, "attacker-user")
@@ -143,11 +145,9 @@ def test_upload_cannot_overwrite_another_members_file(tmp_path):
"data": base64.b64encode(b"attacker content").decode(),
})
- assert original.read_bytes() == b"operator's original content"
- assert any(m.get("type") == "error" for m in attacker.sent), (
- "the upload must be refused outright, not silently dropped")
- assert not (shared_root / "important.mp4.part").exists(), (
- "a refused upload must leave nothing behind")
+ assert original.read_bytes() == b"operator's original content", (
+ "an upload replaced an existing file (C5a)")
+ assert (uploads / "important (2).mp4").read_bytes() == b"attacker content"
def test_upload_second_attempt_cannot_replace_own_completed_file(tmp_path):
@@ -159,32 +159,10 @@ def test_upload_second_attempt_cannot_replace_own_completed_file(tmp_path):
session.sent.clear()
session._do_file_upload(dict(payload))
- assert any(m.get("type") == "error" for m in session.sent)
- stored = session._ctx["shared_root"] / "movie.mp4"
- assert stored.read_bytes() == b"first"
-
-
-@pytest.mark.parametrize("bad_dir", [
- "..", "../..", "/etc", "a/../../b", "./../x", "sub/../../..",
- "\\..\\..", "~", "a/./../..",
-])
-def test_upload_cannot_escape_the_shared_root(tmp_path, bad_dir):
- """
- The destination now arrives from the client, which is a path the node did not
- choose. Every segment goes through the same allowlist as a filename and the
- result must resolve inside the shared root.
- """
- session = _session(tmp_path, "user-1")
- outside = tmp_path / "outside.txt"
-
- session._do_file_upload({
- "filename": "outside.txt", "dir": bad_dir,
- "chunk_index": 0, "total_chunks": 1,
- "data": base64.b64encode(b"escaped").decode(),
- })
-
- assert any(m.get("type") == "error" for m in session.sent), bad_dir
- assert not outside.exists(), f"upload escaped the shared root via {bad_dir!r}"
+ uploads = session._ctx["shared_root"] / "uploads"
+ assert (uploads / "movie.mp4").read_bytes() == b"first", (
+ "the first upload was replaced")
+ assert (uploads / "movie (2).mp4").read_bytes() == b"first"
@pytest.mark.parametrize("bad", [
@@ -205,17 +183,49 @@ 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_still_refuses_unsafe_names_in_a_subdirectory(tmp_path):
- """The name allowlist is not weakened by having somewhere to put the file."""
+def test_upload_ignores_any_directory_the_client_asks_for(tmp_path):
+ """
+ Uploads land in uploads/, chosen by the node. A client that names somewhere
+ else — or nowhere at all — changes nothing, so the traversal surface that a
+ client-chosen destination would open does not exist on this path.
+ """
session = _session(tmp_path, "user-1")
- (session._ctx["shared_root"] / "docs").mkdir()
+ shared_root = session._ctx["shared_root"]
session._do_file_upload({
- "filename": "../escape.txt", "dir": "docs",
+ "filename": "note.txt", "dir": "../../etc",
"chunk_index": 0, "total_chunks": 1,
"data": base64.b64encode(b"x").decode(),
})
- assert any(m.get("type") == "error" for m in session.sent)
+
+ assert (shared_root / "uploads" / "note.txt").read_bytes() == b"x"
+ assert not (tmp_path / "etc").exists()
+
+
+def test_two_members_can_send_the_same_filename(tmp_path):
+ """
+ One shared uploads/ means collisions are ordinary — every camera produces
+ IMG_1234.jpg. The second gets a free name; neither replaces the other.
+ """
+ first = _session(tmp_path, "user-1")
+ first._do_file_upload({
+ "filename": "IMG_1234.jpg", "chunk_index": 0, "total_chunks": 1,
+ "data": base64.b64encode(b"first").decode(),
+ })
+ second = _session(tmp_path, "user-2")
+ second._do_file_upload({
+ "filename": "IMG_1234.jpg", "chunk_index": 0, "total_chunks": 1,
+ "data": base64.b64encode(b"second").decode(),
+ })
+
+ uploads = first._ctx["shared_root"] / "uploads"
+ assert (uploads / "IMG_1234.jpg").read_bytes() == b"first"
+ assert (uploads / "IMG_1234 (2).jpg").read_bytes() == b"second"
+
+ ack = [m for m in second.sent if m.get("type") == "file_upload_ack"][-1]
+ assert ack["stored_as"] == "IMG_1234 (2).jpg", (
+ "the sender must be told the name that was used, or a chat attachment "
+ "points at someone else's file")
# ── H1: group isolation ──────────────────────────────────────────────────────