diff --git a/scripts/html_safe.py b/scripts/html_safe.py index 4563256..40768a0 100644 --- a/scripts/html_safe.py +++ b/scripts/html_safe.py @@ -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 (````, ``
``, +the dynamic parts are escaped while the tool's own structural markup (``
``, ````) is preserved. + +**A report links to nothing outside its own folder.** A remote destination in a +report is a disclosure channel: an ```` 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 ```` 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/`` 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 . 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. @@ -31,25 +41,42 @@ def esc(value): return html.escape(str(value), quote=True) -def safe_url(url, text=None, target='_blank'): - """Build an ```` 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 ```` 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'{label}' + return f'{label}' def safe_join(values, sep='
'):