Skip to content

Commit c714893

Browse files
authored
[4.x] SVG tag sanitization (#8408)
1 parent f806b6b commit c714893

File tree

3 files changed

+132
-2
lines changed

3 files changed

+132
-2
lines changed

Diff for: composer.json

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"ext-json": "*",
1313
"ajthinking/archetype": "^1.0.3",
1414
"composer/composer": "^1.10.22 || ^2.2.12",
15+
"enshrined/svg-sanitize": "^0.16.0",
1516
"facade/ignition-contracts": "^1.0",
1617
"guzzlehttp/guzzle": "^6.3 || ^7.0",
1718
"james-heinrich/getid3": "^1.9.21",

Diff for: src/Tags/Svg.php

+60-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
namespace Statamic\Tags;
44

5+
use enshrined\svgSanitize\data\AllowedAttributes;
6+
use enshrined\svgSanitize\data\AllowedTags;
7+
use enshrined\svgSanitize\Sanitizer;
58
use Statamic\Facades\File;
69
use Statamic\Facades\URL;
710
use Statamic\Support\Str;
@@ -47,11 +50,13 @@ public function index()
4750
$svg = $this->setTitleAndDesc($svg);
4851
}
4952

50-
return str_replace(
53+
$svg = str_replace(
5154
'<svg',
5255
collect(['<svg', $attributes])->filter()->implode(' '),
5356
$svg
5457
);
58+
59+
return $this->sanitize($svg);
5560
}
5661

5762
private function setTitleAndDesc($svg)
@@ -79,4 +84,58 @@ private function setTitleAndDesc($svg)
7984

8085
return $doc->saveHTML();
8186
}
87+
88+
private function sanitize($svg)
89+
{
90+
if ($this->params->bool('sanitize') === false) {
91+
return $svg;
92+
}
93+
94+
$sanitizer = new Sanitizer;
95+
$sanitizer->removeXMLTag(! Str::startsWith($svg, '<?xml'));
96+
$sanitizer->setAllowedAttrs($this->getAllowedAttrs());
97+
$sanitizer->setAllowedTags($this->getAllowedTags());
98+
99+
return $sanitizer->sanitize($svg);
100+
}
101+
102+
private function getAllowedAttrs()
103+
{
104+
$attrs = $this->params->explode('allow_attrs', []);
105+
106+
return new class($attrs) extends AllowedAttributes
107+
{
108+
private static $attrs = [];
109+
110+
public function __construct($attrs)
111+
{
112+
self::$attrs = $attrs;
113+
}
114+
115+
public static function getAttributes()
116+
{
117+
return array_merge(parent::getAttributes(), self::$attrs);
118+
}
119+
};
120+
}
121+
122+
private function getAllowedTags()
123+
{
124+
$tags = $this->params->explode('allow_tags', []);
125+
126+
return new class($tags) extends AllowedTags
127+
{
128+
private static $tags = [];
129+
130+
public function __construct($tags)
131+
{
132+
self::$tags = $tags;
133+
}
134+
135+
public static function getTags()
136+
{
137+
return array_merge(parent::getTags(), self::$tags);
138+
}
139+
};
140+
}
82141
}

Diff for: tests/Tags/SvgTagTest.php

+71-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use Illuminate\Support\Facades\File;
66
use Statamic\Facades\Parse;
7+
use Statamic\View\Antlers\Language\Utilities\StringUtilities;
78
use Tests\TestCase;
89

910
class SvgTagTest extends TestCase
@@ -17,7 +18,10 @@ public function setUp(): void
1718

1819
private function tag($tag)
1920
{
20-
return Parse::template($tag, []);
21+
$output = Parse::template($tag, []);
22+
23+
// Normalize whitespace and line breaks for testing ease.
24+
return trim(StringUtilities::normalizeLineEndings($output));
2125
}
2226

2327
/** @test */
@@ -32,4 +36,70 @@ public function it_renders_svg_with_additional_params()
3236
{
3337
$this->assertStringStartsWith('<svg class="mb-2" xmlns="', $this->tag('{{ svg src="users" class="mb-2" }}'));
3438
}
39+
40+
/** @test */
41+
public function it_sanitizes()
42+
{
43+
File::put(resource_path('xss.svg'), <<<'SVG'
44+
<svg>
45+
<path onload="loadxss" onclick="clickxss"></path>
46+
<script>alert("xss")</script>
47+
<foreignObject></foreignObject>
48+
<mesh></mesh>
49+
</svg>
50+
SVG);
51+
52+
$this->assertEquals(<<<'SVG'
53+
<svg>
54+
<path></path>
55+
</svg>
56+
SVG,
57+
$this->tag('{{ svg src="xss" sanitize="true" }}')
58+
);
59+
60+
$this->assertEquals(<<<'SVG'
61+
<svg>
62+
<path onclick="clickxss"></path>
63+
<foreignObject></foreignObject>
64+
<mesh></mesh>
65+
</svg>
66+
SVG,
67+
$this->tag('{{ svg src="xss" sanitize="true" allow_tags="mesh|foreignObject" allow_attrs="onclick" }}')
68+
);
69+
}
70+
71+
/** @test */
72+
public function sanitizing_doesnt_add_xml_tag()
73+
{
74+
// Thes sanitizer package adds an xml tag by default.
75+
// We want to make sure if there wasn't one to begin with, it doesn't add one.
76+
77+
$svg = <<<'SVG'
78+
<svg>
79+
<path></path>
80+
</svg>
81+
SVG;
82+
83+
File::put(resource_path('xmltag.svg'), $svg);
84+
85+
$this->assertEquals($svg, $this->tag('{{ svg src="xmltag" sanitize="true" }}'));
86+
}
87+
88+
/** @test */
89+
public function sanitizing_doesnt_remove_an_xml_tag()
90+
{
91+
// Thes sanitizer package adds an xml tag by default.
92+
// We want to make sure that we haven't configured it to remove it if we wanted it there to begin with.
93+
94+
$svg = <<<'SVG'
95+
<?xml version="1.0" encoding="UTF-8"?>
96+
<svg>
97+
<path></path>
98+
</svg>
99+
SVG;
100+
101+
File::put(resource_path('xmltag.svg'), $svg);
102+
103+
$this->assertEquals($svg, $this->tag('{{ svg src="xmltag" sanitize="true" }}'));
104+
}
35105
}

0 commit comments

Comments
 (0)