Skills: URI traversal validation skips percent-encoded dot-segments
Maintainers usually reply within 1 day
@Varshith-Kali is already working on this.
Since Oct 13, 2026.
- #3703 by @chauvuusvn — closed without merging
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 78/100
Research direction
Read src/mcp/shared/skills.py, focusing on _validate_resource_uri_in_skill() and validate_directory_result(). Start by tracing how each function splits and checks URI path segments, then add regression coverage for percent-encoded dot-segments in both validation paths. Done means encoded . and .. segments are rejected while valid resource URIs and direct children still pass.
Written by the indexing model from the issue text.
Description
Initial Checks
- I confirm that I'm using the newest release of my line (the latest 2.x, or the latest 1.x if I'm still on v1)
- I confirm that I searched for my issue in https://github.com/modelcontextprotocol/python-sdk/issues before opening this issue
Release line
2.x (current stable)
Description
In src/mcp/shared/skills.py (added in #3485 for the SEP-2640 Skills extension), _validate_resource_uri_in_skill() and validate_directory_result() check for directory traversal segments against the raw URI path segments:
# src/mcp/shared/skills.py (line 253)
if any(segment in (".", "..") for segment in resource_parts.path.split("/")):
raise ValueError(f"resource URI {resource_uri!r} contains a traversal segment")
Because URI paths are not URL-unquoted prior to validation, percent-encoded traversal segments (e.g. %2e%2e, %2E%2E, or %2e) bypass this check.
Specifically:
- In
_validate_resource_uri_in_skill(), a resource URI such asskill://acme/billing/refunds/%2e%2e/other/secret.txtstarts withroot + "/"(/billing/refunds/), but its segments (["", "billing", "refunds", "%2e%2e", "other", "secret.txt"]) do not literally equal".."or".". As a result, the resource is admitted as a valid member ofskill://acme/billing/refunds/SKILL.md. - In
validate_directory_result(), line 293 checksrelative in (".", ".."). A child URI containing%2e%2eor%2ebypasses this check and is admitted as a valid direct child.
Example Code
from mcp.shared.skills import Skill, SkillResource, ReadDirectoryResult, validate_directory_result
from mcp_types import Resource
root = "skill://git-workflow/SKILL.md"
digest = "sha256:" + "a" * 64
# 1. Percent-encoded traversal bypasses manifest validation:
s = Skill(
uri=root,
frontmatter={"name": "git-workflow", "description": "d"},
resources=[
SkillResource(uri=root, digest=digest, size=10),
SkillResource(uri="skill://git-workflow/%2e%2e/evil.md", digest=digest, size=10)
]
)
# Expected: ValueError / ValidationError for traversal segment
# Observed: Accepted without error
# 2. Percent-encoded dot-segment bypasses directory validation:
res = ReadDirectoryResult(resources=[Resource(uri="skill://pdf/templates/%2e%2e", name="x")])
validate_directory_result("skill://pdf/templates", res)
# Expected: ValueError ("is not a direct child")
# Observed: Accepted without error
Proposed Fix
Normalize path segments with urllib.parse.unquote() before splitting and inspecting dot-segments in both _validate_resource_uri_in_skill and validate_directory_result.
I have a minimal regression test and fix ready and would be happy to submit a PR if this looks good.
- Dominant language
- Python
- Stars
- 24.6k
- Forks
- 4.2k
- Avg merge
- 18h
- Merged PRs (30d)
- 39
Getting set up
- No Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from modelcontextprotocol/python-sdk
-
bug v1 v2
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
modelcontextprotocol/python-sdk#3729 ·
Maintainers usually reply within 1 day
-
bug v1 v2
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
modelcontextprotocol/python-sdk#3725 · 4 comments ·
Maintainers usually reply within 1 day
-
bug v1 v2
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
modelcontextprotocol/python-sdk#3698 · 2 comments ·
Maintainers usually reply within 1 day
-
Streamable HTTP: Content-Type handling is order dependent and accepts invalid valuesMay be free again @jayzuccarelli claimed this 7 days ago, and no pull request is open. Openbug v1 v2
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
modelcontextprotocol/python-sdk#3691 · 1 comment ·
Maintainers usually reply within 1 day
-
Type annotation gaps and unhandled FileNotFoundError in src/mcp/cli/cli.pyMay be free again A pull request for this issue was closed without being merged. Openenhancement
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
modelcontextprotocol/python-sdk#3673 · 4 comments ·
Maintainers usually reply within 1 day
All issues in modelcontextprotocol/python-sdk
Similar issues
-
Issue Triage-Needed
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
pulp/pulp_container#2581 ·
Maintainers usually reply within 1 day
-
Difficulty 1/5 Under an hour Newbie friendliness 85/100
521xueweihan/HelloGitHub#4040 ·
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 64/100
ClanGenOfficial/clangen#6313 ·
Maintainers usually reply within 2 days
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
Maintainers usually reply within 1 day
-
bug(trainer): CustomTrainer func_args with non literal values fail inside the TrainJobPossibly taken @avanishkasar claimed this today. Open
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
kubeflow/sdk#879 · 1 comment ·
Maintainers usually reply within 1 day