feat: route whitelist/blacklist

security feature middleware to block/allow routes
This commit is contained in:
Arc
2026-01-29 18:11:28 +00:00
parent 87c1684a63
commit dadff18a9c
3 changed files with 56 additions and 6 deletions
+39 -6
View File
@@ -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)
@@ -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) {
@@ -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"