fix: key slash encoding on route, redact more request secrets, bound JSON depth and uploads
Slash safety is decided per (path template, parameter) so single-segment
routes such as releases/tags/{tag} and editorconfig/{filepath} keep %2F.
Adds authorization_header, remote_password, mirror_password, mirror_token and
the AWS key fields to the redacted request fields. Deeply nested JSON from
Gitea is treated as text instead of raising RecursionError, and base64
uploads are capped before decoding.
This commit is contained in:
1 parent
1a039e32ab
commit
156e963e21
4 files changed
+402
-21
No files matched your search
@@ -39,6 +39,7 @@ from aegis_gitea_mcp.errors import kind_for_status, sanitize_message
|
||||
from aegis_gitea_mcp.gitea.encoding import (
|
||||
RAW_METHODS,
|
||||
ArgumentError,
|
||||
PayloadTooLargeError,
|
||||
RequestBody,
|
||||
build_body,
|
||||
build_query,
|
||||
@@ -115,6 +116,10 @@ _MAX_TEXT_BYTES = 16 * 1024 * 1024
|
||||
#: Cap on the error body inspected for a message.
|
||||
_MAX_ERROR_BODY_BYTES = 64 * 1024
|
||||
_RAW_ACCEPT = "application/json, text/plain;q=0.9, */*;q=0.5"
|
||||
#: Deepest JSON nesting accepted from Gitea. Deeper bodies are returned as text:
|
||||
#: parsing and the recursive redaction/masking passes must never hit the
|
||||
#: interpreter recursion limit (a hostile or broken upstream could try).
|
||||
_MAX_JSON_DEPTH = 100
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -184,6 +189,34 @@ def _decode(body: bytes) -> str:
|
||||
return body.decode("utf-8", errors="replace")
|
||||
|
||||
|
||||
def _too_deep(value: Any, limit: int = _MAX_JSON_DEPTH) -> bool:
|
||||
"""True when ``value`` nests lists/dicts deeper than ``limit`` (iterative walk)."""
|
||||
stack: list[tuple[Any, int]] = [(value, 1)]
|
||||
while stack:
|
||||
item, depth = stack.pop()
|
||||
if isinstance(item, dict):
|
||||
children: Iterable[Any] = item.values()
|
||||
elif isinstance(item, list):
|
||||
children = item
|
||||
else:
|
||||
continue
|
||||
if depth > limit:
|
||||
return True
|
||||
stack.extend((child, depth + 1) for child in children)
|
||||
return False
|
||||
|
||||
|
||||
def _parse_json(text: str) -> Any:
|
||||
"""Parse JSON; raises ``ValueError`` for invalid, or too deeply nested, documents."""
|
||||
try:
|
||||
parsed = json.loads(text)
|
||||
except RecursionError as exc:
|
||||
raise ValueError("JSON nesting too deep") from exc
|
||||
if _too_deep(parsed):
|
||||
raise ValueError("JSON nesting too deep")
|
||||
return parsed
|
||||
|
||||
|
||||
def _declared_size(response: httpx.Response) -> int | None:
|
||||
"""Content-Length when it describes the decoded body (no content-encoding)."""
|
||||
if response.headers.get("content-encoding", "identity").lower() != "identity":
|
||||
@@ -213,7 +246,7 @@ def _error_message(body: bytes, status: int) -> str:
|
||||
text = _decode(body).strip()
|
||||
message = text
|
||||
try:
|
||||
parsed = json.loads(text)
|
||||
parsed = _parse_json(text)
|
||||
except ValueError:
|
||||
parsed = None
|
||||
if isinstance(parsed, dict):
|
||||
@@ -289,7 +322,7 @@ class GiteaExecutor:
|
||||
sudo = validate_sudo(arguments.get("sudo"))
|
||||
encoded_path, display_path = render_path(operation, arguments)
|
||||
query = build_query(operation, arguments)
|
||||
body = build_body(operation, arguments)
|
||||
body = build_body(operation, arguments, self._settings.max_binary_bytes)
|
||||
except ArgumentError as exc:
|
||||
call = _Call(
|
||||
method=operation.method,
|
||||
@@ -299,6 +332,8 @@ class GiteaExecutor:
|
||||
accept="",
|
||||
sudo=sudo,
|
||||
)
|
||||
if isinstance(exc, PayloadTooLargeError):
|
||||
return self._error(call, identity, None, "payload_too_large", str(exc))
|
||||
return self._invalid(call, identity, str(exc))
|
||||
|
||||
body_param = next((p for p in operation.parameters if p.location == "body"), None)
|
||||
@@ -409,6 +444,8 @@ class GiteaExecutor:
|
||||
return await self._handle(response, call, identity)
|
||||
except httpx.RequestError as exc:
|
||||
return self._transport_error(exc, call, identity)
|
||||
except RecursionError:
|
||||
return self._invalid(call, identity, "request body is nested too deeply")
|
||||
finally:
|
||||
if response is not None:
|
||||
await response.aclose()
|
||||
@@ -509,7 +546,7 @@ class GiteaExecutor:
|
||||
def _decode_body(body: bytes, kind: str) -> Any:
|
||||
if kind == "json":
|
||||
try:
|
||||
return json.loads(body.decode("utf-8"))
|
||||
return _parse_json(body.decode("utf-8"))
|
||||
except ValueError:
|
||||
pass
|
||||
return _decode(body)
|
||||
|
||||
Reference in new issue
Block a user