gh-100001: Omit control characters in http.server stderr logs. (GH-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.
(cherry picked from commit d8ab0a4dfa)

Co-authored-by: Gregory P. Smith <greg@krypto.org>
This commit is contained in:
Miss Islington (bot) 2022-12-05 13:39:22 -08:00 committed by GitHub
parent f3a58cece6
commit a726f747e6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 43 additions and 2 deletions

View File

@ -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.

View File

@ -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."""

View File

@ -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])

View File

@ -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.