Skip to content

Commit 0bd95fe

Browse files
SVG sanitization for XSS (#574)
* svg sanitization * remove unnecessary * tests
1 parent 061ed90 commit 0bd95fe

File tree

2 files changed

+182
-2
lines changed

2 files changed

+182
-2
lines changed

apps/codecov-api/graphs/helpers/graph_utils.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from html import escape
12
from math import cos, pi, sin
23

34
style_n_defs = """
@@ -86,13 +87,16 @@ def _svg_rect(x, y, width, height, fill, stroke, stroke_width, _class=None, titl
8687
)
8788
)
8889

90+
# Escape the title to prevent XSS
91+
escaped_title = escape(title, quote=True)
92+
8993
return (
9094
'<rect x="{0}" y="{1}" width="{2}" height="{3}" '
9195
'fill="{4}" stroke="{5}" stroke-width="{6}" class="{8} tooltipped" '
9296
'data-content="{7}">'
9397
"<title>{7}</title>"
9498
"</rect>".format(
95-
x, y, width, height, fill, stroke, stroke_width, title, _class or ""
99+
x, y, width, height, fill, stroke, stroke_width, escaped_title, _class or ""
96100
)
97101
)
98102

apps/codecov-api/graphs/tests/test_graph_utils.py

Lines changed: 177 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from graphs.helpers.graph_utils import _tree_height
1+
from graphs.helpers.graph_utils import _svg_rect, _tree_height
22

33

44
class TestGraphsUtils:
@@ -41,3 +41,179 @@ def test_tree_height(self):
4141
]
4242
height = _tree_height(tree)
4343
assert height == 4
44+
45+
def test_svg_rect_without_title(self):
46+
"""Test that _svg_rect without title works correctly"""
47+
result = _svg_rect(
48+
x=10, y=20, width=100, height=50, fill="#fff", stroke="#000", stroke_width=1
49+
)
50+
51+
assert '<rect x="10" y="20" width="100" height="50"' in result
52+
assert 'fill="#fff"' in result
53+
assert 'stroke="#000"' in result
54+
assert 'stroke-width="1"' in result
55+
assert "<title>" not in result
56+
57+
def test_svg_rect_with_normal_title(self):
58+
"""Test that _svg_rect with normal title includes title and data-content"""
59+
result = _svg_rect(
60+
x=10,
61+
y=20,
62+
width=100,
63+
height=50,
64+
fill="#fff",
65+
stroke="#000",
66+
stroke_width=1,
67+
title="path/to/file.py",
68+
)
69+
70+
assert '<rect x="10" y="20" width="100" height="50"' in result
71+
assert 'data-content="path/to/file.py"' in result
72+
assert "<title>path/to/file.py</title>" in result
73+
assert "tooltipped" in result
74+
75+
def test_svg_rect_escapes_xss_script_tags(self):
76+
"""Test that script tags in file paths are properly escaped"""
77+
malicious_title = '"><script>alert("XSS")</script><path foo="'
78+
result = _svg_rect(
79+
x=10,
80+
y=20,
81+
width=100,
82+
height=50,
83+
fill="#fff",
84+
stroke="#000",
85+
stroke_width=1,
86+
title=malicious_title,
87+
)
88+
89+
# Should not contain unescaped dangerous sequences that would break out of attributes
90+
assert '"><script>' not in result
91+
assert "</script>" not in result
92+
93+
# Should contain escaped version of angle brackets and quotes
94+
assert "&lt;script&gt;" in result or "&#x3C;script&#x3E;" in result
95+
assert "&quot;" in result or "&#x27;" in result
96+
97+
def test_svg_rect_escapes_xss_event_handlers(self):
98+
"""Test that event handlers in file paths are properly escaped"""
99+
malicious_title = '" onload="alert(\'XSS\')" data="'
100+
result = _svg_rect(
101+
x=10,
102+
y=20,
103+
width=100,
104+
height=50,
105+
fill="#fff",
106+
stroke="#000",
107+
stroke_width=1,
108+
title=malicious_title,
109+
)
110+
111+
# Should not contain raw event handlers
112+
assert 'onload="alert' not in result
113+
114+
# Should contain escaped quotes
115+
assert "&quot;" in result or "&#x22;" in result
116+
117+
def test_svg_rect_escapes_xss_html_injection(self):
118+
"""Test that HTML injection attempts in file paths are properly escaped"""
119+
malicious_title = "</rect><text>INJECTED</text><rect"
120+
result = _svg_rect(
121+
x=10,
122+
y=20,
123+
width=100,
124+
height=50,
125+
fill="#fff",
126+
stroke="#000",
127+
stroke_width=1,
128+
title=malicious_title,
129+
)
130+
131+
# Should not contain unescaped closing tags that would break out of attributes
132+
assert "</rect><text>INJECTED</text><rect" not in result
133+
134+
# Should contain escaped version
135+
assert "&lt;" in result or "&#x3C;" in result
136+
assert "&gt;" in result or "&#x3E;" in result
137+
138+
def test_svg_rect_escapes_all_dangerous_chars(self):
139+
"""Test that all dangerous characters are escaped"""
140+
dangerous_title = "& < > \" ' test"
141+
result = _svg_rect(
142+
x=10,
143+
y=20,
144+
width=100,
145+
height=50,
146+
fill="#fff",
147+
stroke="#000",
148+
stroke_width=1,
149+
title=dangerous_title,
150+
)
151+
152+
# Check that dangerous characters are escaped
153+
# Note: Python's html.escape with quote=True escapes these as:
154+
# & -> &amp;
155+
# < -> &lt;
156+
# > -> &gt;
157+
# " -> &quot;
158+
# ' -> &#x27;
159+
assert "&amp;" in result
160+
assert "&lt;" in result
161+
assert "&gt;" in result
162+
assert "&quot;" in result
163+
assert "&#x27;" in result
164+
165+
# Should not contain raw dangerous characters in attribute values
166+
assert 'data-content="&' in result
167+
assert 'data-content="<' not in result
168+
169+
def test_svg_rect_with_realistic_malicious_filename(self):
170+
"""Test with a realistic malicious filename that could exist in a repo"""
171+
# Simulating a file path like: tests/"><img src=x onerror=alert(1)>.py
172+
malicious_title = 'tests/"><img src=x onerror=alert(1)>.py'
173+
result = _svg_rect(
174+
x=10,
175+
y=20,
176+
width=100,
177+
height=50,
178+
fill="#fff",
179+
stroke="#000",
180+
stroke_width=1,
181+
title=malicious_title,
182+
)
183+
184+
# Should not break out of the attribute with the dangerous sequence
185+
assert '"><img' not in result
186+
assert '" onerror=' not in result
187+
assert "> onerror=" not in result
188+
189+
# Should be properly escaped
190+
assert "&quot;&gt;&lt;img" in result or "&#x22;&#x3E;&#x3C;img" in result
191+
192+
def test_svg_rect_preserves_class_parameter(self):
193+
"""Test that _class parameter works with and without title"""
194+
# Without title
195+
result = _svg_rect(
196+
x=10,
197+
y=20,
198+
width=100,
199+
height=50,
200+
fill="#fff",
201+
stroke="#000",
202+
stroke_width=1,
203+
_class="my-class",
204+
)
205+
assert 'class="my-class"' in result
206+
207+
# With title
208+
result_with_title = _svg_rect(
209+
x=10,
210+
y=20,
211+
width=100,
212+
height=50,
213+
fill="#fff",
214+
stroke="#000",
215+
stroke_width=1,
216+
_class="my-class",
217+
title="test/path.py",
218+
)
219+
assert 'class="my-class tooltipped"' in result_with_title

0 commit comments

Comments
 (0)