fix(message): render received external SVG images - #13142
Conversation
External images are fetched through the image proxy and served with a generic application/octet-stream content type. Browsers sniff raster formats in <img> tags, but they refuse to render SVG unless it is served as image/svg+xml, so SVG logos stayed blank and only the alt text and placeholder were shown. Detect SVG markup in the proxied response and serve it as a sanitised image/svg+xml document. A new SvgSanitizer service strips active content (scripts, event handlers, external/javascript references) and rejects DOCTYPE/entity declarations as XXE defence in depth, even though the <img> context does not execute scripts. Assisted-by: Claude:claude-opus-4-8 Signed-off-by: Joël de Jager <contact@joeldejager.nl>
|
Thanks for the contribution |
|
@coderabbitai review and scan for new vulnerabilities |
🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Get the list of changed files in this PR
gh pr view 13142 --repo nextcloud/mail --json files --jq '.files[].path'Length of output: 416 🏁 Script executed: #!/bin/bash
# Read the new SvgSanitizer and any modified proxy/controller files
fd -e php . lib/ --exec echo {} \; | grep -i 'svg\|proxy\|image' | sort
cat -n lib/Service/SvgSanitizer.php 2>/dev/null || echo "NOT FOUND"Length of output: 4248 🏁 Script executed: #!/bin/bash
# Find and read all relevant new/changed files
fd -e php . lib/ | xargs grep -l -i 'svg\|SvgSanitizer' 2>/dev/null | sort
# Also find proxy-related files
fd -e php . lib/ | xargs grep -l -i 'proxy\|imageproxy\|image_proxy' 2>/dev/null | sortLength of output: 730 🏁 Script executed: #!/bin/bash
cat -n lib/Controller/ProxyController.phpLength of output: 6202 🏁 Script executed: #!/bin/bash
cat -n tests/Unit/Service/SvgSanitizerTest.php
cat -n tests/Unit/Controller/ProxyControllerTest.phpLength of output: 14218
🔴 Security findings1.
|
| Concern | Status |
|---|---|
| HMAC validation before proxying | ✅ |
LIBXML_NONET prevents network fetch during parse |
✅ |
DOCTYPE / <!ENTITY pre-rejected by regex |
✅ |
on* event-handler attributes stripped |
✅ |
Namespace-aware element removal (local-name()) |
✅ |
External href / xlink:href stripped |
✅ |
Rate-limiting (#[UserRateLimit]) |
✅ |
LocalServerException caught (SSRF guard) |
✅ |
| Test coverage for sanitizer + controller | ✅ |
✅ Action performed
Review finished.
Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.
📝 WalkthroughWalkthroughA new ChangesSVG Sanitization Feature
Estimated code review effort: 3 (Moderate) | ~25 minutes Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 7
🧹 Nitpick comments (1)
lib/Controller/ProxyController.php (1)
137-146: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winMove SVG detection heuristic out of the controller.
looksLikeSvg()is business logic and makesProxyControllerless of a thin HTTP handler. Consider moving this heuristic intoSvgSanitizer(or a dedicated detector service) and call it from here.As per coding guidelines,
lib/Controller/**/*.php: “Controllers must be thin HTTP handlers; business logic must live in services”.Source: Coding guidelines
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 26c52dd2-524b-4c89-9e7f-305d15c812e0
📒 Files selected for processing (4)
lib/Controller/ProxyController.phplib/Service/SvgSanitizer.phptests/Unit/Controller/ProxyControllerTest.phptests/Unit/Service/SvgSanitizerTest.php
|
Yes |
- Extend URL attribute stripping to cover src, action, formaction - Strip external CSS url() references from <style> elements and style="" attributes while preserving fragment references (url(#...)) - Reject SVG payloads exceeding 2 MiB before DOM parsing (DoS guard) - Fall back to blocked-image.png instead of serving empty body when sanitizer rejects an SVG document - Drop misleading <!-- branch from looksLikeSvg() heuristic - Move looksLikeSvg() from ProxyController into SvgSanitizer service Assisted-by: Claude:claude-sonnet-4-6 Signed-off-by: Joël de Jager <contact@joeldejager.nl>
|
The recommended changes have been implemented. |
|
@coderabbitai re-review |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
cat -n lib/Service/SvgSanitizer.php
echo "---"
cat -n lib/Controller/ProxyController.phpLength of output: 11368
The implementation is clean and all security measures are in place. [approve] |
|
Hello there, We hope that the review process is going smooth and is helpful for you. We want to ensure your pull request is reviewed to your satisfaction. If you have a moment, our community management team would very much appreciate your feedback on your experience with this PR review process. Your feedback is valuable to us as we continuously strive to improve our community developer experience. Please take a moment to complete our short survey by clicking on the following link: https://cloud.nextcloud.com/apps/forms/s/i9Ago4EQRZ7TWxjfmeEpPkf6 Thank you for contributing to Nextcloud and we hope to hear from you soon! (If you believe you should not receive this message, you can add yourself to the blocklist.) |
|
The branch needs an update because there is a conflict |
Signed-off-by: Joël de Jager <contact@joeldejager.nl>
Assisted-by: Codex:gpt-5 Signed-off-by: Joël de Jager <contact@joeldejager.nl>
…oeldj-nl/ncmail into fix/render-received-svg-images
Updated! |
Assisted-by: Codex:gpt-5 Signed-off-by: Joël de Jager <contact@joeldejager.nl>
|
@coderabbitai review again |
|
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Actionable comments posted: 3
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 537ec8a0-7baa-4b21-b7c6-209350983019
📒 Files selected for processing (1)
lib/Controller/ProxyController.php
Assisted-by: Codex:gpt-5 Signed-off-by: Joël de Jager <contact@joeldejager.nl>
|
@ChristophWurst the new findings by CodeRabbitAI have been resolved. |
|
Thanks for your pr 👍 We reject SVG's with XLS/Transform in server (c.f. nextcloud/server#59835) and should apply the same hardening here. |
Signed-off-by: Joël de Jager <contact@joeldejager.nl>
|
@kesselb thanks for your input. This should be fixed now. |
There was a problem hiding this comment.
Pull request overview
This PR fixes external SVG images not rendering in proxied email content by detecting SVG payloads and serving them as image/svg+xml, adding an SVG sanitization layer to reduce XSS/XXE risk when proxying untrusted markup.
Changes:
- Add
SvgSanitizerservice to detect SVG markup and strip active/dangerous content. - Update
ProxyControllerto read streamed proxy responses safely and to sanitize/serve SVG with the correct content type. - Add unit tests for the sanitizer and for proxying sanitized SVG responses.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
lib/Controller/ProxyController.php |
Reads stream bodies, detects SVG responses, sanitizes them, and serves with image/svg+xml (fallbacks to blocked image on unsafe SVG). |
lib/Service/SvgSanitizer.php |
New sanitizer that parses SVG with libxml hardening and strips scripts/event handlers/external references. |
tests/Unit/Controller/ProxyControllerTest.php |
Adds test coverage for streamed bodies and SVG sanitization/content-type behavior in the proxy. |
tests/Unit/Service/SvgSanitizerTest.php |
Adds unit tests for sanitizer security rules and SVG detection heuristics. |
| private function stripCssUrls(string $css): string { | ||
| return preg_replace('/url\s*\((?!\s*[\'"]?#)[^)]*\)/i', 'none', $css) ?? $css; | ||
| } |
|
|
||
| $xpath = new DOMXPath($dom); | ||
|
|
||
| // Remove processing instructions (e.g. <?xml-stylesheet type="text/xsl"?>). |
There was a problem hiding this comment.
The ?> ends php and make the linter fails.
| * parsed safely | ||
| */ | ||
| public function sanitize(string $svg): string { | ||
| if (trim($svg) === '' || strlen($svg) > self::MAX_SVG_BYTES) { |
There was a problem hiding this comment.
Please take a look at nextcloud/server#62162 and check if we have those cases covered.
| @@ -95,17 +101,49 @@ public function proxy(string $src, ?int $id, ?string $hmac): Response { | |||
| try { | |||
| $response = $client->get($src); | |||
| $content = $response->getBody(); | |||
There was a problem hiding this comment.
$content is only a resource with a call like $client->get($src, ['stream' => true]). The actual return for the current request is string.
Please drop is_resource fallback.
To make static code analysis happy add:
// Not a stream request, so the body is a string, never a resource.
assert(is_string($content));
Summary
Received emails containing external SVG images did not render. The message showed only the broken-image placeholder and the image's alt text or name. External images are fetched through the image proxy, which served every response as
tag regardless of content type, but they refuse to render SVG unless it is served as
application/octet-stream. Browsers happily sniff raster formats (PNG/JPEG/GIF) out of animage/svg+xml, so SVG logos and graphics stayed blank.Changes
image/svg+xmlcontent type so browsers render it.SvgSanitizerservice that strips active content (scripts, event handlers, external/javascript: references) and rejects DOCTYPE/entity declarations, as defence in depth against XXE and stored XSS even though theThe first screenshot shows the issue; the second screenshot shows it has been resolved.
Assisted-by: Claude:claude-opus-4-8
Summary by CodeRabbit
Release Notes
New Features
image/svg+xmlcontent type; unsafe/invalid content is replaced with a blocked-image fallback.Bug Fixes
Tests