8 Commits

Author SHA1 Message Date
Miguel Grinberg
f23c78533e Release 0.7.2 2021-09-28 17:21:05 +01:00
Miguel Grinberg
d29ed6aaa1 Document a security risk in the send_file function 2021-09-28 17:15:07 +01:00
Miguel Grinberg
8e5fb92ff1 Validate redirect URLs 2021-09-28 17:12:15 +01:00
Miguel Grinberg
06015934b8 Return a 400 error when request object could not be created 2021-09-28 17:09:02 +01:00
Miguel Grinberg
568cd51fd2 Version 0.7.2.dev0 2021-09-27 23:01:20 +01:00
Miguel Grinberg
2fe9793389 Release 0.7.1 2021-09-27 22:58:32 +01:00
Miguel Grinberg
de9c991a9a Limit the size of each request line 2021-09-27 20:03:18 +01:00
Miguel Grinberg
d75449eb32 Version 0.7.1.dev0 2021-09-27 17:14:48 +01:00
8 changed files with 105 additions and 23 deletions

View File

@@ -1,8 +1,18 @@
# Microdot change log # Microdot change log
**Release 0.7.2** - 2021-09-28
- Document a security risk in the send_file function ([commit](https://github.com/miguelgrinberg/microdot/commit/d29ed6aaa1f2080fcf471bf6ae0f480f95ff1716)) (thanks **Ky Tran**!)
- Validate redirect URLs ([commit](https://github.com/miguelgrinberg/microdot/commit/8e5fb92ff1ccd50972b0c1cb5a6c3bd5eb54d86b)) (thanks **Ky Tran**!)
- Return a 400 error when request object could not be created ([commit](https://github.com/miguelgrinberg/microdot/commit/06015934b834622d39f52b3e13d16bfee9dc8e5a))
**Release 0.7.1** - 2021-09-27
- Breaking change: Limit the size of each request line to 2KB. A different maximum can be set in `Request.max_readline`. ([commit](https://github.com/miguelgrinberg/microdot/commit/de9c991a9ab836d57d5c08bf4282f99f073b502a)) (thanks **Ky Tran**!)
**Release 0.7.0** - 2021-09-27 **Release 0.7.0** - 2021-09-27
- Breaking change: Limit the size of the request body to 16KB. A different maximum can be set in `Request.max_content_length`. ([commit](https://github.com/miguelgrinberg/microdot/commit/5003a5b3d948a7cf365857b419bebf6e388593a1)) (thanks **Ky Tran**!) - Breaking change: Limit the size of the request body to 16KB. A different maximum can be set in `Request.max_content_length`. ([commit](https://github.com/miguelgrinberg/microdot/commit/5003a5b3d948a7cf365857b419bebf6e388593a1))
- Add documentation for `request.client_addr` [#27](https://github.com/miguelgrinberg/microdot/issues/27) ([commit](https://github.com/miguelgrinberg/microdot/commit/833fecb105ce456b95f1d2a6ea96dceca1075814)) (thanks **Mark Blakeney**!) - Add documentation for `request.client_addr` [#27](https://github.com/miguelgrinberg/microdot/issues/27) ([commit](https://github.com/miguelgrinberg/microdot/commit/833fecb105ce456b95f1d2a6ea96dceca1075814)) (thanks **Mark Blakeney**!)
- Added documentation for reason argument in the Response object ([commit](https://github.com/miguelgrinberg/microdot/commit/d527bdb7c32ab918a1ecf6956cf3a9f544504354)) - Added documentation for reason argument in the Response object ([commit](https://github.com/miguelgrinberg/microdot/commit/d527bdb7c32ab918a1ecf6956cf3a9f544504354))

View File

@@ -1,6 +1,6 @@
[metadata] [metadata]
name = microdot name = microdot
version = 0.7.0 version = 0.7.2
author = Miguel Grinberg author = Miguel Grinberg
author_email = miguel.grinberg@gmail.com author_email = miguel.grinberg@gmail.com
description = The impossibly small web framework for MicroPython description = The impossibly small web framework for MicroPython

View File

@@ -198,6 +198,15 @@ class Request():
#: Request.max_content_length = 1 * 1024 * 1024 # 1MB requests allowed #: Request.max_content_length = 1 * 1024 * 1024 # 1MB requests allowed
max_content_length = 16 * 1024 max_content_length = 16 * 1024
#: Specify the maximum length allowed for a line in the request. Requests
#: with longer lines will not be correctly interpreted. Applications can
#: change this maximum as necessary.
#:
#: Example::
#:
#: Request.max_readline = 16 * 1024 # 16KB lines allowed
max_readline = 2 * 1024
class G: class G:
pass pass
@@ -244,7 +253,7 @@ class Request():
This method returns a newly created ``Request`` object. This method returns a newly created ``Request`` object.
""" """
# request line # request line
line = client_stream.readline().strip().decode() line = Request._safe_readline(client_stream).strip().decode()
if not line: if not line:
return None return None
method, url, http_version = line.split() method, url, http_version = line.split()
@@ -254,7 +263,7 @@ class Request():
headers = {} headers = {}
content_length = 0 content_length = 0
while True: while True:
line = client_stream.readline().strip().decode() line = Request._safe_readline(client_stream).strip().decode()
if line == '': if line == '':
break break
header, value = line.split(':', 1) header, value = line.split(':', 1)
@@ -298,6 +307,13 @@ class Request():
self._form = self._parse_urlencoded(self.body.decode()) self._form = self._parse_urlencoded(self.body.decode())
return self._form return self._form
@staticmethod
def _safe_readline(stream):
line = stream.readline(Request.max_readline + 1)
if len(line) > Request.max_readline:
raise ValueError('line too long')
return line
class Response(): class Response():
"""An HTTP response class. """An HTTP response class.
@@ -414,6 +430,8 @@ class Response():
:param status_code: The 3xx status code to use for the redirect. The :param status_code: The 3xx status code to use for the redirect. The
default is 302. default is 302.
""" """
if '\x0d' in location or '\x0a' in location:
raise ValueError('invalid redirect URL')
return cls(status_code=status_code, headers={'Location': location}) return cls(status_code=status_code, headers={'Location': location})
@classmethod @classmethod
@@ -426,6 +444,10 @@ class Response():
:param content_type: The ``Content-Type`` header to use in the :param content_type: The ``Content-Type`` header to use in the
response. If omitted, it is generated response. If omitted, it is generated
automatically from the file extension. automatically from the file extension.
Security note: The filename is assumed to be trusted. Never pass
filenames provided by the user before validating and sanitizing them
first.
""" """
if content_type is None: if content_type is None:
ext = filename.split('.')[-1] ext = filename.split('.')[-1]
@@ -778,7 +800,11 @@ class Microdot():
else: else:
stream = sock stream = sock
req = Request.create(self, stream, addr) req = None
try:
req = Request.create(self, stream, addr)
except Exception as exc: # pragma: no cover
print_exception(exc)
if req: if req:
if req.content_length > req.max_content_length: if req.content_length > req.max_content_length:
if 413 in self.error_handlers: if 413 in self.error_handlers:
@@ -819,11 +845,13 @@ class Microdot():
res = self.error_handlers[500](req) res = self.error_handlers[500](req)
else: else:
res = 'Internal server error', 500 res = 'Internal server error', 500
if isinstance(res, tuple): else:
res = Response(*res) res = 'Bad request', 400
elif not isinstance(res, Response): if isinstance(res, tuple):
res = Response(res) res = Response(*res)
res.write(stream) elif not isinstance(res, Response):
res = Response(res)
res.write(stream)
stream.close() stream.close()
if stream != sock: # pragma: no cover if stream != sock: # pragma: no cover
sock.close() sock.close()

View File

@@ -34,7 +34,7 @@ class Request(BaseRequest):
object. object.
""" """
# request line # request line
line = (await client_stream.readline()).strip().decode() line = (await Request._safe_readline(client_stream)).strip().decode()
if not line: # pragma: no cover if not line: # pragma: no cover
return None return None
method, url, http_version = line.split() method, url, http_version = line.split()
@@ -44,7 +44,8 @@ class Request(BaseRequest):
headers = {} headers = {}
content_length = 0 content_length = 0
while True: while True:
line = (await client_stream.readline()).strip().decode() line = (await Request._safe_readline(
client_stream)).strip().decode()
if line == '': if line == '':
break break
header, value = line.split(':', 1) header, value = line.split(':', 1)
@@ -60,6 +61,13 @@ class Request(BaseRequest):
return Request(app, client_addr, method, url, http_version, headers, return Request(app, client_addr, method, url, http_version, headers,
body) body)
@staticmethod
async def _safe_readline(stream):
line = (await stream.readline())
if len(line) > Request.max_readline:
raise ValueError('line too long')
return line
class Response(BaseResponse): class Response(BaseResponse):
"""An HTTP response class. """An HTTP response class.
@@ -210,8 +218,12 @@ class Microdot(BaseMicrodot):
self.server.close() self.server.close()
async def dispatch_request(self, reader, writer): async def dispatch_request(self, reader, writer):
req = await Request.create(self, reader, req = None
writer.get_extra_info('peername')) try:
req = await Request.create(self, reader,
writer.get_extra_info('peername'))
except Exception as exc: # pragma: no cover
print_exception(exc)
if req: if req:
if req.content_length > req.max_content_length: if req.content_length > req.max_content_length:
if 413 in self.error_handlers: if 413 in self.error_handlers:
@@ -258,11 +270,13 @@ class Microdot(BaseMicrodot):
self.error_handlers[500], req) self.error_handlers[500], req)
else: else:
res = 'Internal server error', 500 res = 'Internal server error', 500
if isinstance(res, tuple): else:
res = Response(*res) res = 'Bad request', 400
elif not isinstance(res, Response): if isinstance(res, tuple):
res = Response(res) res = Response(*res)
await res.write(writer) elif not isinstance(res, Response):
res = Response(res)
await res.write(writer)
await writer.aclose() await writer.aclose()
if self.debug and req: # pragma: no cover if self.debug and req: # pragma: no cover
print('{method} {path} {status_code}'.format( print('{method} {path} {status_code}'.format(

View File

@@ -73,7 +73,10 @@ class TestMicrodot(unittest.TestCase):
mock_socket._requests.append(fd) mock_socket._requests.append(fd)
self._add_shutdown(app) self._add_shutdown(app)
app.run() app.run()
assert fd.response == b'' self.assertTrue(fd.response.startswith(b'HTTP/1.0 400 N/A\r\n'))
self.assertIn(b'Content-Length: 11\r\n', fd.response)
self.assertIn(b'Content-Type: text/plain\r\n', fd.response)
self.assertTrue(fd.response.endswith(b'\r\n\r\nBad request'))
def test_method_decorators(self): def test_method_decorators(self):
app = Microdot() app = Microdot()

View File

@@ -79,6 +79,18 @@ class TestRequest(unittest.TestCase):
req = Request.create('app', fd, 'addr') req = Request.create('app', fd, 'addr')
self.assertIsNone(req.form) self.assertIsNone(req.form)
def test_large_line(self):
saved_max_readline = Request.max_readline
Request.max_readline = 16
fd = get_request_fd('GET', '/foo', headers={
'Content-Type': 'application/x-www-form-urlencoded'},
body='foo=bar&abc=def&x=y')
with self.assertRaises(ValueError):
Request.create('app', fd, 'addr')
Request.max_readline = saved_max_readline
def test_large_payload(self): def test_large_payload(self):
saved_max_content_length = Request.max_content_length saved_max_content_length = Request.max_content_length
Request.max_content_length = 16 Request.max_content_length = 16
@@ -87,6 +99,6 @@ class TestRequest(unittest.TestCase):
'Content-Type': 'application/x-www-form-urlencoded'}, 'Content-Type': 'application/x-www-form-urlencoded'},
body='foo=bar&abc=def&x=y') body='foo=bar&abc=def&x=y')
req = Request.create('app', fd, 'addr') req = Request.create('app', fd, 'addr')
assert req.body == b'' self.assertEqual(req.body, b'')
Request.max_content_length = saved_max_content_length Request.max_content_length = saved_max_content_length

View File

@@ -84,7 +84,10 @@ class TestMicrodotAsync(unittest.TestCase):
mock_socket._requests.append(fd) mock_socket._requests.append(fd)
self._add_shutdown(app) self._add_shutdown(app)
app.run() app.run()
assert fd.response == b'' self.assertTrue(fd.response.startswith(b'HTTP/1.0 400 N/A\r\n'))
self.assertIn(b'Content-Length: 11\r\n', fd.response)
self.assertIn(b'Content-Type: text/plain\r\n', fd.response)
self.assertTrue(fd.response.endswith(b'\r\n\r\nBad request'))
def test_before_after_request(self): def test_before_after_request(self):
app = Microdot() app = Microdot()

View File

@@ -89,6 +89,18 @@ class TestRequestAsync(unittest.TestCase):
req = _run(Request.create('app', fd, 'addr')) req = _run(Request.create('app', fd, 'addr'))
self.assertIsNone(req.form) self.assertIsNone(req.form)
def test_large_line(self):
saved_max_readline = Request.max_readline
Request.max_readline = 16
fd = get_async_request_fd('GET', '/foo', headers={
'Content-Type': 'application/x-www-form-urlencoded'},
body='foo=bar&abc=def&x=y')
with self.assertRaises(ValueError):
_run(Request.create('app', fd, 'addr'))
Request.max_readline = saved_max_readline
def test_large_payload(self): def test_large_payload(self):
saved_max_content_length = Request.max_content_length saved_max_content_length = Request.max_content_length
Request.max_content_length = 16 Request.max_content_length = 16
@@ -97,6 +109,6 @@ class TestRequestAsync(unittest.TestCase):
'Content-Type': 'application/x-www-form-urlencoded'}, 'Content-Type': 'application/x-www-form-urlencoded'},
body='foo=bar&abc=def&x=y') body='foo=bar&abc=def&x=y')
req = _run(Request.create('app', fd, 'addr')) req = _run(Request.create('app', fd, 'addr'))
assert req.body == b'' self.assertEqual(req.body, b'')
Request.max_content_length = saved_max_content_length Request.max_content_length = saved_max_content_length