mirror of
https://github.com/bunkerity/bunkerweb
synced 2026-05-24 09:28:37 +00:00
fix: fix Open Redirect Vulnerability in Loading Page + fix shenanigans with setup wizard
This commit is contained in:
parent
cf64ad4a16
commit
220732b74f
4 changed files with 102 additions and 24 deletions
|
|
@ -4,6 +4,13 @@ access_by_lua_block {
|
|||
local scheme = ngx_var.scheme
|
||||
local http_host = ngx_var.http_host
|
||||
local request_uri = ngx_var.request_uri
|
||||
local x_requested_with = ngx.req.get_headers()["X-Requested-With"]
|
||||
|
||||
-- Bypass redirect for AJAX requests
|
||||
if x_requested_with and x_requested_with:lower() == "xmlhttprequest" then
|
||||
return
|
||||
end
|
||||
|
||||
if scheme == "http" and http_host ~= nil and http_host ~= "" and request_uri and request_uri ~= "" then
|
||||
return ngx.redirect("https://" .. http_host .. request_uri, ngx.HTTP_MOVED_PERMANENTLY)
|
||||
end
|
||||
|
|
@ -88,6 +95,53 @@ location /setup {
|
|||
}
|
||||
}
|
||||
|
||||
location /setup/loading {
|
||||
etag off;
|
||||
add_header Last-Modified "";
|
||||
proxy_pass $backendui;
|
||||
|
||||
# Proxy headers
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $http_host;
|
||||
proxy_set_header X-Forwarded-Prefix "/";
|
||||
|
||||
# WebSocket support
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
|
||||
# Timeouts
|
||||
proxy_connect_timeout 60s;
|
||||
proxy_read_timeout 60s;
|
||||
proxy_send_timeout 60s;
|
||||
|
||||
# Buffering
|
||||
proxy_buffering on;
|
||||
|
||||
# Response body modifications
|
||||
header_filter_by_lua_block {
|
||||
ngx.header.content_length = nil
|
||||
}
|
||||
body_filter_by_lua_block {
|
||||
local str = ngx.arg[1]
|
||||
local patterns = {
|
||||
["/css"] = "/setup/css",
|
||||
["/fonts"] = "/setup/fonts",
|
||||
["/img"] = "/setup/img",
|
||||
["/js"] = "/setup/js",
|
||||
["/libs"] = "/setup/libs",
|
||||
}
|
||||
|
||||
for pattern, replacement in pairs(patterns) do
|
||||
str = ngx.re.gsub(str, pattern, replacement, "ijo")
|
||||
end
|
||||
ngx.arg[1] = str
|
||||
}
|
||||
}
|
||||
|
||||
location /setup/check {
|
||||
etag off;
|
||||
add_header Last-Modified "";
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
from datetime import datetime, timedelta
|
||||
from itertools import chain
|
||||
from os import environ, getenv
|
||||
|
||||
# from secrets import choice
|
||||
# from string import ascii_letters, digits
|
||||
from threading import Thread
|
||||
from time import time
|
||||
from re import escape, match
|
||||
from time import sleep, time
|
||||
|
||||
from flask import Blueprint, Response, flash, redirect, render_template, request, url_for
|
||||
from flask_login import current_user
|
||||
|
|
@ -14,7 +15,7 @@ from flask_login import current_user
|
|||
from app.dependencies import BW_CONFIG, DATA, DB
|
||||
from app.utils import LOGGER, USER_PASSWORD_RX, gen_password_hash
|
||||
|
||||
from app.routes.utils import REVERSE_PROXY_PATH, handle_error, manage_bunkerweb
|
||||
from app.routes.utils import REVERSE_PROXY_PATH, handle_error
|
||||
|
||||
setup = Blueprint("setup", __name__)
|
||||
|
||||
|
|
@ -194,17 +195,17 @@ def setup_page():
|
|||
|
||||
LOGGER.debug(f"Creating new service with base_config: {base_config} and config: {config}")
|
||||
|
||||
operation, error = BW_CONFIG.new_service(base_config, override_method="wizard")
|
||||
operation, error = BW_CONFIG.new_service(base_config, override_method="wizard", check_changes=False)
|
||||
if error:
|
||||
return handle_error(f"Couldn't create the new service: {operation}", "setup", False, "error")
|
||||
|
||||
# deepcode ignore MissingAPI: We don't need to check to wait for the thread to finish
|
||||
Thread(
|
||||
target=manage_bunkerweb,
|
||||
name="Reloading instances",
|
||||
args=("services", config | base_config, request.form["server_name"], request.form["server_name"]),
|
||||
kwargs={"operation": "edit", "threaded": True},
|
||||
).start()
|
||||
operation, error = BW_CONFIG.edit_service(request.form["server_name"], config | base_config, check_changes=False)
|
||||
if error:
|
||||
return handle_error(f"Couldn't edit the new service: {operation}", "setup", False, "error")
|
||||
|
||||
err = DB.checked_changes(["config", "custom_configs"], plugins_changes="all", value=True)
|
||||
if err:
|
||||
LOGGER.error(f"Error while applying changes to the database: {err}, you may need to reload the application")
|
||||
|
||||
return Response(status=200)
|
||||
|
||||
|
|
@ -243,13 +244,30 @@ def setup_loading():
|
|||
if current_user.is_authenticated:
|
||||
return redirect(url_for("home.home_page"))
|
||||
|
||||
if DB.get_ui_user():
|
||||
db_config = BW_CONFIG.get_config(methods=False, filtered_settings=("SERVER_NAME", "USE_UI", "REVERSE_PROXY_URL"))
|
||||
for server_name in db_config["SERVER_NAME"].split(" "):
|
||||
if server_name and db_config.get(f"{server_name}_USE_UI", db_config.get("USE_UI", "no")) == "yes":
|
||||
DATA.load_from_file()
|
||||
|
||||
db_config = DB.get_config(filtered_settings=("SERVER_NAME", "USE_UI", "REVERSE_PROXY_URL"))
|
||||
ui_service = {}
|
||||
ui_admin = DB.get_ui_user()
|
||||
admin_old_enough = ui_admin and ui_admin.creation_date < datetime.now() - timedelta(minutes=5)
|
||||
|
||||
for server_name in db_config["SERVER_NAME"].split(" "):
|
||||
if server_name and db_config.get(f"{server_name}_USE_UI", "no") == "yes":
|
||||
if admin_old_enough:
|
||||
return redirect(url_for("login.login_page"), 301)
|
||||
ui_service = {"server_name": server_name, "url": db_config.get(f"{server_name}_REVERSE_PROXY_URL", "/")}
|
||||
break
|
||||
|
||||
if not ui_service:
|
||||
sleep(1)
|
||||
return redirect(url_for("setup.setup_loading"))
|
||||
|
||||
target_endpoint = request.args.get("target_endpoint", "")
|
||||
if target_endpoint and not match(
|
||||
rf"^https://{escape(ui_service['server_name'])}{escape(ui_service['url'])}/check$".replace("//check", "/check"), target_endpoint
|
||||
):
|
||||
return Response(status=400)
|
||||
|
||||
return render_template(
|
||||
"loading.html",
|
||||
message="Setting up Web UI...",
|
||||
|
|
|
|||
|
|
@ -636,7 +636,9 @@ $(document).ready(() => {
|
|||
})
|
||||
.then((res) => {
|
||||
if (res.status === 200) {
|
||||
window.location.href = redirect;
|
||||
setTimeout(() => {
|
||||
window.location.href = redirect;
|
||||
}, 1000);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
|
|
|
|||
|
|
@ -141,7 +141,7 @@ with app.app_context():
|
|||
@app.context_processor
|
||||
def inject_variables():
|
||||
current_endpoint = request.path.split("/")[-1]
|
||||
if request.path.startswith(("/check_reloading", "/setup", "/loading", "/login", "/totp")):
|
||||
if request.path.startswith(("/check", "/setup", "/loading", "/login", "/totp")):
|
||||
return dict(current_endpoint=current_endpoint, script_nonce=app.config["SCRIPT_NONCE"])
|
||||
|
||||
DATA.load_from_file()
|
||||
|
|
@ -329,7 +329,7 @@ def before_request():
|
|||
|
||||
DB.readonly = DATA.get("READONLY_MODE", False)
|
||||
|
||||
if not request.path.startswith(("/check_reloading", "/loading", "/login", "/totp")) and DB.readonly:
|
||||
if not request.path.startswith(("/check", "/loading", "/login", "/totp")) and DB.readonly:
|
||||
flask_flash("Database connection is in read-only mode : no modifications possible.", "error")
|
||||
|
||||
if current_user.is_authenticated:
|
||||
|
|
@ -410,7 +410,7 @@ def set_security_headers(response):
|
|||
### * MISC ROUTES * ###
|
||||
|
||||
|
||||
@app.route("/", strict_slashes=False)
|
||||
@app.route("/", strict_slashes=False, methods=["GET"])
|
||||
def index():
|
||||
if DB.get_ui_user():
|
||||
if current_user.is_authenticated: # type: ignore
|
||||
|
|
@ -419,12 +419,16 @@ def index():
|
|||
return redirect(url_for("setup.setup_page"), 301)
|
||||
|
||||
|
||||
@app.route("/loading")
|
||||
@app.route("/loading", methods=["GET"])
|
||||
@login_required
|
||||
def loading():
|
||||
return render_template(
|
||||
"loading.html", message=request.values.get("message", "Loading..."), next=request.values.get("next", None) or url_for("home.home_page")
|
||||
)
|
||||
home_url = url_for("home.home_page")
|
||||
next_url = request.values.get("next", None) or home_url
|
||||
|
||||
if not next_url.startswith(home_url.replace("/home", "/", 1).replace("//", "/")):
|
||||
return Response(status=400)
|
||||
|
||||
return render_template("loading.html", message=request.values.get("message", "Loading..."), next=next_url)
|
||||
|
||||
|
||||
@app.route("/check", methods=["GET"])
|
||||
|
|
@ -433,7 +437,7 @@ def check():
|
|||
return Response(status=200, headers={"Access-Control-Allow-Origin": "*"}, response=dumps({"message": "ok"}), content_type="application/json")
|
||||
|
||||
|
||||
@app.route("/check_reloading")
|
||||
@app.route("/check_reloading", methods=["GET"])
|
||||
@login_required
|
||||
def check_reloading():
|
||||
DATA.load_from_file()
|
||||
|
|
|
|||
Loading…
Reference in a new issue