Skip to content

Escape the evidence media name in report media tags - #393

Merged
abrignoni merged 1 commit into
mainfrom
fix/media-tag-escaping
Aug 7, 2026
Merged

Escape the evidence media name in report media tags#393
abrignoni merged 1 commit into
mainfrom
fix/media-tag-escaping

Conversation

@abrignoni

Copy link
Copy Markdown
Owner

Why

html_media_tag() interpolated the media item's name straight into the title= attribute of the <img> it builds, and both it and media_to_html() dropped the raw evidence filename into the fallback anchor text. A crafted attachment filename closed the attribute and ran in the examiner's report (stored XSS, CWE-79).

GHSA-45q2-q93c-cfv2 fixed the artifact-side html_columns builders in July and explicitly deferred this one. It reaches every media column tool-wide — including in cores where no artifact declares html_columns by hand, because the framework adds media columns to the no-escape list itself.

What changed

safe_local_path() is added to html_safe.py. It percent-encodes a report-relative path for an href/src attribute and returns '' for anything that is not report-relative, so a crafted media name can neither point the report at a remote host nor climb out of the report folder. _is_report_relative() is factored out of safe_local_link() so both helpers apply one rule.

media_to_html() passes allow_parent=True. Its relative_paths() deliberately emits ../data/... to reach the extraction folder beside the report — part of the report layout, not an escape — so that case is permitted explicitly rather than silently.

In both functions the media name is now esc()'d in title= and in the fallback link text, the image style is esc()'d, and the malformed </> that closed the fallback anchor is now </a>.

Before / after

A media filename of x" onerror=alert(1) t=".jpg:

before: <img title="x" onerror=alert(1) t=".jpg" src=...>   <- breaks out, fires
after:  <img title="x&quot; onerror=alert(1) t=&quot;.jpg" src=...>  <- inert

The evidence is fully preserved in both cases; only the breakout is gone.

Validation

  • py_compile clean; lint_changed reports no new warnings
  • unit tests OK; PluginLoader loads every plugin
  • injection payload renders escaped with no attribute breakout
  • a <script> filename renders inert in the fallback link
  • a normal HEIC still renders its thumbnail unchanged
  • a style carrying " onload=" is escaped

🤖 Generated with Claude Code

Port of the iLEAPP change.

html_media_tag() interpolated the media item's name straight into the title=
attribute of the <img> it builds, and both it and media_to_html() dropped the
raw evidence filename into the fallback anchor text. A crafted attachment
filename closed the attribute and ran in the examiner's report (stored XSS,
CWE-79). This reaches every media column tool-wide, because the framework adds
media columns to the no-escape list itself.

safe_local_path() is added to html_safe.py: it percent-encodes a
report-relative path for an href/src attribute and returns '' for anything not
report-relative, so a crafted media name can neither point the report at a
remote host nor climb out of the report folder. _is_report_relative() is
factored out of safe_local_link() so both helpers apply one rule.

media_to_html() passes allow_parent=True, because its relative_paths()
deliberately emits ../data/... to reach the extraction folder beside the
report. That case is now permitted explicitly instead of silently.

The media name is esc()'d in title= and in the fallback link text, the image
style is esc()'d, and the malformed </> closing the fallback anchor is </a>.

Validated: py_compile clean; unit tests OK; PluginLoader loads every plugin; a
filename of x" onerror=alert(1) t=".jpg renders escaped with no attribute
breakout, and a normal HEIC still renders its thumbnail unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@abrignoni
abrignoni merged commit 39f32f0 into main Aug 7, 2026
4 checks passed
@abrignoni
abrignoni deleted the fix/media-tag-escaping branch August 7, 2026 01:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant