TLDR: Don’t log sensitive information. Not on Android, and not anywhere else. Still, in 2022 I found two Android apps that do exactly that.

This post starts with an introduction to logging on Android. I describe common pitfalls that lead to sensitive information being leaked to Android logs. I describe why this is bad and what impact it can have. Finally, I show two real-world apps that were/are leaking sensitive information in this way.

Logs on Android

Logs are useful because they allow developers to trace application behaviour. It allows developers to understand what happened to an application, what state it was in, what events occurred. This is useful when troubleshooting.

Apps on Android usually write logs by calling one of the methods of the android.util.Log class:

Log.v("MY_TAG", "Something happened");

These print statements are collected into a log stream. It’s like printing to stdout, but separate.

There are different log levels: verbose, debug, info, warning, error.

Normally, apps can only read their own logs, but not other apps’ logs. However, certain (system) apps can read all logs if they have the READ_LOGS permission (see below).

If debugging is enabled in the phone’s developer settings, you can view all logs on a computer using the ADB command adb logcat or in the logcat tab in Android Studio. Logcat can filter logs by tag and by log level.

Common Pitfalls

There are two common pitfalls that I have seen Android developers fall into:

Pitfall 1: The log level DEBUG is only another tag to filter the stream when viewing logs.

Logs written with Log.d are included in builds with build type “release”. Even when BuildConfig.DEBUG == False these log statements are included and will be printed!

Developers, you need to manually wrap these Log.d statements in an if (BuildConfig.DEBUG) { ... }. Or better, you should strip them completely from the release apk/dexfile (which is non-trivial).

Google could make this more clear, or make it easier for developers to strip DEBUG level statements.

Pitfall 2: Including network logging in release builds.

Logging network requests is useful during development, since you don’t need to setup a proxy and break open TLS. And with OkHttp’s HttpLoggingInterceptor it is as easy as adding three lines of code:

HttpLoggingInterceptor logging = new HttpLoggingInterceptor(); // add this
OkHttpClient client = new OkHttpClient.Builder()
  .addInterceptor(logging) // add this
  .build();

// add this to the build.gradle
implementation("com.squareup.okhttp3:logging-interceptor:1.0.0")

// and - bam! - all network requests are logged and viewable in logcat

Developers, you should only add the HttpLoggingInterceptor temporarily while debugging a specific issue. There is no real need to even commit the HttpLoggingInterceptor to git. And if you do, at least wrap it in if (BuildConfig.DEBUG) { ... } and add the dependency only to debug builds (with flavourDebugImplementation(...), see the docs).

Impact of Logging Sensitive Information

Logging sensitive data can result in data exposure to third parties.

On Android specifically, logging crosses the trust boundary between the app sandbox and the logging system.

For example, an app that uses an access token for network requests can store the token in its “shared preferences”. Despite the name, shared preferences are sandboxed and cannot be accessed by other apps. But when the access token is written to logcat it leaks outside the sandbox. Other apps can read these logs (with the appropriate permission, see below) and can now access the token!

The corresponding CWE is CWE-532 (Insertion of Sensitive Information into Log File).

Exploitability

Reading logs requires the android.permission.READ_LOGS permission.

User apps usually do not have this permission. It can be granted via adb (some apps use this), but for the average user this is an unrealistic hurdle.

System apps that come pre-installed sometimes have this permission.

And starting in Android 13, in addition to requiring the READ_LOGS permission, Android also prompts the user:

READ_LOGS Permission Request

READ_LOGS Permission Request

But how widespread is this?

In my own test 44 system apps had the READ_LOGS permission on OxygenOS 11 (OnePlus 7, as of June 2022), and 21 apps on LineageOS 20 (without GApps, OnePlus 5, as of May 2023).

AppCensus also noted a similar problem of sensitive data exposure via logs back in April 2021. They found that 59 apps can READ_LOGS on a Xiami Redmi Note 9, and 89 apps on a Samsung Galaxy A11.

The following apps/packages can READ_LOGS on OxygenOS 11 (produced with this script):

android
cn.oneplus.nvbackup
com.android.dynsystem
com.android.inputdevices
com.android.keychain
com.android.localtransport
com.android.location.fused
com.android.providers.settings
com.android.server.telecom
com.android.settings
com.android.wallpaperbackup
com.dsi.ant.server
com.fingerprints.fingerprintsensortest
com.google.android.feedback
com.google.android.gms
com.google.android.gsf
com.oem.autotest
com.oem.logkitsdservice
com.oem.nfc
com.oem.oemlogkit
com.oem.rftoolkit
com.oneplus
com.oneplus.backuprestore.remoteservice
com.oneplus.brickmode
com.oneplus.camera.service
com.oneplus.config
com.oneplus.coreservice
com.oneplus.factorymode
com.oneplus.filemanager
com.oneplus.gamespace
com.oneplus.minidumpoptimization
com.oneplus.opbackup
com.oneplus.opbugreportlite
com.oneplus.orm
com.oneplus.screenrecord
com.oneplus.screenshot
com.oneplus.sdcardservice
com.oneplus.security
com.oneplus.setupwizard
com.oneplus.sound.tuner
com.qti.diagservices
com.tencent.soter.soterserver
net.oneplus.commonlogtool
net.oneplus.odm

And on LineageOS 20 (without GApps):

android
com.android.dynsystem
com.android.inputdevices
com.android.keychain
com.android.localtransport
com.android.location.fused
com.android.providers.settings
com.android.server.telecom
com.android.settings
com.android.shell
com.android.wallpaperbackup
com.dsi.ant.server
com.stevesoltys.seedvault
com.tencent.soter.soterserver
lineageos.platform
org.lineageos.lineageparts
org.lineageos.lineagesettings
org.lineageos.pocketmode
org.lineageos.settings.device
org.lineageos.settings.doze
org.lineageos.setupwizard

Notice that this includes Google Play Services (com.google.android.{gms, gsf}) and various proprietary OnePlus apps. It is unclear whether these apps do access the logs and/or sent them to vendors. But they do have the permission do to so.

In addition, both OnePlus and Google Pixel devices have features that allow users to explicitly submit logs to the vendor.

  • OnePlus: Settings > System > Experience Improvement Programmes > System Stability program
  • Googe Pixel: Settings > Tips & Support > Send feedback

Overall, the exploitability is low. Still, sensitive information could end up on the vendors’ servers.

Fixing it

Fixing this is easy. Just remove all logging statements (network and other) that expose sensitive information to logcat (at least in production builds).

Real-World Examples

Below I show the two real-world apps that I found logging sensitive information, prompting me to write this post.

How did I find these? In both cases, the apps were buggy. Some unrelated feature was not working for me. And when I investigate bugs the first thing I do is open logcat. This is where I went “oh dear”.

Both apps have one thing in common (despite the logging issue): they don’t have a security.txt or a Vulnerability Disclosure Policy (let alone a Bug Bounty).1 In both cases I had to jump through hoops to find a contact. For app 1 I luckily had a shared contact at the company that I could reach out to. For app 2 I had to go through normal support.

So in 2022, it was still challenging to report vulnerabilities, even for two apps that both handle financial transactions and that both have 100K+ downloads on Google Play.

In the logcat excerpts below I replaced sensitive information with 🚫🚫🚫.

App 1: Unnamed

The first app is a financial app. It was logging passwords, but also network traffic:

D Action  : Logon(vertragsNr=🚫🚫🚫, password=🚫🚫🚫, appInfo=AppInfo(appIdentifier=🚫🚫🚫, versionNumber=42.0, buildNumber=42, deviceName=OnePlus, ONEPLUS A5000, osVersion=Android 12 (32) | Code name: REL, unlocked=true, pushServiceType=GOOGLE), isBiometricLogon=false, biometryType=null)
D Network : GET /mobile/home/context -> HomeContext(sections=[HomeSection(type=FINANZEN, position=0, title=null), HomeSection(type=SHORTCUTS, position=1, title=null), HomeSection(type=ZU_ERLEDIGEN, position=2, title=You have), HomeSection(type=MARKETING_CONTENT, position=3, title=Discover)])
<!-- more network traffic omitted -->

This log leaks the username (“vertragsNr”) and password. Also notice how the network responses are logged as data classes. They reflect the sections on the app’s home screen. This is bad, because the app was effectively streaming the screen state to logcat.

The log level “D” (debug) demonstrates Pitfall 1.

Disclosure Timeline

  • Wed 2022-06-22 21:30: I reach out to my shared contact at the company.
  • Thu 2022-06-23 08:18: They reply and say they will find the right contact internally.
  • Thu 2022-06-23 10:22: The security team reaches out to me.
  • Thu 2022-06-23 11:35: I send them my writeup.
  • Thu 2022-06-23 15:22: The security team acknowledges that they reproduced and patched it and will make a new release soon.
  • Thu 2022-06-23 22:45: I test their fix, and verify that it works.

Within a single day the issue was acknowledged and fixed! They clearly had the internal processes in place to handle security issues, even if they did not have a public security contact yet (now they do1).

(Why am I not naming them? Their handling was exemplary, it is fixed, there is no public benefit knowing the name. If you keep your apps up-to-date, there is nothing else you as a user need to do to protect yourself. The only public benefit is in knowing that I found more than one app, because this demonstrates that this is a recurring issue that we as a security and developer community need to be aware of.)

App 2: Circuit Laundry

The second app is the app for a laundry service in the UK, called Circuit Laundry. This app allows you to log in, pay with your credit card within the app to top up your account balance, and use that account balance to start washing machines and tumble driers.

Just like App 1, Circuit also streams all network traffic to logcat. This includes credentials (passwords, bearer tokens), personally identifiable information (first name, last name, email address), account balance, and account id.

Logs written during login:

I okhttp.OkHttpClient: --> POST https://phoneadmin.flashcashonline.com/api/user/authenticate/?password=🚫🚫🚫&version=2&email=🚫🚫🚫&platform=Android
I okhttp.OkHttpClient: Content-Length: 0
I okhttp.OkHttpClient: --> END POST (0-byte body)
I okhttp.OkHttpClient: <-- 200 https://phoneadmin.flashcashonline.com/api/user/authenticate/?password=🚫🚫🚫&version=2&email=🚫🚫🚫&platform=Android (1201ms)
I okhttp.OkHttpClient: cache-control: no-cache
I okhttp.OkHttpClient: pragma: no-cache
I okhttp.OkHttpClient: content-type: application/json; charset=utf-8
I okhttp.OkHttpClient: expires: -1
I okhttp.OkHttpClient: vary: Accept-Encoding
I okhttp.OkHttpClient: set-cookie: ARRAffinity=6eaf2cedfb705ed6ce633e6c1ba37f34686cbd9dce05635559dff6fd9e92ea1a;Path=/;HttpOnly;Secure;Domain=phoneadmin2.azurewebsites.net
I okhttp.OkHttpClient: set-cookie: ARRAffinitySameSite=6eaf2cedfb705ed6ce633e6c1ba37f34686cbd9dce05635559dff6fd9e92ea1a;Path=/;HttpOnly;SameSite=None;Secure;Domain=phoneadmin2.azurewebsites.net
I okhttp.OkHttpClient: x-aspnet-version: 4.0.30319
I okhttp.OkHttpClient: x-powered-by: ASP.NET
I okhttp.OkHttpClient: x-cache: CONFIG_NOCACHE
I okhttp.OkHttpClient: x-azure-ref: 0xYNjYwAAAAAEtq35BO3kQa1pmW2pi1HQTFRTRURHRTEzMTAAYWFiZjcyNTctMmE5NC00MDY5LTlkMGMtMWM1NTQzYjNlZWIz
I okhttp.OkHttpClient: date: Thu, 03 Nov 2022 09:03:01 GMT
I okhttp.OkHttpClient: {"Data":{"AppUserId":🚫🚫🚫,"UserIdentification":"🚫🚫🚫","HasPromotions":false,"Token":{"Value":"🚫🚫🚫","Expires":null},"PrimaryLocation":"","AccountBalance":6.80,"InternalId":🚫🚫🚫,"ExternalKey":"🚫🚫🚫","AccountName":"🚫🚫🚫","AccountOperatorID":5,"AccountMinimumPurchaseAmount":5.00,"AccountLowBalanceIndicator":5.00,"AccountCurrencyTypeID":3,"AccountCurrencyUniCode":"20AC","IsRoomViewAvailable":true,"AccountWelcomeTitle":"Welcome to Circuit","AccountWelcomeText":"Welcome to Circuit","FirstName":"Thore","LastName":"Goebel","OptInNotification":false,"OptInMarketing":false,"EmailAddress":"🚫🚫🚫","AppVersion":"2","AppPlatform":"Android","MessageForUser":""},"Success":true,"Message":"Authenticated"}
I okhttp.OkHttpClient: <-- END HTTP (1727-byte body)

Logs written during app usage later:

I okhttp.OkHttpClient: --> GET https://phoneadmin.flashcashonline.com/api/user/
I okhttp.OkHttpClient: authorization: bearer 🚫🚫🚫
I okhttp.OkHttpClient: --> END GET
I okhttp.OkHttpClient: <-- 200 https://phoneadmin.flashcashonline.com/api/user/ (217ms)
I okhttp.OkHttpClient: cache-control: no-cache
I okhttp.OkHttpClient: pragma: no-cache
I okhttp.OkHttpClient: content-type: application/json; charset=utf-8
I okhttp.OkHttpClient: expires: -1
I okhttp.OkHttpClient: vary: Accept-Encoding
I okhttp.OkHttpClient: set-cookie: ARRAffinity=6eaf2cedfb705ed6ce633e6c1ba37f34686cbd9dce05635559dff6fd9e92ea1a;Path=/;HttpOnly;Secure;Domain=phoneadmin2.azurewebsites.net
I okhttp.OkHttpClient: set-cookie: ARRAffinitySameSite=6eaf2cedfb705ed6ce633e6c1ba37f34686cbd9dce05635559dff6fd9e92ea1a;Path=/;HttpOnly;SameSite=None;Secure;Domain=phoneadmin2.azurewebsites.net
I okhttp.OkHttpClient: x-aspnet-version: 4.0.30319
I okhttp.OkHttpClient: x-powered-by: ASP.NET
I okhttp.OkHttpClient: x-cache: CONFIG_NOCACHE
I okhttp.OkHttpClient: x-azure-ref: 0yYNjYwAAAACoPdEYF50OSrWF+8q6w4IZTFRTRURHRTEzMTAAYWFiZjcyNTctMmE5NC00MDY5LTlkMGMtMWM1NTQzYjNlZWIz
I okhttp.OkHttpClient: date: Thu, 03 Nov 2022 09:03:04 GMT
I okhttp.OkHttpClient: {"Data":{"AppUserId":🚫🚫🚫,"UserIdentification":"🚫🚫🚫","HasPromotions":false,"Token":null,"PrimaryLocation":null,"AccountBalance":6.80,"InternalId":🚫🚫🚫,"ExternalKey":"🚫🚫🚫","AccountName":"JLA LAB","AccountOperatorID":5,"AccountMinimumPurchaseAmount":5.00,"AccountLowBalanceIndicator":5.00,"AccountCurrencyTypeID":3,"AccountCurrencyUniCode":"20AC","IsRoomViewAvailable":false,"AccountWelcomeTitle":null,"AccountWelcomeText":null,"FirstName":"Thore","LastName":"Goebel","OptInNotification":false,"OptInMarketing":false,"EmailAddress":"🚫🚫🚫","AppVersion":"2","AppPlatform":"Android","MessageForUser":null},"Success":true,"Message":"Authorized"}
I okhttp.OkHttpClient: <-- END HTTP (669-byte body)

Disclosure Timeline

  • Mon 2022-11-07: Initial outreach to {security, info, contact, postmaster}@circuit.co.uk. {security, contact}@ bounced.
  • Mon 2022-11-14: Outreach to info@circuitcardtopup.com (found on Google Play) and dataprotection@jla.com (found in the Privacy Policy). Interestingly, the data protection contact does not reply.
  • Tue 2022-11-15: Circuit support (info@circuitcardtopup.com) replies. I send them the report.
  • Sun 2023-02-04: I ask for an update and notify them that I plan to publish in 90 days (i.e. on 2023-05-05). I decided to count the 90 days from today since I did not mention my intent to publish in November.
  • Wed 2023-02-08: Circuit support replies: “I have passed this on to our app developers again to look into it for you and have escalated it. I will update you once they come back to me.”
  • Mon 2023-04-17: I ask for an update.
  • Tue 2023-04-18: Circuit support replies: “There have been no updates provided to us as of yet but I will chase up with the app development team and let you know of any updates when we get them.”
  • 2023-05-10: I publish this blog post.

The issue remains unfixed, ~6 months after I first reported it, and despite the fix being super simple. The affected version is 4.1.0 which was released on 31 March 2022.2

As a user there is very little you can do. You could reboot your phone after using the Circuit app (since this wipes logcat). You could also avoid explicitly submitting logs to vendors (Google Pixel’s “Send feedback”). Or you could take your laundry somewhere else.

Conclusion

First, if you are a developer, don’t write sensitive information to logs. On Android, by writing logs you are sending information outside of the app’s sandbox. As I have shown, tens of preinstalled apps can read logs. It is an easy issue to fix and avoid. But it also easily slips through PR reviews, and there are two common pitfalls for developers.

Second, if you are a business, have a security contact and a security.txt. Even if you have never received a report before. Some day you will. And even if your main business is not selling software, you still need to have a security contact.

P.S.: The Expires field in the security.txt is required.


  1. App 1 did not have a security.txt when I found this issue in June 2022. Now in May 2023 they have one. App 2 still doesn’t. ↩︎ ↩︎

  2. There is also another app called “Circuit Laundry Plus”. I haven’t tested it. ↩︎