Back to skill

Security audit

PDF to Markdown with OCR

Security checks for vulnerabilities and agentic risk

Overview

The skill is a real PDF-to-Markdown OCR workflow, but it uploads documents to persistent Modal storage and has unsafe task-name handling that can access or write outside the intended job folder.

Review before installing. Use only documents you are comfortable uploading to Modal, use simple slugs such as letters/numbers/dashes only, avoid sharing volumes with unrelated jobs, and delete remote data when finished. The publisher should validate slugs, use OCR-specific isolated volumes, document retention and cleanup, and pin dependencies before this should be treated as low-risk.

Vulnerability Patterns
  • Insecure DependenciesIntroduces malicious components through unsafe dependency sources
  • Insecure Skill Coding PracticesFinds exploitable flaws such as hardcoded secrets or command injection
  • 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
Findings (2)

T09 · Insecure Skill Coding Practices

Error
Location
src/ocr2markdown.py:58
Finding

Unsanitized Slug Allows Path Traversal Outside the Intended Task Directory

Content
View full analysis
list[dict]: _bootstrap_cache() upload_dir = Path(config.MOUNT_DATA) / slug / config.DIR_UPLOAD output_base = Path(config.MOUNT_DATA) / slug / config.DIR_OUTPUT ``` The `slug` value originates from the local command-line entry point and is passed directly to the remote function: ```python @images.app.local_entrypoint() def main(slug: str) -> None: print(f"[ocr2markdown] processing: {slug}") results = ocr2markdown.remote(slug) ``` ### Technical Analysis The application treats `slug` as a safe directory name without validating or normalizing it. Python's `pathlib` permits both parent-directory components and absolute paths: - A slug containing `..` can escape the intended task directory beneath `/mnt/data`. - If `slug` is absolute, the preceding `config.MOUNT_DATA` component is discarded. - The resulting paths are subsequently used to discover input PDFs, create output directories, and pass paths to MinerU. The subprocess call does not create shell command injection because it uses an argument list rather than `shell=True`. However, the attacker-controlled path still determines which PDF files MinerU reads and where generated files are written. The affected Modal function mounts both the shared data volume at `/mnt/data` and the shared model volume at `/mnt/models`. Consequently, traversal can cross task boundaries and potentially reach other writable mounted or container locations. ### Attack Path 1. An authenticated caller invokes the skill with a crafted slug, for example: ```bash modal run ./src/ocr2markdown.py --slug ../models ``` 2. The application constructs these effective paths: ```text /mnt/data/../models/upload /mnt/data/../models/output ...[truncated 1136 chars]
Remediation
View remediation
str: if not SLUG_PATTERN.fullmatch(slug): raise ValueError("Invalid slug") return slug ``` 2. Resolve constructed paths and verify that they remain beneath the intended base directory: ```python def safe_child(base: Path, slug: str, child: str) -> Path: base = base.resolve() candidate = (base / validate_slug(slug) / child).resolve() if not candidate.is_relative_to(base): raise ValueError("Path escapes the data directory") return candidate ``` 3. Construct both directories through the validated helper: ```python data_root = Path(config.MOUNT_DATA) upload_dir = safe_child(data_root, slug, config.DIR_UPLOAD) output_base = safe_child(data_root, slug, config.DIR_OUTPUT) ``` 4. Explicitly reject absolute paths, path separators, `.` and `..`, even if additional validation is introduced elsewhere. 5. Separate unrelated workloads into distinct Modal volumes and grant the OCR function access only to the volume required for the current task. 6. Add tests covering `../models`, `../../`, absolute paths, repeated separators, encoded separators, and valid identifiers. ]]>

T08 · Insecure Dependencies

Warning
Location
src/images.py:12
Finding

Mutable and Unpinned Third-Party Dependencies Create Supply-Chain Risk

Content
View full analysis
=3.0.0,<4.0.0' opencv-python-headless --break-system-packages -q" ) ``` The setup documentation also recommends installing Modal without pinning a version: ```bash pip install modal ``` ### Technical Analysis The runtime image is not reproducibly defined: - `mineru[core]>=3.0.0,<4.0.0` permits any current or future MinerU 3.x release and all dependencies selected for its `core` extra. - `opencv-python-headless` has no version constraint. - `modal` is installed without a version constraint in the documented setup process. - `vllm/vllm-openai:v0.11.2` is referenced by a registry tag rather than an immutable image digest. Registry tags can be moved or replaced. Python packages can execute code during installation or when imported and invoked. The container image also supplies executable code used by the runtime. Because these components are selected dynamically, a later build may execute code that was not included in or evaluated during this audit. No evidence was found that the currently named packages are malicious. The vulnerability is the absence of immutable, reviewed dependency resolution, which increases exposure to upstream compromise, malicious replacement, and unexpected incompatible releases. ### Attack Path 1. A permitted future package version is published, or an upstream package, transitive dependency, package account, or mutable container tag is compromised. 2. A user runs or rebuilds the skill. 3. The image builder retrieves the currently resolved image and package versions from external registries. 4. Package inst ...[truncated 949 chars]
Remediation
View remediation
opencv-python-headless== modal== ``` 2. Generate a lock file containing exact transitive versions and cryptographic hashes. Install with hash verification, for example through a compiled requirements file and: ```bash pip install --require-hashes -r requirements.lock ``` 3. Reference the base container using an immutable digest: ```python modal.Image.from_registry( "vllm/vllm-openai:v0.11.2@sha256:", add_python="3.12", ) ``` 4. Remove `--break-system-packages` where possible and install dependencies into an isolated virtual environment to avoid silently replacing image-level packages. 5. Use an approved package index or internal artifact mirror, and retain reviewed wheels and container images so builds do not depend on mutable upstream state. 6. Add automated dependency scanning, provenance verification, and update review. Regenerate locks and image digests only after testing and security review. 7. Pin the Modal CLI version in `SKILL.md` or direct users to a locked setup requirements file rather than `pip install modal`. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
Findings (8)

Tp4

High
Category
MCP Tool Poisoning
Confidence
93% confidence
Finding

The core purpose mostly matches: the code performs remote GPU-based PDF-to-Markdown OCR/parsing and preserves generated outputs from an OCR pipeline. However, the declared description says it takes local PDF/image files, while the actual code only enumerates PDF files and invokes processing for PDFs. There is no image-file handling logic in this chunk. No other suspicious or unrelated capabilities are present.

Content

No source excerpt is available for this finding.

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
91% confidence
Finding

The skill invokes shell commands (modal volume/create/put/get, modal run) but does not declare any tool restrictions or allowed tool scope. This weakens least-privilege controls and makes it easier for the skill to gain broader shell capability than users or the platform may expect.

Content

No source excerpt is available for this finding.

Rp1

Medium
Category
MCP Rug Pull
Confidence
70% confidence
Finding

Referencing npx skills without pinning a version can lead to non-reproducible installs and supply-chain risk if a newer or compromised package version is fetched. While this line is setup guidance rather than direct execution logic, it still encourages pulling mutable code from the registry.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
97% confidence
Finding

The skill uploads local documents to a remote Modal volume for processing but does not clearly warn users that potentially sensitive PDFs/images leave the local environment. In an OCR/document-parsing context, this is especially risky because files may contain PII, credentials, contracts, medical data, or other confidential content.

Content

No source excerpt is available for this finding.

Intent-Code Divergence

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

The file-level docstring and surrounding comments indicate this module defines constants for an OCR-to-Markdown pipeline, but the shared volume names are set to "speech2srt-data" and "speech2srt-models". This is not merely incomplete documentation: the inline comment and identifiers suggest OCR-specific resources while the actual configured resources belong to a different skill domain, creating intent-code divergence about what infrastructure the skill uses.

Content

No source excerpt is available for this finding.

Intent-Code Divergence

Medium
Category
Not specified by scanner
Confidence
93% confidence
Finding

The module docstring says this file contains constants for the OCR2Markdown pipeline, but the configured application name is "speech2srt.com", which points to a different service purpose. That creates a direct contradiction between the documented intent of the skill and the concrete identity it will use at runtime.

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 · src/ocr2markdown.py (reported line 81)May include surrounding context.

python
work_dir.mkdir(parents=True, exist_ok=True)
        print(f"  [proc] {stem}.pdf")
        t1 = time.monotonic()
        res = subprocess.run(
            ["mineru", "-p", str(pdf_path), "-o", str(work_dir), "-b", "pipeline"],
            capture_output=True,
            text=True,

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
82% confidence
Finding

The manifest hard-codes trigger phrases in specific languages, including Chinese, but does not explain whether multilingual triggering is optional or configurable. This can create an implicit language/locale policy choice without user opt-in or justification.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.