Skip to content

check_resource_allowed treats /api/../admin as under /api #3464

Description

@Oskii

What happened

check_resource_allowed pads trailing slashes and then does requested_path.startswith(configured_path). It does not collapse . / ...

On main @ 08a3bc8 these return True:

  • requested https://example.com/api/../admin vs configured https://example.com/api
  • requested https://example.com/mcp/.. vs configured https://example.com/mcp
  • requested https://example.com/api/%2e%2e/admin vs configured https://example.com/api

tests/shared/test_auth_utils.py already rejects /api123 vs /api (path-boundary). Dot-segments are not covered.

What I expected

A requested path that walks out of the configured prefix should not match. /api/foo/../v1 vs /api can still match after normalisation, because it stays under /api.

How to reproduce

from mcp.shared.auth_utils import check_resource_allowed

check_resource_allowed("https://example.com/api/../admin", "https://example.com/api")
# True today. I expected False.

I can send a PR that percent-decodes once, runs posixpath.normpath, then keeps the existing trailing-slash prefix rule. Happy to do that if you want it.

Written with AI assistance. I read auth_utils.py next to the path-boundary tests and reproduced it locally.

Activity

  1. added
    v2Affects the v2 line (2.x on main)
    v1Affects the v1.x maintenance line
    on Sep 6, 2026
  2. AbarnaaSree commented on Sep 7, 2026

    @AbarnaaSree

    I'd like to work on this. I'll first verify the current check_resource_allowed behavior and existing path-boundary tests, then add regression coverage for dot-segments and percent-encoded traversal while preserving the existing prefix semantics. I'll also check how URL paths are parsed/decoded elsewhere in the SDK before settling on the normalization approach.

  3. zsxh1990 commented on Sep 7, 2026

    @zsxh1990

    I'd like to work on this security fix.

    Root cause: check_resource_allowed does a naive prefix check on the URI path. A request to /api/../admin passes the /api prefix check because the path is not normalized before comparison.

    Approach: Normalize the request path with posixpath.normpath (or equivalent) before the prefix check, resolving .. and . segments. This ensures /api/../admin correctly resolves to /admin and gets rejected.

    I will include test cases for various traversal patterns (.., encoded variants).

  4. 1747687484-collab commented on Sep 8, 2026

    @1747687484-collab

    Confirmed the reproduction locally on main:

    • check_resource_allowed("https://example.com/api/../admin", "https://example.com/api") evaluates to True because trailing-slash padding produces /api/../admin/ which naively .startswith("/api/").
    • The same applies to /mcp/.. and percent-encoded variants (%2e%2e / %2E%2E).

    To address this cleanly without breaking existing prefix semantics:

    1. Decode percent-encoded characters via urllib.parse.unquote() and normalize with posixpath.normpath() (ensuring a leading /).
    2. Re-apply trailing slash normalization (/foo and /foo/ equivalence) so path-boundary protection against prefix collisions (like /api123/ vs /api/) is preserved.
    3. Apply this normalization to both requested_path and configured_path before checking requested_path.startswith(configured_path).

    I have this implemented and verified locally with comprehensive regression tests covering out-of-boundary traversal (.., %2e%2e, ../../admin, /mcp/..), intra-boundary traversals that legitimately stay under the configured root (/api/v1/../v2, /api/./v1), and all 16 existing auth tests passing cleanly.

    Happy to open the PR if maintainers would like to assign this or open it for external contributions!

    Written with AI assistance; reproduced and tested locally.

  5. Kludex commented on Oct 10, 2026

    @Kludex
    Member

    Both reports reproduce the same check_resource_allowed path-prefix bypass using raw and percent-encoded dot segments. This is tracked in #3303, so I’m closing this as a duplicate. AI-assisted triage; I reviewed both reports.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingv1Affects the v1.x maintenance linev2Affects the v2 line (2.x on main)

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions