MCP server runs as privileged
ID |
mcp-server-runs-as-privileged |
Severity |
high |
Remediation Complexity |
trivial |
Remediation Risk |
medium |
Remediation Effort |
low |
OWASP LLM |
LLM06:2025 (Excessive Agency) |
OWASP ASI |
secondary ASI03:2026 (Privilege Escalation), secondary ASI05:2026 |
Family |
LLM06 — Excessive Agency |
Red-team vectors |
Privilege Escalation / Containment Failure |
Tags |
ai_security |
Description
An MCP-server container / process manifest runs as root / privileged: no privilege-restriction declaration in its Dockerfile, docker-compose service, Kubernetes pod spec, or systemd unit. An MCP server run as root can read or write any file on the system, escalate to the host if a container breakout occurs, and inject code into peer processes — so the containment boundary around a tool-executing surface is gone.
Scoped to MCP manifests: the file must carry an MCP signal in its own content — a case-insensitive match, on any single line, of
(?i)modelcontextprotocol|\bmcp[-_]?server\b|\bmcp\b|\b(?:fastmcp|mcpo)\b
There is no dedicated label or annotation to add: a comment, an image name, a package install, an
ENTRYPOINT path, a container / service name, an ENV, or a systemd Description= all qualify. An
unrelated root container is left to the IaC scanner. Only files whose name matches Dockerfile*,
.yml, .yaml or *.service are read at all.
One finding per manifest, flagging both the missing declaration and explicit root:
-
Dockerfile — no
USERdirective, orUSER root/USER 0/USER 0:0; -
docker-compose — no
user:field, oruser: root/user: "0:0"; -
Kubernetes — no
securityContext.runAsNonRoot: trueand norunAsUser, orrunAsUser: 0; -
systemd unit — no
User=directive, orUser=root.
Only the user part of a user[:group] token decides, so root:root and 0:0 are root.
A Dockerfile USER root is not flagged when the same line carries an explicit justification —
# justification: <text>, also accepted as justified: / reason:, with actual text after the colon:
USER root # justification: needs CAP_NET_RAW to probe the tool network
A bare comment does not suppress it. USER root # TODO and USER root # are flagged, because
silencing a high-severity privilege finding has to be a deliberate act rather than a side effect of
annotating the line. The escape is Dockerfile-only: user: root, runAsUser: 0 and User=root are
flagged whatever comment they carry — mute the finding instead.
Scope limits worth knowing. The signal must be in the file itself: an mcp reference in a
neighbouring requirements.txt / package.json does not bring a Dockerfile into scope, and the file
name is not a signal either (a Dockerfile.mcp with no mcp in its body is out of scope). The
framework package names are listed one by one: fastmcp and mcpo are recognized, another framework
is not until it is added to the alternation. A Containerfile, and a Kubernetes
manifest written in .json, are never read. Each file is a single scope: the first USER / user: /
User= found decides for the whole file — a multi-stage Dockerfile is not evaluated per stage, and a
compose file whose first service is hardened clears its siblings. Likewise, in a multi-document
(---) YAML a runAsNonRoot: true in one document clears the others, and pod-level versus
container-level securityContext is not distinguished.
|
Examples
FROM ubuntu:22.04
RUN apt-get install -y mcp-server
ENTRYPOINT ["/usr/local/bin/mcp-server"] (1)
| 1 | No USER directive → runs as root — flagged. |
A dedicated non-root user produces no finding:
RUN adduser --system mcp-user
USER mcp-user
Neither does a manifest with no MCP signal, root or not — it is out of scope:
FROM nginx:1.25
COPY site /usr/share/nginx/html