diff --git a/CHANGELOG.md b/CHANGELOG.md index 77d950e..46f7083 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,11 @@ All notable changes to this project will be documented here. multiline, special characters, format listing, and unavailable MIME types. Closes #23. +### Fixed +- MIME type validation regex now requires type and subtype to start + with a letter. Rejects nonsensical values like `123/456` and `_/_`. + Also validates parameter syntax (`name=value`). Closes #35. + ## [2.1.1] - 2026-04-12 ### Fixed diff --git a/src/mcp_clipboard/server.py b/src/mcp_clipboard/server.py index 7014590..18cdebd 100644 --- a/src/mcp_clipboard/server.py +++ b/src/mcp_clipboard/server.py @@ -104,8 +104,8 @@ def _load_icons() -> list[Icon]: # image/* entries that are text-readable (not actual binary). _TEXT_READABLE_MIMES = frozenset({"image/svg+xml"}) -# Basic MIME type validation: type/subtype with optional parameters. -_MIME_RE = re.compile(r"^[\w.+\-]+/[\w.+\-]+(;[\w.+\-=]+)*$") +# MIME type validation: type and subtype must start with a letter. +_MIME_RE = re.compile(r"^[a-zA-Z][\w.+\-]*/[a-zA-Z][\w.+\-]*(;\s*[\w.+\-]+=[\w.+\-]+)*$") async def _read_clipboard_content() -> tuple[list[list[str]], str, str]: diff --git a/tests/test_server.py b/tests/test_server.py index 5cb73b5..361cc17 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -1903,6 +1903,29 @@ async def test_read_raw_rejects_octet_stream(): assert "Cannot read binary" in result +@pytest.mark.asyncio +async def test_read_raw_rejects_numeric_mime(): + """clipboard_read_raw rejects MIME types starting with digits.""" + result = await clipboard_read_raw(mime_type="123/456") + assert "Invalid MIME type" in result + + +@pytest.mark.asyncio +async def test_read_raw_rejects_underscore_mime(): + """clipboard_read_raw rejects MIME types like _/_.""" + result = await clipboard_read_raw(mime_type="_/_") + assert "Invalid MIME type" in result + + +@pytest.mark.asyncio +async def test_read_raw_accepts_custom_mime(): + """clipboard_read_raw should accept valid custom MIME types.""" + # This should pass validation (but return empty since it's not on clipboard) + with patch("mcp_clipboard.server.read_clipboard", new_callable=AsyncMock, return_value=""): + result = await clipboard_read_raw(mime_type="application/x-custom") + assert "Invalid MIME type" not in result + + # --------------------------------------------------------------------------- # 24. clipboard_copy() edge cases # ---------------------------------------------------------------------------