diff --git a/packages/apps/src/microsoft_teams/apps/http/http_server.py b/packages/apps/src/microsoft_teams/apps/http/http_server.py index 85fb2fbb..21c09cb2 100644 --- a/packages/apps/src/microsoft_teams/apps/http/http_server.py +++ b/packages/apps/src/microsoft_teams/apps/http/http_server.py @@ -4,6 +4,7 @@ """ import logging +import re from types import SimpleNamespace from typing import Any, Awaitable, Callable, Dict, Optional, cast @@ -18,6 +19,14 @@ logger = logging.getLogger(__name__) +_LOG_CONTROL_CHARS = re.compile(r"[\r\n\t\x00-\x1f\x7f]") + + +def _safe_log_field(value: object) -> str: + """Strip control characters and cap length so an attacker-controlled activity + field cannot forge multi-line log entries (log injection).""" + return _LOG_CONTROL_CHARS.sub("", str(value if value is not None else "unknown"))[:64] + class HttpServer: """ @@ -95,11 +104,20 @@ async def handle_request(self, request: HttpRequest) -> HttpResponse: body = request["body"] headers = request["headers"] + entry_type = _safe_log_field(body.get("type")) + entry_id = _safe_log_field(body.get("id")) + # Validate JWT token authorization = headers.get("authorization") or headers.get("Authorization") or "" if self._token_validator and not self._skip_auth: if not authorization.startswith("Bearer "): + logger.warning( + "inbound activity rejected (type=%s, id=%s): missing or malformed " + "Authorization header (responding 401)", + entry_type, + entry_id, + ) return HttpResponse(status=401, body={"error": "Unauthorized"}) raw_token = authorization.removeprefix("Bearer ")