Explainer · Permissions
6 September 2026
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.
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.
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.
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.
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.
INTERNET permission, I could not establish; see the end of this
page. What is certain either way is that the channel needs deliberate code,
because you have to build the hostname and resolve it, and there is no such
call in Gander.
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
aapt dump permissions app.apk on a downloaded APK.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.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.ACCESS_NETWORK_STATE; it is stripped
in the manifest, and that check is what keeps it stripped across a dependency
bump.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.