Many fixes?
All checks were successful
Datadog Software Composition Analysis / Datadog SBOM Generation and Upload (push) Successful in 50s
Datadog Secrets Scanning / Datadog Static Analyzer (push) Successful in 56s
Release / build (push) Successful in 1m23s
Release / publish_head (push) Successful in 1m17s
Datadog Static Analysis / Datadog Static Analyzer (push) Successful in 5m41s
All checks were successful
Datadog Software Composition Analysis / Datadog SBOM Generation and Upload (push) Successful in 50s
Datadog Secrets Scanning / Datadog Static Analyzer (push) Successful in 56s
Release / build (push) Successful in 1m23s
Release / publish_head (push) Successful in 1m17s
Datadog Static Analysis / Datadog Static Analyzer (push) Successful in 5m41s
This commit is contained in:
@ -7,70 +7,105 @@ import os
|
||||
|
||||
|
||||
class RouteManager:
|
||||
"""
|
||||
RouteManager is responsible for handling and validating file system paths for serving content, styles, and static files in a web application. It ensures that all requested paths are securely resolved within configured base directories, prevents path traversal attacks, and restricts access to hidden files or folders.
|
||||
|
||||
Args:
|
||||
config (Configuration): The configuration object containing directory paths for content, templates, and styles.
|
||||
|
||||
Methods:
|
||||
_validate_and_sanitize_path(base_dir, requested_path_str):
|
||||
Validates and sanitizes a requested path to ensure it is within the specified base directory and not a hidden file/folder. Returns a resolved Path object or None if invalid.
|
||||
|
||||
_ensure_route(path):
|
||||
Ensures the given path is valid and returns the corresponding Path object. Raises an Exception if the path is illegal.
|
||||
|
||||
default_route(path):
|
||||
Handles the default route for serving content files. Returns a rendered page or an error page if the path is invalid or not found.
|
||||
|
||||
get_style(path):
|
||||
Serves style files from the styles directory. Returns the file or an error page if the path is invalid or not found.
|
||||
|
||||
get_static(path):
|
||||
Serves static files from the content directory. If the file is an image, generates and returns a thumbnail. Returns the file or an error page if the path is invalid or not found.
|
||||
"""
|
||||
|
||||
def __init__(self, config: Configuration):
|
||||
self.config = config
|
||||
|
||||
def _validate_and_sanitize_path(self, base_dir, requested_path):
|
||||
def _validate_and_sanitize_path(self, base_dir, requested_path_str: str):
|
||||
"""
|
||||
Validate and sanitize the requested path to ensure it does not traverse above the base directory.
|
||||
Validates and sanitizes a requested file system path to ensure it is safe and allowed.
|
||||
|
||||
:param base_dir: The base directory that the requested path should be within.
|
||||
:param requested_path: The requested file path to validate.
|
||||
:return: A secure version of the requested path if valid, otherwise None.
|
||||
This method resolves the requested path relative to a given base directory, ensuring:
|
||||
- The resolved path exists.
|
||||
- The resolved path is within the base directory (prevents directory traversal attacks).
|
||||
- The path does not access hidden files or directories (those starting with '___').
|
||||
|
||||
Args:
|
||||
base_dir (str or Path): The base directory against which the requested path is resolved.
|
||||
requested_path_str (str): The user-supplied path to validate and sanitize.
|
||||
|
||||
Returns:
|
||||
Path or None: The resolved and validated Path object if the path is safe and allowed;
|
||||
otherwise, None if the path is invalid, does not exist, attempts traversal,
|
||||
or accesses hidden files/directories.
|
||||
"""
|
||||
# Normalize both paths
|
||||
base_dir = Path(base_dir)
|
||||
requested_path: Path = base_dir / requested_path
|
||||
try:
|
||||
base_dir = Path(base_dir).resolve(strict=True)
|
||||
# a requested path of "" or "." should resolve to the base directory
|
||||
if not requested_path_str:
|
||||
requested_path_str = "."
|
||||
secure_path = (base_dir / requested_path_str).resolve(strict=True)
|
||||
except FileNotFoundError:
|
||||
return None # Path does not exist
|
||||
|
||||
# Check if the requested path is within the base directory
|
||||
if requested_path < base_dir:
|
||||
# The most important check: ensure the resolved path is inside the base directory.
|
||||
if not secure_path.is_relative_to(base_dir):
|
||||
print(f"Illegal path traversal attempt: {requested_path_str}")
|
||||
return None
|
||||
|
||||
# Ensure the path does not contain any '..' or '.' components
|
||||
secure_path = os.path.relpath(requested_path, base_dir)
|
||||
secure_path_parts = secure_path.split(os.sep)
|
||||
|
||||
for part in secure_path_parts:
|
||||
if part == "." or part == "..":
|
||||
print("Illegal path nice try")
|
||||
return None
|
||||
|
||||
# Reconstruct the secure path
|
||||
secure_path = os.path.join(base_dir, *secure_path_parts)
|
||||
secure_path = Path(secure_path)
|
||||
|
||||
# Check if path exists
|
||||
if not secure_path.exists():
|
||||
raise Exception("Illegal path")
|
||||
|
||||
for part in secure_path.parts:
|
||||
if part.startswith("___"):
|
||||
print("hidden file")
|
||||
raise Exception("Illegal path")
|
||||
# Check for hidden files/folders (starting with '___')
|
||||
relative_parts = secure_path.relative_to(base_dir).parts
|
||||
# Also check the final component for the case where path is the base_dir itself.
|
||||
if any(
|
||||
part.startswith("___") for part in relative_parts
|
||||
) or secure_path.name.startswith("___"):
|
||||
print(f"Illegal access to hidden path: {requested_path_str}")
|
||||
return None
|
||||
|
||||
return secure_path
|
||||
|
||||
def _ensure_route(self, path: str):
|
||||
file_path: Path = self.config.content_dir / (path if path else "index.md")
|
||||
if file_path < self.config.content_dir:
|
||||
raise Exception("Illegal path")
|
||||
|
||||
if not self._validate_and_sanitize_path(
|
||||
self.config.content_dir, str(file_path)
|
||||
):
|
||||
file_path = self._validate_and_sanitize_path(self.config.content_dir, path)
|
||||
if not file_path:
|
||||
raise Exception("Illegal path")
|
||||
return file_path
|
||||
|
||||
def default_route(self, path: str):
|
||||
"""
|
||||
Handles the default route for serving content pages.
|
||||
|
||||
Attempts to resolve the given path to a file within the content directory.
|
||||
If the path is empty, defaults to "index.md". If the file is not found or an error occurs,
|
||||
renders a 404 error page. Otherwise, renders the requested page using the specified
|
||||
template and style directories.
|
||||
|
||||
Args:
|
||||
path (str): The requested path to resolve and serve.
|
||||
|
||||
Returns:
|
||||
Response: The rendered page or an error page if the file is not found.
|
||||
"""
|
||||
try:
|
||||
self._ensure_route(path)
|
||||
except Exception as e:
|
||||
file_path = self._ensure_route(path if path else "index.md")
|
||||
except Exception as _:
|
||||
return render_error_page(
|
||||
404,
|
||||
"Not Found",
|
||||
"The requested resource was not found on this server.",
|
||||
self.config.templates_dir,
|
||||
)
|
||||
file_path: Path = self.config.content_dir / (path if path else "index.md")
|
||||
return render_page(
|
||||
file_path,
|
||||
base_path=self.config.content_dir,
|
||||
@ -79,19 +114,45 @@ class RouteManager:
|
||||
)
|
||||
|
||||
def get_style(self, path: str):
|
||||
try:
|
||||
self._validate_and_sanitize_path(self.config.styles_dir, path)
|
||||
except Exception as e:
|
||||
"""
|
||||
Retrieves and serves a style file from the configured styles directory.
|
||||
|
||||
Args:
|
||||
path (str): The relative path to the requested style file.
|
||||
|
||||
Returns:
|
||||
Response: A Flask response object containing the requested file if found,
|
||||
or an error page with a 404 status code if the file does not exist.
|
||||
"""
|
||||
file_path = self._validate_and_sanitize_path(self.config.styles_dir, path)
|
||||
if not file_path:
|
||||
return render_error_page(
|
||||
404,
|
||||
"Not Found",
|
||||
f"The requested resource was not found on this server. {e}",
|
||||
"The requested resource was not found on this server.",
|
||||
self.config.templates_dir,
|
||||
)
|
||||
file_path: Path = self.config.styles_dir / path
|
||||
if file_path.exists():
|
||||
return send_file(file_path)
|
||||
else:
|
||||
return send_file(file_path)
|
||||
|
||||
def get_static(self, path: str):
|
||||
"""
|
||||
Serves static files from the configured content directory.
|
||||
|
||||
If the requested file is an image (JPEG, PNG, or GIF), generates and returns a thumbnail
|
||||
with a maximum width specified by the 'max_width' query parameter (default: 2048).
|
||||
Otherwise, serves the file as-is.
|
||||
|
||||
Args:
|
||||
path (str): The relative path to the requested static file.
|
||||
|
||||
Returns:
|
||||
Response:
|
||||
- If the file is not found or invalid, returns a rendered 404 error page.
|
||||
- If the file is an image, returns the thumbnail bytes with appropriate headers.
|
||||
- Otherwise, returns the file using Flask's send_file.
|
||||
"""
|
||||
file_path = self._validate_and_sanitize_path(self.config.content_dir, path)
|
||||
if not file_path:
|
||||
return render_error_page(
|
||||
404,
|
||||
"Not Found",
|
||||
@ -99,35 +160,18 @@ class RouteManager:
|
||||
self.config.templates_dir,
|
||||
)
|
||||
|
||||
def get_static(self, path: str):
|
||||
try:
|
||||
self._validate_and_sanitize_path(self.config.content_dir, path)
|
||||
except Exception as e:
|
||||
return render_error_page(
|
||||
404,
|
||||
"Not Found",
|
||||
"The requested resource was not found on this server.",
|
||||
self.config.templates_dir,
|
||||
# Check to see if the file is an image, if it is, render a thumbnail
|
||||
if file_path.suffix.lower() in [".jpg", ".jpeg", ".png", ".gif"]:
|
||||
max_width = request.args.get("max_width", default=2048, type=int)
|
||||
thumbnail_bytes, img_format = generate_thumbnail(
|
||||
str(file_path), 10, 2048, max_width
|
||||
)
|
||||
file_path: Path = self.config.content_dir / path
|
||||
if file_path.exists():
|
||||
# Check to see if the file is an image, if it is, render a thumbnail
|
||||
if file_path.suffix.lower() in [".jpg", ".jpeg", ".png", ".gif"]:
|
||||
max_width = request.args.get("max_width", default=2048, type=int)
|
||||
thumbnail_bytes, img_format = generate_thumbnail(
|
||||
str(file_path), 10, 2048, max_width
|
||||
)
|
||||
return (
|
||||
thumbnail_bytes,
|
||||
200,
|
||||
{"Content-Type": f"image/{img_format.lower()}",
|
||||
"cache-control": "public, max-age=31536000"},
|
||||
)
|
||||
return send_file(file_path)
|
||||
else:
|
||||
return render_error_page(
|
||||
404,
|
||||
"Not Found",
|
||||
"The requested resource was not found on this server.",
|
||||
self.config.templates_dir,
|
||||
return (
|
||||
thumbnail_bytes,
|
||||
200,
|
||||
{
|
||||
"Content-Type": f"image/{img_format.lower()}",
|
||||
"cache-control": "public, max-age=31536000",
|
||||
},
|
||||
)
|
||||
return send_file(file_path)
|
||||
|
Reference in New Issue
Block a user