From ec3b23f69cb0be53c37b6a5e9c05178abae89ccc Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 31 May 2026 03:50:35 +0000 Subject: [PATCH 1/2] Sentinel: Add -no-shell-escape and timeout to PDF generation This prevents arbitrary command execution (RCE) via LaTeX's \write18 feature and prevents Denial of Service (DoS) attacks from infinite compilation loops. Co-authored-by: anchapin <6326294+anchapin@users.noreply.github.com> --- .jules/sentinel.md | 5 +++++ cli/generators/cover_letter_generator.py | 20 ++++++++++++++++---- cli/pdf/converter.py | 18 ++++++++++++++---- 3 files changed, 35 insertions(+), 8 deletions(-) diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 3a1d237..3d181d8 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -7,3 +7,8 @@ **Vulnerability:** The `CoverLetterGenerator` used a standard Jinja2 environment (intended for HTML/XML or plain text) to render LaTeX templates. This allowed malicious user input (or AI hallucinations) containing LaTeX control characters (e.g., `\input{...}`) to be injected directly into the LaTeX source, leading to potential Local File Inclusion (LFI) or other exploits. **Learning:** Jinja2's default `autoescape` is context-aware based on file extensions, but usually only for HTML/XML. It does NOT automatically escape LaTeX special characters. Relying on manual filters (like `| latex_escape`) in templates is error-prone and brittle, as developers might forget to apply them to every variable. **Prevention:** Always use a dedicated Jinja2 environment for LaTeX generation that enforces auto-escaping via a `finalize` hook (e.g., `tex_env.finalize = latex_escape`). This ensures *all* variable output is sanitized by default, providing defense-in-depth even if the template author forgets explicit filters. + +## 2026-05-31 - [Critical] LaTeX RCE and DoS Vulnerabilities in PDF Generation +**Vulnerability:** Subprocesses generating PDFs using `pdflatex` and `pandoc` (in `cli/pdf/converter.py` and `cli/generators/cover_letter_generator.py`) were executed without the `-no-shell-escape` flag or any timeout limits. +**Learning:** `pdflatex` allows executing arbitrary shell commands via the `\write18` feature if shell escape is not explicitly disabled. Additionally, without timeouts, malicious `.tex` input could cause infinite loops, resulting in a Denial of Service (DoS) by hanging the application or consuming resources indefinitely. +**Prevention:** Always append `-no-shell-escape` (or `--pdf-engine-opt=-no-shell-escape` for pandoc) to all LaTeX compilation subprocess calls. Always enforce a timeout (e.g., `timeout=30`) when executing external commands via `subprocess.communicate`, and ensure proper cleanup by catching `subprocess.TimeoutExpired`, terminating the process, and completing communication to prevent zombie processes. diff --git a/cli/generators/cover_letter_generator.py b/cli/generators/cover_letter_generator.py index aaf0b61..8108086 100644 --- a/cli/generators/cover_letter_generator.py +++ b/cli/generators/cover_letter_generator.py @@ -771,12 +771,18 @@ def _compile_pdf(self, output_path: Path, tex_content: str) -> bool: try: # Use Popen with explicit cleanup to avoid double-free issues process = subprocess.Popen( - ["pdflatex", "-interaction=nonstopmode", tex_path.name], + ["pdflatex", "-interaction=nonstopmode", "-no-shell-escape", tex_path.name], stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=tex_path.parent, ) - stdout, stderr = process.communicate() + try: + stdout, stderr = process.communicate(timeout=30) + except subprocess.TimeoutExpired: + process.kill() + process.communicate() + return False + if process.returncode == 0 or output_path.exists(): pdf_created = True except (subprocess.CalledProcessError, FileNotFoundError): @@ -787,11 +793,17 @@ def _compile_pdf(self, output_path: Path, tex_content: str) -> bool: # Fallback to pandoc try: process = subprocess.Popen( - ["pandoc", str(tex_path), "-o", str(output_path), "--pdf-engine=xelatex"], + ["pandoc", str(tex_path), "-o", str(output_path), "--pdf-engine=xelatex", "--pdf-engine-opt=-no-shell-escape"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) - stdout, stderr = process.communicate() + try: + stdout, stderr = process.communicate(timeout=30) + except subprocess.TimeoutExpired: + process.kill() + process.communicate() + return False + if process.returncode == 0 or output_path.exists(): pdf_created = True except (subprocess.CalledProcessError, FileNotFoundError): diff --git a/cli/pdf/converter.py b/cli/pdf/converter.py index 0b0a200..9e33f04 100644 --- a/cli/pdf/converter.py +++ b/cli/pdf/converter.py @@ -86,12 +86,17 @@ def _compile_pdflatex( """ try: process = subprocess.Popen( - ["pdflatex", "-interaction=nonstopmode", tex_path.name], + ["pdflatex", "-interaction=nonstopmode", "-no-shell-escape", tex_path.name], stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=working_dir, ) - stdout, stderr = process.communicate() + try: + stdout, stderr = process.communicate(timeout=30) + except subprocess.TimeoutExpired: + process.kill() + process.communicate() + return False if process.returncode == 0 or output_path.exists(): return True @@ -121,12 +126,17 @@ def _compile_pandoc( """ try: process = subprocess.Popen( - ["pandoc", str(tex_path), "-o", str(output_path), "--pdf-engine=xelatex"], + ["pandoc", str(tex_path), "-o", str(output_path), "--pdf-engine=xelatex", "--pdf-engine-opt=-no-shell-escape"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=working_dir, ) - stdout, stderr = process.communicate() + try: + stdout, stderr = process.communicate(timeout=30) + except subprocess.TimeoutExpired: + process.kill() + process.communicate() + return False if process.returncode == 0 or output_path.exists(): return True From cd326a7868d64570f9906721036b88af428bb63c Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 31 May 2026 03:53:42 +0000 Subject: [PATCH 2/2] Sentinel: Add -no-shell-escape and timeout to PDF generation This prevents arbitrary command execution (RCE) via LaTeX's \write18 feature and prevents Denial of Service (DoS) attacks from infinite compilation loops. Fixed formatting to pass black CI checks. Co-authored-by: anchapin <6326294+anchapin@users.noreply.github.com> --- cli/generators/cover_letter_generator.py | 9 ++++++++- cli/pdf/converter.py | 9 ++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/cli/generators/cover_letter_generator.py b/cli/generators/cover_letter_generator.py index 8108086..ea6fa2f 100644 --- a/cli/generators/cover_letter_generator.py +++ b/cli/generators/cover_letter_generator.py @@ -793,7 +793,14 @@ def _compile_pdf(self, output_path: Path, tex_content: str) -> bool: # Fallback to pandoc try: process = subprocess.Popen( - ["pandoc", str(tex_path), "-o", str(output_path), "--pdf-engine=xelatex", "--pdf-engine-opt=-no-shell-escape"], + [ + "pandoc", + str(tex_path), + "-o", + str(output_path), + "--pdf-engine=xelatex", + "--pdf-engine-opt=-no-shell-escape", + ], stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) diff --git a/cli/pdf/converter.py b/cli/pdf/converter.py index 9e33f04..763b2ee 100644 --- a/cli/pdf/converter.py +++ b/cli/pdf/converter.py @@ -126,7 +126,14 @@ def _compile_pandoc( """ try: process = subprocess.Popen( - ["pandoc", str(tex_path), "-o", str(output_path), "--pdf-engine=xelatex", "--pdf-engine-opt=-no-shell-escape"], + [ + "pandoc", + str(tex_path), + "-o", + str(output_path), + "--pdf-engine=xelatex", + "--pdf-engine-opt=-no-shell-escape", + ], stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=working_dir,