Skip to content

Commit 8fb3d6f

Browse files
committed
fix: reject null bytes in ResourceSecurity.validate by default
A %00 in a URI decodes to \x00, which defeats the traversal check's string comparison ("..\x00" != "..") and can cause truncation in handlers that pass values to C extensions or subprocess. safe_join already rejects null bytes; this closes the defense-in-depth gap so ResourceSecurity catches them before the handler runs. The check runs first so it also covers the traversal-bypass case.
1 parent 9595740 commit 8fb3d6f

File tree

2 files changed

+22
-0
lines changed

2 files changed

+22
-0
lines changed

src/mcp/server/mcpserver/resources/templates.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ def git_diff(range: str) -> str: ...
4646
reject_absolute_paths: bool = True
4747
"""Reject values that look like absolute filesystem paths."""
4848

49+
reject_null_bytes: bool = True
50+
"""Reject values containing NUL (``\\x00``). Null bytes defeat string
51+
comparisons (``"..\\x00" != ".."``) and can cause truncation in C
52+
extensions or subprocess calls."""
53+
4954
exempt_params: Set[str] = field(default_factory=frozenset[str])
5055
"""Parameter names to skip all checks for."""
5156

@@ -64,6 +69,8 @@ def validate(self, params: Mapping[str, str | list[str]]) -> bool:
6469
continue
6570
values = value if isinstance(value, list) else [value]
6671
for v in values:
72+
if self.reject_null_bytes and "\0" in v:
73+
return False
6774
if self.reject_path_traversal and contains_path_traversal(v):
6875
return False
6976
if self.reject_absolute_paths and is_absolute_path(v):

tests/server/mcpserver/resources/test_resource_template.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,21 @@ def test_matches_disabled_policy_allows_traversal():
6767
assert t.matches("file://docs/..") == {"name": ".."}
6868

6969

70+
def test_matches_rejects_null_byte_by_default():
71+
# %00 decodes to \x00 which defeats string comparisons
72+
# ("..\x00" != "..") and can truncate in C extensions.
73+
t = _make("file://docs/{name}")
74+
assert t.matches("file://docs/key%00.txt") is None
75+
# Null byte also defeats the traversal check's component comparison
76+
assert t.matches("file://docs/..%00%2Fsecret") is None
77+
78+
79+
def test_matches_null_byte_check_can_be_disabled():
80+
policy = ResourceSecurity(reject_null_bytes=False)
81+
t = _make("file://docs/{name}", security=policy)
82+
assert t.matches("file://docs/key%00.txt") == {"name": "key\x00.txt"}
83+
84+
7085
def test_matches_explode_checks_each_segment():
7186
t = _make("api{/parts*}")
7287
assert t.matches("api/a/b/c") == {"parts": ["a", "b", "c"]}

0 commit comments

Comments
 (0)