summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-node')
-rw-r--r--packages/meshbay-node/src/meshbay_node/roots.py15
-rw-r--r--packages/meshbay-node/src/meshbay_node/transport/webrtc/upload_handlers.py12
-rw-r--r--packages/meshbay-node/tests/test_partial_uploads.py21
3 files changed, 47 insertions, 1 deletions
diff --git a/packages/meshbay-node/src/meshbay_node/roots.py b/packages/meshbay-node/src/meshbay_node/roots.py
index a4f9cc9..2de0708 100644
--- a/packages/meshbay-node/src/meshbay_node/roots.py
+++ b/packages/meshbay-node/src/meshbay_node/roots.py
@@ -53,6 +53,21 @@ SAFE_UPLOAD_NAME = re.compile(
re.UNICODE)
+# Files Windows Explorer acts on by itself when it shows a folder: a link's
+# icon, a folder's settings, a search connector. Placed by a member in a folder
+# the operator browses, any of them can make Explorer contact a server of the
+# member's choosing with the operator's Windows credentials — a known attack,
+# and why mail providers refuse the same types. Refused for every node: a
+# Linux node's folder may be shared to Windows machines.
+SHELL_ACTIVE_NAMES = frozenset({"desktop.ini"})
+SHELL_ACTIVE_SUFFIXES = (".lnk", ".url", ".scf", ".library-ms", ".searchconnector-ms")
+
+
+def shell_active(filename: str) -> bool:
+ name = filename.lower()
+ return name in SHELL_ACTIVE_NAMES or name.endswith(SHELL_ACTIVE_SUFFIXES)
+
+
def _free_name(directory: Path, filename: str,
taken: frozenset[str] | set[str] = frozenset()) -> str:
"""
diff --git a/packages/meshbay-node/src/meshbay_node/transport/webrtc/upload_handlers.py b/packages/meshbay-node/src/meshbay_node/transport/webrtc/upload_handlers.py
index f1ed139..02a50af 100644
--- a/packages/meshbay-node/src/meshbay_node/transport/webrtc/upload_handlers.py
+++ b/packages/meshbay-node/src/meshbay_node/transport/webrtc/upload_handlers.py
@@ -8,7 +8,14 @@ from pathlib import Path
from meshbay_common.protocol import UPLOAD_PROBE_INDEX, file_upload_ack_wire, file_upload_payload
from meshbay_node import uploads as uploads_mod
-from meshbay_node.roots import SAFE_UPLOAD_NAME, RootSet, _free_name, off_disk, publish_upload
+from meshbay_node.roots import (
+ SAFE_UPLOAD_NAME,
+ RootSet,
+ _free_name,
+ off_disk,
+ publish_upload,
+ shell_active,
+)
from meshbay_node.transport.webrtc.disk import _append_chunk
from meshbay_node.transport.webrtc.limits import LEASE_NONE, LEASE_QUEUED
@@ -212,6 +219,9 @@ class UploadMixin:
if not SAFE_UPLOAD_NAME.match(filename):
_refuse("Invalid filename", "invalid_filename")
return
+ if shell_active(filename):
+ _refuse("This type of file is not accepted", "file_type_refused")
+ return
roots: RootSet | None = ctx.get("roots")
if not roots:
diff --git a/packages/meshbay-node/tests/test_partial_uploads.py b/packages/meshbay-node/tests/test_partial_uploads.py
index 138ee3b..556b26a 100644
--- a/packages/meshbay-node/tests/test_partial_uploads.py
+++ b/packages/meshbay-node/tests/test_partial_uploads.py
@@ -22,6 +22,7 @@ import time
import types
from pathlib import Path
+import pytest
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from meshbay_common.crypto import generate_gek
from meshbay_common.protocol import (
@@ -552,3 +553,23 @@ def test_without_hard_links_a_file_is_still_not_replaced(tmp_path, monkeypatch):
assert (tmp_path / "a.txt").read_bytes() == b"there first"
assert (tmp_path / "a (2).txt").read_bytes() == b"uploaded"
assert not part.exists()
+
+
+# ── files Explorer acts on by itself ─────────────────────────────────────────
+
+
+@pytest.mark.parametrize("name", ["desktop.ini", "Desktop.INI", "photos.lnk", "site.url",
+ "x.scf", "Docs.library-ms", "s.searchConnector-ms"])
+async def test_a_file_explorer_acts_on_is_refused(tmp_path, name):
+ ctx = _group_ctx(tmp_path)
+ peer = _peer(ctx)
+ await peer._do_file_upload(sealed_upload(peer, filename=name, data=b"[x]"))
+ assert [m.get("code") for m in _errors(peer)] == ["file_type_refused"]
+ assert not any(ctx["roots"].roots[0].path.iterdir())
+
+
+async def test_an_ordinary_file_with_a_near_name_is_accepted(tmp_path):
+ ctx = _group_ctx(tmp_path)
+ peer = _peer(ctx)
+ await peer._do_file_upload(sealed_upload(peer, filename="url-notes.txt", data=b"x"))
+ assert _errors(peer) == []