daaain / daaain/claude-code-log
Stored XSS: unescaped image media_type/data in embedded <img src> (user/assistant path)
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 1.2k
- Forks
- 98
- Avg merge
- 3d 2h
- Merged PRs (30d)
- 13
Description
Summary
The embedded-image rendering path interpolates the image's media_type and base64 data straight into an <img src="..."> attribute without HTML-escaping, base64 validation, or a media-type allowlist. These fields come from transcript JSON and can carry content that did not originate from the user (e.g. images returned by tools/MCP servers, fetched from web pages, or IDE/file attachments), so a crafted media_type can break out of the src attribute and inject a live <script> into the generated HTML — stored XSS that executes when the file is opened in a browser under file:// (local-file read / exfiltration).
Affected code
claude_code_log/image_export.py:43builds the data URL:return f"data:{image.source.media_type};base64,{image.source.data}"- Sinks that embed it with no escaping:
claude_code_log/html/assistant_formatters.py:111claude_code_log/html/renderer.py:476
return f'<img src="{src}" alt="image" class="uploaded-image" />' ImageSource.media_typeand.dataare plain unvalidatedstr(claude_code_log/models.py:110-111), parsed directly from JSON.
embedded is the default HTML image mode, so this is the normal path.
Reproduction
A user-turn image content block with:
media_type = png"><script>alert(document.domain)</script>
renders as:
<img src="data:png"><script>alert(document.domain)</script>;base64,AAAA" ... >
The "> closes the src attribute and the <img> tag; the <script> then executes on open. A " in data would do the same.
Why this is an oversight, not accepted risk
The tool-result image path already does the right thing at claude_code_log/html/tool_formatters.py:1479-1500:
- allowlists
media_typeto{image/png, image/jpeg, image/gif, image/webp}(excludes scriptableimage/svg+xml), - validates the base64 with
base64.b64decode(data, validate=True), - and wraps the final URL in
escape_html(data_url).
The user/assistant embedded-image path does none of the three.
Suggested fix
Centralise a hardened data-URL builder (e.g. in image_export.py) applying the same three guards as the tool-result path, and route both <img> sinks (and ideally the tool-result path) through it:
- allowlist
media_type(png/jpeg/gif/webp), base64.b64decode(validate=True)thedata,escape_html(...)the finalsrc.
Add a regression test for the media_type = 'png"><script>...' breakout and for the data quote case.
Severity
Stored XSS in a file:// context against a page rendered from the user's private logs. High.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with claude_code_log/image_export.py and compare its data-URL construction with the validated tool-result path in claude_code_log/html/tool_formatters.py. Inspect the embedded-image sinks in html/assistant_formatters.py and html/renderer.py, then add regression coverage for quoted media_type and data values. Done means both embedded paths reject invalid image data or types and produce safely escaped image URLs.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- html, python
- Domain
- security
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 72/100