| tiêu đề | jkawamoto mcp-florence2 Latest Server-Side Request Forgery |
|---|
| Mô tả | Summary
The ocr and caption tools in mcp-florence2 are vulnerable to Server-Side Request Forgery (SSRF). The user-supplied src parameter is passed directly to Python's requests.get() when it begins with http:// or https://, without any validation of the destination host, IP range, or protocol beyond that prefix check. This allows an attacker to make the MCP server issue arbitrary HTTP requests to internal network services, cloud metadata endpoints, or any other host reachable from the server process. Even when the target does not return a valid image (causing the tool call to fail during image parsing), the outbound HTTP request is still initiated server-side — confirming SSRF via a local listener.
Detail
mcp-florence2 provides AI programming assistants (such as Claude, Cursor, and Goose) with image-processing capabilities via the Model Context Protocol, using Microsoft's Florence-2 model for OCR and caption generation. Both exposed tools accept a src argument described as "A file path or URL to the image file". When src is an HTTP(S) URL, the shared helper get_images() fetches it directly with no host or IP restrictions.
The vulnerable fetch logic is centralized in get_images():
Version: 0.3.13
File: src/mcp_florence2/__init__.py
@contextmanager
def get_images(src: PathLike | str) -> Iterator[list[Image]]:
"""Opens and returns a list of images from a file path or URL."""
if isinstance(src, str) and (src.startswith("http://") or src.startswith("https://")):
res = requests.get(src)
res.raise_for_status()
# ... processes response as PDF or image ...
The only gate before the outbound request is a string prefix check (http:// or https://). There is no parsing or validation of the resolved host, IP address, or port. The full user-controlled URL is passed to requests.get() unchanged.
Both MCP tools invoke this helper without any additional sanitization:
Version: 0.3.13
File: src/mcp_florence2/__init__.py
@mcp.tool()
def ocr(
ctx: Context,
src: PathLike | str = Field(description="A file path or URL to the image file that needs to be processed."),
) -> list[str]:
"""Process an image file or URL using OCR to extract text."""
with get_images(src) as images:
app_ctx: AppContext = ctx.request_context.lifespan_context
return app_ctx.processor.ocr(images)
@mcp.tool()
def caption(
ctx: Context,
src: PathLike | str = Field(description="A file path or URL to the image file that needs to be processed."),
) -> list[str]:
"""Processes an image file and generates captions for the image."""
with get_images(src) as images:
app_ctx: AppContext = ctx.request_context.lifespan_context
return app_ctx.processor.caption(images, CaptionLevel.MORE_DETAILED)
URL-based input is an intentional, documented feature. The README explicitly states that users can "process images or PDF files stored on a local or web server", and both tool descriptions advertise URL support:
Version: 0.3.13
File: README.md
You can process images or PDF files stored on a local or web server to extract text using OCR … or generate descriptive captions …
src: A file path or URL to the image file that needs to be processed.
The server manifest likewise advertises both tools with no security constraints:
Version: 0.3.13
File: manifest.json
{
"name": "ocr",
"description": "Process an image file or URL using OCR to extract text."
},
{
"name": "caption",
"description": "Processes an image file and generates captions for the image."
}
There is no validation at any point in this call chain:
No allowlist of permitted domains
No blocklist of private/internal IP ranges (127.x, 10.x, 172.16–31.x, 192.168.x, 169.254.x)
No DNS rebinding or resolved-IP checks after URL parsing
No restriction beyond http:// / https:// prefix matching (scheme validation is minimal)
No limit on HTTP redirects (requests.get() follows redirects by default, enabling redirect-based bypass into internal space)
No timeout configured on the outbound request
When the target returns a valid image or PDF, the fetched content is processed by Florence-2 and the extracted text or caption is returned to the MCP client — constituting direct data exfiltration for image-based secrets (e.g., screenshots of internal dashboards). When the target returns non-image content (e.g., HTML, plain text, or JSON from an internal API), raise_for_status() may succeed and the tool then fails during image decoding — but the HTTP request has already been issued by the server process. A side-channel listener (such as python -m http.server) confirms SSRF regardless of whether the tool call ultimately succeeds.
Clarified: This attack does not require OS-level shell access. The practically feasible triggering path is: an attacker performs prompt injection into an AI agent integrated with mcp-florence2, instructing the agent to invoke ocr or caption with an internal URL (often framed as "extract text from this image URL" or "describe this image at …"). The agent, having no awareness of SSRF risks, will comply. No direct MCP client access is required beyond the ability to influence the agent's prompts (e.g., via crafted file content, web pages, or emails the agent is asked to process).
Note on affected tools: Both ocr and caption share the same get_images() entry point and are equally affected. There are no other MCP tools in this server.
Additional note (local file access): When src is not an HTTP(S) URL, get_images() opens the value as a local filesystem path (open_image(src) / PdfDocument(src)), which constitutes a separate arbitrary local file read risk. That is outside the scope of this SSRF report but shares the same unconstrained src parameter.
Proof of Concept Using MCP Inspector
Prerequisites
Start the MCP server via Inspector:
cd mcp-florence2
uv sync
npx @modelcontextprotocol/[email protected] uv run mcp-florence2 --model base --cache-model
This launches the MCP Inspector web UI.
In a separate terminal, start a local HTTP listener to capture SSRF requests: python -m http.server 8000
Steps
Open the Inspector URL in a browser (include the auth token if required) and click Connect.
Go to the Tools tab, click List Tools, and select ocr (or caption — both are vulnerable).
Invoke the tool with the following arguments: "http://127.0.0.1:8000/ssrf-test"
Observe the outgoing MCP request:
{
"method": "tools/call",
"params": {
"name": "ocr",
"arguments": {
"src": "http://127.0.0.1:8000/ssrf-test"
},
"_meta": {
"progressToken": 11
}
}
}
{
"method": "tools/call",
"params": {
"name": "caption",
"arguments": {
"src": "http://127.0.0.1:8000/ssrf-test"
},
"_meta": {
"progressToken": 12
}
}
}
Observe the listener output — the MCP server process (not the browser) issues the request:
PS C:\Users\skywings> python -m http.server 8000
Serving HTTP on :: port 8000 (http://[::]:8000/) ...
::ffff:127.0.0.1 - - [30/Jun/2026 16:30:03] code 404, message File not found
::ffff:127.0.0.1 - - [30/Jun/2026 16:30:03] "GET /ssrf-test HTTP/1.1" 404 - |
|---|
| Nguồn | ⚠️ https://github.com/jkawamoto/mcp-florence2/issues/59 |
|---|
| Người dùng | TianyuLi (UID 99360) |
|---|
| Đệ trình | 30/06/2026 10:48 (cách đây 2 các tháng) |
|---|
| Kiểm duyệt | 16/08/2026 15:50 (2 months later) |
|---|
| Trạng thái | được chấp nhận |
|---|
| Mục VulDB | 391184 [jkawamoto mcp-florence2 đến 0.3.13 __init__.py get_images src nâng cao đặc quyền] |
|---|
| điểm | 20 |
|---|