diff --git a/frappe/core/doctype/file/utils.py b/frappe/core/doctype/file/utils.py index 20ad5d7253..b966df9770 100644 --- a/frappe/core/doctype/file/utils.py +++ b/frappe/core/doctype/file/utils.py @@ -480,3 +480,16 @@ def find_file_by_url(path: str, name: str | None = None) -> "File" | None: def get_safe_file_name(file_name: str) -> str: return re.sub(r"[/\\%?#]", "_", file_name) + + +def check_path_safety(base_path: str, requested_path: str) -> bool: + """Util to check path safety by ensuring sandboxing and logging unsuccessful attempts""" + base_path = os.path.realpath(base_path) + requested_path = os.path.realpath(requested_path) + if os.path.commonpath([base_path, requested_path]) != base_path: + frappe.log_error( + title="Attempted Unauthorized File Access", + message=f"Blocked access to: {requested_path}", + ) + return False + return True diff --git a/frappe/utils/response.py b/frappe/utils/response.py index 5fdd459c95..ec48d524ea 100644 --- a/frappe/utils/response.py +++ b/frappe/utils/response.py @@ -26,6 +26,7 @@ import frappe.sessions import frappe.utils from frappe import _ from frappe.core.doctype.access_log.access_log import make_access_log +from frappe.core.doctype.file.utils import check_path_safety from frappe.utils import format_timedelta, orjson_dumps if TYPE_CHECKING: @@ -280,6 +281,13 @@ def download_backup(path): _("You need to be logged in and have System Manager Role to be able to access backups.") ) + filename = path.split("/backups/", 1)[1] + backup_path = frappe.get_site_path("private", "backups") + requested_path = frappe.get_site_path("private", "backups", filename) + is_safe = check_path_safety(base_path=backup_path, requested_path=requested_path) + if not is_safe: + frappe.throw(_("Invalid backup path"), frappe.PermissionError) + return send_private_file(path)