Fix BadHost path handling

This commit is contained in:
BigBodyCobain
2026-05-28 01:24:14 -06:00
parent 41799f9891
commit 017f383096
6 changed files with 95 additions and 22 deletions
+10 -8
View File
@@ -113,8 +113,14 @@ def _scoped_admin_tokens() -> dict[str, list[str]]:
return normalized
def _request_scope_path(request: Request) -> str:
"""Return the ASGI request-line path, not the Host-derived URL path."""
scope = getattr(request, "scope", {}) or {}
return str(scope.get("path") or "")
def _required_scope_for_request(request: Request) -> str:
path = str(request.url.path or "")
path = _request_scope_path(request)
if path.startswith("/api/wormhole/gate/"):
return "gate"
if path.startswith("/api/wormhole/dm/"):
@@ -443,7 +449,7 @@ async def _verify_openclaw_hmac(request: Request) -> bool:
# Compute expected signature: HMAC-SHA256(secret, METHOD|path|ts|nonce|body_digest)
method = str(request.method or "").upper()
path = str(request.url.path or "")
path = _request_scope_path(request)
message = f"{method}|{path}|{ts_str}|{nonce}|{body_digest}"
expected = hmac.new(
secret.encode("utf-8"),
@@ -744,8 +750,7 @@ def _is_debug_test_request(request: Request) -> bool:
if not _debug_mode_enabled():
return False
client_host = (request.client.host or "").lower() if request.client else ""
url_host = (request.url.hostname or "").lower() if request.url else ""
return client_host == "test" or url_host == "test"
return client_host == "test"
# ---------------------------------------------------------------------------
@@ -1397,10 +1402,7 @@ def _peer_hmac_url_from_request(request: Request) -> str:
header_url = normalize_peer_url(str(request.headers.get("x-peer-url", "") or ""))
if header_url:
return header_url
if not request.url:
return ""
base_url = f"{request.url.scheme}://{request.url.netloc}".rstrip("/")
return normalize_peer_url(base_url)
return ""
def _verify_peer_push_hmac(request: Request, body_bytes: bytes) -> bool:
+5 -4
View File
@@ -310,6 +310,7 @@ from auth import (
_private_plane_access_denied_payload,
_private_infonet_policy_snapshot,
_private_plane_refusal_response,
_request_scope_path,
_scoped_admin_tokens,
_scoped_view_authenticated as _scoped_view_authenticated_auth,
_security_headers,
@@ -2728,7 +2729,7 @@ async def json_decode_error_handler(_request: Request, _exc: JSONDecodeError):
@app.exception_handler(StarletteHTTPException)
async def private_plane_http_exception_handler(request: Request, exc: StarletteHTTPException):
if exc.status_code == 403 and _is_private_plane_access_path(request.url.path, request.method):
if exc.status_code == 403 and _is_private_plane_access_path(_request_scope_path(request), request.method):
return await _private_plane_refusal_response(
request,
status_code=403,
@@ -2762,7 +2763,7 @@ async def mesh_security_headers(request: Request, call_next):
@app.middleware("http")
async def mesh_no_store_headers(request: Request, call_next):
response = await call_next(request)
if request.url.path.startswith("/api/mesh/"):
if _request_scope_path(request).startswith("/api/mesh/"):
response.headers["Cache-Control"] = "no-store, max-age=0"
response.headers["Pragma"] = "no-cache"
return response
@@ -3464,7 +3465,7 @@ def _refresh_lookup_handle_rotation_background(*, reason: str) -> dict[str, Any]
@app.middleware("http")
async def enforce_high_privacy_mesh(request: Request, call_next):
path = request.url.path
path = _request_scope_path(request)
private_mesh_path = path.startswith("/api/mesh") and not _is_public_meshtastic_lane_path(
path,
request.method,
@@ -3624,7 +3625,7 @@ async def enforce_high_privacy_mesh(request: Request, call_next):
@app.middleware("http")
async def apply_no_store_to_sensitive_paths(request: Request, call_next):
response = await call_next(request)
if _is_sensitive_no_store_path(request.url.path):
if _is_sensitive_no_store_path(_request_scope_path(request)):
for key, value in _NO_STORE_HEADERS.items():
response.headers[key] = value
return response
+2 -1
View File
@@ -15,7 +15,7 @@ dependencies = [
"cachetools==5.5.2",
"cryptography>=41.0.0",
"defusedxml>=0.7.1",
"fastapi==0.115.12",
"fastapi==0.136.3",
"feedparser==6.0.10",
"httpx==0.28.1",
"playwright==1.59.0",
@@ -33,6 +33,7 @@ dependencies = [
"paho-mqtt>=1.6.0,<2.0.0",
"PyNaCl>=1.5.0",
"slowapi==0.1.9",
"starlette==1.0.1",
"vaderSentiment>=3.3.0",
"uvicorn==0.34.0",
"yfinance==1.3.0",
+7 -2
View File
@@ -38,6 +38,11 @@ _REVOCATION_TTL_CACHE: dict[str, dict[str, Any]] = {}
_REVOCATION_TTL_LOCK = threading.Lock()
_REVOCATION_REFRESH_LOCK = threading.Lock()
_REVOCATION_REFRESH_FAIL_FAST_WINDOW_S = 5.0
def _request_scope_path(request: Request) -> str:
scope = getattr(request, "scope", {}) or {}
return str(scope.get("path") or "")
_REVOCATION_REFRESH_RETRY_AFTER_S = 5
_REVOCATION_PRECHECK_UNAVAILABLE_DETAIL = "Signed event integrity preflight unavailable"
@@ -166,7 +171,7 @@ def _canonical_signed_write_retry_payload(
signed_context = build_signed_context(
event_type=prepared.event_type,
kind=prepared.kind.value,
endpoint=str(request.url.path or ""),
endpoint=_request_scope_path(request),
lane_floor=_content_private_required_transport_tier(prepared.kind),
sequence_domain=_signed_context_sequence_domain(prepared),
node_id=prepared.node_id,
@@ -540,7 +545,7 @@ def _apply_signed_context_policy(prepared: "PreparedSignedWrite", request: Reque
ok, reason = validate_signed_context(
event_type=prepared.event_type,
kind=prepared.kind.value,
endpoint=str(request.url.path or ""),
endpoint=_request_scope_path(request),
lane_floor=_content_private_required_transport_tier(prepared.kind),
sequence_domain=_signed_context_sequence_domain(prepared),
node_id=prepared.node_id,
+50
View File
@@ -0,0 +1,50 @@
from starlette.requests import Request
import auth
async def _empty_receive():
return {"type": "http.request", "body": b"", "more_body": False}
def _request(path: str, *, host: str = "example.com/health?x=", client_host: str = "203.0.113.10") -> Request:
return Request(
{
"type": "http",
"method": "GET",
"scheme": "http",
"server": ("127.0.0.1", 8000),
"client": (client_host, 12345),
"path": path,
"raw_path": path.encode("ascii"),
"query_string": b"",
"headers": [(b"host", host.encode("ascii"))],
},
receive=_empty_receive,
)
def test_scope_auth_uses_asgi_path_not_host_derived_url_path():
request = _request("/api/mesh/gate/alpha/message")
assert auth._request_scope_path(request) == "/api/mesh/gate/alpha/message"
assert auth._required_scope_for_request(request) == "mesh"
def test_debug_test_request_does_not_trust_host_header(monkeypatch):
monkeypatch.setattr(auth, "_debug_mode_enabled", lambda: True)
request = _request("/api/admin", host="test/api/public?x=")
assert auth._is_debug_test_request(request) is False
def test_peer_hmac_identity_requires_explicit_peer_url_header():
request = _request("/api/mesh/infonet/push", host="https://peer.example/api/public?x=")
assert auth._peer_hmac_url_from_request(request) == ""
request = _request("/api/mesh/infonet/push")
request.scope["headers"].append((b"x-peer-url", b"https://peer.example/"))
assert auth._peer_hmac_url_from_request(request) == "https://peer.example"
Generated
+21 -7
View File
@@ -17,6 +17,15 @@ members = [
"shadowbroker",
]
[[package]]
name = "annotated-doc"
version = "0.0.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" },
]
[[package]]
name = "annotated-types"
version = "0.7.0"
@@ -100,6 +109,7 @@ dependencies = [
{ name = "reverse-geocoder" },
{ name = "sgp4" },
{ name = "slowapi" },
{ name = "starlette" },
{ name = "uvicorn" },
{ name = "vadersentiment" },
{ name = "yfinance" },
@@ -120,7 +130,7 @@ requires-dist = [
{ name = "cachetools", specifier = "==5.5.2" },
{ name = "cryptography", specifier = ">=41.0.0" },
{ name = "defusedxml", specifier = ">=0.7.1" },
{ name = "fastapi", specifier = "==0.115.12" },
{ name = "fastapi", specifier = "==0.136.3" },
{ name = "feedparser", specifier = "==6.0.10" },
{ name = "httpx", specifier = "==0.28.1" },
{ name = "meshtastic", specifier = ">=2.5.0" },
@@ -138,6 +148,7 @@ requires-dist = [
{ name = "reverse-geocoder", specifier = "==1.5.1" },
{ name = "sgp4", specifier = "==2.25" },
{ name = "slowapi", specifier = "==0.1.9" },
{ name = "starlette", specifier = "==1.0.1" },
{ name = "uvicorn", specifier = "==0.34.0" },
{ name = "vadersentiment", specifier = ">=3.3.0" },
{ name = "yfinance", specifier = "==1.3.0" },
@@ -621,16 +632,18 @@ wheels = [
[[package]]
name = "fastapi"
version = "0.115.12"
version = "0.136.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "annotated-doc" },
{ name = "pydantic" },
{ name = "starlette" },
{ name = "typing-extensions" },
{ name = "typing-inspection" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f4/55/ae499352d82338331ca1e28c7f4a63bfd09479b16395dce38cf50a39e2c2/fastapi-0.115.12.tar.gz", hash = "sha256:1e2c2a2646905f9e83d32f04a3f86aff4a286669c6c950ca95b5fd68c2602681", size = 295236, upload-time = "2025-03-23T22:55:43.822Z" }
sdist = { url = "https://files.pythonhosted.org/packages/81/2d/ff8d91d7b564d464629a0fd50a4489c97fcb836ac230bf3a7269232a9b1f/fastapi-0.136.3.tar.gz", hash = "sha256:e487fae93ad408e6f47641ee4dfe389864fd7bec92e547ea8498fc13f43e83ab", size = 396410, upload-time = "2026-05-23T18:53:15.192Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/50/b3/b51f09c2ba432a576fe63758bddc81f78f0c6309d9e5c10d194313bf021e/fastapi-0.115.12-py3-none-any.whl", hash = "sha256:e94613d6c05e27be7ffebdd6ea5f388112e5e430c8f7d6494a9d1d88d43e814d", size = 95164, upload-time = "2025-03-23T22:55:42.101Z" },
{ url = "https://files.pythonhosted.org/packages/e0/82/45359b62a067409bd929ae8a56b8ed13e5a8c8a61194b3c236920999ab83/fastapi-0.136.3-py3-none-any.whl", hash = "sha256:3d2a69bdf04b7e9f3afa292c3bc7a98816bbfafa10bc9b45f3f3700d2f761620", size = 117481, upload-time = "2026-05-23T18:53:16.924Z" },
]
[[package]]
@@ -2271,14 +2284,15 @@ wheels = [
[[package]]
name = "starlette"
version = "0.46.2"
version = "1.0.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ce/20/08dfcd9c983f6a6f4a1000d934b9e6d626cff8d2eeb77a89a68eef20a2b7/starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5", size = 2580846, upload-time = "2025-04-13T13:56:17.942Z" }
sdist = { url = "https://files.pythonhosted.org/packages/08/a3/84e821cc54b4ab50ae6dbc6ac3800a651b65ec35f045cc73785380654057/starlette-1.0.1.tar.gz", hash = "sha256:512399c5f1de7fac99c88572212ded9ddeddef2fb32afa82d724000e88b38f4f", size = 2659596, upload-time = "2026-05-21T21:58:58.433Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35", size = 72037, upload-time = "2025-04-13T13:56:16.21Z" },
{ url = "https://files.pythonhosted.org/packages/ec/e1/b2df4bc09a1e51ff664c1e17018a4274b42e5e9352e4a478ea540512dc88/starlette-1.0.1-py3-none-any.whl", hash = "sha256:7c0e69b2ee1c848bd54669d908500117a3ee13de603a21427e5c6fc1adf98dcd", size = 72802, upload-time = "2026-05-21T21:58:56.551Z" },
]
[[package]]