From dadff18a9c8f8fbae825ea1d18aa047bf4e2e29b Mon Sep 17 00:00:00 2001 From: Arc Date: Thu, 29 Jan 2026 18:11:28 +0000 Subject: [PATCH] feat: route whitelist/blacklist security feature middleware to block/allow routes --- lnbits/middleware.py | 45 ++++++++++++++++--- .../components/admin/lnbits-admin-security.js | 13 ++++++ .../templates/components/admin/security.vue | 4 ++ 3 files changed, 56 insertions(+), 6 deletions(-) diff --git a/lnbits/middleware.py b/lnbits/middleware.py index 784421b17..4fd7077c4 100644 --- a/lnbits/middleware.py +++ b/lnbits/middleware.py @@ -20,6 +20,18 @@ from lnbits.helpers import normalize_path, template_renderer from lnbits.settings import settings _LOCALHOST_IPS = {"127.0.0.1", "::1"} +_PUBLIC_ASSET_PATHS = { + "/favicon.ico", + "/service-worker.js", +} + + +def _normalize_match_path(path: str) -> str: + if not path: + return "/" + if path != "/" and path.endswith("/"): + return path.rstrip("/") + return path def _route_pattern_matches(pattern: str, path: str) -> bool: @@ -27,6 +39,11 @@ def _route_pattern_matches(pattern: str, path: str) -> bool: return False if not pattern.startswith("/"): pattern = f"/{pattern}" + pattern = _normalize_match_path(pattern) + path = _normalize_match_path(path) + if pattern.endswith("*"): + prefix = pattern.rstrip("*") + return path.startswith(prefix) if pattern == path: return True escaped = re.escape(pattern) @@ -34,6 +51,22 @@ def _route_pattern_matches(pattern: str, path: str) -> bool: return re.fullmatch(escaped, path) is not None +def _response_by_accepted_type(request: Request, msg: str, status_code: HTTPStatus): + accept_header = request.headers.get("accept", "") + if "text/html" in accept_header.split(","): + return HTMLResponse( + status_code=status_code, + content=template_renderer() + .TemplateResponse( + request, + "error.html", + {"err": msg, "status_code": status_code, "message": msg}, + ) + .body, + ) + return JSONResponse(status_code=status_code, content={"detail": msg}) + + class InstalledExtensionMiddleware: # This middleware class intercepts calls made to the extensions API and: # - it blocks the calls if the extension has been disabled or uninstalled. @@ -264,21 +297,21 @@ def add_route_access_middleware(app: FastAPI): return await call_next(request) path = request.url.path or "/" + if "/static/" in path or path in _PUBLIC_ASSET_PATHS: + return await call_next(request) whitelist = settings.lnbits_route_access_whitelist blacklist = settings.lnbits_route_access_blacklist if whitelist: if any(_route_pattern_matches(route, path) for route in whitelist): return await call_next(request) - return JSONResponse( - status_code=HTTPStatus.FORBIDDEN, - content={"detail": "Route not whitelisted"}, + return _response_by_accepted_type( + request, f"Route not whitelisted: {path}", HTTPStatus.FORBIDDEN ) if blacklist and any(_route_pattern_matches(route, path) for route in blacklist): - return JSONResponse( - status_code=HTTPStatus.FORBIDDEN, - content={"detail": "Route is blacklisted"}, + return _response_by_accepted_type( + request, f"Route is blacklisted: {path}", HTTPStatus.FORBIDDEN ) return await call_next(request) diff --git a/lnbits/static/js/components/admin/lnbits-admin-security.js b/lnbits/static/js/components/admin/lnbits-admin-security.js index 779b09d1f..6c21850c2 100644 --- a/lnbits/static/js/components/admin/lnbits-admin-security.js +++ b/lnbits/static/js/components/admin/lnbits-admin-security.js @@ -35,6 +35,19 @@ window.app.component('lnbits-admin-security', { this.routeOptionsLoading = false } }, + addRouteOption(value, done) { + const route = value.trim() + if (!route) { + done() + return + } + if (!this.routeOptions.includes(route)) { + this.routeOptions.push(route) + this.routeOptions.sort() + } + this.routeOptionsFiltered = this.routeOptions + done(route) + }, filterRouteOptions(val, update) { update(() => { if (!val) { diff --git a/lnbits/templates/components/admin/security.vue b/lnbits/templates/components/admin/security.vue index 30fc30595..587f9bcd0 100644 --- a/lnbits/templates/components/admin/security.vue +++ b/lnbits/templates/components/admin/security.vue @@ -316,6 +316,8 @@ use-chips use-input input-debounce="0" + new-value-mode="add-unique" + @new-value="addRouteOption" :options="routeOptionsFiltered" :loading="routeOptionsLoading" @filter="filterRouteOptions" @@ -331,6 +333,8 @@ use-chips use-input input-debounce="0" + new-value-mode="add-unique" + @new-value="addRouteOption" :options="routeOptionsFiltered" :loading="routeOptionsLoading" @filter="filterRouteOptions"