Gander

Explainer · Permissions

Can an app without the INTERNET permission phone home?

Yes, by two of the three. Neither of the two is a hole in the app, which is what makes them interesting: one is a feature every app on your phone needs, and the other is the operating system doing it for you.

Gander is an Android file viewer that requests no permissions at all. The usual reply to that is some version of "the permission list is theatre, it can still get data out". It is a fair question with a real answer, so here is what the permission gates, the three routes around it people raise, and what one app does about each.

Two things here are worth checking in your own app before you finish reading. One is a line of WebView code most tutorials get wrong. The other is a manifest attribute that quietly stopped covering half of what it says.

01 What the permission actually stops

INTERNET is granted at install time, with no prompt, and it is not enforced where people assume. It is enforced at the moment a socket is created, underneath anything you can write in Java.

The mechanism changed once, and the answer did not. An app holding the permission is put into a supplementary group, AID_INET, and for years an out-of-tree kernel patch called paranoid networking refused an internet socket to any app outside it. From Android 10, on 4.14 and newer kernels, an eBPF filter on cgroup socket creation does that job instead: an app without android.permission.INTERNET cannot open an INET or INET6 socket, not even the kind ping uses. Gander runs on Android 8 and up, so both are live somewhere in its range.

It is not a check inside the Java API that can be reflected around, and not a policy an app can ask to have relaxed at runtime. OkHttp, Retrofit, HttpURLConnection, a raw Socket, the NDK: all of them end at the same socket() call, and without the permission that call fails. Nothing you write inside the app gets past it. The rest of this page is about the fact that it only covers sockets the app opens itself.

02 Route one: "but it has a WebView"

This is the first thing people raise, and it is the one that does not work. Gander renders PDF, Word, Excel and PowerPoint inside a WebView, and shipping a browser engine sounds like it should come with a browser's network access.

It does not. WebView's network requests are made under the app's own UID, so they meet exactly the same refusal as everything in section 01, and Android's own WebView documentation is blunt about it: an app that wants to load web content has to request the INTERNET permission like anything else. Without it, a WebView cannot load a remote URL. On most phones from Android 8, and on all of them from Android 11, the renderer is a separate sandboxed process, but it has no network of its own, so that changes nothing.

Gander closes it a second time anyway, because a document is untrusted input and PDFs can carry links, embedded JavaScript and references to remote resources. Every request the WebView makes is answered locally: shouldInterceptRequest serves the viewer libraries out of app assets and streams the document from the content URI, and shouldOverrideUrlLoading refuses every host except appassets.androidplatform.net.

That second one is where a lot of apps go wrong. The usual implementation of shouldOverrideUrlLoading hands the URL to the system browser with startActivity. Write it that way and a hostile document has a one-tap path to route two below, taken by your app, in the user's name. Gander returns true and drops it. There is also no addJavascriptInterface, the call that reflects a Java object into script running on the document. All a page gets is one message channel. Text goes in, like a search query, and only integers come back.

03 Route two: ask an app that does have it

This one is real. It needs no permission, and the platform cannot close it.

Any app can call startActivity with ACTION_VIEW and an https:// URL, which opens the user's browser. Put your payload in the query string and the browser makes the request for you. Same with ACTION_SEND into a mail client. No permission is checked, because the app is not making a network request. It is asking another app to.

It cannot be closed, because it is the same mechanism as "open this link", which every app needs. What limits it is that it is loud: it brings another app to the foreground in front of you, and since Android 10 an app in the background generally cannot start an activity at all. So it cannot be done while you are not looking, which makes it close to useless as a covert channel.

Gander only starts an activity when you tap something: Share, "show in folder", "save a copy", the system pickers for opening a file or adding a folder, the links on the About screen, and Rate and Share in the home screen's menu. Nothing in a document can reach a browser. That is what the shouldOverrideUrlLoading line in section 02 is for.

04 Route three: let Android carry it for you

This one is real, it is silent, it is on by default, and it is the one that caught this app.

Android Auto Backup copies an app's private storage into the user's Google Drive. The app requests nothing and calls nothing: the system does it, on one manifest attribute that defaults to on. No permission is consulted, because the app is not the party making the request, and none of it shows up in the permission list a user can see.

Gander shipped with that attribute on through version 1.9. What went into the backup was the recents list: the names of files you had opened, the system references to them, and when. Not to me. To the user's own Drive, and since Android 9, on a phone with a lock screen set, end-to-end encrypted with a key that only that phone's PIN, pattern or password can recover, so Google cannot read it. Even so, "nothing leaves the device" was not true while it was on, and that is the one claim this app is built around.

It has been off since 1.10, in commit 2c3076d. The fix cost nothing, which is the annoying part: recents are Storage Access Framework content URIs, filtered when read against the grants the app still holds, and those grants do not survive a restore onto another phone. A restored list rendered empty however faithfully it had been copied. It was putting file names into a Drive backup in exchange for nothing at all.

There is a second half to that attribute which almost nothing tells you about, and it is the most useful thing on this page. For an app that targets Android 12 or higher, allowBackup="false" stops cloud backup but, on some manufacturers' phones, not device-to-device transfer, the copy made when you set up a new phone from your old one. That transfer reads a separate file, android:dataExtractionRules, and when the file is absent it copies everything outside the cache, code-cache and no-backup directories. Declaring it is not the default, so an app that set allowBackup="false" years ago, considered the matter closed and has since raised its target is now covering half of what it thinks it covers. Android's lint, which now calls allowBackup deprecated, says so against the exact manifest line, which is how this one surfaced.

I would rather explain why that is not a phone-home route than let it sound like one. A device transfer is started by you, runs between two phones you own, and reaches no third party: there is no server and no developer at the far end. The recents list arrives dead there too, for the same reason it arrives dead from a restore. It is on this page because of its shape rather than its size. The attribute everyone reaches for still closes exactly what it closed the year it was written, and the other half quietly became a file you have to know exists.

Gander has declared that file since 2.0, in commit aa4d18e, excluding everything, so the recents list no longer makes the trip at all.

This is the route worth taking seriously in other apps. It is invisible in the permission list, invisible in the app's own code, and enabled unless someone turned it off on purpose.

05 The ones that come up next

06 So what is the permission worth?

Not a sandbox, and anyone telling you a missing INTERNET permission proves an app cannot exfiltrate anything is overselling it. What it removes is the silent, general purpose route, leaving only routes that are loud, deliberate, or the platform's rather than the app's. An app with the permission can talk to anyone, at any time, in the background, and you would never know. An app without it has to do something you could catch.

Which is why the useful question is never "does it hold the permission" but "can I check, without trusting the developer".

Checking it yourself

  • Read the permission list from the OS rather than the store listing: Settings > Apps > the app > Permissions, or aapt dump permissions app.apk on a downloaded APK.
  • Check whether Auto Backup is on. It is android:allowBackup in the manifest, it defaults to true, and almost nothing written for users mentions it. If you ship an app that targets Android 12 or higher: on some phones that attribute no longer covers device transfers, so check for an android:dataExtractionRules file next to it.
  • If you ship a WebView: look at what your shouldOverrideUrlLoading does with a URL it does not recognise. If it hands it to startActivity, every document you render can reach the network through the user's browser, using route two, over your app's name. That is the single most common version of this bug.
  • For Gander: the About screen asks Android for the app's own permission list at runtime and prints what comes back. It leaves out one entry, a signature permission that androidx declares under Gander's own name. The rest is the OS's answer, not mine.
  • The build fails if a dependency adds a permission that has not been signed off, on the bundle uploaded to Play as well as the APK, since those are separate builds. Media3 contributes ACCESS_NETWORK_STATE; it is stripped in the manifest, and that check is what keeps it stripped across a dependency bump.
  • All of it is MIT licensed at github.com/mokshablr/gander, including everything asserted on this page.

Two things on this page are genuinely open, and I would rather be corrected than agreed with. The first is DNS: I could not establish from the public source whether a current Android resolver refuses a UID that holds no INTERNET permission, and someone reading this almost certainly knows. The second is whether there is a fourth route at all. Three is a suspiciously round number, and the two real ones here are both cases of the app getting something else to do the work, which does not feel like a category that only has two members in it.

If you have one, github.com/mokshablr/gander/discussions is the right place, or github.com/mokshablr/gander/issues if it is specific to the app.