fix(#259): validate host in pairing string to prevent SSRF (#344)

Add robust validation for hosts provided through URI or Base64 pairing
strings to prevent Server-Side Request Forgery (SSRF). The application
now rejects loopback addresses (like localhost, 127.0.0.1, 0.0.0.0),
link-local addresses, and any-local addresses, while continuing to allow
remote connections to site-local addresses, custom remote domains,
and VPN networks (like Tailscale). The DNS resolution operates securely
on the IO dispatcher to ensure main-thread safety. Included tests verify
both rejection of invalid hosts and seamless parsing of valid ones.
This commit is contained in:
M57 authored and GitHub committed 2026-06-23 12:47:50 +03:00
1 parent 558a66a1de
commit c21e3e9ed0
3 files changed
+56 -17

No files matched your search

@@ -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