feat: route whitelist/blacklist
security feature middleware to block/allow routes
This commit is contained in:
+39
-6
@@ -20,6 +20,18 @@ from lnbits.helpers import normalize_path, template_renderer
|
|||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
|
|
||||||
_LOCALHOST_IPS = {"127.0.0.1", "::1"}
|
_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:
|
def _route_pattern_matches(pattern: str, path: str) -> bool:
|
||||||
@@ -27,6 +39,11 @@ def _route_pattern_matches(pattern: str, path: str) -> bool:
|
|||||||
return False
|
return False
|
||||||
if not pattern.startswith("/"):
|
if not pattern.startswith("/"):
|
||||||
pattern = f"/{pattern}"
|
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:
|
if pattern == path:
|
||||||
return True
|
return True
|
||||||
escaped = re.escape(pattern)
|
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
|
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:
|
class InstalledExtensionMiddleware:
|
||||||
# This middleware class intercepts calls made to the extensions API and:
|
# This middleware class intercepts calls made to the extensions API and:
|
||||||
# - it blocks the calls if the extension has been disabled or uninstalled.
|
# - 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)
|
return await call_next(request)
|
||||||
|
|
||||||
path = request.url.path or "/"
|
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
|
whitelist = settings.lnbits_route_access_whitelist
|
||||||
blacklist = settings.lnbits_route_access_blacklist
|
blacklist = settings.lnbits_route_access_blacklist
|
||||||
|
|
||||||
if whitelist:
|
if whitelist:
|
||||||
if any(_route_pattern_matches(route, path) for route in whitelist):
|
if any(_route_pattern_matches(route, path) for route in whitelist):
|
||||||
return await call_next(request)
|
return await call_next(request)
|
||||||
return JSONResponse(
|
return _response_by_accepted_type(
|
||||||
status_code=HTTPStatus.FORBIDDEN,
|
request, f"Route not whitelisted: {path}", HTTPStatus.FORBIDDEN
|
||||||
content={"detail": "Route not whitelisted"},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if blacklist and any(_route_pattern_matches(route, path) for route in blacklist):
|
if blacklist and any(_route_pattern_matches(route, path) for route in blacklist):
|
||||||
return JSONResponse(
|
return _response_by_accepted_type(
|
||||||
status_code=HTTPStatus.FORBIDDEN,
|
request, f"Route is blacklisted: {path}", HTTPStatus.FORBIDDEN
|
||||||
content={"detail": "Route is blacklisted"},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return await call_next(request)
|
return await call_next(request)
|
||||||
|
|||||||
@@ -35,6 +35,19 @@ window.app.component('lnbits-admin-security', {
|
|||||||
this.routeOptionsLoading = false
|
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) {
|
filterRouteOptions(val, update) {
|
||||||
update(() => {
|
update(() => {
|
||||||
if (!val) {
|
if (!val) {
|
||||||
|
|||||||
@@ -316,6 +316,8 @@
|
|||||||
use-chips
|
use-chips
|
||||||
use-input
|
use-input
|
||||||
input-debounce="0"
|
input-debounce="0"
|
||||||
|
new-value-mode="add-unique"
|
||||||
|
@new-value="addRouteOption"
|
||||||
:options="routeOptionsFiltered"
|
:options="routeOptionsFiltered"
|
||||||
:loading="routeOptionsLoading"
|
:loading="routeOptionsLoading"
|
||||||
@filter="filterRouteOptions"
|
@filter="filterRouteOptions"
|
||||||
@@ -331,6 +333,8 @@
|
|||||||
use-chips
|
use-chips
|
||||||
use-input
|
use-input
|
||||||
input-debounce="0"
|
input-debounce="0"
|
||||||
|
new-value-mode="add-unique"
|
||||||
|
@new-value="addRouteOption"
|
||||||
:options="routeOptionsFiltered"
|
:options="routeOptionsFiltered"
|
||||||
:loading="routeOptionsLoading"
|
:loading="routeOptionsLoading"
|
||||||
@filter="filterRouteOptions"
|
@filter="filterRouteOptions"
|
||||||
|
|||||||
Reference in New Issue
Block a user