External storage permissions on Android

The permission model introduced in Android 6.0 offers a new layer of security for users. Some actions have been refined to be more secure without requesting dangerous permissions to interact with files. For instance, using the right intent action for firing up the Android SAF picker does not even require the storage permissions. But why?
In this article, we’ll delve into the Android permission model — in particular, the storage permissions. We’ll have a look at how permissions work, starting from a high level and working our way down to the lower level managed by FUSE, the native Android module that interacts with the file system.
The dawn of the new permissions era
The main purpose of the Android permission model is to provide final users with a better understanding of which resources an app is going to use. If a device is running Android 6.0 (API level 23) or higher and the app’s targetSdkVersion
(opens in a new tab) is 23 or higher, the user can refuse a specific permission. But if an app is well implemented, it should work even without accessing the resource that has been prohibited. That said, an app that must work without a resource uses a different implementation flow, and it is the responsibility of professional developers to make sure the user won’t experience crashes or strange behaviors.
The four protection levels
There are four protection levels that affect third-party apps(opens in a new tab): normal, signature, dangerous, and special permissions.
Using the adb
tool, we can list all the permissions. Adding the -d
and -g
options, we’ll list only dangerous permissions organized by group:
adb shell pm list permissions -d -g
To grant or revoke a single permission for an app, use the following:
adb shell pm [grant|revoke] <package-name> <permission-name>
You can grant as many permissions as you like by using this command multiple times.
✅ Normal permissions
Normal permissions are permissions that are not considered harmful. Examples of this include the permission to set the time zone. Normal permissions are granted at installation time, and the only precondition is declaring them in the manifest of the app.
⛔️ Signature permissions
Signature permissions are generally permissions defined by one app and used by another. The system grants these app permissions at install time only if the requesting application is signed with the same certificate as the application that declared the permission.
💀 Dangerous permissions
Dangerous permissions could potentially expose the user’s private information or share operations with other apps. For example, the ability to read the user’s contacts is a dangerous permission. Dangerous permissions are granted at runtime, and the user must explicitly grant the permission to the app.
🦄 Special permissions
Special permissions are a small set of permissions that are particularly sensitive, so most apps should not use them. Examples of these include SYSTEM_ALERT_WINDOW
(opens in a new tab) and WRITE_SETTINGS
(opens in a new tab). Special permissions must be declared in the manifest, and the app will send an intent requesting the user’s authorization by showing a detailed management screen to the user.
Storage permissions
Storage permissions are dangerous permissions for accessing the shared external storage. Full read and write access to any location of the volume is protected by two permissions marked as dangerous: READ_EXTERNAL_STORAGE
(opens in a new tab) and WRITE_EXTERNAL_STORAGE
(opens in a new tab).
When an app is granted storage permission, it can access the device storage at any time. This means it can upload personal files or even delete sensitive information from the device, so it’s better to think twice before giving storage permission to untrusted apps, as it can be harmful.
Only when the external storage is mounted and the permissions are granted will Android let you call Environment#getExternalStorageDirectory()
(opens in a new tab).
Calling a method that requires storage access without the right permissions will throw the exception java.lang.SecurityException
.
Accessing storage volume without storage permissions
There are some special paths that can be accessed without reading and writing permissions that are particularly useful for storing app private data: Context#getExternalFilesDir(String)
(opens in a new tab), Context#getExternalCacheDir()
(opens in a new tab), and Context#getExternalMediaDirs()
(opens in a new tab).
Another way to access a specific file without requiring dangerous permissions is by relying on the Android Storage Access Framework (SAF) picker(opens in a new tab).
The SAF picker does not allow an app to have full control of the storage, is much more restricted, and requires some interaction with the user to choose the right location: This can be a new file name to save or a specific document to open. There is also a special case where the SAF picker can open an entire directory using the intent action ACTION_OPEN_DOCUMENT_TREE
(opens in a new tab).
The outcome of the SAF picker will be a Uri
(opens in a new tab) that can be opened by a ContentResolver
(opens in a new tab).
For example, if you want to open a document selected by the SAF picker, use the following code:
val inputStream = context.contentResolver.openInputStream(documentUri)
InputStream inputStream = context.getContentResolver().openInputStream(documentUri);
See the SAF picker in action on our free PDF Viewer for Android(opens in a new tab) app.
Security
Android manages external storage using FUSE(opens in a new tab), a Unix-like daemon that can be seen as a virtual file system that prevents malicious users from accessing protected code. The actual FUSE implementation(opens in a new tab) is written in C++ and can be executed only as root.
Final thoughts and reference
Android permissions must be treated wisely, and here at Nutrient, we focus heavily on security and privacy. When using our Android PDF SDK, we ensure that the use of dangerous and special permissions is avoided when not strictly required, and an app should work even when a permission isn’t granted. There are many tutorials on the internet about the Android permission model, but because it evolves rapidly, it’s always a good idea to check if the information is up to date, as in the latest Android API, many methods have changes.
Other useful resources
- Permissions usage notes(opens in a new tab)
- The great Nick Butcher talking about permissions in Android Marshmallow 6.0(opens in a new tab)
- Permissions overview(opens in a new tab)
- Android runtime permissions example project(opens in a new tab)
- “Mother, May I?” Asking for Permissions (Android Dev Summit 2015)(opens in a new tab)
- Forget the Storage Permission: Alternatives for sharing and collaborating by Ian Lake(opens in a new tab)
- On the Edge of the Sandbox: External Storage Permissions(opens in a new tab)
- Open files using storage access framework(opens in a new tab)
FAQ
What permissions are considered “dangerous” on Android?
Dangerous permissions are those that could impact user privacy, such as accessing contacts or external storage.
How does Android FUSE contribute to file security?
FUSE acts as a virtual file system that prevents unauthorized access, enhancing data protection on Android devices.
When does an Android app need storage permissions?
Apps need storage permissions when they require full access to the external storage, especially to read or write files directly.
Can apps access external storage without permissions?
Yes, apps can use the Storage Access Framework (SAF) picker to access files selectively without needing full storage permissions.
What happens if an app requests a denied permission?
If a permission is denied, the app should have a fallback to prevent errors or crashes and provide a smooth user experience.