Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
## 2024-06-05 - Fix XSS in Manual HTML F-String Generation
**Vulnerability:** A Cross-Site Scripting (XSS) vulnerability was found in the `backend/app/services/report_service.py` where manual string interpolations built the HTML report, leaving dynamic and user-provided properties like `job_id`, `data_source` and exceptions unsanitized.
**Learning:** `html` string building using f-strings inside a service function without an HTML templating engine leaves the application highly vulnerable to XSS. Also, when renaming the `html` string variable to `html_content`, we avoided a classic python shadowing issue with the `html` module which would have raised `UnboundLocalError`. Furthermore, truncation must occur *before* HTML escaping (`html.escape(str(job_id)[:12])`) to avoid splitting an HTML entity into malformed HTML.
**Prevention:** Avoid building raw HTML templates with f-strings, or if necessary, ensure every user input or dynamic string is systematically wrapped in `html.escape()`.
53 changes: 27 additions & 26 deletions backend/app/services/report_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"""
from __future__ import annotations

import html
from datetime import datetime
from typing import Any, Optional

Expand Down Expand Up @@ -216,17 +217,17 @@ def generate_html_report(
for a in (alerts or [])[:25]:
alert_rows += f"""
<tr>
<td class="font-mono">{a.get('frame_index', '—')}</td>
<td>{str(a.get('type', '—')).replace('_', ' ').capitalize()}</td>
<td class="font-mono">{html.escape(str(a.get('frame_index', '—')))}</td>
<td>{html.escape(str(a.get('type', '—')).replace('_', ' ').capitalize())}</td>
<td>{_sev_badge(a.get('severity', 'low'))}</td>
<td style="color: #444444;">{a.get('description', '—')[:140]}</td>
<td style="color: #444444;">{html.escape(str(a.get('description', '—')[:140]))}</td>
</tr>"""

traj_rows = ""
for t in (trajectories or [])[:15]:
traj_rows += f"""
<tr>
<td class="font-mono">{t.get('id', '—')}</td>
<td class="font-mono">{html.escape(str(t.get('id', '—')))}</td>
<td class="font-mono">{t.get('speed', 0):.5f}</td>
<td class="font-mono">{t.get('direction_deg', 0):.1f}&deg;</td>
<td class="font-mono">{t.get('intensity', 0):.4f}</td>
Expand All @@ -236,8 +237,8 @@ def generate_html_report(
for iss in (consistency_issues or [])[:20]:
issue_rows += f"""
<tr>
<td class="font-mono">{iss.get('frame', '—')}</td>
<td>{iss.get('issue', '—')}</td>
<td class="font-mono">{html.escape(str(iss.get('frame', '—')))}</td>
<td>{html.escape(str(iss.get('issue', '—')))}</td>
<td>{_sev_badge(iss.get('severity', 'low'))}</td>
<td class="font-mono">{iss.get('mad_score', '—')}</td>
</tr>"""
Expand All @@ -252,12 +253,12 @@ def generate_html_report(
frame_stats["by_confidence"]

# Build the comprehensive HTML report
html = f"""<!DOCTYPE html>
html_content = f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>AetherGIS Technical Report — {job_id[:12]}</title>
<title>AetherGIS Technical Report — {html.escape(str(job_id)[:12])}</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Barlow:wght@400;500;600&family=Barlow+Condensed:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
Expand Down Expand Up @@ -719,11 +720,11 @@ def generate_html_report(
<p class="report-subtitle">Satellite Imagery Frame Interpolation Pipeline — Technical Documentation</p>

<div class="report-meta">
<span><span class="meta-label">Job ID:</span> {job_id}</span>
<span><span class="meta-label">Layer:</span> {layer_id}</span>
<span><span class="meta-label">Job ID:</span> {html.escape(str(job_id))}</span>
<span><span class="meta-label">Layer:</span> {html.escape(str(layer_id))}</span>
<span><span class="meta-label">Status:</span>
<span class="status-badge {'status-success' if status == 'completed' else 'status-error' if error_msg else 'status-warning'}">
<span class="status-dot"></span>{status.upper()}
<span class="status-dot"></span>{html.escape(str(status).upper())}
</span>
</span>
<span><span class="meta-label">Generated:</span> {now}</span>
Expand All @@ -742,7 +743,7 @@ def generate_html_report(
<p>
This report documents the execution of the AetherGIS temporal interpolation pipeline
for satellite imagery sequence generation. The pipeline processed <strong>{n_obs} observed frames</strong>
from {data_source} source data, generating <strong>{n_interp} AI-interpolated intermediate frames</strong>
from {html.escape(str(data_source))} source data, generating <strong>{n_interp} AI-interpolated intermediate frames</strong>
for temporal gap filling. Total output sequence comprises <strong>{n_total} frames</strong>.
{" Execution completed with errors." if error_msg else f" Execution completed successfully in {duration}."}
</p>
Expand Down Expand Up @@ -788,19 +789,19 @@ def generate_html_report(
<div class="info-grid">
<div class="info-row">
<div class="info-label">Job ID</div>
<div class="info-value">{job_id}</div>
<div class="info-value">{html.escape(str(job_id))}</div>
</div>
<div class="info-row">
<div class="info-label">Data Source</div>
<div class="info-value">{data_source}</div>
<div class="info-value">{html.escape(str(data_source))}</div>
</div>
<div class="info-row">
<div class="info-label">Layer ID</div>
<div class="info-value">{layer_id}</div>
<div class="info-value">{html.escape(str(layer_id))}</div>
</div>
<div class="info-row">
<div class="info-label">Status</div>
<div class="info-value">{status.upper()}</div>
<div class="info-value">{html.escape(str(status).upper())}</div>
</div>
<div class="info-row">
<div class="info-label">Created At</div>
Expand All @@ -821,7 +822,7 @@ def generate_html_report(
</div>

{f'''<div class="note-box warning">
<strong>Execution Error:</strong> This pipeline run encountered an error during execution: {error_msg}
<strong>Execution Error:</strong> This pipeline run encountered an error during execution: {html.escape(str(error_msg))}
</div>''' if error_msg else ''}
</div>

Expand Down Expand Up @@ -973,31 +974,31 @@ def generate_html_report(
<h3 class="section-subtitle">Video Sequences</h3>
<ul class="artifact-list">
<li>
<span class="artifact-path">/exports/{job_id}/original.mp4</span>
<span class="artifact-path">/exports/{html.escape(str(job_id))}/original.mp4</span>
<span class="artifact-desc">Original observed frame sequence (no interpolation)</span>
</li>
<li>
<span class="artifact-path">/exports/{job_id}/interpolated.mp4</span>
<span class="artifact-path">/exports/{html.escape(str(job_id))}/interpolated.mp4</span>
<span class="artifact-desc">Full interpolated sequence (observed + AI frames)</span>
</li>
</ul>

<h3 class="section-subtitle">Frame Archive</h3>
<ul class="artifact-list">
<li>
<span class="artifact-path">/exports/{job_id}/frames/frame_*.png</span>
<span class="artifact-path">/exports/{html.escape(str(job_id))}/frames/frame_*.png</span>
<span class="artifact-desc">Individual frame images ({n_total} frames, PNG format)</span>
</li>
</ul>

<h3 class="section-subtitle">Metadata & Documentation</h3>
<ul class="artifact-list">
<li>
<span class="artifact-path">/exports/{job_id}/metadata.json</span>
<span class="artifact-path">/exports/{html.escape(str(job_id))}/metadata.json</span>
<span class="artifact-desc">Complete frame metadata with per-frame metrics</span>
</li>
<li>
<span class="artifact-path">/exports/{job_id}/report.html</span>
<span class="artifact-path">/exports/{html.escape(str(job_id))}/report.html</span>
<span class="artifact-desc">This technical analysis report</span>
</li>
</ul>
Expand Down Expand Up @@ -1100,7 +1101,7 @@ def generate_html_report(
<h3 class="section-subtitle">Traceability Statement</h3>
<p style="font-size: 11px; color: var(--t3); line-height: 1.6;">
This report was auto-generated by AetherGIS v2.0 pipeline system. All metrics
are computed from the actual execution artifacts stored at <code>/exports/{job_id}/</code>.
are computed from the actual execution artifacts stored at <code>/exports/{html.escape(str(job_id))}/</code>.
Frame-level metadata includes: source timestamp, interpolation model used,
PSNR/SSIM scores (for interpolated frames), confidence classification, and gap
category. In case of database record loss, results can be fully reconstructed
Expand All @@ -1118,13 +1119,13 @@ def generate_html_report(
<strong>Primary Source:</strong> NASA GIBS Earthdata API (Global Imagery Browse Services)<br>
<strong>Interpolation Engine:</strong> AetherGIS v2.0 with RIFE/FILM optical flow models<br>
<strong>Processing Location:</strong> AetherGIS Analysis Pipeline (Module 15)<br>
<strong>Report ID:</strong> RPT-{job_id[:12]}-{datetime.utcnow().strftime('%Y%m%d')}
<strong>Report ID:</strong> RPT-{html.escape(str(job_id)[:12])}-{datetime.utcnow().strftime('%Y%m%d')}
</p>
</div>

<div class="footer-meta">
<span>AetherGIS Technical Report</span>
<span>Job: {job_id[:16]}</span>
<span>Job: {html.escape(str(job_id)[:16])}</span>
<span>Generated: {now}</span>
</div>
</div>
Expand All @@ -1133,4 +1134,4 @@ def generate_html_report(
</body>
</html>"""

return html
return html_content