Skip to content

check_resource_allowed(): path matching skips dot-segment/percent-encoding normalization (auth-boundary bypass) #3303

Description

@Choppaaahh

Summary

check_resource_allowed() in src/mcp/shared/auth_utils.py performs hierarchical path matching with str.startswith() after only trailing-slash normalization. It does not resolve dot-segments (.., .) or decode percent-encoding, so a requested resource can satisfy startswith(configured) while resolving to a path outside the configured resource.

This is a confused-deputy / path-traversal gap at the resource-authorization boundary: any downstream resource server that normalizes paths (most do) will serve a different resource than the one the SDK authorized.

Reproduction

from mcp.shared.auth_utils import check_resource_allowed

# Access to /api is configured; /admin is NOT
configured = "https://mcp.example.com/api"
requested  = "https://mcp.example.com/api/../admin"

print(check_resource_allowed(requested, configured))  # -> True (should be False)

The current code normalizes to /api/../admin/ vs /api/, and "/api/../admin/".startswith("/api/") is True. The resolved path is /admin. Percent-encoded variants (%2e%2e) bypass it the same way.

Impact

Where the result gates access to a protected resource, a caller can craft a resource indicator that passes the check but points elsewhere. Severity depends on deployment, but it's an auth-boundary correctness bug, not cosmetic.

Fix

Decode percent-encoding and resolve dot-segments (posixpath.normpath after unquote) on both paths before the startswith comparison, preserving trailing-slash semantics. A patch implementing exactly this was opened as #2585 and closed in the post-v2 backlog sweep with the note to reopen if still relevant against v2 — filing this issue per CONTRIBUTING so it can be triaged. The bug is present verbatim on current main. Happy to refresh the PR against v2 once triaged.

Activity

  1. EshwarSanthosh commented on Aug 13, 2026

    @EshwarSanthosh

    Confirmed still present on main at 6e30452:

    requested configured result expected
    https://mcp.example.com/api/../admin https://mcp.example.com/api True False
    https://mcp.example.com/api/%2e%2e/admin https://mcp.example.com/api True False
    https://mcp.example.com/api/./v1 https://mcp.example.com/api True True
    https://mcp.example.com/api123 https://mcp.example.com/api False False

    The 15 existing tests in tests/shared/test_auth_utils.py all pass, so this is
    a genuine gap in coverage rather than a regression. Only two call sites are
    affected, both in src/mcp/client/auth/oauth2.py (lines 210 and 577).

    Two implementation details worth settling before anyone writes the patch, since
    the straightforward unquote + posixpath.normpath approach has sharp edges:

    1. Unquoting before normalizing conflates %2F with a real separator.
      unquote("/api/a%2Fb") → /api/a/b, so an encoded slash becomes a path
      boundary. Per RFC 3986 §2.2 those are not equivalent, so this could turn one
      class of false positive into another. Decoding only the dot-segment triples
      (%2e/%2E), or normalizing per-segment without decoding separators, avoids
      that. Do you have a preference?

    2. normpath strips trailing slashes (normpath("/api/") → /api), so the
      existing trailing-slash normalization has to run after it. Otherwise the
      /api123 vs /api boundary guard regresses.

    Questions before I open anything:

    • Security policy. SECURITY.md asks for vulnerabilities to go through the
      private advisory process rather than public issues or PRs. This one is already
      public and not mine to re-route, but I'd rather not push a public patch for an
      auth-boundary bypass if you'd prefer to handle it privately or bundle it into a
      coordinated fix. Happy either way — just let me know which you want.
    • @Choppaaahh offered to refresh fix(auth): normalize URL paths before hierarchical resource check #2585 against v2. If you're already on it,
      I'll stay out of the way; if not, I'm glad to pick it up.
    • Does this need a [v1.x] backport? The same code is on v1.x, and
      SECURITY.md lists 1.x as receiving security fixes.

    Happy to take it once it's triaged and there's a direction on (1).

    Disclosure: I used AI assistance to search the tracker and produce the
    reproduction above. I've reviewed the code and results myself and will answer
    review questions directly.

  2. Choppaaahh commented on Aug 13, 2026

    @Choppaaahh
    Author

    1. %2F conflation — agreed: decode only the dot-segment triples (%2e/%2E), not a full unquote. Per RFC 3986 §2.2 an encoded %2F isn't a separator, so decoding it would just trade one false-positive class for another. Dot-triple-only (or per-segment normalization that never decodes separators) is the right scope.

    2. Trailing slash — yes, run the existing trailing-slash normalization after normpath, else the /api123 vs /api boundary regresses. Good catch.

    3. Security policy — it's already public (my routing miss), so the advisory path is moot for this one; I'd defer to maintainers on keep-vs-convert, but a public fix seems pragmatic now.

    I won't be refreshing this myself — please feel free to take the patch (dot-triple decode + post-normpath trailing-slash). Happy to review. Thanks!

  3. SarathChandraBellam commented on Aug 14, 2026

    @SarathChandraBellam

    I’d like to take this issue and prepare a fix for main.

    I’m planning to normalize raw and percent-encoded dot segments such as .. and %2e%2e, while leaving encoded separators like %2F untouched. I’ve added regression tests for those cases, and the targeted checks plus the full test suite pass locally.

    Before I open the PR, could a maintainer confirm whether this should also be backported to v1.x? Since this touches resource authorization, please also let me know if you’d prefer this handled through a coordinated security process.

    I used AI assistance while investigating and implementing this change, and I reviewed the resulting code and tests myself.

  4. added
    bugSomething isn't working
    P2Moderate issues affecting some users, edge cases, potentially valuable feature
    needs confirmationNeeds confirmation that the PR is actually required or needed.
    authIssues and PRs related to Authentication / OAuth
    v1Affects the v1.x maintenance line
    v2Affects the v2 line (2.x on main)
    on Aug 14, 2026
  5. EshwarSanthosh commented on Aug 19, 2026

    @EshwarSanthosh

    @maxisbey — could you assign this to me? Happy to do the work, but the process
    gate means nobody can move it forward until someone is assigned.

    Context on why it's stuck:

    I'd like to pick it up. @Choppaaahh, who filed this, handed the patch to me
    explicitly above ("please feel free to take the patch") after I worked through
    the two design pitfalls with them, and offered to review. To be clear about what
    I have and haven't done: I've confirmed the bug and settled the approach, but I
    haven't written the fix yet — I held off because of the needs confirmation
    label. I can have it up quickly once assigned.

    The approach is already agreed in-thread:

    • Decode only the %2e/%2E dot triples rather than a full unquote, since an
      encoded %2F isn't a path separator per RFC 3986 §2.2 and decoding it would
      trade one false-positive class for another.
    • Run the existing trailing-slash normalization after normpath, so the
      /api123 vs /api boundary guard doesn't regress.
    • Regression tests for the raw .., encoded %2e%2e, ., and %2F cases.

    @SarathChandraBellam has independently implemented something along these lines
    on #3308 — if you'd rather assign them and reopen that PR, that's a completely
    reasonable call and I'll happily review instead. Just don't want this to sit
    unassigned while three of us wait.

    Two things I still need a ruling on before opening anything:

    1. v1.x backport? The v1 + v2 labels you added on 18 Aug suggest both
      lines are affected, and SECURITY.md lists 1.x as receiving security fixes.
      Should I plan a [v1.x] backport PR alongside the main one?
    2. Public or advisory? SECURITY.md asks for vulnerabilities to go private,
      and this is an auth-boundary bypass. It's already public so I assume a public
      fix is fine, but I'd rather you say so than assume.

    Disclosure: AI assistance used for the tracker search and reproduction above;
    I've reviewed the code and results myself and will answer review questions
    directly.

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

    P2Moderate issues affecting some users, edge cases, potentially valuable featureauthIssues and PRs related to Authentication / OAuthbugSomething isn't workingneeds confirmationNeeds confirmation that the PR is actually required or needed.v1Affects 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