diff --git a/nextcloud_mcp_server/client/webdav.py b/nextcloud_mcp_server/client/webdav.py
index 197ccc17..bceafacb 100644
--- a/nextcloud_mcp_server/client/webdav.py
+++ b/nextcloud_mcp_server/client/webdav.py
@@ -6,6 +6,7 @@ import xml.etree.ElementTree as ET
from email.utils import parsedate_to_datetime
from typing import Any, Dict, List, Optional, Tuple
from urllib.parse import unquote
+from xml.sax.saxutils import escape as xml_escape
from httpx import HTTPStatusError
@@ -759,7 +760,12 @@ class WebDAVClient(BaseNextcloudClient):
# producer omits fileid. ``id(item)`` is a last resort so an item
# missing both never collapses into another under a shared ``None``
# key (which would silently drop rows from the result set).
- return item.get("file_id") or item.get("path") or id(item)
+ # ``is not None`` rather than truthiness so a (hypothetical)
+ # file_id of 0 isn't treated as absent.
+ file_id = item.get("file_id")
+ if file_id is not None:
+ return file_id
+ return item.get("path") or id(item)
results: List[Dict[str, Any]] = []
seen: set[Any] = set()
@@ -894,7 +900,9 @@ class WebDAVClient(BaseNextcloudClient):
limit_parts = []
if limit:
limit_parts.append(f"{limit}")
- if offset:
+ # ``is not None`` (not truthiness) so a future explicit offset=0 is
+ # emitted rather than silently dropped.
+ if offset is not None:
limit_parts.append(f"{offset}")
limit_xml = f"{''.join(limit_parts)}" if limit_parts else ""
@@ -1160,12 +1168,15 @@ class WebDAVClient(BaseNextcloudClient):
@staticmethod
def _type_search_args(mime_type: str) -> Tuple[str, List[str]]:
"""Build the where-clause + property list for a MIME-type SEARCH."""
+ # Escape so a caller-supplied MIME type can't break the SEARCH XML or
+ # inject elements. All current callers pass literal strings, but this
+ # keeps the boundary safe for any future user-supplied value.
where_conditions = f"""
- {mime_type}
+ {xml_escape(mime_type)}
"""
diff --git a/tests/unit/test_webdav_search_paging.py b/tests/unit/test_webdav_search_paging.py
index 34bb875f..fae02d83 100644
--- a/tests/unit/test_webdav_search_paging.py
+++ b/tests/unit/test_webdav_search_paging.py
@@ -164,6 +164,17 @@ async def test_find_all_by_type_delegates_to_search_files_all(mocker):
assert "application/pdf" in kwargs["where_conditions"]
+def test_type_search_args_escapes_mime_type(mocker):
+ """A MIME value with XML metacharacters must not break / inject into the SEARCH."""
+ client = _make_client(mocker)
+ where, properties = client._type_search_args("application/pdf<&>")
+ # The injected metacharacters are escaped inside the , so they
+ # can't break the SEARCH XML or introduce new elements.
+ assert "pdf<&>" in where
+ assert "pdf<&>" not in where
+ assert "fileid" in properties
+
+
def test_build_search_xml_emits_offset_only_when_set(mocker):
client = _make_client(mocker)