Coordinated Disclosure Timeline
- 2026-04-14: Sent reports via email to support@osmand.net
- 2026-06-28: Maintainer confirmed all vulnerabilities fixed.
Summary
OsmAnd version 5.3.3 is vulnerable because of the exported and permissionless OsmandAidlServiceV2, which auto-approves any caller via the isAppEnabled method. This vulnerability allows unauthorized applications to exploit all 97 AIDL methods, enabling attacks such as path traversal via copyFileImpl and real-time GPS location theft via getAppInfo.
Project
osmand
Tested Version
3043c92b27b24a0b2c848bc7041cf5f69ede4ed6
Details
The exported OsmandAidlServiceV2 auto-approves any caller via isAppEnabled() in OsmandAidlApi.java, granting a zero-permission app access to all 97 AIDL methods — enabling both path traversal and real-time GPS location theft (GHSL-2026-109)
Attack surface
OsmandAidlServiceV2 is exported with no android:permission attribute (AndroidManifest.xml):
<service android:name="net.osmand.aidl.OsmandAidlServiceV2" android:exported="true" >
<intent-filter>
<action android:name="net.osmand.aidl.OsmandAidlServiceV2"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</service>
The V1 service OsmandAidlService is identically exported without a permission (AndroidManifest.xml). Compare to DownloadService which does declare android:permission="${applicationId}.SERVICE_PERMISSION".
Any app on the device can bindService() to these services with zero Android permissions and zero user interaction.
Authorization bypass — isAppEnabled() auto-approves all callers
Every AIDL method is gated by getApi(), which calls isAppEnabled():
public boolean isAppEnabled(@NonNull String pack) {
ConnectedApp connectedApp = connectedApps.get(pack);
if (connectedApp == null) {
connectedApp = new ConnectedApp(app, pack, true); // ← enabled=true
connectedApps.put(pack, connectedApp);
saveConnectedApps();
}
return connectedApp.isEnabled(); // ← always true for first-time callers
}
This is a permission check that grants the permission it is supposed to enforce. Any unknown caller is auto-registered with enabled=true on first contact. The entry is persisted to API_CONNECTED_APPS_JSON in SharedPreferences (OsmandAidlApi.java#L1956-L1968).
Once getApi() returns the OsmandAidlApi singleton, the caller has unrestricted access to all 97 AIDL methods.
Vulnerability 1: Path traversal in copyFileImpl() — arbitrary file write/delete
Entry point: OsmandAidlServiceV2.copyFile(CopyFileParams params) forwards attacker-controlled destinationDir, fileName, and filePartData to api.copyFileV2().
copyFileV2() — no sanitization: Checks only isEmpty(fileName), null, and length > 256KB. Does not check for .. or path separators in either destinationDir or fileName.
copyFileImpl() — the sink (OsmandAidlApi.java#L2572-L2615):
private int copyFileImpl(String fileName, byte[] filePartData, long startTime,
boolean done, String destinationDir) {
File tempDir = FileUtils.getTempDir(app);
File file = new File(tempDir, fileName); // line 2574
File destFile = app.getAppPath(new File(destinationDir, fileName).getPath()); // line 2575 ★
// ...
}
getAppPath() blindly concatenates:
public File getAppPath(@Nullable String path) {
String child = path != null ? path : "";
return new File(externalStorageDirectory, child); // no canonicalization
}
finishFileCopy() then deletes the existing file at the traversed destination and renames the temp file onto it:
private boolean finishFileCopy(byte[] data, File file, FileOutputStream fos,
String fileName, File destFile) throws IOException {
// ...
if (destFile.exists() && !destFile.delete()) { // line 2622 — DELETE
res = false;
}
if (res && !file.renameTo(destFile)) { // line 2625 — WRITE
file.delete();
res = false;
}
// ...
}
V1 also vulnerable: copyFile() (V1) checks only fileName.endsWith(".sqlitedb") but does not reject ... A filename like "../poc.sqlitedb" passes the extension check and traverses via copyFileImpl().
Vulnerability 2: GPS location theft via getAppInfo()
getAppInfo() returns:
AppInfoParams getAppInfo() {
ALatLon lastKnownLocation = null;
Location location = app.getLocationProvider().getLastKnownLocation(); // line 1831
if (location != null) {
lastKnownLocation = new ALatLon(location.getLatitude(), location.getLongitude());
}
// ...
ALatLon mapLocation = new ALatLon(mapLoc.getLatitude(), mapLoc.getLongitude()); // line 1841
// ...
destinationLocation = new ALatLon(latLon.getLatitude(), latLon.getLongitude()); // line 1855
// ...
}
This returns:
lastKnownLocation— the device’s real-time GPS coordinates (from OsmAnd’sLocationProvider, which uses OsmAnd’s ownACCESS_FINE_LOCATIONpermission)mapLocation— where the user is currently looking on the mapdestinationLocation— the user’s navigation destination (if routing is active)leftDistance,leftTime,arrivalTime— full route state
Combined with registerForUpdates(1000, callback) (minimum 1-second interval), the attacker polls getAppInfo() on each tick for continuous real-time GPS tracking — all from a zero-permission app that never requested ACCESS_FINE_LOCATION.
Additional data exfiltration via the same auto-enable bypass
The same isAppEnabled() auto-approve grants access to:
-
getImportedGpxV2()— enumerates all GPX track files with full metadata: start/end timestamps, total distance, speed (min/avg/max), elevation profile, waypoint counts. Track filenames often encode dates and place names. This is complete movement-history catalogue access. -
registerLogcatListener()— executesRuntime.exec("logcat", filterLevel, "--pid=" + myPid(), "-T", "10000")and streams OsmAnd’s process logs (last 10,000 lines of history + live tail) to the calling app. OsmAnd logs raw lat/lon coordinates at debug level, search queries, navigation events, and map download selections.
Impact
A zero-permission malicious app installed on the same device can:
- Steal real-time GPS location without
ACCESS_FINE_LOCATIONby pollinggetAppInfo()every second — enabling stalkerware-grade tracking. - Write arbitrary files within OsmAnd’s storage sandbox via path traversal in
copyFileImpl()— overwriting map data, GPX tracks, favorites, or (on internal-storage mode) SharedPreferences. - Delete arbitrary files within OsmAnd’s sandbox —
finishFileCopy()deletes the destination file before attemptingrenameTo(), so even a failed rename still destroys the target. - Enumerate complete movement history via
getImportedGpxV2()— when, how far, how fast, at what altitude the user has traveled. - Stream OsmAnd’s full process logcat including GPS coordinates, search queries, and navigation events.
CWEs
- CWE-22: “Improper Limitation of a Pathname to a Restricted Directory (‘Path Traversal’)”
- CWE-862: “Missing Authorization”
- CWE-200: “Exposure of Sensitive Information to an Unauthorized Actor”
- CWE-926: “Improper Export of Android Application Components”
Proof of Concept
A proof of concept was created with the following operations. The source code of the entire application can be requested.
Buttons:
| Button | What it does | Verification |
|---|---|---|
| 1. Bind | bindService() to OsmandAidlServiceV2 — auto-enabled by isAppEnabled() |
Logs CONNECTED + AUTO-ENABLED |
| E3. getAppInfo() | Calls getAppInfo() → returns lastKnownLocation (GPS lat/lon), mapLocation, destinationLocation |
Displays coordinates + OpenStreetMap link |
| E3-live | registerForUpdates(1000ms) + polls getAppInfo() each tick → continuous GPS tracking |
GPS fixes stream every ~1 second |
| E2. logcat | registerForLogcatMessages("*:D") → OsmAnd’s full process logcat streamed to PoC |
10k-line history dump + live tail |
| 2A. Traversal | copyFile with destinationDir=".." → file written above the storage root |
adb shell ls /sdcard/Android/data/<pkg>/GHSL_traversal_proof_*.txt |
GPS theft demo (E3):
██ GPS LOCATION ██
██ lat = 37.4219983
██ lon = -122.084
██ https://www.openstreetmap.org/?mlat=37.42&mlon=-122.08#map=15/37.42/-122.08
Path traversal demo (2A):
[*] copyFile() -> 0 (OK_RESPONSE)
[!!!] TRAVERSAL CONFIRMED — file written ABOVE storage root.
Verify:
adb shell ls -la /sdcard/Android/data/net.osmand.plus/GHSL_traversal_proof_1.txt
adb shell cat /sdcard/Android/data/net.osmand.plus/GHSL_traversal_proof_1.txt
CVE
- CVE-2026-65995
Credit
This issue was discovered and reported by GHSL team member @Kwstubbs (Kevin Stubbings).
Contact
You can contact the GHSL team at securitylab@github.com, please include a reference to GHSL-2026-109 in any communication regarding this issue.