9 Commits

Author SHA1 Message Date
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
Miguel Grinberg
e508abc333 Release 0.7.0 2021-09-27 17:12:42 +01:00
Miguel Grinberg
5003a5b3d9 Limit the size of the request body 2021-09-27 17:01:43 +01:00
Miguel Grinberg
4ed101dfc6 Add security policy 2021-09-27 13:57:04 +01:00
Mark Blakeney
833fecb105 Add documentation for request.client_addr (#27) 2021-09-22 12:04:28 +01:00
Miguel Grinberg
d527bdb7c3 Added documentation for reason argument in the Response object 2021-08-11 12:00:46 +01:00
Miguel Grinberg
2516b296a7 Version 0.6.1.dev0 2021-08-11 10:37:04 +01:00
9 changed files with 267 additions and 73 deletions

View File

@@ -1,5 +1,15 @@
# Microdot change log # Microdot change log
**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
- 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**!)
- Added documentation for reason argument in the Response object ([commit](https://github.com/miguelgrinberg/microdot/commit/d527bdb7c32ab918a1ecf6956cf3a9f544504354))
**Release 0.6.0** - 2021-08-11 **Release 0.6.0** - 2021-08-11
- Better handling of content types in form and json methods [#24](https://github.com/miguelgrinberg/microdot/issues/24) ([commit](https://github.com/miguelgrinberg/microdot/commit/da32f23e35f871470a40638e7000e84b0ff6d17f)) - Better handling of content types in form and json methods [#24](https://github.com/miguelgrinberg/microdot/issues/24) ([commit](https://github.com/miguelgrinberg/microdot/commit/da32f23e35f871470a40638e7000e84b0ff6d17f))

9
SECURITY.md Normal file
View File

@@ -0,0 +1,9 @@
# Security Policy
## Reporting a Vulnerability
If you think you've found a vulnerability on this project, please send me (Miguel Grinberg) an email at miguel.grinberg@gmail.com with a description of the problem. I will personally review the issue and respond to you with next steps.
If the issue is highly sensitive, you are welcome to encrypt your message. Here is my [PGP key](https://keyserver.ubuntu.com/pks/lookup?search=miguel.grinberg%40gmail.com&fingerprint=on&op=index).
Please do not disclose vulnerabilities publicly before discussing how to proceed with me.

View File

@@ -1,6 +1,6 @@
[metadata] [metadata]
name = microdot name = microdot
version = 0.6.0 version = 0.7.1
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

@@ -172,6 +172,7 @@ class Request():
"""An HTTP request class. """An HTTP request class.
:var app: The application instance to which this request belongs. :var app: The application instance to which this request belongs.
:var client_addr: The address of the client, as a tuple (host, port).
:var method: The HTTP method of the request. :var method: The HTTP method of the request.
:var path: The path portion of the URL. :var path: The path portion of the URL.
:var query_string: The query string portion of the URL. :var query_string: The query string portion of the URL.
@@ -188,6 +189,24 @@ class Request():
:var g: A general purpose container for applications to store data during :var g: A general purpose container for applications to store data during
the life of the request. the life of the request.
""" """
#: Specify the maximum payload size that is accepted. Requests with larger
#: payloads will be rejected with a 413 status code. Applications can
#: change this maximum as necessary.
#:
#: Example::
#:
#: Request.max_content_length = 1 * 1024 * 1024 # 1MB requests allowed
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
@@ -234,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()
@@ -244,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)
@@ -254,7 +273,8 @@ class Request():
content_length = int(value) content_length = int(value)
# body # body
body = client_stream.read(content_length) if content_length else b'' body = client_stream.read(content_length) if content_length and \
content_length <= Request.max_content_length else b''
return Request(app, client_addr, method, url, http_version, headers, return Request(app, client_addr, method, url, http_version, headers,
body) body)
@@ -287,6 +307,14 @@ 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)
print(line, Request.max_readline)
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.
@@ -296,6 +324,9 @@ class Response():
:param status_code: The numeric HTTP status code of the response. The :param status_code: The numeric HTTP status code of the response. The
default is 200. default is 200.
:param headers: A dictionary of headers to include in the response. :param headers: A dictionary of headers to include in the response.
:param reason: A custom reason phrase to add after the status code. The
default is "OK" for responses with a 200 status code and
"N/A" for any other status codes.
""" """
types_map = { types_map = {
'css': 'text/css', 'css': 'text/css',
@@ -766,6 +797,12 @@ class Microdot():
req = Request.create(self, stream, addr) req = Request.create(self, stream, addr)
if req: if req:
if req.content_length > req.max_content_length:
if 413 in self.error_handlers:
res = self.error_handlers[413](req)
else:
res = 'Payload too large', 413
else:
f = self.find_route(req) f = self.find_route(req)
try: try:
res = None res = None

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)
@@ -54,12 +55,19 @@ class Request(BaseRequest):
content_length = int(value) content_length = int(value)
# body # body
body = await client_stream.read(content_length) \ body = await client_stream.read(content_length) if content_length and \
if content_length else b'' content_length <= Request.max_content_length else b''
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.
@@ -69,6 +77,9 @@ class Response(BaseResponse):
:param status_code: The numeric HTTP status code of the response. The :param status_code: The numeric HTTP status code of the response. The
default is 200. default is 200.
:param headers: A dictionary of headers to include in the response. :param headers: A dictionary of headers to include in the response.
:param reason: A custom reason phrase to add after the status code. The
default is "OK" for responses with a 200 status code and
"N/A" for any other status codes.
""" """
async def write(self, stream): async def write(self, stream):
self.complete() self.complete()
@@ -210,6 +221,13 @@ class Microdot(BaseMicrodot):
req = await Request.create(self, reader, req = await Request.create(self, reader,
writer.get_extra_info('peername')) writer.get_extra_info('peername'))
if req: if req:
if req.content_length > req.max_content_length:
if 413 in self.error_handlers:
res = await self._invoke_handler(
self.error_handlers[413], req)
else:
res = 'Payload too large', 413
else:
f = self.find_route(req) f = self.find_route(req)
try: try:
res = None res = None

View File

@@ -192,6 +192,42 @@ class TestMicrodot(unittest.TestCase):
self.assertIn(b'Content-Type: text/plain\r\n', fd.response) self.assertIn(b'Content-Type: text/plain\r\n', fd.response)
self.assertTrue(fd.response.endswith(b'\r\n\r\n404')) self.assertTrue(fd.response.endswith(b'\r\n\r\n404'))
def test_413(self):
app = Microdot()
@app.route('/')
def index(req):
return 'foo'
mock_socket.clear_requests()
fd = mock_socket.add_request('GET', '/foo', body='x' * 17000)
self._add_shutdown(app)
app.run()
self.assertTrue(fd.response.startswith(b'HTTP/1.0 413 N/A\r\n'))
self.assertIn(b'Content-Length: 17\r\n', fd.response)
self.assertIn(b'Content-Type: text/plain\r\n', fd.response)
self.assertTrue(fd.response.endswith(b'\r\n\r\nPayload too large'))
def test_413_handler(self):
app = Microdot()
@app.route('/')
def index(req):
return 'foo'
@app.errorhandler(413)
def handle_413(req):
return '413', 400
mock_socket.clear_requests()
fd = mock_socket.add_request('GET', '/foo', body='x' * 17000)
self._add_shutdown(app)
app.run()
self.assertTrue(fd.response.startswith(b'HTTP/1.0 400 N/A\r\n'))
self.assertIn(b'Content-Length: 3\r\n', fd.response)
self.assertIn(b'Content-Type: text/plain\r\n', fd.response)
self.assertTrue(fd.response.endswith(b'\r\n\r\n413'))
def test_500(self): def test_500(self):
app = Microdot() app = Microdot()

View File

@@ -78,3 +78,27 @@ class TestRequest(unittest.TestCase):
body='foo=bar&abc=def&x=%2f%%') body='foo=bar&abc=def&x=%2f%%')
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):
saved_max_content_length = Request.max_content_length
Request.max_content_length = 16
fd = get_request_fd('GET', '/foo', headers={
'Content-Type': 'application/x-www-form-urlencoded'},
body='foo=bar&abc=def&x=y')
req = Request.create('app', fd, 'addr')
self.assertEqual(req.body, b'')
Request.max_content_length = saved_max_content_length

View File

@@ -170,6 +170,42 @@ class TestMicrodotAsync(unittest.TestCase):
self.assertIn(b'Content-Type: text/plain\r\n', fd.response) self.assertIn(b'Content-Type: text/plain\r\n', fd.response)
self.assertTrue(fd.response.endswith(b'\r\n\r\n404')) self.assertTrue(fd.response.endswith(b'\r\n\r\n404'))
def test_413(self):
app = Microdot()
@app.route('/')
def index(req):
return 'foo'
mock_socket.clear_requests()
fd = mock_socket.add_request('GET', '/foo', body='x' * 17000)
self._add_shutdown(app)
app.run()
self.assertTrue(fd.response.startswith(b'HTTP/1.0 413 N/A\r\n'))
self.assertIn(b'Content-Length: 17\r\n', fd.response)
self.assertIn(b'Content-Type: text/plain\r\n', fd.response)
self.assertTrue(fd.response.endswith(b'\r\n\r\nPayload too large'))
def test_413_handler(self):
app = Microdot()
@app.route('/')
def index(req):
return 'foo'
@app.errorhandler(413)
async def handle_413(req):
return '413', 400
mock_socket.clear_requests()
fd = mock_socket.add_request('GET', '/foo', body='x' * 17000)
self._add_shutdown(app)
app.run()
self.assertTrue(fd.response.startswith(b'HTTP/1.0 400 N/A\r\n'))
self.assertIn(b'Content-Length: 3\r\n', fd.response)
self.assertIn(b'Content-Type: text/plain\r\n', fd.response)
self.assertTrue(fd.response.endswith(b'\r\n\r\n413'))
def test_500(self): def test_500(self):
app = Microdot() app = Microdot()

View File

@@ -88,3 +88,27 @@ class TestRequestAsync(unittest.TestCase):
body='foo=bar&abc=def&x=%2f%%') body='foo=bar&abc=def&x=%2f%%')
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):
saved_max_content_length = Request.max_content_length
Request.max_content_length = 16
fd = get_async_request_fd('GET', '/foo', headers={
'Content-Type': 'application/x-www-form-urlencoded'},
body='foo=bar&abc=def&x=y')
req = _run(Request.create('app', fd, 'addr'))
self.assertEqual(req.body, b'')
Request.max_content_length = saved_max_content_length