mirror of
https://github.com/soxoj/maigret.git
synced 2026-08-17 19:25:41 +02:00
Merge commit from fork
This commit is contained in:
+3
-1
@@ -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
@@ -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
@@ -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 '<img src=x onerror=alert(document.domain)>' 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)
|
||||
|
||||
@@ -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 '<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) == ''
|
||||
|
||||
Reference in New Issue
Block a user