httpx resolves `..` before sending, so save_flow("../ai/config") reached
PUT /ai/config. Each segment is now percent-encoded, dots included.
42 lines
1.3 KiB
Python
42 lines
1.3 KiB
Python
"""What the MCP tools send, whatever an agent hands them."""
|
|
|
|
import asyncio
|
|
|
|
import httpx
|
|
|
|
from fluksio.mcp import server
|
|
|
|
|
|
def test_a_name_cannot_climb_out_of_its_route() -> None:
|
|
"""httpx resolves ``..``, so ``save_flow("../ai/config")`` was ``PUT /ai/config``.
|
|
|
|
A prompt-injected assistant acting for a superuser could repoint the
|
|
provider that way while the stored key stayed.
|
|
"""
|
|
seen: list[str] = []
|
|
|
|
def record(request: httpx.Request) -> httpx.Response:
|
|
seen.append(f"{request.method} {request.url.raw_path.decode()}")
|
|
return httpx.Response(200, json={})
|
|
|
|
async def calls() -> None:
|
|
previous = server._client
|
|
mock = httpx.AsyncClient(
|
|
transport=httpx.MockTransport(record), base_url="http://test"
|
|
)
|
|
server.set_client(mock)
|
|
try:
|
|
await server.save_flow("../ai/config", {})
|
|
await server.delete_flow("..")
|
|
await server.get_node_source("..", "../source")
|
|
finally:
|
|
server.set_client(previous)
|
|
await mock.aclose()
|
|
|
|
asyncio.run(calls())
|
|
assert seen == [
|
|
"PUT /api/v1/flows/%2E%2E%2Fai%2Fconfig",
|
|
"DELETE /api/v1/flows/%2E%2E",
|
|
"GET /api/v1/flows/%2E%2E/nodes/%2E%2E%2Fsource/source",
|
|
]
|