From 639feabb166e75b2d666fbd9f879eaef0a274460 Mon Sep 17 00:00:00 2001 From: Neeraj Sathish Kumar Date: Tue, 30 Jun 2026 14:51:25 +0530 Subject: [PATCH] fix: enforce link relay input permissions --- apis/link/src/relay.ts | 37 +++++++++++++++++++++++++++--- crates/orphix-core/src/link/mod.rs | 20 +++++++++++++++- 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/apis/link/src/relay.ts b/apis/link/src/relay.ts index fb0a031..093f010 100644 --- a/apis/link/src/relay.ts +++ b/apis/link/src/relay.ts @@ -37,6 +37,7 @@ interface Client { deviceId: string; role: "desktop" | "mobile"; sessionId: string; + mode: string; } const sessions = new Map(); @@ -86,6 +87,12 @@ export function handleRelaySocket(socket: WebSocket, app: FastifyInstance) { return; } + if (session.status !== "approved") { + sendJson(socket, { type: "relay.reject", reason: "Session not approved" }); + socket.close(); + return; + } + // Verify deviceId matches session participants if (role === "desktop" && session.desktopDeviceId !== deviceId) { sendJson(socket, { type: "relay.reject", reason: "deviceId mismatch" }); @@ -100,7 +107,14 @@ export function handleRelaySocket(socket: WebSocket, app: FastifyInstance) { console.log(`[relay] ${role} authenticated for session ${sessionId}, device ${deviceId}`); - client = { socket, userId: payload.sub, deviceId, role, sessionId }; + client = { + socket, + userId: payload.sub, + deviceId, + role, + sessionId, + mode: String(session.mode || ""), + }; authenticated = true; // Register in session @@ -114,7 +128,8 @@ export function handleRelaySocket(socket: WebSocket, app: FastifyInstance) { // Set presence const redis = getRedis(); - const presenceKey = role === "desktop" ? `presence:desktop:${deviceId}` : `presence:mobile:${deviceId}`; + const presenceKey = + role === "desktop" ? `presence:desktop:${deviceId}` : `presence:mobile:${deviceId}`; await redis.setex(presenceKey, 60, "online"); sendJson(socket, { type: "relay.ready", sessionId, role }); @@ -124,6 +139,19 @@ export function handleRelaySocket(socket: WebSocket, app: FastifyInstance) { // Forward messages to the other peer if (client) { + if ( + client.role === "mobile" && + msg.type === "relay.message" && + msg.direction === "input" && + client.mode !== "full_control" + ) { + sendJson(socket, { + type: "relay.reject", + reason: "Terminal input requires full_control mode", + }); + return; + } + const sess = sessions.get(client.sessionId); if (!sess) return; @@ -156,7 +184,10 @@ export function handleRelaySocket(socket: WebSocket, app: FastifyInstance) { // Clear presence const redis = getRedis(); - const presenceKey = client.role === "desktop" ? `presence:desktop:${client.deviceId}` : `presence:mobile:${client.deviceId}`; + const presenceKey = + client.role === "desktop" + ? `presence:desktop:${client.deviceId}` + : `presence:mobile:${client.deviceId}`; await redis.del(presenceKey); console.log(`[relay] ${client.role} disconnected from session ${client.sessionId}`); diff --git a/crates/orphix-core/src/link/mod.rs b/crates/orphix-core/src/link/mod.rs index 9c050e6..a3a34f5 100644 --- a/crates/orphix-core/src/link/mod.rs +++ b/crates/orphix-core/src/link/mod.rs @@ -135,6 +135,16 @@ impl LinkManager { self.relay_output_tx.clone() } + fn session_allows_terminal_input(&self, session_id: &str, terminal_id: &str) -> bool { + if self.session.session_id.as_deref() != Some(session_id) { + return false; + } + if self.session.mode.as_deref() != Some("full_control") { + return false; + } + self.relay.get_terminal_for_session(session_id).as_deref() == Some(terminal_id) + } + /// Phase 1 (sync): Validate params and create device identity. No network I/O. /// Must be called with the parking_lot lock held. pub fn prepare_enable(&mut self, params: &EnableParams) -> Result { @@ -495,8 +505,16 @@ impl LinkManager { self.send_workspace_snapshot(); } - LinkMessage::RelayMessage { terminal_id, data, direction, .. } => { + LinkMessage::RelayMessage { session_id, terminal_id, data, direction } => { if direction == "input" { + if !self.session_allows_terminal_input(&session_id, &terminal_id) { + eprintln!( + "[link] Denied relay input: session={}, terminal={}, mode={:?}", + session_id, terminal_id, self.session.mode + ); + return; + } + // Check if this is an RPC call (JSON with type field) if data.starts_with('{') { if let Ok(msg) = serde_json::from_str::(&data) {