Skip to content

Commit f41c332

Browse files
authored
[5.x] Sanitize SVGs (#14077)
1 parent f8094c4 commit f41c332

4 files changed

Lines changed: 29 additions & 9 deletions

File tree

resources/js/components/SvgIcon.vue

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
<script>
66
import { defineAsyncComponent } from 'vue';
77
import { data_get } from '../bootstrap/globals.js'
8+
import DOMPurify from 'dompurify';
89
910
export default {
1011
@@ -53,13 +54,13 @@ export default {
5354
evaluateIcon() {
5455
if (this.customIcon) {
5556
return defineAsyncComponent(() => {
56-
return new Promise(resolve => resolve({ template: this.customIcon }));
57+
return new Promise(resolve => resolve({ template: DOMPurify.sanitize(this.customIcon) }));
5758
});
5859
}
5960
6061
if (this.name.startsWith('<svg')) {
6162
return defineAsyncComponent(() => {
62-
return new Promise(resolve => resolve({ template: this.name }));
63+
return new Promise(resolve => resolve({ template: DOMPurify.sanitize(this.name) }));
6364
});
6465
}
6566

resources/js/components/nav/Branch.vue

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
<div class="flex items-center flex-1 p-2 rtl:mr-2 ltr:ml-2 text-xs leading-normal">
66
<div class="flex items-center flex-1" :class="{ 'opacity-50': isHidden || isInHiddenSection }">
77
<template v-if="! isSection && ! isChild">
8-
<i v-if="isAlreadySvg" class="w-4 h-4 rtl:ml-2 ltr:mr-2" v-html="icon"></i>
9-
<svg-icon v-else class="w-4 h-4 rtl:ml-2 ltr:mr-2" :name="'light/'+icon" />
8+
<svg-icon
9+
class="w-4 h-4 rtl:ml-2 ltr:mr-2"
10+
:name="isAlreadySvg ? icon : 'light/'+icon"
11+
/>
1012
</template>
1113

1214
<a

src/CP/Navigation/NavItem.php

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Statamic\CP\Navigation;
44

55
use Illuminate\Support\Collection;
6+
use Rhukster\DomSanitizer\DOMSanitizer;
67
use Statamic\Facades\CP\Nav;
78
use Statamic\Facades\URL;
89
use Statamic\Statamic;
@@ -200,7 +201,22 @@ public function svg()
200201
{
201202
$value = $this->icon() ?? 'entries';
202203

203-
return Str::startsWith($value, '<svg') ? $value : Statamic::svg('icons/light/'.$value);
204+
$svg = Str::startsWith($value, '<svg') ? $value : Statamic::svg('icons/light/'.$value);
205+
206+
return $this->sanitizeSvg($svg);
207+
}
208+
209+
private function sanitizeSvg(string $svg): string
210+
{
211+
try {
212+
$sanitizer = new DOMSanitizer(DOMSanitizer::SVG);
213+
214+
return $sanitizer->sanitize($svg, [
215+
'remove-xml-tags' => ! Str::startsWith($svg, '<?xml'),
216+
]);
217+
} catch (\Throwable $e) {
218+
return '';
219+
}
204220
}
205221

206222
/**

tests/CP/Navigation/NavTest.php

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -130,14 +130,15 @@ public function it_can_create_a_nav_item_with_a_custom_inline_svg_icon()
130130
$this->actingAs(tap(User::make()->makeSuper())->save());
131131

132132
Nav::utilities('Test')
133-
->icon('<svg><circle cx="50" cy="50" r="40" stroke="black" stroke-width="3" fill="red" /></svg>');
133+
->icon('<svg onerror="foo"><circle cx="50" cy="50" r="40" stroke="black" stroke-width="3" fill="red" /></svg>');
134134

135135
$item = $this->build()->get('Utilities')->last();
136136

137-
$expected = '<svg><circle cx="50" cy="50" r="40" stroke="black" stroke-width="3" fill="red" /></svg>';
137+
$expectedIcon = '<svg onerror="foo"><circle cx="50" cy="50" r="40" stroke="black" stroke-width="3" fill="red" /></svg>';
138+
$this->assertEquals($expectedIcon, $item->icon());
138139

139-
$this->assertEquals($expected, $item->icon());
140-
$this->assertEquals($expected, $item->svg());
140+
$expectedSvg = '<svg><circle cx="50" cy="50" r="40" stroke="black" stroke-width="3" fill="red"/></svg>';
141+
$this->assertEquals($expectedSvg, $item->svg());
141142
}
142143

143144
#[Test]

0 commit comments

Comments
 (0)