T09 · Insecure Skill Coding Practices
Warning
- Location
- scripts/generate_pdf.py:31
- Finding
- Pandoc Option Injection Through an Attacker-Controlled Input Filename## Vulnerability Details **File Location**: `scripts/generate_pdf.py`, lines 31-91 **Vulnerability Type**: Pandoc argument and option injection **Risk Level**: Medium ### Vulnerable Code ```python # Verify markdown file exists if not os.path.exists(markdown_file): print(f"Error: Markdown file not found: {markdown_file}") return False # Set default output path if output_pdf is None: output_pdf = Path(markdown_file).with_suffix('.pdf') # Build pandoc command cmd = [ 'pandoc', markdown_file, '-o', str(output_pdf), '--pdf-engine=xelatex', '-V', 'geometry:margin=1in', '-V', 'fontsize=11pt', '-V', 'colorlinks=true', '-V', 'linkcolor=blue', '-V', 'urlcolor=blue', '-V', 'citecolor=blue', ] # Execute pandoc try: print(f"Generating PDF: {output_pdf}") print(f"Command: {' '.join(cmd)}") result = subprocess.run(cmd, capture_output=True, text=True, check=True) ``` ### Technical Analysis The Markdown filename is supplied through the command line and passed directly to Pandoc before an option terminator. Although `subprocess.run()` uses an argument array and therefore prevents shell metacharacter injection, it does not prevent option injection into Pandoc itself. A valid local filename beginning with a hyphen can be interpreted as a Pandoc command-line option rather than as an input filename. For example, a filename such as `--lua-filter=payload.lua` could cause Pandoc to load a Lua filter. Pandoc Lua filters can execute local operations with the privileges of the process running Pandoc. The existing `os.path.exists(markdown_file)` check only establishes that a filesystem entry with the supplied name exists. It does not establish that Pandoc will treat the value as a positional input file. ### Attack Path 1. An attacker gains the ability to create or supply files in the working directory used by the script. 2. The attacker creates a malicious Pandoc Lua filter, such as `payload.lua`. 3. The attacker cr ...[truncated 1428 chars]
- Remediation
- ## Remediation Suggestions 1. Resolve the input to a normalized absolute path before passing it to Pandoc: ```python input_path = Path(markdown_file).resolve(strict=True) if not input_path.is_file(): print(f"Error: Markdown input is not a regular file: {input_path}") return False ``` 2. Pass only the normalized absolute path to Pandoc. On POSIX systems, an absolute path starts with `/` and therefore cannot be interpreted as a command-line option. 3. Where supported by the target Pandoc version, insert an option terminator before positional input paths: ```python cmd = [ "pandoc", "-o", str(output_path), "--pdf-engine=xelatex", "-V", "geometry:margin=1in", "-V", "fontsize=11pt", "-V", "colorlinks=true", "-V", "linkcolor=blue", "-V", "urlcolor=blue", "-V", "citecolor=blue", "--", str(input_path), ] ``` 4. Apply equivalent normalization and regular-file validation to the output path, bibliography, CSL file, and template path. 5. If inputs are expected to reside in a controlled workspace, enforce that the resolved path remains under that directory using `Path.is_relative_to()` or an equivalent containment check. 6. Add regression tests using filenames beginning with `-`, including names that resemble `--lua-filter`, `--filter`, `--template`, and other Pandoc options. 7. Run document conversion in a restricted container or sandbox with no secrets, minimal filesystem access, no unnecessary network access, and a non-privileged user.
