Files
searxng/searx/templates/simple/result_templates/map.html
Renaud Allard 23fb76f08f Fix XSS via unsafe rendering of untrusted external data in templates (#5826)
Remove |safe filter from 6 template locations where data from external
search engine APIs was rendered as raw HTML without sanitization. Jinja2
autoescape now properly escapes these fields.

The |safe filter was originally added in commit 213041adc (March 2021)
by copying the pattern from result.title|safe and result.content|safe.
However, title and content are pre-escaped via escape() in webapp.py
lines 704-706 before highlight_content() adds trusted <span> tags for
search term highlighting. The metadata, info.value, link.url_label,
repository, and filename fields never go through any escaping and flow
directly from external API responses to the template.

Affected templates and their untrusted data sources:
- macros.html: result.metadata from DuckDuckGo, Reuters, Presearch,
  Podcast Index, Fyyd, bpb, moviepilot, mediawiki, and others
- paper.html: result.metadata from academic search engines
- map.html: info.value and link.url_label from OpenStreetMap
  user-contributed extratags
- code.html: result.repository and result.filename from GitHub API

Example exploit: a search engine API returning
metadata='<img src=x onerror=alert(document.cookie)>' would execute
arbitrary JavaScript in every user's browser viewing that result.
2026-03-13 13:28:31 +01:00

53 lines
2.7 KiB
HTML

{% from 'simple/macros.html' import result_header, result_sub_header, result_sub_footer, result_footer with context %}
{% from 'simple/icons.html' import icon_small %}
{{ result_header(result, favicons, image_proxify) -}}
{{- result_sub_header(result) -}}
{%- if result.content %}<p class="content">{{ result.content|safe }}</p>{% endif -%}
<table>
{%- if result.address -%}
<tr>
<th scope="row">{{ result.address_label or _('address') }}</th>
<td itemscope itemtype="http://schema.org/PostalAddress">
{%- if result.address.name -%}
<strong itemprop="name" class="hidden">{{ result.address.name }}</strong>
{%- endif -%}
{% if result.address.road -%}
<span itemprop="streetAddress">
{%- if result.address.house_number -%}{{- result.address.house_number -}}, {% endif %}
{{- result.address.road -}}
</span><br>
{%- endif %}
{%- if result.address.locality -%}
<span itemprop="addressLocality">{{- result.address.locality -}}</span>
{%- if result.address.postcode -%}, <span itemprop="postalCode">{{- result.address.postcode -}}</span>{% endif %}
<br>
{%- endif -%}
{%- if result.address.country -%}
<span itemprop="addressCountry">{{- result.address.country -}}</span>
{%- endif -%}
</td>
</tr>
{%- endif %}
{%- for info in result.data -%}
<tr><th scope="row">{{ info.label }}</th><td>{{ info.value }}</td></tr>
{%- endfor -%}
{%- for link in result.links -%}
<tr><th scope="row">{{ link.label }}</th><td><a class="text-info cursor-pointer" href="{{ link.url }}">{{ link.url_label }}</a></td></tr>
{%- endfor -%}
</table>
{%- if (result.latitude and result.longitude) or result.boundingbox -%}
<small> <a class="btn-collapse collapsed searxng_init_map hide_if_nojs" data-target="#result-map-{{ index }}" data-btn-text-collapsed="{{ _('show map') }}" data-btn-text-not-collapsed="{{ _('hide map') }}" data-leaflet-target="osm-map-{{ index }}" data-map-lon="{{ result.longitude }}" data-map-lat="{{ result.latitude }}" {% if result.boundingbox %}data-map-boundingbox='{{ result.boundingbox|tojson|safe }}'{% endif %} {% if result.geojson %}data-map-geojson='{{ result.geojson|tojson|safe }}'{% endif %}>{{ icon_small( 'globe') }} {{ _('show map') }}</a></small>
{%- endif -%}
{{- result_sub_footer(result) -}}
{% if (result.latitude and result.longitude) or result.boundingbox -%}
<div id="result-map-{{ index }}" class="invisible"><div id="osm-map-{{ index }}" class="osm-map-box"></div></div>
{%- endif %}
{{- result_footer(result) }}