From 3e9672f0ad055ffd1934359d64b194f204aa0788 Mon Sep 17 00:00:00 2001 From: M57 <41645758+Hy4ri@users.noreply.github.com> Date: Fri, 7 Aug 2026 22:42:39 +0300 Subject: [PATCH] fix(security): validate intent action before consuming notification session id (issue #832) (#845) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MainActivity is exported (launcher requirement) and consumed the session-id extra from any incoming intent without checking the action — a foreign app could launch it explicitly and force-navigate to a chat screen. Guard consumeNotificationIntent() on the app's own notification action and hoist the action string into a shared const so ChatNotificationService and MainActivity can never drift. Co-authored-by: M57 --- .../java/com/m57/hermescontrol/MainActivity.kt | 15 +++++++++++++++ .../notification/ChatNotificationService.kt | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/m57/hermescontrol/MainActivity.kt b/app/src/main/java/com/m57/hermescontrol/MainActivity.kt index 9e7b9da4..9aed099e 100644 --- a/app/src/main/java/com/m57/hermescontrol/MainActivity.kt +++ b/app/src/main/java/com/m57/hermescontrol/MainActivity.kt @@ -18,6 +18,18 @@ import com.m57.hermescontrol.theme.HermesControlTheme import com.m57.hermescontrol.util.LocaleContextWrapper class MainActivity : ComponentActivity() { + companion object { + /** + * Action that identifies notification-driven "open this chat" intents. + * [ChatNotificationService] stamps it on the content intent; the + * consumer refuses anything else (issue #832 — MainActivity is + * exported as the launcher, so a foreign app could otherwise inject + * a session id via an explicit intent). + */ + const val ACTION_OPEN_CHAT_FROM_NOTIFICATION = + "com.m57.hermescontrol.ACTION_OPEN_CHAT_FROM_NOTIFICATION" + } + /** * Apply the user-selected display language before any view is inflated. * Reads the persisted code from [AuthManager]; an uninitialized store @@ -62,6 +74,9 @@ class MainActivity : ComponentActivity() { } private fun consumeNotificationIntent(intent: Intent?) { + // Issue #832: MainActivity is exported (launcher requirement) — only + // honor intents stamped with our own notification action. + if (intent?.action != ACTION_OPEN_CHAT_FROM_NOTIFICATION) return val sessionId = intent?.getStringExtra(NotificationReplyReceiver.EXTRA_SESSION_ID) intent?.removeExtra(NotificationReplyReceiver.EXTRA_SESSION_ID) sessionId?.takeIf { it.isNotBlank() }?.let(NavigationController::openChatSession) diff --git a/app/src/main/java/com/m57/hermescontrol/notification/ChatNotificationService.kt b/app/src/main/java/com/m57/hermescontrol/notification/ChatNotificationService.kt index 4eec1d68..240460aa 100644 --- a/app/src/main/java/com/m57/hermescontrol/notification/ChatNotificationService.kt +++ b/app/src/main/java/com/m57/hermescontrol/notification/ChatNotificationService.kt @@ -185,7 +185,7 @@ class ChatNotificationService : Service() { private fun buildContentIntent(sessionId: String?): PendingIntent { val intent = Intent(this, MainActivity::class.java).apply { - action = "$packageName.ACTION_OPEN_CHAT_FROM_NOTIFICATION" + action = MainActivity.ACTION_OPEN_CHAT_FROM_NOTIFICATION flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP } if (!sessionId.isNullOrBlank()) {