fix: fix Open Redirect Vulnerability in Loading Page + fix shenanigans with setup wizard

This commit is contained in:
Théophile Diot 2024-11-25 12:07:38 +01:00
parent cf64ad4a16
commit 220732b74f
No known key found for this signature in database
GPG key ID: FA995104A0BA376A
4 changed files with 102 additions and 24 deletions

View file

@ -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 "";

View file

@ -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...",

View file

@ -636,7 +636,9 @@ $(document).ready(() => {
})
.then((res) => {
if (res.status === 200) {
window.location.href = redirect;
setTimeout(() => {
window.location.href = redirect;
}, 1000);
}
})
.catch((err) => {

View file

@ -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()