Skip to content

Commit 7d6270c

Browse files
Add restricted execution flags to pdflatex and pandoc commands
Co-authored-by: anchapin <6326294+anchapin@users.noreply.github.com>
1 parent 0638d12 commit 7d6270c

3 files changed

Lines changed: 53 additions & 8 deletions

File tree

.jules/sentinel.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,8 @@
77
**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.
88
**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.
99
**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.
10+
11+
## 2026-08-02 - [Critical] PDF Compilation RCE Vulnerability
12+
**Vulnerability:** PDF compilation via pdflatex and pandoc lacked restricted execution flags (-no-shell-escape), allowing potential Remote Code Execution if malicious LaTeX is compiled.
13+
**Learning:** Subprocess calls invoking LaTeX engines must always include -no-shell-escape to sandbox execution and prevent \write18 or shell-escape attacks.
14+
**Prevention:** Ensure -no-shell-escape is explicitly passed to all LaTeX engine subprocess calls, and handle TimeoutExpired to safely kill stalled processes.

cli/generators/cover_letter_generator.py

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -770,13 +770,20 @@ def _compile_pdf(self, output_path: Path, tex_content: str) -> bool:
770770
pdf_created = False
771771
try:
772772
# Use Popen with explicit cleanup to avoid double-free issues
773+
# Use -no-shell-escape to prevent RCE during PDF compilation
773774
process = subprocess.Popen(
774-
["pdflatex", "-interaction=nonstopmode", tex_path.name],
775+
["pdflatex", "-interaction=nonstopmode", "-no-shell-escape", tex_path.name],
775776
stdout=subprocess.PIPE,
776777
stderr=subprocess.PIPE,
777778
cwd=tex_path.parent,
778779
)
779-
stdout, stderr = process.communicate()
780+
try:
781+
stdout, stderr = process.communicate(timeout=30)
782+
except subprocess.TimeoutExpired:
783+
process.kill()
784+
stdout, stderr = process.communicate()
785+
return False
786+
780787
if process.returncode == 0 or output_path.exists():
781788
pdf_created = True
782789
except (subprocess.CalledProcessError, FileNotFoundError):
@@ -786,12 +793,26 @@ def _compile_pdf(self, output_path: Path, tex_content: str) -> bool:
786793
else:
787794
# Fallback to pandoc
788795
try:
796+
# Use -no-shell-escape to prevent RCE during PDF compilation
789797
process = subprocess.Popen(
790-
["pandoc", str(tex_path), "-o", str(output_path), "--pdf-engine=xelatex"],
798+
[
799+
"pandoc",
800+
str(tex_path),
801+
"-o",
802+
str(output_path),
803+
"--pdf-engine=xelatex",
804+
"--pdf-engine-opt=-no-shell-escape",
805+
],
791806
stdout=subprocess.PIPE,
792807
stderr=subprocess.PIPE,
793808
)
794-
stdout, stderr = process.communicate()
809+
try:
810+
stdout, stderr = process.communicate(timeout=30)
811+
except subprocess.TimeoutExpired:
812+
process.kill()
813+
stdout, stderr = process.communicate()
814+
return False
815+
795816
if process.returncode == 0 or output_path.exists():
796817
pdf_created = True
797818
except (subprocess.CalledProcessError, FileNotFoundError):

cli/pdf/converter.py

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,13 +85,19 @@ def _compile_pdflatex(
8585
True if PDF was created successfully
8686
"""
8787
try:
88+
# Use -no-shell-escape to prevent RCE during PDF compilation
8889
process = subprocess.Popen(
89-
["pdflatex", "-interaction=nonstopmode", tex_path.name],
90+
["pdflatex", "-interaction=nonstopmode", "-no-shell-escape", tex_path.name],
9091
stdout=subprocess.PIPE,
9192
stderr=subprocess.PIPE,
9293
cwd=working_dir,
9394
)
94-
stdout, stderr = process.communicate()
95+
try:
96+
stdout, stderr = process.communicate(timeout=30)
97+
except subprocess.TimeoutExpired:
98+
process.kill()
99+
stdout, stderr = process.communicate()
100+
return False
95101

96102
if process.returncode == 0 or output_path.exists():
97103
return True
@@ -120,13 +126,26 @@ def _compile_pandoc(
120126
True if PDF was created successfully
121127
"""
122128
try:
129+
# Use -no-shell-escape to prevent RCE during PDF compilation
123130
process = subprocess.Popen(
124-
["pandoc", str(tex_path), "-o", str(output_path), "--pdf-engine=xelatex"],
131+
[
132+
"pandoc",
133+
str(tex_path),
134+
"-o",
135+
str(output_path),
136+
"--pdf-engine=xelatex",
137+
"--pdf-engine-opt=-no-shell-escape",
138+
],
125139
stdout=subprocess.PIPE,
126140
stderr=subprocess.PIPE,
127141
cwd=working_dir,
128142
)
129-
stdout, stderr = process.communicate()
143+
try:
144+
stdout, stderr = process.communicate(timeout=30)
145+
except subprocess.TimeoutExpired:
146+
process.kill()
147+
stdout, stderr = process.communicate()
148+
return False
130149

131150
if process.returncode == 0 or output_path.exists():
132151
return True

0 commit comments

Comments
 (0)