Skip to content

Commit 725c811

Browse files
committed
Added checking for .. and backslash in path, introduced custome exceptions
1 parent e7a2deb commit 725c811

File tree

3 files changed

+123
-24
lines changed

3 files changed

+123
-24
lines changed

adafruit_httpserver/exceptions.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2022 Dan Halbert for Adafruit Industries
2+
#
3+
# SPDX-License-Identifier: MIT
4+
"""
5+
`adafruit_httpserver.exceptions`
6+
====================================================
7+
* Author(s): Michał Pokusa
8+
"""
9+
10+
11+
class InvalidPathError(Exception):
12+
"""
13+
Parent class for all path related errors.
14+
"""
15+
16+
17+
class ParentDirectoryReferenceError(InvalidPathError):
18+
"""
19+
Path contains ``..``, a reference to the parent directory.
20+
"""
21+
22+
def __init__(self, path: str) -> None:
23+
"""Creates a new ``ParentDirectoryReferenceError`` for the ``path``."""
24+
super().__init__(f"Parent directory reference in path: {path}")
25+
26+
27+
class BackslashInPathError(InvalidPathError):
28+
"""
29+
Backslash ``\\`` in path.
30+
"""
31+
32+
def __init__(self, path: str) -> None:
33+
"""Creates a new ``BackslashInPathError`` for the ``path``."""
34+
super().__init__(f"Backslash in path: {path}")
35+
36+
37+
class ResponseAlreadySentException(Exception):
38+
"""
39+
Another ``HTTPResponse`` has already been sent. There can only be one per ``HTTPRequest``.
40+
"""
41+
42+
43+
class FileNotExistsError(Exception):
44+
"""
45+
Raised when a file does not exist.
46+
"""
47+
48+
def __init__(self, path: str) -> None:
49+
"""
50+
Creates a new ``FileNotExistsError`` for the file at ``path``.
51+
"""
52+
super().__init__(f"File does not exist: {path}")

adafruit_httpserver/response.py

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@
1717
import os
1818
from errno import EAGAIN, ECONNRESET
1919

20+
from .exceptions import (
21+
BackslashInPathError,
22+
FileNotExistsError,
23+
ParentDirectoryReferenceError,
24+
ResponseAlreadySentException,
25+
)
2026
from .mime_type import MIMEType
2127
from .request import HTTPRequest
2228
from .status import HTTPStatus, CommonHTTPStatus
@@ -153,7 +159,7 @@ def send(
153159
Should be called **only once** per response.
154160
"""
155161
if self._response_already_sent:
156-
raise RuntimeError("Response was already sent")
162+
raise ResponseAlreadySentException
157163

158164
if getattr(body, "encode", None):
159165
encoded_response_message_body = body.encode("utf-8")
@@ -167,11 +173,39 @@ def send(
167173
self._send_bytes(self.request.connection, encoded_response_message_body)
168174
self._response_already_sent = True
169175

176+
@staticmethod
177+
def _check_file_path_is_valid(file_path: str) -> bool:
178+
"""
179+
Checks if ``file_path`` is valid.
180+
If not raises error corresponding to the problem.
181+
"""
182+
183+
# Check for backslashes
184+
if "\\" in file_path: # pylint: disable=anomalous-backslash-in-string
185+
raise BackslashInPathError(file_path)
186+
187+
# Check each component of the path for parent directory references
188+
for part in file_path.split("/"):
189+
if part == "..":
190+
raise ParentDirectoryReferenceError(file_path)
191+
192+
@staticmethod
193+
def _get_file_length(file_path: str) -> int:
194+
"""
195+
Tries to get the length of the file at ``file_path``.
196+
Raises ``FileNotExistsError`` if file does not exist.
197+
"""
198+
try:
199+
return os.stat(file_path)[6]
200+
except OSError:
201+
raise FileNotExistsError(file_path) # pylint: disable=raise-missing-from
202+
170203
def send_file(
171204
self,
172205
filename: str = "index.html",
173206
root_path: str = "./",
174207
buffer_size: int = 1024,
208+
safe: bool = True,
175209
) -> None:
176210
"""
177211
Send response with content of ``filename`` located in ``root_path``.
@@ -181,23 +215,24 @@ def send_file(
181215
Should be called **only once** per response.
182216
"""
183217
if self._response_already_sent:
184-
raise RuntimeError("Response was already sent")
218+
raise ResponseAlreadySentException
219+
220+
if safe:
221+
self._check_file_path_is_valid(filename)
185222

186223
if not root_path.endswith("/"):
187224
root_path += "/"
188-
try:
189-
file_length = os.stat(root_path + filename)[6]
190-
except OSError:
191-
# If the file doesn't exist, return 404.
192-
HTTPResponse(self.request, status=CommonHTTPStatus.NOT_FOUND_404).send()
193-
return
225+
226+
full_file_path = root_path + filename
227+
228+
file_length = self._get_file_length(full_file_path)
194229

195230
self._send_headers(
196231
content_type=MIMEType.from_file_name(filename),
197232
content_length=file_length,
198233
)
199234

200-
with open(root_path + filename, "rb") as file:
235+
with open(full_file_path, "rb") as file:
201236
while bytes_read := file.read(buffer_size):
202237
self._send_bytes(self.request.connection, bytes_read)
203238
self._response_already_sent = True

adafruit_httpserver/server.py

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
from errno import EAGAIN, ECONNRESET, ETIMEDOUT
1818

19+
from .exceptions import FileNotExistsError, InvalidPathError
1920
from .methods import HTTPMethod
2021
from .request import HTTPRequest
2122
from .response import HTTPResponse
@@ -160,22 +161,33 @@ def poll(self):
160161
_HTTPRoute(request.path, request.method)
161162
)
162163

163-
# If a handler for route exists and is callable, call it.
164-
if handler is not None and callable(handler):
165-
handler(request)
166-
167-
# If no handler exists and request method is GET, try to serve a file.
168-
elif handler is None and request.method == HTTPMethod.GET:
169-
filename = "index.html" if request.path == "/" else request.path
170-
HTTPResponse(request).send_file(
171-
filename=filename,
172-
root_path=self.root_path,
173-
buffer_size=self.request_buffer_size,
164+
try:
165+
# If a handler for route exists and is callable, call it.
166+
if handler is not None and callable(handler):
167+
handler(request)
168+
169+
# If no handler exists and request method is GET, try to serve a file.
170+
elif handler is None and request.method == HTTPMethod.GET:
171+
filename = "index.html" if request.path == "/" else request.path
172+
HTTPResponse(request).send_file(
173+
filename=filename,
174+
root_path=self.root_path,
175+
buffer_size=self.request_buffer_size,
176+
)
177+
else:
178+
HTTPResponse(
179+
request, status=CommonHTTPStatus.BAD_REQUEST_400
180+
).send()
181+
182+
except InvalidPathError as error:
183+
HTTPResponse(request, status=CommonHTTPStatus.FORBIDDEN_403).send(
184+
str(error)
185+
)
186+
187+
except FileNotExistsError as error:
188+
HTTPResponse(request, status=CommonHTTPStatus.NOT_FOUND_404).send(
189+
str(error)
174190
)
175-
else:
176-
HTTPResponse(
177-
request, status=CommonHTTPStatus.BAD_REQUEST_400
178-
).send()
179191

180192
except OSError as error:
181193
# Handle EAGAIN and ECONNRESET

0 commit comments

Comments
 (0)