Skip to content

πŸ›‘οΈ Sentinel: [CRITICAL] Fix RCE and DoS vulnerabilities in PDF compilation#212

Open
anchapin wants to merge 1 commit intomainfrom
sentinel-fix-pdf-rce-15689215248428004751
Open

πŸ›‘οΈ Sentinel: [CRITICAL] Fix RCE and DoS vulnerabilities in PDF compilation#212
anchapin wants to merge 1 commit intomainfrom
sentinel-fix-pdf-rce-15689215248428004751

Conversation

@anchapin
Copy link
Copy Markdown
Owner

@anchapin anchapin commented Mar 27, 2026

🚨 Severity: CRITICAL
πŸ’‘ Vulnerability: Remote Code Execution (RCE) via \write18 shell escape in PDF compilation and Denial of Service (DoS) via infinite loops. The -no-shell-escape flag and a timeout were missing when executing pdflatex or pandoc directly.
🎯 Impact: If an attacker managed to inject malicious LaTeX code into the generated templates (e.g., via AI hallucination, intentional template overrides, or flaws in escaping logic), they could execute arbitrary shell commands on the server running the compilation. Runaway compilation could also exhaust server resources.
πŸ”§ Fix: Added the -no-shell-escape flag to both pdflatex and pandoc (--pdf-engine-opt=-no-shell-escape) commands in cli/pdf/converter.py and cli/generators/cover_letter_generator.py. Added a 30-second timeout to process.communicate() calls and implemented proper cleanup for subprocess.TimeoutExpired exceptions.
βœ… Verification: Verified by reading the modified files, running the test suite (pytest tests/test_pdf_security.py and pytest for the whole suite) and receiving code review approval. The new vulnerability finding has been recorded in .jules/sentinel.md.


PR created automatically by Jules for task 15689215248428004751 started by @anchapin

Summary by Sourcery

Harden PDF compilation against remote code execution and denial-of-service by restricting LaTeX shell escapes and enforcing timeouts on external tools.

Bug Fixes:

  • Prevent potential RCE by disabling shell escape when invoking pdflatex and pandoc during PDF generation.
  • Mitigate DoS risks in PDF compilation by adding timeouts and cleanup for long-running or stuck subprocesses.

Documentation:

  • Document the newly discovered PDF compilation RCE/DoS vulnerability and its mitigations in the security learnings log.

Co-authored-by: anchapin <6326294+anchapin@users.noreply.github.com>
@google-labs-jules
Copy link
Copy Markdown
Contributor

πŸ‘‹ Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a πŸ‘€ emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai bot commented Mar 27, 2026

Reviewer's Guide

Hardened PDF compilation against RCE and DoS by disabling LaTeX shell escapes for both pdflatex and pandoc, and adding timeouts plus error handling for long‑running or hanging subprocesses, with the changes documented in the security log.

Sequence diagram for secure PDF compilation in CoverLetterGenerator

sequenceDiagram
    participant CoverLetterGenerator
    participant Subprocess
    participant Pdflatex
    participant Pandoc
    participant FileSystem

    CoverLetterGenerator->>Subprocess: Popen [pdflatex -interaction=nonstopmode -no-shell-escape tex_path]
    activate Subprocess
    Subprocess->>Pdflatex: start compilation
    Pdflatex-->>Subprocess: running

    Subprocess-->>CoverLetterGenerator: communicate(timeout=30)
    alt pdflatex finishes in time
        Pdflatex-->>Subprocess: exit with returncode
        Subprocess-->>CoverLetterGenerator: stdout, stderr
        CoverLetterGenerator->>FileSystem: check output_path.exists
        alt success or output_path exists
            FileSystem-->>CoverLetterGenerator: pdf exists
            CoverLetterGenerator-->>CoverLetterGenerator: pdf_created = True
        else failure
            FileSystem-->>CoverLetterGenerator: pdf missing
            CoverLetterGenerator-->>CoverLetterGenerator: raise CalledProcessError or handle nonzero
        end
    else pdflatex hangs or infinite loop
        Subprocess-->>CoverLetterGenerator: TimeoutExpired
        CoverLetterGenerator->>Subprocess: kill
        Subprocess-->>CoverLetterGenerator: communicate() cleanup
        CoverLetterGenerator-->>CoverLetterGenerator: raise RuntimeError
    end

    Note over CoverLetterGenerator: Exceptions include CalledProcessError, FileNotFoundError, RuntimeError

    alt first attempt fails or times out
        CoverLetterGenerator->>Subprocess: Popen [pandoc ... --pdf-engine=xelatex --pdf-engine-opt=-no-shell-escape]
        activate Subprocess
        Subprocess->>Pandoc: start compilation
        Pandoc-->>Subprocess: running
        Subprocess-->>CoverLetterGenerator: communicate(timeout=30)
        alt pandoc finishes in time
            Pandoc-->>Subprocess: exit with returncode
            Subprocess-->>CoverLetterGenerator: stdout, stderr
            CoverLetterGenerator->>FileSystem: check output_path.exists
            alt success or output_path exists
                FileSystem-->>CoverLetterGenerator: pdf exists
                CoverLetterGenerator-->>CoverLetterGenerator: pdf_created = True
            else failure
                FileSystem-->>CoverLetterGenerator: pdf missing
                CoverLetterGenerator-->>CoverLetterGenerator: pdf_created remains False
            end
        else pandoc hangs or infinite loop
            Subprocess-->>CoverLetterGenerator: TimeoutExpired
            CoverLetterGenerator->>Subprocess: kill
            Subprocess-->>CoverLetterGenerator: communicate() cleanup
            CoverLetterGenerator-->>CoverLetterGenerator: raise RuntimeError
        end
    else first attempt succeeded
        CoverLetterGenerator-->>CoverLetterGenerator: skip pandoc fallback
    end
Loading

Sequence diagram for secure PDF compilation helpers in converter

sequenceDiagram
    participant PdfConverter
    participant Subprocess
    participant Pdflatex
    participant Pandoc
    participant FileSystem

    rect rgb(230, 230, 255)
        PdfConverter->>Subprocess: Popen [pdflatex -interaction=nonstopmode -no-shell-escape tex_path]
        activate Subprocess
        Subprocess->>Pdflatex: start compilation
        Subprocess-->>PdfConverter: communicate(timeout=30)
        alt pdflatex finishes in time
            Pdflatex-->>Subprocess: exit with returncode
            Subprocess-->>PdfConverter: stdout, stderr
            PdfConverter->>FileSystem: check output_path.exists
            alt success or output_path exists
                FileSystem-->>PdfConverter: pdf exists
                PdfConverter-->>PdfConverter: return True
            else failure
                FileSystem-->>PdfConverter: pdf missing
                PdfConverter-->>PdfConverter: fall through to False
            end
        else pdflatex hangs or infinite loop
            Subprocess-->>PdfConverter: TimeoutExpired
            PdfConverter->>Subprocess: kill
            Subprocess-->>PdfConverter: communicate() cleanup
            PdfConverter-->>PdfConverter: raise RuntimeError
        end
        PdfConverter-->>PdfConverter: catch CalledProcessError, FileNotFoundError, RuntimeError
        PdfConverter->>FileSystem: check output_path.exists
        alt pdf exists despite error
            FileSystem-->>PdfConverter: pdf exists
            PdfConverter-->>PdfConverter: return True
        else no pdf
            FileSystem-->>PdfConverter: pdf missing
            PdfConverter-->>PdfConverter: return False
        end
    end

    rect rgb(230, 255, 230)
        PdfConverter->>Subprocess: Popen [pandoc ... --pdf-engine=xelatex --pdf-engine-opt=-no-shell-escape]
        activate Subprocess
        Subprocess->>Pandoc: start compilation
        Subprocess-->>PdfConverter: communicate(timeout=30)
        alt pandoc finishes in time
            Pandoc-->>Subprocess: exit with returncode
            Subprocess-->>PdfConverter: stdout, stderr
            PdfConverter->>FileSystem: check output_path.exists
            alt success or output_path exists
                FileSystem-->>PdfConverter: pdf exists
                PdfConverter-->>PdfConverter: return True
            else failure
                FileSystem-->>PdfConverter: pdf missing
                PdfConverter-->>PdfConverter: return False
            end
        else pandoc hangs or infinite loop
            Subprocess-->>PdfConverter: TimeoutExpired
            PdfConverter->>Subprocess: kill
            Subprocess-->>PdfConverter: communicate() cleanup
            PdfConverter-->>PdfConverter: raise RuntimeError
        end
        PdfConverter-->>PdfConverter: catch CalledProcessError, FileNotFoundError, RuntimeError
        PdfConverter-->>PdfConverter: return False
    end
Loading

Flow diagram for subprocess timeout and cleanup logic in PDF compilation

flowchart TD
    A[Start PDF compilation subprocess] --> B[Call process.communicate with timeout 30]
    B --> C{Did communicate finish before timeout}
    C -->|Yes| D[Read stdout and stderr]
    D --> E[Check process.returncode and output_path.exists]
    E --> F{PDF created or returncode 0}
    F -->|Yes| G[Set pdf_created True or return True]
    F -->|No| H[Handle failure and possibly try fallback]

    C -->|No, TimeoutExpired| I[Catch TimeoutExpired]
    I --> J[Call process.kill]
    J --> K[Call process.communicate again for cleanup]
    K --> L[Raise RuntimeError PDF compilation timed out]
    L --> M[Catch RuntimeError in caller]
    M --> N[Mark attempt failed and optionally check for fallback or existing PDF]
    N --> O[End]
Loading

File-Level Changes

Change Details Files
Disable LaTeX shell escape during PDF compilation to mitigate RCE risk.
  • Add -no-shell-escape flag to pdflatex invocations when compiling cover letters.
  • Add --pdf-engine-opt=-no-shell-escape to pandoc invocations that use xelatex as the PDF engine.
cli/generators/cover_letter_generator.py
cli/pdf/converter.py
Introduce subprocess timeouts and error handling to prevent DoS via hanging PDF compilations.
  • Wrap process.communicate() calls with a 30-second timeout for both pdflatex and pandoc.
  • On subprocess.TimeoutExpired, kill the process, drain stdout/stderr, and raise a RuntimeError to signal timeout.
  • Extend exception handlers to catch RuntimeError while still treating an already-created PDF as success when possible.
cli/generators/cover_letter_generator.py
cli/pdf/converter.py
Document the newly discovered PDF compilation vulnerability and mitigations in the Sentinel security log.
  • Add a new Sentinel entry describing the missing shell-escape protections and DoS vector.
  • Record the security learnings and required prevention measures for future PDF compilation code.
.jules/sentinel.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • The timeout/kill/communicate pattern is duplicated in all four subprocess calls; consider extracting a small helper (e.g., run_with_timeout(cmd, cwd=None, timeout=30)) to keep the behavior consistent and reduce maintenance overhead.
  • Catching RuntimeError alongside CalledProcessError and FileNotFoundError makes it less clear that this path is specifically handling timeouts; a dedicated exception type (or a more descriptive name) for the timeout case would make the control flow easier to understand and safer if other RuntimeErrors are introduced later.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The timeout/kill/communicate pattern is duplicated in all four subprocess calls; consider extracting a small helper (e.g., `run_with_timeout(cmd, cwd=None, timeout=30)`) to keep the behavior consistent and reduce maintenance overhead.
- Catching `RuntimeError` alongside `CalledProcessError` and `FileNotFoundError` makes it less clear that this path is specifically handling timeouts; a dedicated exception type (or a more descriptive name) for the timeout case would make the control flow easier to understand and safer if other `RuntimeError`s are introduced later.

## Individual Comments

### Comment 1
<location path="cli/generators/cover_letter_generator.py" line_range="787" />
<code_context>
             if process.returncode == 0 or output_path.exists():
                 pdf_created = True
-        except (subprocess.CalledProcessError, FileNotFoundError):
+        except (subprocess.CalledProcessError, FileNotFoundError, RuntimeError):
             # Check if PDF was created anyway
             if output_path.exists():
</code_context>
<issue_to_address>
**suggestion:** The `CalledProcessError` in this `except` is never raised by the current usage of `subprocess.Popen`.

Because this code uses `Popen` + `communicate()` rather than helpers like `check_call` or `run(check=True)`, `CalledProcessError` is never raised here. Please remove it from the `except` tuple so the handled failure modes reflect what can actually occur.

```suggestion
        except (FileNotFoundError, RuntimeError):
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click πŸ‘ or πŸ‘Ž on each comment and I'll use the feedback to improve your reviews.

if process.returncode == 0 or output_path.exists():
pdf_created = True
except (subprocess.CalledProcessError, FileNotFoundError):
except (subprocess.CalledProcessError, FileNotFoundError, RuntimeError):
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: The CalledProcessError in this except is never raised by the current usage of subprocess.Popen.

Because this code uses Popen + communicate() rather than helpers like check_call or run(check=True), CalledProcessError is never raised here. Please remove it from the except tuple so the handled failure modes reflect what can actually occur.

Suggested change
except (subprocess.CalledProcessError, FileNotFoundError, RuntimeError):
except (FileNotFoundError, RuntimeError):

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant