Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions IPython/core/display.py
Original file line number Diff line number Diff line change
Expand Up @@ -1065,7 +1065,7 @@ def _repr_html_(self):
if self.alt:
alt = ' alt="%s"' % html.escape(self.alt)
return '<img src="{url}"{width}{height}{klass}{alt}/>'.format(
url=self.url,
url=html.escape(self.url or ""),
width=width,
height=height,
klass=klass,
Expand Down Expand Up @@ -1228,7 +1228,7 @@ def _repr_html_(self):
url = self.url if self.url is not None else self.filename
output = """<video src="{}" {} {} {}>
Your browser does not support the <code>video</code> element.
</video>""".format(url, self.html_attributes, width, height)
</video>""".format(html.escape(url or ""), self.html_attributes, width, height)
return output

# Embedded videos are base64-encoded.
Expand Down
40 changes: 26 additions & 14 deletions IPython/lib/display.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ def src_attr(self):
return """data:{type};base64,{base64}""".format(type=self.mimetype,
base64=data)
elif self.url is not None:
return self.url
return html_escape(self.url)
else:
return ""

Expand All @@ -252,7 +252,7 @@ def autoplay_attr(self):

def element_id_attr(self):
if (self.element_id):
return f'id="{self.element_id}"'
return f'id="{html_escape(self.element_id)}"'
else:
return ''

Expand Down Expand Up @@ -292,9 +292,9 @@ def _repr_html_(self):
else:
params = ""
return self.iframe.format(
src=self.src,
width=self.width,
height=self.height,
src=html_escape(self.src),
width=html_escape(str(self.width)),
height=html_escape(str(self.height)),
params=params,
extras=" ".join(self.extras),
)
Expand Down Expand Up @@ -513,7 +513,12 @@ def __init__(self,
self.recursive = recursive

def _get_display_formatter(
self, dirname_output_format, fname_output_format, fp_format, fp_cleaner=None
self,
dirname_output_format,
fname_output_format,
fp_format,
fp_cleaner=None,
escape_names=False,
):
"""generate built-in formatter function

Expand All @@ -531,7 +536,11 @@ def _get_display_formatter(
fp_format: string to use for formatting filepaths, must contain
exactly two "%s" and the dirname will be substituted for the first
and fname will be substituted for the second
escape_names: whether directory and file names must be HTML-escaped
before being substituted, as they are for the notebook formatter
"""
escape = html_escape if escape_names else str

def f(dirname, fnames, included_suffixes=None):
result = []
# begin by figuring out which filenames, if any,
Expand All @@ -550,18 +559,18 @@ def f(dirname, fnames, included_suffixes=None):
else:
# otherwise print the formatted directory name followed by
# the formatted filenames
dirname_output_line = dirname_output_format % dirname
dirname_output_line = dirname_output_format % escape(dirname)
result.append(dirname_output_line)
for fname in display_fnames:
fp = fp_format % (dirname,fname)
fp = fp_format % (escape(dirname), escape(fname))
if fp_cleaner is not None:
fp = fp_cleaner(fp)
try:
# output can include both a filepath and a filename...
fname_output_line = fname_output_format % (fp, fname)
fname_output_line = fname_output_format % (fp, escape(fname))
except TypeError:
# ... or just a single filepath
fname_output_line = fname_output_format % fname
fname_output_line = fname_output_format % escape(fname)
result.append(fname_output_line)
return result
return f
Expand All @@ -586,10 +595,13 @@ def fp_cleaner(fp):
else:
fp_cleaner = None

return self._get_display_formatter(dirname_output_format,
fname_output_format,
fp_format,
fp_cleaner)
return self._get_display_formatter(
dirname_output_format,
fname_output_format,
fp_format,
fp_cleaner,
escape_names=True,
)

def _get_terminal_display_formatter(self,
spacer=" "):
Expand Down
27 changes: 27 additions & 0 deletions tests/test_display.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,11 +188,38 @@ def test_recursive_FileLinks():
assert len(actual) == 2, actual


def test_escaped_names_FileLinks():
"""FileLinks: html metacharacters in file names are escaped"""
td = mkdtemp()
# links are emitted as href='...', so the apostrophe is what breaks out of
# the attribute; "<" and ">" are not usable as they are invalid on windows
name = "a' onmouseover='alert(1)&.txt"
with open(pjoin(td, name), "w"):
pass
actual = display.FileLinks(td)._repr_html_()
assert "a&#x27; onmouseover=&#x27;alert(1)&amp;.txt" in actual
assert name not in actual


def test_audio_from_file():
path = pjoin(dirname(__file__), "test.wav")
display.Audio(filename=path)


def test_escaped_url_Audio():
"""Audio: quotes in url and element_id do not break out of the attribute"""
audio = display.Audio(url='http://example.com/a.wav" onerror="alert(1)')
assert (
'src="http://example.com/a.wav&quot; onerror=&quot;alert(1)"'
in audio._repr_html_()
)

audio = display.Audio(
url="http://example.com/a.wav", element_id='x" onload="alert(1)'
)
assert 'id="x&quot; onload=&quot;alert(1)"' in audio._repr_html_()


@skipif_not_numpy
def test_audio_from_numpy_array():
test_tone = get_test_tone()
Expand Down
30 changes: 30 additions & 0 deletions tests/test_display_2.py
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,36 @@ def test_image_alt_tag():
assert md["alt"] == "an image"


def test_image_url_escaping():
"""Image: a quote in the url does not break out of the src attribute"""
img = display.Image(url='http://example.com/i.png" onerror="alert(1)')
assert (
'<img src="http://example.com/i.png&quot; onerror=&quot;alert(1)"/>'
== img._repr_html_()
)


def test_video_url_escaping():
"""Video: a quote in the url does not break out of the src attribute"""
v = display.Video('http://example.com/v.mp4" onerror="alert(1)')
assert (
'src="http://example.com/v.mp4&quot; onerror=&quot;alert(1)"'
in v._repr_html_()
)


def test_iframe_escaping():
"""IFrame: quotes in src, width and height stay inside their attributes"""
html = display.IFrame('http://example.com/?a=1"><script>', 400, 300)._repr_html_()
assert 'src="http://example.com/?a=1&quot;&gt;&lt;script&gt;"' in html

html = display.YouTubeVideo('abc"><script>')._repr_html_()
assert '"><script>' not in html

html = display.IFrame("http://example.com", '400" onload="alert(1)', 300)
assert 'width="400&quot; onload=&quot;alert(1)"' in html._repr_html_()


def test_image_bad_filename_raises_proper_exception():
with pytest.raises(FileNotFoundError):
display.Image("/this/file/does/not/exist/")._repr_png_()
Loading