← MCP server and skill grades

docx

anthropics/skills/skills/docx

C
Type
Skill
Scanned
Oct 29, 2026
Version
ca45fc496a34
Files read
61

This skill provides instructions and Python helper scripts for creating, editing, commenting on, and validating Microsoft Word (.docx) documents using docx-js, LibreOffice, and Pandoc. The biggest risk is that the XML validators use lxml with default settings that resolve external entities, allowing a maliciously crafted document to read local files on the user's machine.

What it can reach

Files
Reads, unpacks, modifies, validates, and writes Office document archives (.docx, .dotx, .pptx, .xlsx) and temporary files in /tmp.
Shell commands
Executes external binaries including LibreOffice (soffice), gcc, git, pandoc, pdftoppm, zip, and unzip.

Findings

1 high3 medium1 low
  1. HighXML External Entity (XXE) resolution in document schema validators

    skills/docx/scripts/office/validators/base.py:167

    Although other parts of the codebase use defusedxml, BaseSchemaValidator and its subclasses (DOCXSchemaValidator and PPTXSchemaValidator) parse untrusted XML files from unpacked documents using lxml.etree.parse() and lxml.etree.fromstring() with default parser settings. By default, lxml resolves XML entities (including local file:// URIs). A stranger who supplies a crafted .docx file containing an external entity declaration can cause local files to be read and leaked into validation error messages (such as whitespace, deletion, or XSD error previews) or trigger denial-of-service via entity expansion.

    Fix: Pass a hardened parser instance such as lxml.etree.XMLParser(resolve_entities=False, no_network=True, load_dtd=False) to all lxml.etree.parse() and lxml.etree.fromstring() calls.

  2. MediumPredictable /tmp path for LibreOffice macro profile allows local code execution

    skills/docx/scripts/accept_changes.py:16

    accept_changes.py uses a hardcoded directory (/tmp/libreoffice_docx_profile) to store and run a LibreOffice Basic macro. In _setup_libreoffice_macro(), if Module1.xba already exists and contains the string 'AcceptAllTrackedChanges', the script reuses the file without overwriting it. On a multi-user system, a local attacker can pre-create this file with malicious StarBasic commands that will execute under the user's privileges when accept_changes.py runs.

    Fix: Create a fresh per-run directory using tempfile.TemporaryDirectory() instead of a fixed path in /tmp.

  3. MediumPredictable /tmp path for LD_PRELOAD shared library shim

    skills/docx/scripts/office/soffice.py:50

    When Unix domain sockets are blocked by a sandbox, soffice.py compiles a C shim to a fixed path (/tmp/lo_socket_shim.so) and injects it into LibreOffice via LD_PRELOAD. Because _ensure_shim() reuses the file if it already exists without checking ownership, another user on a shared environment could pre-plant a malicious /tmp/lo_socket_shim.so to run arbitrary code.

    Fix: Compile and store the shared library inside a secure, user-owned temporary directory created with tempfile.mkdtemp().

  4. MediumPost-extraction symlink deletion does not prevent symlink traversal during unzip

    skills/docx/SKILL.md:53

    The skill instructions direct the agent to run 'unzip -q doc.docx -d unpacked/' on untrusted documents and only afterwards delete symlinks with 'find unpacked -type l -delete'. A malicious archive containing a symlink entry followed by a file entry targeting that symlink path can overwrite arbitrary files outside the target directory during extraction before the find command runs.

    Fix: Instruct the agent to extract archives using the safe_extract() helper in scripts/office/helpers/__init__.py, which validates paths and rejects symlinks before extracting each entry.

  5. LowMissing path containment check in relationship file reference validation

    skills/docx/scripts/office/validators/base.py:364

    In validate_file_references(), internal relationship Target paths from .rels files are resolved against unpacked_dir using target_path.resolve(), but the validator does not check whether the resolved path stays inside unpacked_dir. A crafted document with relative path traversal (../) in a .rels file can probe the existence of arbitrary files on the host filesystem based on whether a broken reference error is reported.

    Fix: Check target_path.is_relative_to(self.unpacked_dir) (or use the opc_target() helper) before checking whether target_path exists.