Merge commit from fork

* Check validity of Element Call url host.

* Prepare release 25.04.2
This commit is contained in:
Benoit Marty 2025-04-17 11:39:38 +02:00 committed by GitHub
parent be1c9b793b
commit dc64d9cf74
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 37 additions and 18 deletions

View file

@ -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 Changes in Element X v25.04.1
============================= =============================

View file

@ -0,0 +1,2 @@
Main changes in this version: security fix.
Full changelog: https://github.com/element-hq/element-x-android/releases

View file

@ -12,25 +12,26 @@ import javax.inject.Inject
class CallIntentDataParser @Inject constructor() { class CallIntentDataParser @Inject constructor() {
private val validHttpSchemes = sequenceOf("https") private val validHttpSchemes = sequenceOf("https")
private val knownHosts = sequenceOf(
"call.element.io",
)
fun parse(data: String?): String? { fun parse(data: String?): String? {
val parsedUrl = data?.let { Uri.parse(data) } ?: return null val parsedUrl = data?.let { Uri.parse(data) } ?: return null
val scheme = parsedUrl.scheme val scheme = parsedUrl.scheme
return when { return when {
scheme in validHttpSchemes && parsedUrl.host == "call.element.io" -> parsedUrl scheme in validHttpSchemes -> parsedUrl
scheme == "element" && parsedUrl.host == "call" -> { 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() parsedUrl.getUrlParameter()
} }
scheme == "io.element.call" && parsedUrl.host == null -> { 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() parsedUrl.getUrlParameter()
} }
// This should never be possible, but we still need to take into account the possibility // This should never be possible, but we still need to take into account the possibility
else -> null else -> null
}?.withCustomParameters() }
?.takeIf { it.host in knownHosts }
?.withCustomParameters()
} }
private fun Uri.getUrlParameter(): Uri? { private fun Uri.getUrlParameter(): Uri? {

View file

@ -45,6 +45,17 @@ class CallIntentDataParserTest {
doTest("http://call.element.io/some-actual-call?with=parameters", null) 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 @Test
fun `Element Call urls will be returned as is`() { fun `Element Call urls will be returned as is`() {
doTest( doTest(
@ -64,7 +75,7 @@ class CallIntentDataParserTest {
@Test @Test
fun `HTTP and HTTPS urls that don't come from EC return null`() { fun `HTTP and HTTPS urls that don't come from EC return null`() {
doTest("http://app.element.io", 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("http://", null)
doTest("https://", 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 // Test direct parsing
assertThat(callIntentDataParser.parse(url)).isEqualTo(expectedResult) assertThat(callIntentDataParser.parse(url)).isEqualTo(expectedResult)
if (testEmbedded) { // Test embedded url, scheme 1
// Test embedded url, scheme 1 val encodedUrl = URLEncoder.encode(url, "utf-8")
val encodedUrl = URLEncoder.encode(url, "utf-8") val urlScheme1 = "element://call?url=$encodedUrl"
val urlScheme1 = "element://call?url=$encodedUrl" assertThat(callIntentDataParser.parse(urlScheme1)).isEqualTo(expectedResult)
assertThat(callIntentDataParser.parse(urlScheme1)).isEqualTo(expectedResult)
// Test embedded url, scheme 2 // Test embedded url, scheme 2
val urlScheme2 = "io.element.call:/?url=$encodedUrl" val urlScheme2 = "io.element.call:/?url=$encodedUrl"
assertThat(callIntentDataParser.parse(urlScheme2)).isEqualTo(expectedResult) assertThat(callIntentDataParser.parse(urlScheme2)).isEqualTo(expectedResult)
}
} }
companion object { companion object {

View file

@ -32,7 +32,7 @@ private const val versionYear = 25
private const val versionMonth = 4 private const val versionMonth = 4
// Note: must be in [0,99] // Note: must be in [0,99]
private const val versionReleaseNumber = 1 private const val versionReleaseNumber = 2
object Versions { object Versions {
const val VERSION_CODE = (2000 + versionYear) * 10_000 + versionMonth * 100 + versionReleaseNumber const val VERSION_CODE = (2000 + versionYear) * 10_000 + versionMonth * 100 + versionReleaseNumber