mirror of
https://github.com/BigBodyCobain/Shadowbroker.git
synced 2026-05-31 19:41:37 +02:00
Fix BadHost path handling
This commit is contained in:
+10
-8
@@ -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
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
@@ -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]]
|
||||
|
||||
Reference in New Issue
Block a user