]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
Issue #26585: Eliminate _quote_html() and use html.escape(quote=False)
authorMartin Panter <vadmium+py@gmail.com>
Mon, 11 Apr 2016 00:40:08 +0000 (00:40 +0000)
committerMartin Panter <vadmium+py@gmail.com>
Mon, 11 Apr 2016 00:40:08 +0000 (00:40 +0000)
Patch by Xiang Zhang.

Lib/http/server.py
Lib/test/test_httpservers.py
Misc/NEWS

index f4ad2609256a346baa83bdb91dd14c51dcc9b816..fbee6a932de108a310a8bb7bd689cf9838a1eaa0 100644 (file)
@@ -127,9 +127,6 @@ DEFAULT_ERROR_MESSAGE = """\
 
 DEFAULT_ERROR_CONTENT_TYPE = "text/html;charset=utf-8"
 
-def _quote_html(html):
-    return html.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
-
 class HTTPServer(socketserver.TCPServer):
 
     allow_reuse_address = 1    # Seems to make sense in testing environment
@@ -449,9 +446,12 @@ class BaseHTTPRequestHandler(socketserver.StreamRequestHandler):
         if explain is None:
             explain = longmsg
         self.log_error("code %d, message %s", code, message)
-        # using _quote_html to prevent Cross Site Scripting attacks (see bug #1100201)
-        content = (self.error_message_format %
-                   {'code': code, 'message': _quote_html(message), 'explain': _quote_html(explain)})
+        # HTML encode to prevent Cross Site Scripting attacks (see bug #1100201)
+        content = (self.error_message_format % {
+            'code': code,
+            'message': html.escape(message, quote=False),
+            'explain': html.escape(explain, quote=False)
+        })
         body = content.encode('UTF-8', 'replace')
         self.send_response(code, message)
         self.send_header("Content-Type", self.error_content_type)
@@ -710,7 +710,7 @@ class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):
                                                errors='surrogatepass')
         except UnicodeDecodeError:
             displaypath = urllib.parse.unquote(path)
-        displaypath = html.escape(displaypath)
+        displaypath = html.escape(displaypath, quote=False)
         enc = sys.getfilesystemencoding()
         title = 'Directory listing for %s' % displaypath
         r.append('<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" '
@@ -734,7 +734,7 @@ class SimpleHTTPRequestHandler(BaseHTTPRequestHandler):
             r.append('<li><a href="%s">%s</a></li>'
                     % (urllib.parse.quote(linkname,
                                           errors='surrogatepass'),
-                       html.escape(displayname)))
+                       html.escape(displayname, quote=False)))
         r.append('</ul>\n<hr>\n</body>\n</html>\n')
         encoded = '\n'.join(r).encode(enc, 'surrogateescape')
         f = io.BytesIO()
index c752fd8ecadd7c752913939befe8513c9accf66e..3856d00b2b38f63797aa9169bc3f5f15fe66502d 100644 (file)
@@ -344,7 +344,7 @@ class SimpleHTTPServerTestCase(BaseTestCase):
         quotedname = urllib.parse.quote(filename, errors='surrogatepass')
         self.assertIn(('href="%s"' % quotedname)
                       .encode(enc, 'surrogateescape'), body)
-        self.assertIn(('>%s<' % html.escape(filename))
+        self.assertIn(('>%s<' % html.escape(filename, quote=False))
                       .encode(enc, 'surrogateescape'), body)
         response = self.request(self.base_url + '/' + quotedname)
         self.check_status_and_reason(response, HTTPStatus.OK,
@@ -422,6 +422,27 @@ class SimpleHTTPServerTestCase(BaseTestCase):
         self.assertEqual(response.getheader("Location"),
                          self.tempdir_name + "/?hi=1")
 
+    def test_html_escape_filename(self):
+        filename = '<test&>.txt'
+        fullpath = os.path.join(self.tempdir, filename)
+
+        try:
+            open(fullpath, 'w').close()
+        except OSError:
+            raise unittest.SkipTest('Can not create file %s on current file '
+                                    'system' % filename)
+
+        try:
+            response = self.request(self.base_url + '/')
+            body = self.check_status_and_reason(response, HTTPStatus.OK)
+            enc = response.headers.get_content_charset()
+        finally:
+            os.unlink(fullpath)  # avoid affecting test_undecodable_filename
+
+        self.assertIsNotNone(enc)
+        html_text = '>%s<' % html.escape(filename, quote=False)
+        self.assertIn(html_text.encode(enc), body)
+
 
 cgi_file1 = """\
 #!%s
@@ -883,6 +904,13 @@ class BaseHTTPRequestHandlerTestCase(unittest.TestCase):
         self.assertFalse(self.handler.get_called)
         self.assertEqual(self.handler.requestline, 'GET / HTTP/1.1')
 
+    def test_html_escape_on_error(self):
+        result = self.send_typical_request(
+            b'<script>alert("hello")</script> / HTTP/1.1')
+        result = b''.join(result)
+        text = '<script>alert("hello")</script>'
+        self.assertIn(html.escape(text, quote=False).encode('ascii'), result)
+
     def test_close_connection(self):
         # handle_one_request() should be repeatedly called until
         # it sets close_connection
index 7ac9b341015f330a4229b2bd8d663644f8dea2e3..7284d3f1cafcb175b7f82c735cf167dc6e456bbf 100644 (file)
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -240,6 +240,9 @@ Core and Builtins
 Library
 -------
 
+- Issue #26585: Eliminate http.server._quote_html() and use
+  html.escape(quote=False).  Patch by Xiang Zhang.
+
 - Issue #26685: Raise OSError if closing a socket fails.
 
 - Issue #16329: Add .webm to mimetypes.types_map.  Patch by Giampaolo Rodola'.