Android Jetpack Navigation: Go Even Deeper

Author

Application Security Expert

Previous research

Some time ago, my colleague discovered an interesting vulnerability in the Jetpack Navigation library, which allows someone to open any screen of the application, bypassing existing restrictions for components that are not exported and therefore inaccessible to other applications. The issue lies with an implicit deep link processing mechanism, which any application on the device can interact with.  This investigation prompted Google to add the following warning to the library documentation:

The issue with this warning is that, based on the documentation, it only concerns the APIs for creating explicit deep links when the problem is actually much deeper. But let’s take it step by step.

Jetpack Compose navigation specifics

Jetpack Compose is a new approach to building UIs in Android, replacing the previously widely used fragments. Consequently, the navigation between screens, now represented by composable functions or widgets, is also different. As a result, an extension for the Jetpack Navigation library was created to work with screens built using Jetpack Compose. A typical example of the implementation of this new type of navigation:

The key components of the navigation graph are the composable functions that describe all transitions between screens, similar to how it used to be done for fragments using XML. These functions also allow us to declare deep links intended to land the user on a specific screen, bypassing the usual sequence of transitions. Such a deep link can be declared as follows:

And this is where the problems begin. Because this kind of deep link does exactly what I mentioned above: it takes the user to a specific screen, ignoring what would normally be the start screen. Wait a minute. We already went through this in our previous research, and Google suggests solving such problems with conditional navigation mechanisms. After all, a developer adding deep links to an application should understand the risks of their improper use and respond accordingly! In the end, the developer decided not to bother with the murky deep link mechanism and removed them from their application and from the manifest. It would appear that the developer can now relax and not worry about deep links… Remember how I said that problems begin with deep links? Well, I lied. Problems start much earlier. Let’s analyze them with an example of a test application with the сompose navigation but without deep links.

Exploiting implicit deep links

The test application consists of the following three screens: the PIN code input screen, the main screen, and the web content screen:

On the navigation graph, the PIN code input screen is always the start screen (as the developer intended), and accessing the main screen without entering the PIN code is impossible. Similarly, navigating to the WebView screen is only possible from the main screen. Here’s how the navigation with all transitions looks:

The application manifest also looks very simple:

A developer uploads such an application to the store, not suspecting that it is vulnerable, even though the developer hasn’t done anything wrong yet. All transitions in the navigation graph are predefined, and there is a single activity in the manifest. There are no other entry points, and no deep links in the application either. So where is the vulnerability? Let’s figure it out. First, I’ll show you how to bypass the PIN code input screen, and then we’ll do something even more interesting.

Remember, the PIN code is entered on the start screen, and accessing the main screen without entering the PIN code is impossible. Or is it?

Here is the exploit code:

Too simple to be true? Let’s take a look at another example involving account hijacking, and then we’ll figure out what’s happening here. The application has a screen displaying web content. To display the content relevant to the user, the authorization header is added to the request when opening the page:

The way the developer sees it, this seems secure because the URL is hardcoded into the screen navigation handler, making the entire scheme reliable.

In essence, it is. Until the attacker gets the opportunity to open arbitrary links on this screen:

For your information, a session hijacking just happened without any interaction with the user.

Here is the exploit code:

How did this even become possible, and where did the deep links come from if they were not declared in the application?! This is another gift from Google, which they do not consider a vulnerability. How did it get into the application, and can anything be done about it? Let’s figure it out.

Investigating the problem of implicit deep links

First, let’s explore the issue in the debugger, and then find the source code that made this vulnerability possible. We’ll set a breakpoint in the code executed during the transition. After examining the objects in memory, you can see that by the time the transition to the screen occurs, an implicit deep link has already been created without any involvement from the developer.

From the library’s source code, it is clear that the library automatically assigns internal deep links to each created route, which the developer isn’t aware of. We couldn’t find a description of this mechanism, and judging by the annotations in the code, it is internal, meaning any documentation is likely internal as well.

Let’s return to the declaration of a new route according to the official documentation:

After this function is executed, the deep link android-app://androidx.navigation/WebContent/{url} will appear in the application, which can be accessed from another application by passing the url parameter without any restrictions. Let’s look at the source code of the composable function:

Lines 4 and 16 of the code clearly describe the standard mechanism for adding deep links to routes, and the list is empty by default. So the problem lies somewhere else. After looking a bit more, we found the problem in line 12. If we look at the definition of the route field, we can see a custom setter that calls the createRoute method, which creates an internal deep link for each route passed:

There is another interesting point: even if the developer specifies their own set of deep links for the route, they will simply be added to the existing implicit deep link. This is evident from the code of the addDeepLink method, which we saw called in the composable function body above:

Let’s also check this in the debugger:

Everything works as intended: first, an implicit deep link is added, and then the deep link specified by the developer is added. If we look at the handleDeeplink function, discussed in the previous research, we can see that when it does not find deep link identifiers, the matchDeepLink function (1) is called, which attempts to find the passed URI in the deepLinks array.

Of course, it finds it, and then the function builds a chain of identifiers (2) for navigating the graph and adds the arguments passed in the deep link to the globalArgs object (3). It is then processed as a regular deep link, after which the target screen is opened with the passed arguments.

Conclusions

There have been many different publications about the risks related to deep links. The issue is well known to security researchers. Errors related to explicit deep links are regularly found and exploited by attackers. It’s hard to understand why anyone would want to make the issue more complicated than it already is.

What should application developers do? The most obvious advice is not to use this library. It may not suit everyone, but it is the most reliable solution. It’s better to take the time to develop and debug your own navigation solution, ensuring that nothing goes wrong. Other options may have issues of one kind or another. The official advice from Google is to use conditional navigation. Let’s discuss what’s wrong with this approach. The official documentation does not provide examples for Compose, so let’s use code from this article. The author opts for the following option of conditional navigation, which does not allow access to the main screen of the application without authorization:

The author suggests performing the authorization check on the home screen to avoid checking authorization on each screen. This is a great optimization from a development perspective, but it makes exploiting vulnerabilities related to implicit deep links even easier. If the check is only performed on the main screen, all attackers need to do is bypass this screen. Let’s assume the developer decided to check authorization on each screen. Now, a PIN code or something similar will be required when navigating to any screen via an implicit deep link. Is it safe now? Nope. For an attacker, this scenario only turns their zero-click exploit into a one-click exploit. Moreover, it’s a rather symbolic single click because, when entering the PIN code, the user doesn’t know which screen will open or what parameters will be used by that screen. For the user, this won’t differ from the usual way of opening the application. Whether this is a vulnerability or not is for you to decide. Google has made their choice. Now it’s your turn.