Back to skill

Security audit

Plantuml

Security checks for vulnerabilities and agentic risk

Overview

This PlantUML skill is mostly coherent, but it needs Review because its renderer can automatically run unverified local JAR files and mutable Docker images.

Install only if you are comfortable with local renderer execution. Prefer a pinned, trusted Docker image or an explicitly chosen verified PlantUML JAR; avoid running this from untrusted project directories where a malicious ./plantuml.jar could be present. Do not use --use-public-server for confidential diagrams, and review PLANTUML_PUBLIC_SERVER before enabling remote rendering.

Vulnerability Patterns
  • Insecure DependenciesIntroduces malicious components through unsafe dependency sources
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • Agent Memory PoisoningWrites attacker-controlled rules into memory that affect later sessions
  • Remote Payload Retrieval and ExecutionFetches external code whose behavior can change after review
  • Embedded Malicious CodeShips malicious scripts inside the skill and executes them locally
Findings (2)

T08 · Insecure Dependencies

Warning
Location
scripts/generate_plantuml.py:588
Finding

Mutable Docker Image Executed Without Version or Digest Pinning

Content
View full analysis
/dev/null; plantuml -t{ext} /data/{src.name}", ] ``` The standard rendering path uses the same mutable image: ```python cmd = base + ["plantuml/plantuml:latest", f"-t{ext}", f"/data/{src.name}"] ``` ## Technical Analysis The preferred rendering backend executes `plantuml/plantuml:latest`. The `latest` tag is mutable and does not identify a cryptographically fixed container image. The effective code executed by the Skill can therefore change after the Skill package has been reviewed. If the image is not already available locally, Docker can retrieve the current image associated with this tag. A user or deployment process may also update the local image independently. A compromise of the upstream registry account, image build pipeline, or tag could consequently cause attacker-controlled container code to run during diagram rendering. The container receives a writable mount of the temporary rendering directory through `/data`. In CJK mode, it may additionally receive read-only mounts of host font directories. No network isolation, read-only root filesystem, capability dropping, or `no-new-privileges` restriction is configured. ### Attack Path 1. An attacker compromises the upstream container publishing process or otherwise causes a malicious image to be associated with `plantuml/plantuml:latest`. 2. The malicious image is pulled because it is absent locally, or the local mutable image is updated. 3. A user invokes `generate_plantuml.py`. 4. `convert_via_docker()` starts the unpinned image. 5. Attacker-controlled code r ...[truncated 1111 chars]
Remediation
View remediation
" "@sha256:" ) ``` 2. Establish a controlled update process that verifies the image digest, release provenance, changelog, and vulnerability scan results before changing the pinned reference. 3. Disable container networking unless rendering explicitly requires it: ```python base = [ "docker", "run", "--rm", "--network=none", "--cap-drop=ALL", "--security-opt=no-new-privileges", "--read-only", "-v", f"{mount}:/data", ] ``` 4. Where practical, mount the source read-only and use a separate narrowly scoped writable output directory. 5. Run the container as a non-root user and apply resource limits such as memory, process, and CPU limits. 6. Avoid mounting broad host font directories. Copy only the required font files into a dedicated temporary directory and mount that directory read-only. 7. Document and verify the expected image digest during startup so an unexpected local image cannot be used silently. ]]>

T08 · Insecure Dependencies

Error
Location
scripts/generate_plantuml.py:650
Finding

Implicit Execution of Unverified PlantUML JAR Files from User-Writable Locations

Content
View full analysis
Optional[Path]: candidates = [ "/usr/local/bin/plantuml.jar", "/usr/share/plantuml/plantuml.jar", os.path.expanduser("~/plantuml.jar"), os.path.join(os.environ.get("USERPROFILE", ""), "plantuml.jar") if os.environ.get("USERPROFILE") else "", os.path.join(os.environ.get("PROGRAMFILES", ""), "PlantUML", "plantuml.jar") if os.environ.get("PROGRAMFILES") else "", os.path.join(os.environ.get("LOCALAPPDATA", ""), "PlantUML", "plantuml.jar") if os.environ.get("LOCALAPPDATA") else "", "./plantuml.jar", ] for c in candidates: if c and Path(c).is_file(): return Path(c) return None ``` The selected file is then executed without signature, hash, ownership, or permission validation: ```python r = subprocess.run( ["java", "-jar", str(jar), f"-t{ext}", "-o", str(tmpdir), str(tmpdir / src.name)], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) ``` ## Technical Analysis A JAR file is executable code. `_find_local_jar()` treats any regular file named `plantuml.jar` in a candidate location as trusted. Several candidate locations are commonly writable by the invoking user or by other actors capable of controlling project contents, including: - `~/plantuml.jar` - `%LOCALAPPDATA%\PlantUML\plantuml.jar` - `./plantuml.jar` The script does not validate a cryptographic digest, package signature, expected version, file owner, or filesystem permissions before passing the file to `java -jar`. The current-directory candidate is especially dangerous when the Skill is run inside a downloaded, shared, o ...[truncated 2037 chars]
Remediation
View remediation
str: digest = hashlib.sha256() with path.open("rb") as fh: for chunk in iter(lambda: fh.read(1024 * 1024), b""): digest.update(chunk) return digest.hexdigest() ``` 4. Reject the JAR unless its digest matches the reviewed PlantUML release artifact. 5. Where supported, verify the publisher's digital signature and release provenance in addition to checking the digest. 6. Validate that the JAR is a regular file, is not a symbolic link, has an expected owner, and is not writable by untrusted users. 7. Prefer an administrator-managed absolute path, such as a dedicated application dependency directory, rather than relying on the current working directory. 8. Fail closed when no verified JAR is available. Do not silently execute the first file whose name matches `plantuml.jar`. 9. Run the Java renderer in a sandbox or container with restricted filesystem access, disabled networking, and resource limits. ]]>
Vulnerability Patterns
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
Findings (15)

Tainted flow: 'req' from os.environ.get (line 730, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Confidence
90% confidence
Finding

The script allows the destination host for uploads to be overridden by the PLANTUML_PUBLIC_SERVER environment variable and then POSTs the full diagram source there. Even though network use is gated behind --use-public-server and accompanied by warnings, this still creates a real exfiltration path for sensitive diagram contents if a user enables the option in a hostile or misconfigured environment.

Content

Scanner excerpt · scripts/generate_plantuml.py (reported line 736)May include surrounding context.

python
method="POST",
            headers={"Content-Type": "text/plain"},
        )
        with urllib.request.urlopen(req, timeout=60) as resp:
            payload = resp.read()
        output_file.write_bytes(payload)
    except (urllib.error.URLError, OSError, TimeoutError):

Env Variable Harvesting

High
Category
Data Exfiltration
Confidence
60% confidence
Finding

Code enumerates, copies, or searches environment variables for secrets. Bulk environment access can collect credentials unrelated to the skill's stated purpose.

Content

Scanner excerpt · scripts/generate_plantuml.py (reported line 585)May include surrounding context.

python
shutil.copy2(src, tmpdir / src.name)
        mount = _docker_mount_path(tmpdir)

        env = os.environ.copy()
        env.setdefault("MSYS_NO_PATHCONV", "1")

        base = ["docker", "run", "--rm", "-v", f"{mount}:/data"]

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
97% confidence
Finding

The skill documentation clearly directs use of shell commands, file read/write, network access, environment variables, and Docker/Java tooling, yet the manifest declares no explicit tool scope such as permissions or allowed-tools. That mismatch weakens policy enforcement and transparency, making it easier for an agent to invoke more capability than users or the platform expect.

Content

No source excerpt is available for this finding.

Vague Triggers

Medium
Category
Not specified by scanner
Confidence
90% confidence
Finding

The phrase "Any request involving diagram generation from text descriptions" is very broad and lacks constraints about when this PlantUML-specific skill should activate versus other diagraming or visualization skills. Because it is a markdown file and this line serves as a trigger description, it could overlap with common requests for non-UML diagrams or generic visualization help.

Content

No source excerpt is available for this finding.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Confidence
70% confidence
Finding

Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Content

Scanner excerpt · SKILL.md (reported line 174)May include surrounding context.

md
- For Docker: mounts host font directories (`/usr/share/fonts`, `/usr/local/share/fonts`) into the container and refreshes the font cache before rendering
- For local JAR: uses system-installed CJK fonts
- If CJK fonts are not installed on the system, characters will not render correctly. Install them via:
  - Debian/Ubuntu: `sudo apt install fonts-wqy-zenhei`
  - Fedora: `sudo dnf install wqy-zenhei-fonts`
  - macOS: CJK fonts are pre-installed (PingFang SC)

Sudo/Root Execution

Medium
Category
Privilege Escalation
Confidence
70% confidence
Finding

Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Content

Scanner excerpt · SKILL.md (reported line 175)May include surrounding context.

md
- For Docker: mounts host font directories (`/usr/share/fonts`, `/usr/local/share/fonts`) into the container and refreshes the font cache before rendering
- For local JAR: uses system-installed CJK fonts
- If CJK fonts are not installed on the system, characters will not render correctly. Install them via:
  - Debian/Ubuntu: `sudo apt install fonts-wqy-zenhei`
  - Fedora: `sudo dnf install wqy-zenhei-fonts`
  - macOS: CJK fonts are pre-installed (PingFang SC)

Rp1

Medium
Category
MCP Rug Pull
Confidence
93% confidence
Finding

Referencing a floating Docker tag like plantuml/plantuml:latest makes builds non-reproducible and allows unreviewed upstream image changes to alter execution behavior. If the upstream image is compromised or simply changes incompatibly, the skill may run attacker-controlled code during rendering.

Content

No source excerpt is available for this finding.

Rp1

Medium
Category
MCP Rug Pull
Confidence
93% confidence
Finding

The use of docker pull plantuml/plantuml:latest again relies on a mutable tag, which can silently change over time and undermine supply-chain integrity. In a skill that executes local rendering through Docker, this means arbitrary upstream image changes may execute on the user's machine.

Content

No source excerpt is available for this finding.

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/generate_plantuml.py (reported line 542)May include surrounding context.

python
str(dark),
    ]
    try:
        subprocess.run(base_cmd + args, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
        return dark.is_file()
    except (OSError, subprocess.CalledProcessError):
        return False

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/generate_plantuml.py (reported line 620)May include surrounding context.

python
]
                log("  → CJK mode: mounting host fonts and refreshing font cache")
                try:
                    r = subprocess.run(cmd, env=env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
                    if r.returncode == 0:
                        gen = _iter_output_candidate(tmpdir, (ext, fmt), src.name)
                        if gen:

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/generate_plantuml.py (reported line 635)May include surrounding context.

python
]
                log("  → CJK mode: mounting host fonts and refreshing font cache")
                try:
                    r = subprocess.run(cmd, env=env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
                    if r.returncode == 0:
                        gen = _iter_output_candidate(tmpdir, (ext, fmt), src.name)
                        if gen:

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/generate_plantuml.py (reported line 684)May include surrounding context.

python
try:
        shutil.copy2(src, tmpdir / src.name)
        try:
            r = subprocess.run(
                ["java", "-jar", str(jar), f"-t{ext}", "-o", str(tmpdir), str(tmpdir / src.name)],
                stdout=subprocess.DEVNULL,
                stderr=subprocess.DEVNULL,

Rp1

Medium
Category
MCP Rug Pull
Confidence
87% confidence
Finding

Using the mutable tag latest for the Docker image weakens reproducibility and increases supply-chain risk because future pulls may retrieve different code than expected. In a tool that renders attacker-controlled input, unpinned renderer changes can silently introduce vulnerabilities or malicious behavior.

Content

No source excerpt is available for this finding.

Intent-Code Divergence

Low
Category
Not specified by scanner
Confidence
96% confidence
Finding

The manifest comment states the skill reads no environment variables, but later documentation instructs use of PLANTUML_PUBLIC_SERVER. This discrepancy can mislead reviewers and automated controls about the skill's true inputs, especially where environment variables influence network destinations.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
76% confidence
Finding

The instruction says that when CJK characters are present, the skill should use the --cjk flag to switch fonts automatically, which imposes a locale-specific rendering path rather than offering the user a choice. The policy allows locale constraints when clearly justified, but here the behavior is framed as mandatory and does not explicitly present user opt-in or choice.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.