diff --git a/CHANGES.md b/CHANGES.md index 39b2c1edeb..2cb9347e1d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,10 @@ +Changes in Element X v25.04.2 +============================= + +Security fixes 🔐 +----------------- +- Fix for [GHSA-m5px-pwq3-4p5m](https://github.com/element-hq/element-x-android/security/advisories/GHSA-m5px-pwq3-4p5m) / [CVE-2025-27599](https://www.cve.org/CVERecord?id=CVE-2025-27599) + Changes in Element X v25.04.1 ============================= diff --git a/fastlane/metadata/android/en-US/changelogs/202504020.txt b/fastlane/metadata/android/en-US/changelogs/202504020.txt new file mode 100644 index 0000000000..66578477e6 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/202504020.txt @@ -0,0 +1,2 @@ +Main changes in this version: security fix. +Full changelog: https://github.com/element-hq/element-x-android/releases diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/CallIntentDataParser.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/CallIntentDataParser.kt index 22b9f6bef7..827427e0cf 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/CallIntentDataParser.kt +++ b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/utils/CallIntentDataParser.kt @@ -12,25 +12,26 @@ import javax.inject.Inject class CallIntentDataParser @Inject constructor() { private val validHttpSchemes = sequenceOf("https") + private val knownHosts = sequenceOf( + "call.element.io", + ) fun parse(data: String?): String? { val parsedUrl = data?.let { Uri.parse(data) } ?: return null val scheme = parsedUrl.scheme return when { - scheme in validHttpSchemes && parsedUrl.host == "call.element.io" -> parsedUrl + scheme in validHttpSchemes -> parsedUrl scheme == "element" && parsedUrl.host == "call" -> { - // We use this custom scheme to load arbitrary URLs for other instances of Element Call, - // so we can only verify it's an HTTP/HTTPs URL with a non-empty host parsedUrl.getUrlParameter() } scheme == "io.element.call" && parsedUrl.host == null -> { - // We use this custom scheme to load arbitrary URLs for other instances of Element Call, - // so we can only verify it's an HTTP/HTTPs URL with a non-empty host parsedUrl.getUrlParameter() } // This should never be possible, but we still need to take into account the possibility else -> null - }?.withCustomParameters() + } + ?.takeIf { it.host in knownHosts } + ?.withCustomParameters() } private fun Uri.getUrlParameter(): Uri? { diff --git a/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/CallIntentDataParserTest.kt b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/CallIntentDataParserTest.kt index 098f396d22..bafbe3f570 100644 --- a/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/CallIntentDataParserTest.kt +++ b/features/call/impl/src/test/kotlin/io/element/android/features/call/utils/CallIntentDataParserTest.kt @@ -45,6 +45,17 @@ class CallIntentDataParserTest { doTest("http://call.element.io/some-actual-call?with=parameters", null) } + @Test + fun `Element Call urls with unknown host returns null`() { + // Check valid host first, should not return null + doTest("https://call.element.io", "https://call.element.io#?appPrompt=false&confineToRoom=true") + // Unknown host should return null + doTest("https://unknown.io", null) + doTest("https://call.unknown.io", null) + doTest("https://call.element.com", null) + doTest("https://call.element.io.tld", null) + } + @Test fun `Element Call urls will be returned as is`() { doTest( @@ -64,7 +75,7 @@ class CallIntentDataParserTest { @Test fun `HTTP and HTTPS urls that don't come from EC return null`() { doTest("http://app.element.io", null) - doTest("https://app.element.io", null, testEmbedded = false) + doTest("https://app.element.io", null) doTest("http://", null) doTest("https://", null) } @@ -193,20 +204,18 @@ class CallIntentDataParserTest { ) } - private fun doTest(url: String, expectedResult: String?, testEmbedded: Boolean = true) { + private fun doTest(url: String, expectedResult: String?) { // Test direct parsing assertThat(callIntentDataParser.parse(url)).isEqualTo(expectedResult) - if (testEmbedded) { - // Test embedded url, scheme 1 - val encodedUrl = URLEncoder.encode(url, "utf-8") - val urlScheme1 = "element://call?url=$encodedUrl" - assertThat(callIntentDataParser.parse(urlScheme1)).isEqualTo(expectedResult) + // Test embedded url, scheme 1 + val encodedUrl = URLEncoder.encode(url, "utf-8") + val urlScheme1 = "element://call?url=$encodedUrl" + assertThat(callIntentDataParser.parse(urlScheme1)).isEqualTo(expectedResult) - // Test embedded url, scheme 2 - val urlScheme2 = "io.element.call:/?url=$encodedUrl" - assertThat(callIntentDataParser.parse(urlScheme2)).isEqualTo(expectedResult) - } + // Test embedded url, scheme 2 + val urlScheme2 = "io.element.call:/?url=$encodedUrl" + assertThat(callIntentDataParser.parse(urlScheme2)).isEqualTo(expectedResult) } companion object { diff --git a/plugins/src/main/kotlin/Versions.kt b/plugins/src/main/kotlin/Versions.kt index 08c4a55d2e..0902f84d53 100644 --- a/plugins/src/main/kotlin/Versions.kt +++ b/plugins/src/main/kotlin/Versions.kt @@ -32,7 +32,7 @@ private const val versionYear = 25 private const val versionMonth = 4 // Note: must be in [0,99] -private const val versionReleaseNumber = 1 +private const val versionReleaseNumber = 2 object Versions { const val VERSION_CODE = (2000 + versionYear) * 10_000 + versionMonth * 100 + versionReleaseNumber