Merge commit from fork

This commit is contained in:
Soxoj
2026-06-17 15:15:32 +02:00
committed by GitHub
parent 06850e50c8
commit f2e4f8d3a2
4 changed files with 124 additions and 3 deletions
+3 -1
View File
@@ -436,7 +436,9 @@ def generate_report_template(is_pdf: bool):
template_content = get_resource_content("simple_report.tpl")
css_content = None
template = Template(template_content)
# autoescape: report data comes from scanned profiles and must be escaped
# to avoid XSS in the generated report.
template = Template(template_content, autoescape=True)
template.globals["title"] = CaseConverter.snake_to_title # type: ignore
template.globals["detect_link"] = enrich_link_str # type: ignore
return template, css_content
+6 -1
View File
@@ -6,6 +6,8 @@ import random
import string
from typing import Any
from markupsafe import Markup, escape
DEFAULT_USER_AGENTS = [
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36",
@@ -38,7 +40,10 @@ def is_country_tag(tag: str) -> bool:
def enrich_link_str(link: str) -> str:
link = link.strip()
if link.startswith("www.") or (link.startswith("http") and "//" in link):
return f'<a class="auto-link" href="{link}">{link}</a>'
# escape the link to avoid XSS; Markup keeps the <a> tag itself intact
# under the template's autoescaping.
safe = escape(link)
return Markup(f'<a class="auto-link" href="{safe}">{safe}</a>')
return link
+92 -1
View File
@@ -11,6 +11,7 @@ from io import StringIO
import xmind # type: ignore[import-untyped]
from jinja2 import Template
from markupsafe import escape
from maigret.report import (
filter_supposed_data,
@@ -416,11 +417,101 @@ def test_html_report():
report_text = open(report_name).read()
assert SUPPOSED_BRIEF in report_text
# the HTML report escapes its context, so the brief is rendered with
# HTML entities (e.g. the apostrophe in "target's")
assert str(escape(SUPPOSED_BRIEF)) in report_text
assert SUPPOSED_GEO in report_text
assert SUPPOSED_INTERESTS in report_text
# profile data from scanned sites must be escaped so a planted payload cannot
# execute in the report
XSS_NAME_PAYLOAD = '<img src=x onerror=alert(document.domain)>'
XSS_IMAGE_PAYLOAD = 'x" onerror="alert(1)'
XSS_LINK_PAYLOAD = 'http://evil.example/"><script>alert(1)</script>'
def _xss_username_results():
result = copy.deepcopy(GOOD_RESULT)
result.tags = ['photo', 'us']
result.ids_data = {
"name": XSS_NAME_PAYLOAD,
"bio": XSS_NAME_PAYLOAD,
"image": XSS_IMAGE_PAYLOAD,
"external_url": XSS_LINK_PAYLOAD,
}
data = {
'EvilSite': {
'username': 'victimtarget',
'parsing_enabled': True,
'url_main': 'https://evil.example/',
'url_user': 'https://evil.example/victimtarget',
'status': result,
'http_status': 200,
'is_similar': False,
'rank': 1,
'site': MaigretSite('EvilSite', {}),
'found': True,
'ids_data': result.ids_data,
},
}
return [('victimtarget', 'username', data)]
def _assert_no_xss(rendered: str):
# no executable payload markup survives, only escaped (harmless) text
assert XSS_NAME_PAYLOAD not in rendered
assert '<img src=x onerror' not in rendered
assert '<script>alert(1)</script>' not in rendered
assert 'onerror="alert(1)"' not in rendered # image attribute breakout
assert '&lt;img src=x onerror=alert(document.domain)&gt;' in rendered
def test_html_report_escapes_extracted_profile_data():
context = generate_report_context(_xss_username_results())
template, _ = generate_report_template(is_pdf=False)
rendered = template.render(**context)
_assert_no_xss(rendered)
def test_pdf_report_escapes_extracted_profile_data():
context = generate_report_context(_xss_username_results())
template, _ = generate_report_template(is_pdf=True)
rendered = template.render(**context)
_assert_no_xss(rendered)
def test_report_preserves_legit_auto_link():
# A benign extracted link must still render as a real, clickable anchor.
result = copy.deepcopy(GOOD_RESULT)
result.ids_data = {"external_url": "https://example.com/profile"}
data = {
'Site': {
'username': 'u',
'parsing_enabled': True,
'url_main': 'https://example.com/',
'url_user': 'https://example.com/u',
'status': result,
'http_status': 200,
'is_similar': False,
'rank': 1,
'site': MaigretSite('Site', {}),
'found': True,
'ids_data': result.ids_data,
},
}
context = generate_report_context([('u', 'username', data)])
template, _ = generate_report_template(is_pdf=False)
rendered = template.render(**context)
assert (
'<a class="auto-link" href="https://example.com/profile">'
'https://example.com/profile</a>'
) in rendered
def test_html_report_broken():
report_name = 'report_test_broken.html'
BROKEN_DATA = copy.deepcopy(TEST)
+23
View File
@@ -3,6 +3,8 @@
import itertools
import re
from markupsafe import Markup
from maigret.utils import (
CaseConverter,
is_country_tag,
@@ -76,6 +78,27 @@ def test_enrich_link_str():
)
def test_enrich_link_str_escapes_payload():
# markup inside a link must be escaped while the <a> wrapper is preserved
payload = 'http://evil.example/"><img src=x onerror=alert(1)>'
result = enrich_link_str(payload)
assert isinstance(result, Markup)
assert '<img' not in result
assert '&lt;img' in result
assert '"><img' not in result
assert result.startswith('<a class="auto-link" href="')
def test_enrich_link_str_non_link_is_plain_str():
# non-link values stay plain str so template autoescaping neutralizes them
payload = '<script>alert(1)</script>'
result = enrich_link_str(payload)
assert not isinstance(result, Markup)
assert result == payload
def test_url_extract_main_part_negative():
url_main_part = 'None'
assert URLMatcher.extract_main_part(url_main_part) == ''