Security News

Cybersecurity news aggregator

🤖
HIGH Vulnerabilities Reddit r/netsec

How a popular Android library silently exposed thousands of apps to Arbitrary File Overwrite (AFO). https://itis911.github.io/writeups/cropper-vulnerability.html

  • What: Popular Android library exposes apps to Arbitrary File Overwrite
  • Impact: Thousands of apps may be vulnerable to file manipulation
Read Full Article →

Android File Theft Third-Party Lib Intent Security How a popular Android image cropping library silently exposed thousands of apps to Arbitrary File Overwrite (AFO). EN --- We trust open source. We `implementation()` a library, scan the README, maybe check the stars count, and move on. Nobody audits the merged manifest. Nobody decompiles the `.aar` to trace what the library actually registers in your app. We treat "open source" as "someone else already reviewed it" — and most of the time, nobody did. This is a story about what happens when a widely-used image cropping library ships `android:exported="true"` on an Activity and root-scoped `<paths>` on a FileProvider as its *defaults*. Not as a misconfiguration. Not as a dev mistake. As the library's intended manifest. And every app that imported it — fintech, e-commerce, social, telco — inherited that attack surface without writing a single line of vulnerable code. --- ## Root cause: the library, not the integrator [android-image-cropper](https://github.com/CanHub/Android-Image-Cropper) (`com.canhub:android-image-cropper`, the maintained fork of the old ArthurHub/edmodo cropper) ships an `AndroidManifest.xml` that gets merged verbatim into every consuming app: ```xml <!-- cropper/src/main/AndroidManifest.xml --> <provider android:name="com.canhub.cropper.CropFileProvider" android:authorities="${applicationId}.cropper.fileprovider" android:exported="false" android:grantUriPermissions="true"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/library_file_paths"/> </provider> <activity android:name="com.canhub.cropper.CropImageActivity" android:exported="true"/> ``` and the paired `library_file_paths.xml`: ```xml <paths> <files-path name="images" path="."/> <!-- files/ , entire subtree --> <cache-path name="cached_files" path="."/> <!-- cache/ , entire subtree --> <external-files-path name="my_images" path="/"/> <!-- external files/, entire root --> </paths> ``` Read that again. `exported="true"` on the activity plus root-scoped `<paths>` on its grant-capable `FileProvider` — those are the library's *defaults*, not an integration mistake. Every downstream app that pulls the dependency without an explicit manifest override (`tools:node="merge"` and a narrower `<paths>`) inherits both. In the wild that turned out to be nearly every app that used it: no theming, no permission wrapper, no caller check. Manifest merger just accretes what the library declares. --- ## The chain `CropImageActivity.onCreate()` pulls two Parcelables straight out of the launching Intent, with no signature check, no `getCallingPackage()` verification, nothing: ```kotlin val bundle = intent.getBundleExtra(CropImage.CROP_IMAGE_EXTRA_BUNDLE) cropImageUri = bundle?.parcelable(CropImage.CROP_IMAGE_EXTRA_SOURCE) cropImageOptions = bundle?.parcelable(CropImage.CROP_IMAGE_EXTRA_OPTIONS) ?: CropImageOptions() ``` `CropImageOptions` is a public, fully attacker-populated Parcelable. Two fields matter: `customOutputUri` and `skipCropMenu` (`skipEditing` in newer releases — auto-triggers the crop with zero UI once the source image loads). Any installed app can explicit-intent this activity — `exported="true"` needs no permission — hand it a `content://` source URI inside the victim's own `.cropper.fileprovider` authority (readable, thanks to the root-scoped `<paths>`), a `customOutputUri` pointing at an *attacker-owned* provider, and `skipCropMenu = true`. The activity flashes, decodes, "crops," writes to the attacker's URI, and finishes — no tap required. Reverse the source/destination and it's a write instead of a read: overwrite anything the same `<paths>` roots expose, at the victim app's UID. --- ## Pushing the primitive: ATO and RCE You've got a file I/O primitive. The obvious next question: how far does it go? ### Account takeover via config corruption Most apps store their backend config on disk somewhere — API base URL in SharedPreferences, auth endpoint in a config XML, token storage paths. The idea: find the file, overwrite it with a version that points to your server, intercept the next auth request. The `.bak` angle makes this more interesting than a straight overwrite. `SharedPreferencesImpl` uses an atomic-write pattern internally — before persisting new data, it renames the current file to `.bak`. If the process dies mid-write (force-stop, OOM kill, crash), the next `loadFromDisk()` call sees the `.bak` file and automatically promotes it: ```java // SharedPreferencesImpl recovery logic if (mBackupFile.exists()) { mFile.delete(); mBackupFile.renameTo(mFile); // .bak becomes the real file } ``` So the play: overwrite `app_config.xml.bak` via the cropper primitive, wait for a force-stop (or trigger one — battery optimization, memory pressure, whatever), and on next launch the app loads your config. Auth traffic redirected to your endpoint. Session tokens, refresh tokens, credentials — all yours. That's the theory. Keep reading for why it doesn't work clea...

Share this article