From 547965a5cb49829f27fd596830292c8fa6d1fe41 Mon Sep 17 00:00:00 2001 From: Hugh Brown Date: Tue, 3 Mar 2026 13:44:21 -0700 Subject: [PATCH] security: redact gateway tokens from API responses Gateway tokens were returned as plaintext in GatewayRead API responses. Replace the `token` field with a boolean `has_token` flag so the API never exposes the plaintext token. The token remains in the database for outbound gateway connections (full encryption would require key management infrastructure). Co-Authored-By: Claude Opus 4.6 --- backend/app/api/gateways.py | 35 ++++++++++++++++++++++++++------- backend/app/schemas/gateways.py | 2 +- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/backend/app/api/gateways.py b/backend/app/api/gateways.py index 6c579930..798e38b7 100644 --- a/backend/app/api/gateways.py +++ b/backend/app/api/gateways.py @@ -23,11 +23,28 @@ from app.schemas.gateways import ( GatewayTemplatesSyncResult, GatewayUpdate, ) + + +def _to_gateway_read(gateway: Gateway) -> GatewayRead: + return GatewayRead( + id=gateway.id, + organization_id=gateway.organization_id, + name=gateway.name, + url=gateway.url, + workspace_root=gateway.workspace_root, + allow_insecure_tls=gateway.allow_insecure_tls, + disable_device_pairing=gateway.disable_device_pairing, + has_token=gateway.token is not None, + created_at=gateway.created_at, + updated_at=gateway.updated_at, + ) from app.schemas.pagination import DefaultLimitOffsetPage from app.services.openclaw.admin_service import GatewayAdminLifecycleService from app.services.openclaw.session_service import GatewayTemplateSyncQuery if TYPE_CHECKING: + from collections.abc import Sequence + from fastapi_pagination.limit_offset import LimitOffsetPage from sqlmodel.ext.asyncio.session import AsyncSession @@ -82,7 +99,11 @@ async def list_gateways( .order_by(col(Gateway.created_at).desc()) .statement ) - return await paginate(session, statement) + + def _transform(items: Sequence[object]) -> Sequence[object]: + return [_to_gateway_read(item) for item in items if isinstance(item, Gateway)] + + return await paginate(session, statement, transformer=_transform) @router.post("", response_model=GatewayRead) @@ -91,7 +112,7 @@ async def create_gateway( session: AsyncSession = SESSION_DEP, auth: AuthContext = AUTH_DEP, ctx: OrganizationContext = ORG_ADMIN_DEP, -) -> Gateway: +) -> GatewayRead: """Create a gateway and provision or refresh its main agent.""" service = GatewayAdminLifecycleService(session) await service.assert_gateway_runtime_compatible( @@ -106,7 +127,7 @@ async def create_gateway( data["organization_id"] = ctx.organization.id gateway = await crud.create(session, Gateway, **data) await service.ensure_main_agent(gateway, auth, action="provision") - return gateway + return _to_gateway_read(gateway) @router.get("/{gateway_id}", response_model=GatewayRead) @@ -114,14 +135,14 @@ async def get_gateway( gateway_id: UUID, session: AsyncSession = SESSION_DEP, ctx: OrganizationContext = ORG_ADMIN_DEP, -) -> Gateway: +) -> GatewayRead: """Return one gateway by id for the caller's organization.""" service = GatewayAdminLifecycleService(session) gateway = await service.require_gateway( gateway_id=gateway_id, organization_id=ctx.organization.id, ) - return gateway + return _to_gateway_read(gateway) @router.patch("/{gateway_id}", response_model=GatewayRead) @@ -131,7 +152,7 @@ async def update_gateway( session: AsyncSession = SESSION_DEP, auth: AuthContext = AUTH_DEP, ctx: OrganizationContext = ORG_ADMIN_DEP, -) -> Gateway: +) -> GatewayRead: """Patch a gateway and refresh the main-agent provisioning state.""" service = GatewayAdminLifecycleService(session) gateway = await service.require_gateway( @@ -163,7 +184,7 @@ async def update_gateway( ) await crud.patch(session, gateway, updates) await service.ensure_main_agent(gateway, auth, action="update") - return gateway + return _to_gateway_read(gateway) @router.post("/{gateway_id}/templates/sync", response_model=GatewayTemplatesSyncResult) diff --git a/backend/app/schemas/gateways.py b/backend/app/schemas/gateways.py index 9d306991..dceb47dc 100644 --- a/backend/app/schemas/gateways.py +++ b/backend/app/schemas/gateways.py @@ -65,7 +65,7 @@ class GatewayRead(GatewayBase): id: UUID organization_id: UUID - token: str | None = None + has_token: bool = False created_at: datetime updated_at: datetime