[Android] Remote Access: protect control receiver < API 33

Duncan McNamara git at videolan.org
Fri Oct 2 07:29:56 UTC 2026


vlc-android | branch: master | Duncan McNamara <dcn.mcnamara at gmail.com> | Wed Sep 23 14:12:24 2026 +0200| [225b82dbf0ec44c88b0e025b0ceed6089930980c] | committer: Nicolas Pomepuy

Remote Access: protect control receiver < API 33

On Android < 13, exported=false doesn't actually stop other apps
from reaching a dynamically-registered receiver, so the Remote
Access control broadcasts (start/stop/disable/restart) were
still open to anyone.

Add a signature permission and require it on that receiver. Our
own broadcasts already hold it, so nothing else changes.

> https://code.videolan.org/videolan/vlc-android/commit/225b82dbf0ec44c88b0e025b0ceed6089930980c
---

 .../remote-access-server/src/main/AndroidManifest.xml        |  6 ++++++
 .../videolan/vlc/remoteaccessserver/RemoteAccessService.kt   |  2 +-
 .../src/main/java/org/videolan/resources/util/Extensions.kt  | 12 ++++++++++--
 3 files changed, 17 insertions(+), 3 deletions(-)

diff --git a/application/remote-access-server/src/main/AndroidManifest.xml b/application/remote-access-server/src/main/AndroidManifest.xml
index a8f5d1feb3..0c67cde4eb 100644
--- a/application/remote-access-server/src/main/AndroidManifest.xml
+++ b/application/remote-access-server/src/main/AndroidManifest.xml
@@ -26,6 +26,12 @@
     <uses-sdk tools:overrideLibrary="androidx.security"/>
     <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
 
+    <!-- Restricts who can send the Remote Access control broadcasts (start/stop/restart/disable) -->
+    <permission
+        android:name="${applicationId}.permission.REMOTE_ACCESS_CONTROL"
+        android:protectionLevel="signature" />
+    <uses-permission android:name="${applicationId}.permission.REMOTE_ACCESS_CONTROL" />
+
     <application>
         <service
                 android:name=".RemoteAccessService"
diff --git a/application/remote-access-server/src/main/java/org/videolan/vlc/remoteaccessserver/RemoteAccessService.kt b/application/remote-access-server/src/main/java/org/videolan/vlc/remoteaccessserver/RemoteAccessService.kt
index f333d9d4cf..1b88388ae3 100644
--- a/application/remote-access-server/src/main/java/org/videolan/vlc/remoteaccessserver/RemoteAccessService.kt
+++ b/application/remote-access-server/src/main/java/org/videolan/vlc/remoteaccessserver/RemoteAccessService.kt
@@ -147,7 +147,7 @@ class RemoteAccessService : LifecycleService(), CoroutineScope by MainScope() {
         filter.addAction(ACTION_START_SERVER)
         filter.addAction(ACTION_DISABLE_SERVER)
         filter.addAction(ACTION_RESTART_SERVER)
-        registerReceiverCompat(receiver, filter, false)
+        registerReceiverCompat(receiver, filter, false, "$packageName.permission.REMOTE_ACCESS_CONTROL")
     }
 
     private fun forceForeground() {
diff --git a/application/resources/src/main/java/org/videolan/resources/util/Extensions.kt b/application/resources/src/main/java/org/videolan/resources/util/Extensions.kt
index ce5af19613..fdd87b01f4 100644
--- a/application/resources/src/main/java/org/videolan/resources/util/Extensions.kt
+++ b/application/resources/src/main/java/org/videolan/resources/util/Extensions.kt
@@ -224,14 +224,22 @@ fun Service.startForegroundCompat(serviceNotificationId: NotificationIds, notifi
 /**
  * Use the new registerReceiver API when needed
  *
+ * Below API 33 there is no equivalent of [Context.RECEIVER_NOT_EXPORTED]: passing
+ * `exported = false` on those versions still leaves the receiver reachable from other
+ * apps. If that matters for a given receiver, also pass [permission] so senders are
+ * required to hold it on every API level.
+ *
  * @param receiver the receiver to register
  * @param filter the filter to apply
  * @param exported true if it needs to be exported
+ * @param permission a signature-level permission senders must hold, or null for none
  */
 @SuppressLint("UnspecifiedRegisterReceiverFlag")
-fun Context.registerReceiverCompat(receiver: BroadcastReceiver, filter: IntentFilter, exported: Boolean) {
+fun Context.registerReceiverCompat(receiver: BroadcastReceiver, filter: IntentFilter, exported: Boolean, permission: String? = null) {
     if (SDK_INT >= Build.VERSION_CODES.TIRAMISU)
-        registerReceiver(receiver, filter, if (exported) Context.RECEIVER_EXPORTED else Context.RECEIVER_NOT_EXPORTED)
+        registerReceiver(receiver, filter, permission, null, if (exported) Context.RECEIVER_EXPORTED else Context.RECEIVER_NOT_EXPORTED)
+    else if (permission != null)
+        registerReceiver(receiver, filter, permission, null)
     else
         registerReceiver(receiver, filter)
 }



More information about the Android mailing list