A report recently surfaced on r/netsec describing an arbitrary file overwrite in Android-Image-Cropper, the library behind the crop screen in a very large number of apps. Search for the library plus "vulnerability" and you get the GitHub repo, a JitPack listing, and years of usage questions. Not one of them mentions a security issue. The repo cannot tell you about one, because nobody has maintained it for years.
What is confirmed, and what is still unverified
Start with the honest part, because it changes how you should treat the claim. The original thread was not retrievable at the time of writing (the page returned a network security block), so the specific trigger, the affected version range, the researcher, and the "thousands of apps" count are all unverified. We found no CVE reference either. If someone tells you they know the exact affected versions, ask where they read it.
What is verified comes from the primary artifacts, and it is damning enough on its own:
- ArthurHub/Android-Image-Cropper sits at 6.4k stars, 1.4k forks, and 576 commits, and its README states flatly: "The Project is NOT currently maintained ... Please use CanHub's fork !"
- The visible changelog ends at 2.8.0 with an Android O crash fix, an AndroidX migration, and handling for non-image file selection. No security fixes appear anywhere in it.
- The documented API is URI in, URI out:
CropImage.activity(imageUri),cropImageView.setImageUriAsync(uri), and a result returned as a Uri viaresult.getUri(). - Its setup historically requested both READ_EXTERNAL_STORAGE and WRITE_EXTERNAL_STORAGE.
An unmaintained, widely copied file-handling library with a permissive storage model and no security release process. Whatever the precise bug turns out to be, that artifact does not belong in a shipping app. Our working position is simple: treat every release of the abandoned line as affected, because no patched version of it exists or ever will.
Why this library is close to a worst case
The same codebase answers to three different names

The lineage runs edmodo/cropper (2013) to ArthurHub's fork (2016) to the maintained CanHub fork, Apache 2.0 throughout. Each hop left a Maven coordinate behind:
com.theartofdev.edmodo:android-image-cropper:2.8.+ (ArthurHub line, abandoned)
com.github.arthurhub:android-image-cropper:2.7.0 (JitPack build of the same)
com.vanniktech:android-image-cropper:4.7.0 (CanHub fork, maintained)An SCA rule keyed on one groupId sees at most a third of this family. And look at that first coordinate: 2.8.+. A wildcard version means two builds of the same app, a week apart, can resolve different artifacts, so even a correct audit expires. An older documentation site still tells people to use 2.4.+ against API Level 10, which is how ancient version strings keep circulating through tutorials long after the repo moved on.
The handoff note itself is candid about the pattern: "The same way I took an unmaintained initial implementation from edmodo, I'm happy to see CanHub taking it now." That is the lifecycle of a huge amount of mobile infrastructure. Adoption first, stewardship later, security review never.
The API contract is "give me a Uri, I will write to a Uri"
The library accepts images as Android Uris from Gallery, Camera, and Dropbox, and hands back the crop as a Uri. Between those two points it decodes a bitmap and writes it somewhere. Pair that with WRITE_EXTERNAL_STORAGE, which the setup historically requested, and you have a component whose entire job is crossing the boundary between data someone else picked and bytes on disk. That is exactly where overwrite flaws in Android libraries live. Note the 2.8.0 changelog item about gracefully failing on non-image files: the input side accepted whatever it was handed until very late in the project's life.
The exit ramp has friction built in
CanHub's migration path is a stepping-stone exercise. Pin 4.3.3 first, rewrite every import and XML reference from com.theartofdev.edmodo.cropper to com.canhub.cropper, then climb one minor at a time (4.4.0, 4.5.0, 4.6.0) up to the current 4.7.0. Real work, the kind that gets deferred quarter after quarter. The fork also deprecates the convenience flows, with this attached: "Note: This way is deprecated and will be removed in future versions." And its stated direction: "The path forward is to write your own Activity, handle all the Uri stuff yourself and use CropImageView."
Read that again. The maintainers' answer is that Uri handling belongs to the app. Even after a clean migration, the part of the system where an overwrite happens is yours.
The overwrite pattern, for anyone who has not read the thread

Since the original research sits behind a block page, here is the bug class as it manifests in this shape of API. The dangerous flow looks like this. An app launches the crop flow and the output destination is a file path or content Uri. Somewhere, that destination is influenced by input the app does not control: a filename derived from the source image's Uri, an extra on the intent that launched the flow, a deep link parameter. The library decodes and writes. If nothing canonicalizes the destination or confines it to a directory the app owns, the write lands wherever the attacker aimed it. On devices from the pre-scoped-storage era, with WRITE_EXTERNAL_STORAGE granted, "wherever" includes shared external storage, meaning files other apps can see or rely on. Inside app-private storage, a traversable path can still clobber your own configuration and caches.
Whether the reported instance triggers through the library's default destination handling or through how apps wire the API, the audit below is the same. Convenient, because you cannot wait for the write-up.
The audit you can run this afternoon
1. Enumerate every coordinate, including transitives
./gradlew :app:dependencies --configuration releaseRuntimeClasspath > deps.txt
grep -niE "theartofdev|arthurhub|canhub|vanniktech" deps.txtIf you get a hit and do not remember adding it, find the path:
./gradlew :app:dependencyInsight --configuration releaseRuntimeClasspath --dependency android-image-cropperCrop widgets ride inside larger SDKs (messaging, profile onboarding, verification flows) more often than teams expect. dependencyInsight will name the parent. While you are in the build files, grep for "+" and replace any wildcard versions you find.
2. Check which permissions ride along
Library manifests merge into yours. Check the merged manifest under app/build/intermediates (the exact subdirectory varies with your AGP version), or interrogate the built artifact:
aapt dump permissions app-release.apkIf READ_EXTERNAL_STORAGE or WRITE_EXTERNAL_STORAGE shows up and your only broad-storage consumer is a crop flow, the library is expanding your blast radius. On current Android versions, scoped storage gives you app-specific directories with no permission at all, so the broad grants buy you little.
3. Trace the output Uri in your own code
grep -rnE "CropImage\.activity|setImageUriAsync|getUri\(\)|CropImageView" app/srcFor every call site, answer two questions. Who chooses the destination of the cropped file? Does anything from outside your process (an intent extra, a deep link, the source image's filename) feed into that choice? If the destination name derives from the input Uri's last path segment, you have the pattern, full stop. The fix ordering is absolute: destinations you generate, in storage you own, always.
4. Fix the SCA rules that missed it
This is the part that outlives the bug.
- Match artifact families, not single groupIds. Maintain a coordinate list for anything you blacklist, including fork and JitPack coordinates. A rule for
com.theartofdev.edmodoalone would greenlight the same jar fromcom.github.arthurhub. - Treat maintenance state as a finding. An archived repo, a README banner like the ArthurHub one, or a changelog with long silence and no security entries should page someone even when no advisory exists. For abandoned Android libraries, an advisory often never exists.
- Flag wildcard and plus-suffixed versions at CI time. You cannot audit a version you cannot pin.
If you cannot migrate this week
Wrap the library so your code owns both ends of the Uri contract. Use CropImageView inside your own Activity, which is, not coincidentally, CanHub's recommended end state. Generate the output file yourself: app-private storage via getExternalFilesDir() or internal storage, a name you created, never anything derived from the source Uri or from request input. Validate that the input actually is an image before decode; the old line accepted non-image selections until its final release. Then schedule the migration with the 4.3.3 anchor and the minor-by-minor climb, and protect the date. The friction of that path is precisely why the abandoned artifact is still everywhere.
The pushback, answered
"No CVE, no advisory, the thread itself is unreachable. This could be nothing." It could. The asymmetric bet still favors the audit: it costs an afternoon, and the artifact is end of life either way. Waiting for formal confirmation from an unmaintained project is waiting for a signal that cannot come.
"We just call the API. A library bug is the library's problem." The maintained fork explicitly disagrees, and it is moving Uri handling into app code on purpose. Your exposure lives in your call sites, not in someone else's repo.
"The thousands-of-apps number is hype." Treat the count as unverified; we do. But 6.4k stars and 1.4k forks on the abandoned repo, a JitPack mirror carrying 6,415 stars, and three live coordinates add up to a large reachable install base, most of it silent because the dependency arrives transitively. The exact number is the least important claim in the story.
Key takeaways
- Treat the whole ArthurHub line, both
com.theartofdev.edmodoand the JitPackcom.github.arthurhubbuild, as end of life. No patch is coming, so "affected versions" effectively means all of them until you migrate. - SCA rules keyed on a single groupId miss artifact families. Enumerate coordinates, including forks and JitPack builds, and ban wildcard versions at CI.
- Own the output Uri. Write crops to app-private storage under names you generate, and drop broad storage permissions you no longer need.
- Migration to the CanHub fork is a stepping-stone path: 4.3.3 first, then one minor at a time to 4.7.0. Budget for it, because the friction is real and so is the alternative.
- No advisory for an abandoned library is normal, not reassuring. Maintenance state belongs in your dependency policy next to CVE data.




