diff --git a/src/fides/api/common_exceptions.py b/src/fides/api/common_exceptions.py index c12d52946f..45e7937f5d 100644 --- a/src/fides/api/common_exceptions.py +++ b/src/fides/api/common_exceptions.py @@ -223,6 +223,10 @@ class SSHTunnelConfigNotFoundException(Exception): """Exception for when Fides is configured to use an SSH tunnel without config provided.""" +class MalisciousUrlException(Exception): + """Fides has detected a potentially maliscious URL.""" + + class AuthenticationError(HTTPException): """To be raised when attempting to fetch an access token using invalid credentials. diff --git a/src/fides/api/main.py b/src/fides/api/main.py index 000e6049fc..721d301fa7 100644 --- a/src/fides/api/main.py +++ b/src/fides/api/main.py @@ -1,6 +1,7 @@ """ Contains the code that sets up the API. """ +import os import sys from datetime import datetime, timezone from logging import WARNING @@ -11,6 +12,7 @@ from fideslog.sdk.python.event import AnalyticsEvent from loguru import logger from starlette.background import BackgroundTask +from urllib.parse import unquote from uvicorn import Config, Server import fides @@ -21,6 +23,7 @@ log_startup, run_database_startup, ) +from fides.api.common_exceptions import MalisciousUrlException from fides.api.middleware import handle_audit_log_resource from fides.api.schemas.analytics import Event, ExtraData @@ -151,6 +154,17 @@ def read_index() -> Response: return get_admin_index_as_response() +def sanitise_url_path(path: str) -> str: + """Returns a URL path that does not contain any ../ or //""" + path = unquote(path) + path = os.path.normpath(path) + for token in path.split("/"): + if ".." in token: + logger.warning(f"Potentially dangerous use of URL hierarchy in path: {path}") + raise MalisciousUrlException() + return path + + @app.get("/{catchall:path}", response_class=Response, tags=["Default"]) def read_other_paths(request: Request) -> Response: """ @@ -158,6 +172,12 @@ def read_other_paths(request: Request) -> Response: """ # check first if requested file exists (for frontend assets) path = request.path_params["catchall"] + logger.debug(f"Catch all path detected: {path}") + try: + path = sanitise_url_path(path) + except MalisciousUrlException: + # if a maliscious URL is detected, route the user to the index + return get_admin_index_as_response() # search for matching route in package (i.e. /dataset) ui_file = match_route(get_ui_file_map(), path) diff --git a/tests/ops/util/test_api_router.py b/tests/ops/util/test_api_router.py index ab8d16ec64..5b371657a6 100644 --- a/tests/ops/util/test_api_router.py +++ b/tests/ops/util/test_api_router.py @@ -45,3 +45,24 @@ def test_non_existent_route_404( f"{V1_URL_PREFIX}/route/does/not/exist/", headers=auth_header ) assert resp_4.status_code == HTTP_404_NOT_FOUND + + def test_malicious_url( + self, + api_client: TestClient, + url, + ) -> None: + malicious_paths = [ + "../../../../../../../../../etc/passwd", + "..%2f..%2f..%2f..%2f..%2f..%2f..%2f..%2f..%2fetc/passwd", + "%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd", + "%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2f%2e%2e%2fetc/passwd", + "..%c0%2f..%c0%2f..%c0%2f..%c0%2f..%c0%2f..%c0%2f..%c0%2f..%c0%2f..%c0%2f/etc/passwd", + ".../...//.../...//.../...//.../...//.../...//.../...//.../...//.../...//.../...//etc/passwd", + "...%2f...%2f%2f...%2f...%2f%2f...%2f...%2f%2f...%2f...%2f%2f...%2f...%2f%2f...%2f...%2f%2f...%2f...%2f%2f...%2f...%2f%2f...%2f...%2f%2fetc/passwd", + "%2e%2e%2e/%2e%2e%2e//%2e%2e%2e/%2e%2e%2e//%2e%2e%2e/%2e%2e%2e//%2e%2e%2e/%2e%2e%2e//%2e%2e%2e/%2e%2e%2e//%2e%2e%2e/%2e%2e%2e//%2e%2e%2e/%2e%2e%2e//%2e%2e%2e/%2e%2e%2e//%2e%2e%2e/%2e%2e%2e//etc/passwd", + "%2e%2e%2e%2f%2e%2e%2e%2f%2f%2e%2e%2e%2f%2e%2e%2e%2f%2f%2e%2e%2e%2f%2e%2e%2e%2f%2f%2e%2e%2e%2f%2e%2e%2e%2f%2f%2e%2e%2e%2f%2e%2e%2e%2f%2f%2e%2e%2e%2f%2e%2e%2e%2f%2f%2e%2e%2e%2f%2e%2e%2e%2f%2f%2e%2e%2e%2f%2e%2e%2e%2f%2f%2e%2e%2e%2f%2e%2e%2e%2f%2fetc/passwd", + ] + for path in malicious_paths: + resp = api_client.get(f"{url}/{path}") + assert resp.status_code == 200 + assert resp.text == "

Privacy is a Human Right!

"