Back to skill

Security audit

ZUGFeRD Invoice Merger

Security checks for vulnerabilities and agentic risk

Overview

The skill mostly matches its stated ZUGFeRD invoice purpose, but it ships plaintext invoice data and relies on an unverified persistent executable download, so it needs Review before installation.

Review this skill before installing. Remove the packaged temp XML invoice artifacts, verify the MustangProject JAR with a trusted checksum or signature before use, avoid running the workflows with elevated privileges, and process sensitive or untrusted PDFs only in an isolated directory where temporary files are cleaned after each run.

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 (3)

T09 · Insecure Skill Coding Practices

Error
Location
temp/extracted.xml:12
Finding
Real Customer, Banking, Tax, and Invoice Data Stored in Plaintext Artifacts<![CDATA[ ## Vulnerability Details **File Location**: `temp/extracted.xml:12-154`; duplicated in `temp/extracted_zugferd.xml:12-154` **Vulnerability Type**: Plaintext sensitive-data exposure **Risk Level**: High ### Vulnerable Code/Data Snippet ```xml <rsm:ExchangedDocument> <ram:ID>RE0182</ram:ID> <ram:TypeCode>380</ram:TypeCode> <ram:IssueDateTime> <udt:DateTimeString format="102">20260228</udt:DateTimeString> </ram:IssueDateTime> </rsm:ExchangedDocument> <ram:SellerTradeParty> <ram:Name>QuantX GmbH</ram:Name> <ram:DefinedTradeContact> <ram:PersonName>Heiko Hänsel</ram:PersonName> <ram:TelephoneUniversalCommunication> <ram:CompleteNumber>+491604784131</ram:CompleteNumber> </ram:TelephoneUniversalCommunication> <ram:EmailURIUniversalCommunication> <ram:URIID>Info@quantx.gmbh</ram:URIID> </ram:EmailURIUniversalCommunication> </ram:DefinedTradeContact> <ram:SpecifiedTaxRegistration> <ram:ID schemeID="FC">207/116/00362</ram:ID> </ram:SpecifiedTaxRegistration> <ram:SpecifiedTaxRegistration> <ram:ID schemeID="VA">DE328725694</ram:ID> </ram:SpecifiedTaxRegistration> </ram:SellerTradeParty> <ram:SpecifiedTradeSettlementPaymentMeans> <ram:TypeCode>1</ram:TypeCode> <ram:Information>Überweisung</ram:Information> <ram:PayeePartyCreditorFinancialAccount> <ram:IBANID>DE43110101015027834960</ram:IBANID> <ram:AccountName>QuantX GmbH</ram:AccountName> </ram:PayeePartyCreditorFinancialAccount> <ram:PayeeSpecifiedCreditorFinancialInstitution> <ram:BICID>SOBKDEB2XXX</ram:BICID> </ram:PayeeSpecifiedCreditorFinancialInstitution> </ram:SpecifiedTradeSettlementPaymentMeans> ``` The same invoice record, including the same sensitive values, is present in `temp/extracted_zugferd.xml`. ### Technical Analysis The distributed project includes runtime-generated invoice artifacts containing real-looking pers ...[truncated 1502 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Remove `temp/extracted.xml` and `temp/extracted_zugferd.xml` from the distributed package and version-control history. 2. Review whether the exposed personal and financial identifiers require incident notification, credential review, or other action under applicable privacy and financial-data policies. 3. Add generated artifacts to `.gitignore`, for example: ```gitignore temp/* !temp/.gitkeep ``` 4. Do not use real invoices as repository fixtures. Replace them with synthetic records containing fictitious names, addresses, tax numbers, account identifiers, and transaction values. 5. Add automated secret and sensitive-data scanning to commits and release pipelines. 6. Store temporary invoice data only in owner-restricted, per-execution directories and delete it when processing completes. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/zugferd_pages_workflow.py:33
Finding
Predictable Shared Temporary Files Retain Sensitive Data and Enable Local File-Clobbering Conditions<![CDATA[ ## Vulnerability Details **File Location**: `scripts/zugferd_pages_workflow.py:33,142-195`; `scripts/zugferd_workflow.py:30,91-133` **Vulnerability Type**: Unsafe temporary-file handling **Risk Level**: Medium ### Vulnerable Code Snippet From `scripts/zugferd_pages_workflow.py`: ```python WORK_DIR = Path(__file__).parent.parent / "temp" def workflow(invoice_pdf, attachment_pdf, output_path): """Main workflow with sichtbare Seiten""" WORK_DIR.mkdir(parents=True, exist_ok=True) # Step 1: Identify e-invoice and extract XML xml_temp = WORK_DIR / "extracted_zugferd.xml" invoice_file = None for test_file in [invoice_pdf, attachment_pdf]: if extract_xml(test_file, str(xml_temp)): invoice_file = test_file break # Step 2: Merge PDFs merged_pdf = WORK_DIR / "merged.pdf" if not merge_pdfs_ghostscript(invoice_file, other_file, str(merged_pdf)): return False # Step 3: Convert to PDF/A-3 pdfa3_pdf = WORK_DIR / "merged_pdfa3.pdf" if not convert_to_pdfa3(str(merged_pdf), str(pdfa3_pdf)): return False # Step 4: Re-embed XML combined_pdf = WORK_DIR / "zugferd_combined.pdf" if not combine_with_xml(str(pdfa3_pdf), str(xml_temp), str(combined_pdf)): return False ``` From `scripts/zugferd_workflow.py`: ```python WORK_DIR = Path(__file__).parent.parent / "temp" def workflow(invoice_pdf, attachment_pdf, output_path): """Main workflow""" WORK_DIR.mkdir(parents=True, exist_ok=True) xml_temp = WORK_DIR / "extracted_zugferd.xml" if not extract_xml(einvoice, str(xml_temp)): return False temp_out = WORK_DIR / "combined.pdf" if not combine(einvoice, str(xml_temp), z_anhang, str(temp_out)): return False ``` ### Technical Analysis Both workflows use a persistent project-relative directory and fixed filenames for sensitive intermediate output. The files are not unique to an execution and are not removed after success ...[truncated 2402 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Create a unique private directory for each execution: ```python import tempfile from pathlib import Path def workflow(invoice_pdf, attachment_pdf, output_path, keep_temp=False): with tempfile.TemporaryDirectory(prefix="zugferd-") as temp_dir: work_dir = Path(temp_dir) xml_temp = work_dir / "extracted_zugferd.xml" merged_pdf = work_dir / "merged.pdf" pdfa3_pdf = work_dir / "merged_pdfa3.pdf" combined_pdf = work_dir / "zugferd_combined.pdf" # Perform processing and copy only the validated final result. ``` 2. Ensure the temporary directory is owner-only. `TemporaryDirectory` normally creates a securely randomized directory, but deployment should also use a restrictive process umask such as `0o077`. 3. Never reuse project-relative intermediate paths across executions. 4. Clean temporary files in a `finally` block or with a context manager, including after exceptions and timeouts. 5. If debugging artifacts are required, implement the documented `--keep-temp` option and copy them only to a newly created owner-restricted directory. 6. Validate that expected intermediate paths are regular files and not symbolic links before consuming them. 7. Avoid running the workflow with elevated privileges and restrict write access to the installed Skill directory. 8. Add concurrency tests to ensure two simultaneous invoice jobs cannot exchange or overwrite intermediate data. ]]>

T08 · Insecure Dependencies

Warning
Location
SKILL.md:23
Finding
Executable MustangProject JAR Is Downloaded Without Integrity Verification<![CDATA[ ## Vulnerability Details **File Location**: `SKILL.md:23-29`; repeated in `README.md:94-99` **Vulnerability Type**: Unverified executable dependency installation **Risk Level**: Medium ### Vulnerable Code Snippet ```bash mkdir -p ~/.openclaw/tools/mustang curl -L https://github.com/ZUGFeRD/mustangproject/releases/download/core-2.22.0/mustang.jar \ -o ~/.openclaw/tools/mustang/mustang.jar ``` The downloaded artifact is subsequently executed by the workflow: ```python MUSTANG_JAR = os.path.expanduser("~/.openclaw/tools/mustang/mustang.jar") def run_mustang(args): """Execute mustang.jar with Java 21""" cmd = ["java", "-jar", MUSTANG_JAR] + args env = os.environ.copy() env["PATH"] = "/opt/homebrew/opt/openjdk@21/bin:" + env.get("PATH", "") result = subprocess.run(cmd, capture_output=True, text=True, env=env, timeout=60) return result.stdout, result.stderr, result.returncode ``` ### Technical Analysis The documentation pins MustangProject to a specific version and downloads it over HTTPS from the stated upstream GitHub release. These are positive controls, but the installation instructions do not verify a cryptographic checksum or digital signature. The downloaded JAR is executable code and is later launched with `java -jar`. If the release artifact, upstream account, hosting path, trust chain, local download process, or destination file is compromised or substituted, the workflow will execute the replacement with the invoking user's permissions. This is a supply-chain hardening weakness rather than evidence that the referenced upstream release is malicious. ### Attack Path 1. An attacker compromises or substitutes an artifact in the dependency delivery chain, or replaces the local JAR after installation. 2. A user follows the documented `curl` command and stores the artifact at the trusted fixed path. 3. No checksum or signature comparison detects the substitution. 4. The user invokes either workflow. 5. `java -jar ~ ...[truncated 551 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Publish an expected SHA-256 digest for the exact pinned JAR and verify it before installation: ```bash curl --fail --location --proto '=https' --tlsv1.2 \ 'https://github.com/ZUGFeRD/mustangproject/releases/download/core-2.22.0/mustang.jar' \ -o mustang.jar printf '%s %s\n' 'EXPECTED_SHA256_DIGEST' 'mustang.jar' | shasum -a 256 -c - install -m 0555 mustang.jar "$HOME/.openclaw/tools/mustang/mustang.jar" ``` 2. Source the expected digest from an independently authenticated release manifest rather than from the same unverified download response. 3. Prefer upstream signature verification when the project publishes signed artifacts. 4. Make the installation command fail on HTTP errors by using `curl --fail`. 5. Before each execution, optionally verify the installed JAR against the pinned digest and refuse to run on mismatch. 6. Restrict write permissions on `~/.openclaw/tools/mustang/mustang.jar` and its parent directory so other local users cannot replace the artifact. 7. Document a controlled dependency-update procedure that requires review and digest updates. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Rogue AgentSelf-Modification, Session Persistence
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
Findings (14)

Env Variable Harvesting

High
Category
Data Exfiltration
Content
def run_mustang(args):
    """Execute mustang.jar with Java 21"""
    cmd = ["java", "-jar", MUSTANG_JAR] + args
    env = os.environ.copy()
    env["PATH"] = "/opt/homebrew/opt/openjdk@21/bin:" + env.get("PATH", "")
    result = subprocess.run(cmd, capture_output=True, text=True, env=env, timeout=60)
    return result.stdout, result.stderr, result.returncode
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.

Env Variable Harvesting

High
Category
Data Exfiltration
Content
def run_mustang(args):
    """Execute mustang.jar with Java 21"""
    cmd = ["java", "-jar", MUSTANG_JAR] + args
    env = os.environ.copy()
    env["PATH"] = "/opt/homebrew/opt/openjdk@21/bin:" + env.get("PATH", "")
    result = subprocess.run(cmd, capture_output=True, text=True, env=env, timeout=60)
    return result.stdout, result.stderr, result.returncode
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.

Natural-Language Policy Violations

Medium
Confidence
96% confidence
Finding
The file is written entirely in German, including workflow descriptions, usage guidance, and operational notes, with no indication that users may choose another language. Under the stated policy, forcing a specific language without user opt-in is a natural-language policy violation unless the locale constraint is clearly justified.

Session Persistence

Medium
Category
Rogue Agent
Content
- GhostScript (`brew install ghostscript`)
- MustangProject: `~/.openclaw/tools/mustang/mustang.jar`
  ```bash
  mkdir -p ~/.openclaw/tools/mustang
  curl -L https://github.com/ZUGFeRD/mustangproject/releases/download/core-2.22.0/mustang.jar \
       -o ~/.openclaw/tools/mustang/mustang.jar
  ```
Confidence
60% confidence
Finding
Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Session Persistence

Medium
Category
Rogue Agent
Content
### Install mustang.jar

```bash
mkdir -p ~/.openclaw/tools/mustang
curl -L https://github.com/ZUGFeRD/mustangproject/releases/download/core-2.22.0/mustang.jar \
     -o ~/.openclaw/tools/mustang/mustang.jar
```
Confidence
81% confidence
Finding
The skill instructs users to download a JAR from the internet into a persistent user-local tools directory without any integrity verification such as a checksum, signature, or pinned provenance check. Persisting an executable artifact across sessions increases the blast radius: if the download is tampered with or later replaced, subsequent runs may repeatedly execute untrusted code.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
def run_ghostscript(args):
    """Execute GhostScript (gs)"""
    cmd = ["gs"] + args
    result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
    return result.stdout, result.stderr, result.returncode

def validate(pdf_path):
Confidence
77% confidence
Finding
The script invokes Ghostscript on attacker-controlled PDF inputs. Ghostscript has a long history of sandbox bypass and file access vulnerabilities, so processing untrusted PDFs in-process on the host can lead to arbitrary file read/write or code execution if Ghostscript is vulnerable or misconfigured.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
if not os.path.exists(MUSTANG_JAR):
        missing.append(f"mustang.jar nicht gefunden: {MUSTANG_JAR}")
    
    gs_check = subprocess.run(["which", "gs"], capture_output=True)
    if gs_check.returncode != 0:
        missing.append("GhostScript (gs) nicht installiert: brew install ghostscript")
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
cmd = ["java", "-jar", MUSTANG_JAR] + args
    env = os.environ.copy()
    env["PATH"] = "/opt/homebrew/opt/openjdk@21/bin:" + env.get("PATH", "")
    result = subprocess.run(cmd, capture_output=True, text=True, env=env, timeout=60)
    return result.stdout, result.stderr, result.returncode

def validate(pdf_path):
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
cmd = ["java", "-jar", MUSTANG_JAR] + args
    env = os.environ.copy()
    env["PATH"] = "/opt/homebrew/opt/openjdk@21/bin:" + env.get("PATH", "")
    result = subprocess.run(cmd, capture_output=True, text=True, env=env, timeout=60)
    return result.stdout, result.stderr, result.returncode

def validate(pdf_path):
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Natural-Language Policy Violations

Low
Confidence
84% confidence
Finding
The markdown explicitly states 'Germany/EU only', which is a locale restriction in natural language. While the skill is German invoicing-related, the file does not frame this as a user-selectable option or clearly explain whether this is merely applicability guidance versus a hard policy constraint.

Intent-Code Divergence

Low
Confidence
96% confidence
Finding
The argument help for --invoice states it must be the PDF containing ZUGFeRD XML, implying a fixed role for that input. However, the workflow validates both provided PDFs and will swap roles if the attachment file contains the XML instead, so the documentation contradicts the actual behavior rather than merely omitting detail.

Natural-Language Policy Violations

Low
Confidence
83% confidence
Finding
This XML file contains multiple natural-language fields in German, starting with the document note 'Rechnung' and continuing throughout the invoice text. Under the policy check for language or locale constraints, the file forces a specific language without any visible opt-in or indication that the document is intentionally region-specific.

Natural-Language Policy Violations

Low
Confidence
94% confidence
Finding
The natural-language fields in the document, such as "Rechnung" and the surrounding explanatory notes, are fixed in German. Under the policy for all file types, forcing a specific language without user opt-in or a documented region-specific justification is a locale-policy concern.

Natural-Language Policy Violations

Low
Confidence
95% confidence
Finding
The payment-terms description is user-visible natural language and is hardcoded in German. Because no language choice or explicit locale limitation is stated in the file, this can violate the language/locale policy.

Static analysis

No suspicious patterns detected.