diff --git a/mobile/packages/expo-mobile-web-shell/android/src/main/java/expo/modules/mobilewebshell/MobileWebBridgeDocumentUrl.kt b/mobile/packages/expo-mobile-web-shell/android/src/main/java/expo/modules/mobilewebshell/MobileWebBridgeDocumentUrl.kt index 96b8bc667b5..be7b1a3ede2 100644 --- a/mobile/packages/expo-mobile-web-shell/android/src/main/java/expo/modules/mobilewebshell/MobileWebBridgeDocumentUrl.kt +++ b/mobile/packages/expo-mobile-web-shell/android/src/main/java/expo/modules/mobilewebshell/MobileWebBridgeDocumentUrl.kt @@ -9,7 +9,7 @@ internal fun isAllowedMobileWebBridgeDocumentUrl(value: String, sessionId: Strin val url = URI(value) value.length <= MOBILE_WEB_DOCUMENT_URL_LIMIT && url.scheme == MOBILE_WEB_ORIGIN_SCHEME && - isMobileWebOriginForSession(url, sessionId) && + url.host == mobileWebOriginHostForSession(sessionId) && url.port == -1 && url.userInfo == null && url.fragment == sessionId diff --git a/mobile/packages/expo-mobile-web-shell/android/src/main/java/expo/modules/mobilewebshell/MobileWebOrigin.kt b/mobile/packages/expo-mobile-web-shell/android/src/main/java/expo/modules/mobilewebshell/MobileWebOrigin.kt index 0e1de007f75..9d457f4c10a 100644 --- a/mobile/packages/expo-mobile-web-shell/android/src/main/java/expo/modules/mobilewebshell/MobileWebOrigin.kt +++ b/mobile/packages/expo-mobile-web-shell/android/src/main/java/expo/modules/mobilewebshell/MobileWebOrigin.kt @@ -14,6 +14,10 @@ internal fun mobileWebOriginForSession(sessionId: String): String { internal fun mobileWebOriginUriForSession(sessionId: String): Uri = Uri.parse(mobileWebOriginForSession(sessionId)) +/** Android-free host projection so JVM unit tests can check origins without android.net.Uri. */ +internal fun mobileWebOriginHostForSession(sessionId: String): String = + mobileWebOriginForSession(sessionId).substringAfter("://") + internal fun isMobileWebOriginForSession(url: Uri, sessionId: String): Boolean = url.scheme == MOBILE_WEB_ORIGIN_SCHEME && url.host == mobileWebOriginUriForSession(sessionId).host && diff --git a/mobile/packages/expo-mobile-web-shell/android/src/main/java/expo/modules/mobilewebshell/MobileWebPackageStore.kt b/mobile/packages/expo-mobile-web-shell/android/src/main/java/expo/modules/mobilewebshell/MobileWebPackageStore.kt index 7db1098c604..abf26135866 100644 --- a/mobile/packages/expo-mobile-web-shell/android/src/main/java/expo/modules/mobilewebshell/MobileWebPackageStore.kt +++ b/mobile/packages/expo-mobile-web-shell/android/src/main/java/expo/modules/mobilewebshell/MobileWebPackageStore.kt @@ -103,8 +103,14 @@ internal class MobileWebPackageStore internal constructor( manifestJson.toByteArray(Charsets.UTF_8).size + canonicalManifestJson.toByteArray(Charsets.UTF_8).size reserveCacheCapacity(hostKey, reservedByteLength) - val stageRoot = File(cacheRoot, "$hostKey/staging/$stageId") + val hostRoot = File(cacheRoot, hostKey) + val stagingRoot = File(hostRoot, "staging") + val stageRoot = File(stagingRoot, stageId) try { + // mkdirs() would create the stage *through* a symlinked ancestor before any later check + // could reject it, so the ancestors are validated first. + require(isMobileWebUnlinkedPath(hostRoot, cacheRoot)) { "mobile_web_stage_create_failed" } + require(isMobileWebUnlinkedPath(stagingRoot, cacheRoot)) { "mobile_web_stage_create_failed" } require(stageRoot.mkdirs()) { "mobile_web_stage_create_failed" } require(isMobileWebUnlinkedPath(stageRoot, cacheRoot)) { "mobile_web_stage_create_failed" diff --git a/mobile/packages/expo-mobile-web-shell/android/src/main/java/expo/modules/mobilewebshell/MobileWebShellView.kt b/mobile/packages/expo-mobile-web-shell/android/src/main/java/expo/modules/mobilewebshell/MobileWebShellView.kt index 482754a6dea..8cfe757c49c 100644 --- a/mobile/packages/expo-mobile-web-shell/android/src/main/java/expo/modules/mobilewebshell/MobileWebShellView.kt +++ b/mobile/packages/expo-mobile-web-shell/android/src/main/java/expo/modules/mobilewebshell/MobileWebShellView.kt @@ -64,7 +64,7 @@ private val MOBILE_WEB_MERMAID_FRAME_CSP = listOf( @SuppressLint("ViewConstructor", "SetJavaScriptEnabled") internal class MobileWebShellView( context: Context, - private val appContext: AppContext + appContext: AppContext ) : ExpoView(context, appContext) { private val onBridgeMessage by EventDispatcher>() private val onNavigationBlocked by EventDispatcher>() @@ -345,32 +345,35 @@ internal class MobileWebShellView( ) } - private fun isAllowedDocumentUrl(url: Uri): Boolean = - activeSessionId != null && - isMobileWebOriginForSession(url, activeSessionId ?: return false) && + private fun isAllowedDocumentUrl(url: Uri): Boolean { + val sessionId = activeSessionId ?: return false + return isMobileWebOriginForSession(url, sessionId) && url.path == "/" && url.encodedPath == "/" && url.query == null && - url.fragment == activeSessionId && - url.toString().length <= 8 * 1024 + url.fragment == sessionId && + url.toString().length <= 8 * 1024 + } - private fun isAllowedDocumentRequestUrl(url: Uri): Boolean = - activeSessionId != null && - isMobileWebOriginForSession(url, activeSessionId ?: return false) && - url.path == "/" && - url.encodedPath == "/" && - url.query == null && - (url.fragment == null || url.fragment == activeSessionId) && - url.toString().length <= 8 * 1024 + private fun isAllowedDocumentRequestUrl(url: Uri): Boolean { + val sessionId = activeSessionId ?: return false + return isMobileWebOriginForSession(url, sessionId) && + url.path == "/" && + url.encodedPath == "/" && + url.query == null && + (url.fragment == null || url.fragment == sessionId) && + url.toString().length <= 8 * 1024 + } - private fun isAllowedEmbeddedDocumentUrl(url: Uri): Boolean = - activeSessionId != null && - isMobileWebOriginForSession(url, activeSessionId ?: return false) && + private fun isAllowedEmbeddedDocumentUrl(url: Uri): Boolean { + val sessionId = activeSessionId ?: return false + return isMobileWebOriginForSession(url, sessionId) && url.path == "/$MOBILE_WEB_MERMAID_FRAME_PATH" && url.encodedPath == "/$MOBILE_WEB_MERMAID_FRAME_PATH" && url.query == null && url.fragment == null && url.toString().length <= 8 * 1024 + } private fun blockedResponse(): WebResourceResponse = WebResourceResponse( "text/plain",