fix: reject {expr}{+var} adjacency to close ReDoS gap
The adjacency check rejected {+a}{b} but not the symmetric {a}{+b}.
Both produce overlapping greedy quantifiers; a 64KB crafted input
against prefix{a}{+b}.json takes ~23s to reject.
Added prev_path_expr tracking so {+var} immediately after any path
expression is rejected. {expr}{#var} remains allowed since the #
operator prepends a literal '#' that the preceding group's character
class excludes, giving a natural boundary.
Also adds the missing 'from typing import Any' to the three low-level
server examples in docs/server/resources.md.
This commit is contained in:
@@ -251,6 +251,8 @@ There's no decorator; you return the protocol types yourself.
|
||||
For fixed URIs, keep a registry and dispatch on exact match:
|
||||
|
||||
```python
|
||||
from typing import Any
|
||||
|
||||
from mcp.server.lowlevel import Server
|
||||
from mcp.types import (
|
||||
ListResourcesResult,
|
||||
@@ -309,6 +311,8 @@ Parse your templates once, then match incoming URIs against them in
|
||||
your read handler:
|
||||
|
||||
```python
|
||||
from typing import Any
|
||||
|
||||
from mcp.server.context import ServerRequestContext
|
||||
from mcp.server.lowlevel import Server
|
||||
from mcp.shared.uri_template import UriTemplate
|
||||
@@ -373,6 +377,8 @@ the protocol `ResourceTemplate` type, using the same template strings
|
||||
you parsed above:
|
||||
|
||||
```python
|
||||
from typing import Any
|
||||
|
||||
from mcp.types import ListResourceTemplatesResult, PaginatedRequestParams, ResourceTemplate
|
||||
|
||||
|
||||
|
||||
@@ -819,8 +819,11 @@ def _check_ambiguous_adjacency(template: str, parts: list[_Part]) -> None:
|
||||
trailing match fails the engine backtracks through O(n) split
|
||||
points. Two conditions trigger this:
|
||||
|
||||
- ``{+var}`` immediately adjacent to any expression
|
||||
(``{+a}{b}``, ``{+a}{/b*}``)
|
||||
- ``{+var}`` immediately adjacent to any expression on either
|
||||
side (``{+a}{b}``, ``{a}{+b}``, ``{/a}{+b}``). The ``#``
|
||||
operator is exempt from the preceded-by case since it
|
||||
prepends a literal ``#`` that the preceding group cannot
|
||||
match.
|
||||
- Two ``{+var}``/``{#var}`` anywhere in the path, even with a
|
||||
literal between them (``{+a}/x/{+b}``) — the literal does not
|
||||
disambiguate since ``[^?#]*`` matches it too
|
||||
@@ -836,6 +839,7 @@ def _check_ambiguous_adjacency(template: str, parts: list[_Part]) -> None:
|
||||
"""
|
||||
prev_explode = False
|
||||
prev_reserved = False
|
||||
prev_path_expr = False
|
||||
seen_reserved = False
|
||||
for part in parts:
|
||||
if isinstance(part, str):
|
||||
@@ -843,6 +847,7 @@ def _check_ambiguous_adjacency(template: str, parts: list[_Part]) -> None:
|
||||
# the seen-reserved count: [^?#]* matches most literals.
|
||||
prev_explode = False
|
||||
prev_reserved = False
|
||||
prev_path_expr = False
|
||||
continue
|
||||
for var in part.variables:
|
||||
# ?/& are stripped before pattern building and never reach
|
||||
@@ -850,11 +855,12 @@ def _check_ambiguous_adjacency(template: str, parts: list[_Part]) -> None:
|
||||
if var.operator in ("?", "&"):
|
||||
prev_explode = False
|
||||
prev_reserved = False
|
||||
prev_path_expr = False
|
||||
continue
|
||||
|
||||
if prev_reserved:
|
||||
if prev_reserved or (var.operator == "+" and prev_path_expr):
|
||||
raise InvalidUriTemplate(
|
||||
"{+var} or {#var} immediately followed by another expression "
|
||||
"{+var} or {#var} immediately adjacent to another expression "
|
||||
"causes quadratic-time matching; separate them with a literal",
|
||||
template=template,
|
||||
)
|
||||
@@ -872,5 +878,6 @@ def _check_ambiguous_adjacency(template: str, parts: list[_Part]) -> None:
|
||||
|
||||
prev_explode = var.explode
|
||||
prev_reserved = var.operator in ("+", "#")
|
||||
prev_path_expr = True
|
||||
if prev_reserved:
|
||||
seen_reserved = True
|
||||
|
||||
@@ -166,7 +166,7 @@ def test_parse_rejects_adjacent_explodes(template: str):
|
||||
@pytest.mark.parametrize(
|
||||
"template",
|
||||
[
|
||||
# {+var} immediately adjacent to any expression
|
||||
# {+var} immediately adjacent to any expression (either side)
|
||||
"{+a}{b}",
|
||||
"{+a}{/b}",
|
||||
"{+a}{/b*}",
|
||||
@@ -175,6 +175,10 @@ def test_parse_rejects_adjacent_explodes(template: str):
|
||||
"{#a}{b}",
|
||||
"{+a,b}", # multi-var in one expression: same adjacency
|
||||
"prefix/{+path}{.ext}", # literal before doesn't help
|
||||
"{a}{+b}", # + preceded by expression: same overlap
|
||||
"{.a}{+b}",
|
||||
"{/a}{+b}",
|
||||
"x{name}{+path}y",
|
||||
# Two {+var}/{#var} anywhere, even with literals between
|
||||
"{+a}/x/{+b}",
|
||||
"{+a},{+b}",
|
||||
@@ -199,7 +203,7 @@ def test_parse_rejects_reserved_quadratic_patterns(template: str):
|
||||
"api/{+path}{?v,page}", # + followed by query (stripped before regex)
|
||||
"api/{+path}{&next}", # + followed by query-continuation
|
||||
"page{#section}", # # at end
|
||||
"{a}{+b}", # + preceded by expression is fine; only following matters
|
||||
"{a}{#b}", # # prepends literal '#' that {a}'s class excludes
|
||||
"{+a}/sep/{b}", # literal + bounded expression after: linear
|
||||
"{+a},{b}", # same: literal disambiguates when second is bounded
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user