mirror of
				https://github.com/python/cpython.git
				synced 2025-11-04 07:31:38 +00:00 
			
		
		
		
	gh-100001: Omit control characters in http.server stderr logs. (#100002)
Replace control characters in http.server.BaseHTTPRequestHandler.log_message with an escaped \xHH sequence to avoid causing problems for the terminal the output is printed to.
This commit is contained in:
		
							parent
							
								
									530cc9dbb6
								
							
						
					
					
						commit
						d8ab0a4dfa
					
				
					 4 changed files with 43 additions and 2 deletions
				
			
		| 
						 | 
					@ -512,3 +512,10 @@ Security Considerations
 | 
				
			||||||
:class:`SimpleHTTPRequestHandler` will follow symbolic links when handling
 | 
					:class:`SimpleHTTPRequestHandler` will follow symbolic links when handling
 | 
				
			||||||
requests, this makes it possible for files outside of the specified directory
 | 
					requests, this makes it possible for files outside of the specified directory
 | 
				
			||||||
to be served.
 | 
					to be served.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Earlier versions of Python did not scrub control characters from the
 | 
				
			||||||
 | 
					log messages emitted to stderr from ``python -m http.server`` or the
 | 
				
			||||||
 | 
					default :class:`BaseHTTPRequestHandler` ``.log_message``
 | 
				
			||||||
 | 
					implementation. This could allow to remote clients connecting to your
 | 
				
			||||||
 | 
					server to send nefarious control codes to your terminal.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -93,6 +93,7 @@
 | 
				
			||||||
import html
 | 
					import html
 | 
				
			||||||
import http.client
 | 
					import http.client
 | 
				
			||||||
import io
 | 
					import io
 | 
				
			||||||
 | 
					import itertools
 | 
				
			||||||
import mimetypes
 | 
					import mimetypes
 | 
				
			||||||
import os
 | 
					import os
 | 
				
			||||||
import posixpath
 | 
					import posixpath
 | 
				
			||||||
| 
						 | 
					@ -562,6 +563,10 @@ def log_error(self, format, *args):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.log_message(format, *args)
 | 
					        self.log_message(format, *args)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # https://en.wikipedia.org/wiki/List_of_Unicode_characters#Control_codes
 | 
				
			||||||
 | 
					    _control_char_table = str.maketrans(
 | 
				
			||||||
 | 
					            {c: fr'\x{c:02x}' for c in itertools.chain(range(0x20), range(0x7f,0xa0))})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def log_message(self, format, *args):
 | 
					    def log_message(self, format, *args):
 | 
				
			||||||
        """Log an arbitrary message.
 | 
					        """Log an arbitrary message.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -577,12 +582,16 @@ def log_message(self, format, *args):
 | 
				
			||||||
        The client ip and current date/time are prefixed to
 | 
					        The client ip and current date/time are prefixed to
 | 
				
			||||||
        every message.
 | 
					        every message.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Unicode control characters are replaced with escaped hex
 | 
				
			||||||
 | 
					        before writing the output to stderr.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        message = format % args
 | 
				
			||||||
        sys.stderr.write("%s - - [%s] %s\n" %
 | 
					        sys.stderr.write("%s - - [%s] %s\n" %
 | 
				
			||||||
                         (self.address_string(),
 | 
					                         (self.address_string(),
 | 
				
			||||||
                          self.log_date_time_string(),
 | 
					                          self.log_date_time_string(),
 | 
				
			||||||
                          format%args))
 | 
					                          message.translate(self._control_char_table)))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def version_string(self):
 | 
					    def version_string(self):
 | 
				
			||||||
        """Return the server software version string."""
 | 
					        """Return the server software version string."""
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -26,7 +26,7 @@
 | 
				
			||||||
import datetime
 | 
					import datetime
 | 
				
			||||||
import threading
 | 
					import threading
 | 
				
			||||||
from unittest import mock
 | 
					from unittest import mock
 | 
				
			||||||
from io import BytesIO
 | 
					from io import BytesIO, StringIO
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import unittest
 | 
					import unittest
 | 
				
			||||||
from test import support
 | 
					from test import support
 | 
				
			||||||
| 
						 | 
					@ -990,6 +990,25 @@ def verify_http_server_response(self, response):
 | 
				
			||||||
        match = self.HTTPResponseMatch.search(response)
 | 
					        match = self.HTTPResponseMatch.search(response)
 | 
				
			||||||
        self.assertIsNotNone(match)
 | 
					        self.assertIsNotNone(match)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_unprintable_not_logged(self):
 | 
				
			||||||
 | 
					        # We call the method from the class directly as our Socketless
 | 
				
			||||||
 | 
					        # Handler subclass overrode it... nice for everything BUT this test.
 | 
				
			||||||
 | 
					        self.handler.client_address = ('127.0.0.1', 1337)
 | 
				
			||||||
 | 
					        log_message = BaseHTTPRequestHandler.log_message
 | 
				
			||||||
 | 
					        with mock.patch.object(sys, 'stderr', StringIO()) as fake_stderr:
 | 
				
			||||||
 | 
					            log_message(self.handler, '/foo')
 | 
				
			||||||
 | 
					            log_message(self.handler, '/\033bar\000\033')
 | 
				
			||||||
 | 
					            log_message(self.handler, '/spam %s.', 'a')
 | 
				
			||||||
 | 
					            log_message(self.handler, '/spam %s.', '\033\x7f\x9f\xa0beans')
 | 
				
			||||||
 | 
					        stderr = fake_stderr.getvalue()
 | 
				
			||||||
 | 
					        self.assertNotIn('\033', stderr)  # non-printable chars are caught.
 | 
				
			||||||
 | 
					        self.assertNotIn('\000', stderr)  # non-printable chars are caught.
 | 
				
			||||||
 | 
					        lines = stderr.splitlines()
 | 
				
			||||||
 | 
					        self.assertIn('/foo', lines[0])
 | 
				
			||||||
 | 
					        self.assertIn(r'/\x1bbar\x00\x1b', lines[1])
 | 
				
			||||||
 | 
					        self.assertIn('/spam a.', lines[2])
 | 
				
			||||||
 | 
					        self.assertIn('/spam \\x1b\\x7f\\x9f\xa0beans.', lines[3])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_http_1_1(self):
 | 
					    def test_http_1_1(self):
 | 
				
			||||||
        result = self.send_typical_request(b'GET / HTTP/1.1\r\n\r\n')
 | 
					        result = self.send_typical_request(b'GET / HTTP/1.1\r\n\r\n')
 | 
				
			||||||
        self.verify_http_server_response(result[0])
 | 
					        self.verify_http_server_response(result[0])
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,6 @@
 | 
				
			||||||
 | 
					``python -m http.server`` no longer allows terminal control characters sent
 | 
				
			||||||
 | 
					within a garbage request to be printed to the stderr server log.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					This is done by changing the :mod:`http.server` :class:`BaseHTTPRequestHandler`
 | 
				
			||||||
 | 
					``.log_message`` method to replace control characters with a ``\xHH`` hex escape
 | 
				
			||||||
 | 
					before printing.
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue