diff --git a/app/src/main/java/com/m57/hermescontrol/ui/connect/ConnectViewModel.kt b/app/src/main/java/com/m57/hermescontrol/ui/connect/ConnectViewModel.kt index 055ed8e6..171214ae 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/connect/ConnectViewModel.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/connect/ConnectViewModel.kt @@ -39,6 +39,22 @@ class ConnectViewModel(private val app: Application) : ViewModel() { loadSavedValues() } + private suspend fun isHostAllowed(host: String): Boolean = + withContext(Dispatchers.IO) { + if (host == "localhost" || host == "127.0.0.1" || host == "0.0.0.0") return@withContext false + if (host.startsWith("169.254.")) return@withContext false + if (host == "metadata.google.internal") return@withContext false + try { + val address = java.net.InetAddress.getByName(host) + if (address.isLoopbackAddress || address.isAnyLocalAddress || address.isLinkLocalAddress) { + return@withContext false + } + true + } catch (e: Exception) { + false + } + } + fun loadSavedValues() { val savedToken = AuthManager.getToken() ?: "" val savedHost = AuthManager.getHost() @@ -267,15 +283,25 @@ class ConnectViewModel(private val app: Application) : ViewModel() { val port = uri.getQueryParameter("port") val token = uri.getQueryParameter("token") if (host != null && port != null && token != null) { - _uiState.update { - it.copy( - host = host, - port = port, - token = token, - errorMessage = null, - ) + viewModelScope.launch { + if (!isHostAllowed(host)) { + _uiState.update { + it.copy( + errorMessage = app.getString(R.string.connect_error_invalid_host), + ) + } + return@launch + } + _uiState.update { + it.copy( + host = host, + port = port, + token = token, + errorMessage = null, + ) + } + connect() } - connect() return } } @@ -293,15 +319,25 @@ class ConnectViewModel(private val app: Application) : ViewModel() { val port = json.get("port")?.asInt?.toString() ?: json.get("port")?.asString val token = json.get("token")?.asString if (host != null && port != null && token != null) { - _uiState.update { - it.copy( - host = host, - port = port, - token = token, - errorMessage = null, - ) + viewModelScope.launch { + if (!isHostAllowed(host)) { + _uiState.update { + it.copy( + errorMessage = app.getString(R.string.connect_error_invalid_host), + ) + } + return@launch + } + _uiState.update { + it.copy( + host = host, + port = port, + token = token, + errorMessage = null, + ) + } + connect() } - connect() return } // Decoded valid JSON but missing required fields diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 726baa29..9b24cfbc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -178,6 +178,7 @@ Connection refused – is Hermes running? Could not resolve host Connection failed: %1$s + Invalid or forbidden host in pairing string Malformed pairing string — missing host, port, or token Malformed pairing string — expected URL or Base64-encoded JSON Failed to parse pairing string: %1$s diff --git a/app/src/test/java/com/m57/hermescontrol/ui/connect/ConnectViewModelTest.kt b/app/src/test/java/com/m57/hermescontrol/ui/connect/ConnectViewModelTest.kt index 8883e87c..7061d915 100644 --- a/app/src/test/java/com/m57/hermescontrol/ui/connect/ConnectViewModelTest.kt +++ b/app/src/test/java/com/m57/hermescontrol/ui/connect/ConnectViewModelTest.kt @@ -612,12 +612,14 @@ class ConnectViewModelTest { val viewModel = ConnectViewModel(mockApp) viewModel.onPairingString("hermes://connect?host=192.168.1.1&port=8888&token=abc123") + // Wait for parsing coroutine to finish + advanceUntilIdle() + val state = viewModel.uiState.value assertEquals("192.168.1.1", state.host) assertEquals("8888", state.port) assertEquals("abc123", state.token) // Should have triggered connect - advanceUntilIdle() assertTrue("connection should succeed after pairing", viewModel.uiState.value.connectionSuccess) } }