Skip to content
Merged
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
61 changes: 44 additions & 17 deletions scripts/html_safe.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,27 @@
the examiner's report (stored XSS, CWE-79).

Route every evidence-derived value in an ``html_columns`` cell through these helpers so
the dynamic parts are escaped while the tool's own structural markup (``<a>``, ``<br>``,
the dynamic parts are escaped while the tool's own structural markup (``<br>``,
``<table>``) is preserved.

**A report links to nothing outside its own folder.** A remote destination in a
report is a disclosure channel: an ``<img src="https://...">`` is fetched the moment
the report is opened, with no click, which tells whoever controls that host that the
account is under examination, when, and from which IP address. An ``<a href>`` needs a
click but reaches the same place. Reports are read on analyst workstations and mailed
to counsel, so no ``href`` or ``src`` may leave the report folder -- not ``http``,
``https``, ``ftp``, and not ``mailto``/``tel``, which hand the subject's own address or
number to a mail client or dialer.

Evidence URLs are still *evidence*, so nothing is dropped: they are rendered as escaped
text the examiner can read and copy. Only the anchor goes away. Report-relative
destinations -- ``media/<file>`` thumbnails, files an artifact writes beside the report
-- stay clickable through ``safe_local_link()``; they resolve offline and reach nothing.
"""

import html
from urllib.parse import urlparse

# Schemes we are willing to turn into a live <a href>. Anything else (notably
# javascript: and data:) is rendered as escaped text, never as a clickable link.
ALLOWED_URL_SCHEMES = ('http', 'https', 'mailto', 'tel', 'ftp', 'ftps')


def esc(value):
"""HTML-escape a single evidence value for safe use in text or an attribute.
Expand All @@ -31,25 +41,42 @@ def esc(value):
return html.escape(str(value), quote=True)


def safe_url(url, text=None, target='_blank'):
"""Build an ``<a href>`` anchor with an escaped, scheme-checked href.
def safe_url(url, text=None, target=None): # pylint: disable=unused-argument
"""Render an evidence URL as escaped text. **Never returns a link.**

A URL read out of an extraction names a host chosen by whoever wrote the data, so
it is exactly the destination a report must not reach; see the module docstring.
The URL itself is evidence and is preserved verbatim as escaped text, so an
examiner can read it, copy it, and open it deliberately elsewhere.

Returns the escaped visible text with **no** anchor when the URL is empty or its
scheme is not allowlisted, so ``javascript:``/``data:`` URLs never become live
links. ``text`` (the visible label) defaults to the URL itself.
``text`` overrides the visible string when the caller has a better label than the
raw URL. ``target`` is accepted and ignored: it exists so the call sites that
passed it before this became text-only keep working.
"""
url = '' if url is None else str(url).strip()
label = esc(text if text is not None else url)
if not url:
return esc(text if text is not None else url)


def safe_local_link(path, text=None):
"""Build an ``<a href>`` to a file inside the report folder.

This is the only helper that still emits an anchor. The destination must be
report-relative -- no scheme, no protocol-relative ``//host``, no absolute path,
and no ``..`` escaping the folder -- so following it can never leave the report.
Anything else is returned as escaped text with no anchor.
"""
path = '' if path is None else str(path).strip()
label = esc(text if text is not None else path)
if not path:
return label
if path.startswith(('/', '\\', '//')) or '..' in path.replace('\\', '/').split('/'):
return label
try:
scheme = urlparse(url).scheme.lower()
if urlparse(path).scheme:
return label
except ValueError:
return label
if scheme not in ALLOWED_URL_SCHEMES:
return label
rel = ' rel="noopener noreferrer"' if target == '_blank' else ''
return f'<a href="{esc(url)}" target="{esc(target)}"{rel}>{label}</a>'
return f'<a href="{esc(path)}" target="_blank">{label}</a>'


def safe_join(values, sep='<br>'):
Expand Down
Loading