Coordinated Disclosure Timeline

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

OsmAnd_5.3.3

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:

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:

Impact

A zero-permission malicious app installed on the same device can:

  1. Steal real-time GPS location without ACCESS_FINE_LOCATION by polling getAppInfo() every second — enabling stalkerware-grade tracking.
  2. 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.
  3. Delete arbitrary files within OsmAnd’s sandbox — finishFileCopy() deletes the destination file before attempting renameTo(), so even a failed rename still destroys the target.
  4. Enumerate complete movement history via getImportedGpxV2() — when, how far, how fast, at what altitude the user has traveled.
  5. Stream OsmAnd’s full process logcat including GPS coordinates, search queries, and navigation events.

    CWEs

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

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.