mirror of
https://github.com/bunkerity/bunkerweb
synced 2026-05-24 09:28:37 +00:00
feat: enhance URL validation and sanitization in loading page; improve file extraction safety in plugins
This commit is contained in:
parent
bad9c3cc5e
commit
10c9f29f13
5 changed files with 63 additions and 25 deletions
|
|
@ -1,3 +1,4 @@
|
|||
from html import escape
|
||||
from importlib.machinery import SourceFileLoader
|
||||
from io import BytesIO
|
||||
from json import JSONDecodeError, loads as json_loads
|
||||
|
|
@ -102,7 +103,18 @@ def run_action(plugin: str, function_name: str = "", *, tmp_dir: Optional[Path]
|
|||
tmp_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with tar_open(fileobj=BytesIO(page), mode="r:gz") as tar:
|
||||
tar.extractall(tmp_dir)
|
||||
for member in tar.getmembers():
|
||||
# Prevent absolute paths and paths with '..'
|
||||
if member.name.startswith("/") or ".." in Path(member.name).parts:
|
||||
return {"status": "ko", "code": 400, "message": "Invalid file path"}
|
||||
|
||||
# Construct the target path and ensure it is within tmp_dir
|
||||
target_path = tmp_dir.joinpath(member.name).resolve()
|
||||
if not str(target_path).startswith(str(tmp_dir)):
|
||||
return {"status": "ko", "code": 400, "message": "Invalid file path"}
|
||||
|
||||
# Extract the file safely
|
||||
tar.extract(member, tmp_dir)
|
||||
|
||||
tmp_dir = tmp_dir.joinpath("ui")
|
||||
except BaseException as e:
|
||||
|
|
@ -479,16 +491,15 @@ def custom_plugin_page(plugin: str):
|
|||
return handle_error("Invalid plugin id, (must be between 1 and 64 characters, only letters, numbers, underscores and hyphens)", "plugins")
|
||||
|
||||
if request.method == "POST":
|
||||
|
||||
action_result = run_action(plugin)
|
||||
|
||||
if isinstance(action_result, Response):
|
||||
LOGGER.info(f"Plugin {plugin} action executed successfully")
|
||||
LOGGER.info("Plugin action executed successfully")
|
||||
return action_result
|
||||
|
||||
# case error
|
||||
if action_result["status"] == "ko":
|
||||
return error_message(action_result["message"]), action_result["code"]
|
||||
return error_message(escape(action_result["message"])), action_result["code"]
|
||||
|
||||
LOGGER.info(f"Plugin {plugin} action executed successfully")
|
||||
|
||||
|
|
@ -541,8 +552,19 @@ def custom_plugin_page(plugin: str):
|
|||
tmp_page_dir = TMP_DIR.joinpath("ui", "page", str(uuid4()))
|
||||
tmp_page_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with tar_open(fileobj=BytesIO(page), mode="r:gz") as tar_file:
|
||||
tar_file.extractall(tmp_page_dir)
|
||||
with tar_open(fileobj=BytesIO(page), mode="r:gz") as tar:
|
||||
for member in tar.getmembers():
|
||||
# Prevent absolute paths and paths with '..'
|
||||
if member.name.startswith("/") or ".." in Path(member.name).parts:
|
||||
return {"status": "ko", "code": 400, "message": "Invalid file path"}
|
||||
|
||||
# Construct the target path and ensure it is within tmp_dir
|
||||
target_path = tmp_page_dir.joinpath(member.name).resolve()
|
||||
if not str(target_path).startswith(str(tmp_page_dir)):
|
||||
return {"status": "ko", "code": 400, "message": "the plugin page has an invalid file path"}
|
||||
|
||||
# Extract the file safely
|
||||
tar.extract(member, tmp_page_dir)
|
||||
|
||||
tmp_page_dir = tmp_page_dir.joinpath("ui")
|
||||
|
||||
|
|
@ -570,15 +592,8 @@ def custom_plugin_page(plugin: str):
|
|||
.from_string(page_content)
|
||||
.render(pre_render=pre_render, **current_app.jinja_env.globals)
|
||||
)
|
||||
except BaseException as e:
|
||||
except BaseException:
|
||||
LOGGER.exception("An error occurred while rendering the plugin page")
|
||||
plugin_page = f'<div class="mt-2 mb-2 alert alert-danger text-center" role="alert">An error occurred while rendering the plugin page: {e}<br/>See logs for more details</div>'
|
||||
plugin_page = '<div class="mt-2 mb-2 alert alert-danger text-center" role="alert">An error occurred while rendering the plugin page<br/>See logs for more details</div>'
|
||||
|
||||
return render_template(
|
||||
"plugin_page.html",
|
||||
plugin_page=plugin_page,
|
||||
plugin=plugin_data,
|
||||
is_used=is_used,
|
||||
is_metrics=is_metrics_on,
|
||||
pre_render=pre_render,
|
||||
)
|
||||
return render_template("plugin_page.html", plugin_page=plugin_page, plugin=plugin_data, is_used=is_used, is_metrics=is_metrics_on, pre_render=pre_render)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ from os import environ, getenv
|
|||
# from secrets import choice
|
||||
# from string import ascii_letters, digits
|
||||
from re import escape, match
|
||||
from time import sleep, time
|
||||
from time import sleep
|
||||
|
||||
from flask import Blueprint, Response, flash, redirect, render_template, request, url_for
|
||||
from flask_login import current_user
|
||||
|
|
@ -140,9 +140,6 @@ def setup_page():
|
|||
if not REVERSE_PROXY_PATH.match(request.form["ui_host"]):
|
||||
return handle_error("The hostname is not valid.", "setup")
|
||||
|
||||
DATA["RELOADING"] = True
|
||||
DATA["LAST_RELOAD"] = time()
|
||||
|
||||
base_config = {
|
||||
"SERVER_NAME": request.form["server_name"],
|
||||
"USE_UI": "yes",
|
||||
|
|
|
|||
|
|
@ -5,7 +5,26 @@ $(document).ready(function () {
|
|||
? $targetEndpoint.val().trim()
|
||||
: null;
|
||||
|
||||
const nextEndpoint = $("#next-endpoint").val().trim();
|
||||
let nextEndpoint = $("#next-endpoint").val().trim();
|
||||
|
||||
// Function to validate and sanitize the URL
|
||||
function sanitizeUrl(url) {
|
||||
try {
|
||||
// Ensure the URL is either relative or shares the same origin
|
||||
const validUrl = new URL(url, window.location.origin);
|
||||
return validUrl.href;
|
||||
} catch (e) {
|
||||
console.error("Invalid URL detected:", url);
|
||||
}
|
||||
return null; // Return null if the URL is invalid
|
||||
}
|
||||
|
||||
// Sanitize nextEndpoint
|
||||
nextEndpoint = sanitizeUrl(nextEndpoint);
|
||||
if (!nextEndpoint) {
|
||||
console.error("Invalid or missing nextEndpoint. Redirect aborted.");
|
||||
return; // Abort further execution if the endpoint is invalid
|
||||
}
|
||||
|
||||
// Start the reloading interval to check every 2 seconds
|
||||
var reloadingInterval = setInterval(check_reloading, 2000);
|
||||
|
|
@ -28,8 +47,12 @@ $(document).ready(function () {
|
|||
// Clear all intervals and timeout
|
||||
clearInterval(reloadingInterval);
|
||||
|
||||
// Redirect to the next page with the current hash if present
|
||||
window.location.replace(nextEndpoint + (window.location.hash || ""));
|
||||
// Redirect to the sanitized nextEndpoint with the current hash if present
|
||||
const redirectUrl = new URL(nextEndpoint, window.location.origin);
|
||||
if (window.location.hash) {
|
||||
redirectUrl.hash = window.location.hash;
|
||||
}
|
||||
window.location.href = redirectUrl.href; // Use window.location.href for compatibility
|
||||
}
|
||||
},
|
||||
error: function (jqXHR, textStatus, errorThrown) {
|
||||
|
|
|
|||
|
|
@ -270,8 +270,10 @@ $(document).ready(function () {
|
|||
const content = generateSessionContent(session, index);
|
||||
const placeholder = $(`#session-placeholder-${index}`);
|
||||
|
||||
const sanitizedText = DOMPurify.sanitize(content);
|
||||
|
||||
placeholder
|
||||
.html(content)
|
||||
.html(sanitizedText)
|
||||
.removeClass("placeholder-transition fade-in")
|
||||
.addClass("card-transition")
|
||||
.toggleClass(currentCardClasses, session.current)
|
||||
|
|
|
|||
|
|
@ -354,7 +354,8 @@ $(document).ready(() => {
|
|||
$bannerText.removeClass("slide-out");
|
||||
|
||||
// Update the text content
|
||||
$bannerText.html(newsItems[nextIndex]);
|
||||
const sanitizedText = DOMPurify.sanitize(newsItems[nextIndex]);
|
||||
$bannerText.html(sanitizedText);
|
||||
|
||||
// Trigger reflow to ensure the browser applies the changes
|
||||
$bannerText[0].offsetHeight;
|
||||
|
|
|
|||
Loading…
Reference in a new issue