MASWE-0005: Sensitive Data Leakage via Logging (CWE-532 / CWE-359)
Standard: OWASP MASVS-STORAGE-2 | Test: MASTG-TEST-0011 | Best Practice: MASTG-BEST-0002
π Overview
This document serves as the developer reference guide for all security mitigations implemented
in :app-secure for the MASWE-0005 (Sensitive Data Leakage via Logging) vulnerability class.
Sensitive data β including PII (Personally Identifiable Information), PCI-DSS cardholder data, system cryptographic keys, and OAuth tokens β must never cross trust boundaries unencrypted, especially not into system logs, local files, or third-party telemetry pipelines.
The mitigations below are organized into 9 security groups, each targeting a distinct attack vector.
β Vulnerable Implementation (:app-vulnerable)
The insecure application violates multiple OWASP MASVS requirements by directly logging and persisting unredacted data. Key violations:
| Vector | Vulnerability |
|---|---|
| System Console | Master Key, TCKN, and Plaintext Password dumped to Logcat with string concatenation (Heap leak) |
| Network Interceptor | Authorization: Bearer <token> and X-CSRF-Token printed in cleartext via OkHttp |
| Local File Dump | PAN, CVV, and PIN written to a plaintext JSON file on disk |
| SDK Telemetry | Raw email, clipboard data, and draft messages sent to third-party Analytics SDKs |
| WebView Console | OAuth Refresh Token and Session Cookie forwarded from JavaScript to native Logcat |
β
Secure Implementation (:app-secure) β Mitigation Reference
Group 1 β Custom Logging Infrastructure
File: app-secure/src/main/java/com/hasantuncay/mobsec/secure/utils/SecureLog.kt
Instead of using Android's native android.util.Log directly (which allows arbitrary string
concatenation), a custom logging gateway is implemented. This is the "silver bullet" solution
recommended by OWASP MASTG-BEST-0002.
| # | Method | Security Level | Description |
|---|---|---|---|
| 1.1 | dUnsafe(tag, message) |
β Vulnerable (Demo Only) | No @CompileTimeConstant. Allows string concatenation β Heap leak. Included for educational comparison only. |
| 1.2 | dStrict(tag, @CompileTimeConstant message) |
β Maximum Security | Accepts only compile-time constants. Any runtime variable in the message triggers a compile-time error via ErrorProne. |
| 1.3 | d/e/i/w/wtf(tag, @CompileTimeConstant message, vararg args) |
β Recommended | Hybrid model. Format string is constant (@CompileTimeConstant). Dynamic data is passed via vararg, preventing StringBuilder Heap allocation. |
Why not Timber? Timber uses
varargto prevent Heap leaks but does not enforce@CompileTimeConstant. A developer writingTimber.d("Password: " + pass)would bypass static analysis silently.SecureLogenforces this at compile time.
Group 2 β Static Analysis Enforcement (ErrorProne)
Files: gradle/libs.versions.toml Β· app-secure/build.gradle.kts Β· SecureLog.kt
| # | Implementation | Mechanism |
|---|---|---|
| 2.1 | com.google.errorprone:error_prone_annotations:2.50.0 dependency added |
libs.versions.toml |
| 2.2 | @CompileTimeConstant annotation enforced on all safe log method signatures |
Produces a compiler error if a runtime value is passed as the message |
// β
PASS β Compile-time constant
SecureLog.dStrict("Tag", "System initialized successfully.")
// β COMPILE ERROR β Runtime value rejected by ErrorProne
SecureLog.dStrict("Tag", "User: " + userId)
Group 3 β R8 / ProGuard Log Stripping
File: app-secure/proguard-rules.pro
R8 uses -assumenosideeffects to treat log calls as dead code and completely removes them from
the Release APK bytecode. A reverse-engineered APK will contain zero log statements.
Three configurations are provided:
Option 1 β Total Stripping (Paranoid Mode) β ACTIVE
Strips all log levels from both the native Android logger and SecureLog:
-assumenosideeffects class android.util.Log {
public static boolean isLoggable(java.lang.String, int);
public static int v(...);
public static int d(...);
public static int i(...);
public static int w(...);
public static int e(...);
public static int wtf(...);
}
-assumenosideeffects class com.hasantuncay.mobsec.secure.utils.SecureLog {
public static void d(...);
public static void dUnsafe(...);
public static void dStrict(...);
public static void i(...);
public static void w(...);
public static void e(...);
public static void wtf(...);
}
Option 2 β Selective Stripping (commented out)
Strips v, d, i only. Keeps w and e for production crash reporting.
Option 3 β Log Stripping Without Shrinking (commented out)
For apps that disable obfuscation/shrinking (e.g., to avoid R8 bugs) but still need log stripping.
Disables the shrinking engine while keeping -assumenosideeffects active.
Group 4 β Incident Response Kill Switch
File: app-secure/src/main/java/com/hasantuncay/mobsec/secure/utils/RemoteConfigSim.kt
Per Google's official Android Security Guidelines: "If you're going to log in Production, prepare flags you can use to shut down logging conditionally in case of an incident."
| # | Implementation | Mechanism |
|---|---|---|
| 4.1 | RemoteConfigSim β simulates Firebase Remote Config |
@Volatile Boolean flag (isLoggingKilled) |
| 4.2 | Every SecureLog method checks the flag as its first instruction |
if (RemoteConfigSim.isLoggingKilled) return |
| 4.3 | Live kill-switch demonstration in secureSystemConsoleLeak() |
Demonstration 4 block |
Why this matters: Fixing code, recompiling, and waiting for Play Store approval can take days
. With a kill switch, the backend broadcasts isLoggingKilled = true and all logging across
millions of devices stops instantly, with zero app update required.
Group 5 β Network Traffic Logging
File: features/maswe0005/src/main/java/.../Maswe0005SecureRepository.kt β secureNetworkLeak()
| # | Implementation | Mechanism |
|---|---|---|
| 5.1 | HttpLoggingInterceptor.Level.NONE forced in all builds |
OkHttp β no headers or body ever printed |
| 5.2 | Redacting Interceptor β logs existence of sensitive headers, never their values | Custom Interceptor (Defense-in-Depth) |
// β
SAFE: Only signals that sensitive headers exist, not their content
SecureLog.d("SecureNetwork", "Outgoing request with REDACTED sensitive headers.")
Group 6 β Local File Dumping (PCI-DSS Compliance)
File: features/maswe0005/src/main/java/.../Maswe0005SecureRepository.kt β secureLocalFileLeak()
| # | Implementation | Mechanism |
|---|---|---|
| 6.1 | CVV and PIN completely excluded from the diagnostic payload | Explicit field omission (PCI-DSS DSS Req. 3.2) |
| 6.2 | Primary Account Number (PAN) masked β 123456******7890 |
pan.take(6) + "******" + pan.takeLast(4) |
| 6.3 | Diagnostic file AES-256-GCM encrypted via hardware-backed Keystore | Jetpack Security EncryptedFile |
Group 7 β Third-Party SDK Telemetry (GDPR)
File: features/maswe0005/src/main/java/.../Maswe0005SecureRepository.kt β secureSdkTelemetryLeak()
| # | Implementation | Mechanism |
|---|---|---|
| 7.1 | User email replaced with a one-way salted SHA-256 hash before sending to analytics | MessageDigest("SHA-256") + SSAID salt (Rainbow Table mitigation) |
| 7.2 | Draft message content replaced with a boolean flag (has_drafts) |
Data Minimization (GDPR Article 5.1.c) |
Group 8 β WebView Console Filtering
File: features/maswe0005/src/main/java/.../Maswe0005SecureRepository.kt β secureWebViewConsoleLeak()
| Approach | Model | Verdict |
|---|---|---|
Blacklist (block known bad terms: cookie, token) |
Negative Security | β Bypassable via obfuscation (c00kie, SessionID) |
Whitelist (allow only UI_STATE: and ANALYTICS_EVENT: prefixes) |
Positive Security / Zero Trust | β Default Deny β anything unknown is silently dropped |
// Default Deny: only explicitly allowlisted prefixes pass through
val safeWhitelistRegex = Regex("^(UI_STATE|ANALYTICS_EVENT):.*")
if (!safeWhitelistRegex.matches(msg)) {
SecureLog.w("SecureWebView", "Blocked unknown WebView console message (Not in Whitelist).")
return true
}
Group 9 β Domain Layer Data Sanitization
Files:
common/src/main/java/.../GdprPiiData.ktβ Class-level redactioncommon/src/main/java/.../ToMask.ktβ Field-level masking- All domain data classes (
SystemData,UserData,NetworkSessionData, etc.)
9.1 β Class-Level Redaction (toString() Override)
Every domain data class overrides toString() to return a static redacted label:
data class SystemData(...) {
override fun toString() = "[REDACTED_SYSTEM_DATA]"
}
This ensures that even if a developer accidentally logs an entire object (
SecureLog.d("Tag", "%s", obj)), the output is always [REDACTED_SYSTEM_DATA] β never real data.
9.2 β Field-Level Masking (ToMask<T> Generic Wrapper)
Based on Google's official recommendation. Applied to the most critical GDPR fields (TCKN, Email):
data class DirectIdentifiers(
val nationalIdentificationNumber: ToMask<String> = ToMask("10987654321"),
val personalEmail: ToMask<String> = ToMask("john.doe@personal.domain.com")
)
// Accidental logging β always safe
Log.d("Tag", person.email.toString()) // prints: "MASKED_FIELD_XX"
// Intentional access β explicit, traceable, auditable
val email = person.email.getDataToMask()
Defense comparison:
| Approach | Risk if developer forgets |
|---|---|
ToMask<T> field wrapper |
Only that specific field leaks |
Class-level toString() override |
Entire object is always safe |
Both layers are applied simultaneously for maximum defense-in-depth.
9.3 β Memory Scrubbing (CWE-226)
Passwords are stored in a mutable CharArray instead of an immutable String. When the password is
no longer needed, it is immediately zeroed out without waiting for the Garbage Collector:
// Wipe sensitive data from Heap RAM immediately
appData.userContext.scrubPassword() // internally: Arrays.fill(passwordArray, '0')
This prevents the password from being discoverable via a Heap Dump or debugger memory inspection.
π Summary
| Group | Category | Controls Applied |
|---|---|---|
| 1 | Custom Logging | 3 logging models (dUnsafe, dStrict, d/vararg) |
| 2 | Static Analysis | ErrorProne @CompileTimeConstant (v2.50.0) |
| 3 | R8 / ProGuard | Total, Selective, and No-Shrink stripping options |
| 4 | Kill Switch | Remote Config Incident Response flag |
| 5 | Network | Level.NONE + Redacting Interceptor |
| 6 | Local File | CVV/PIN exclusion, PAN masking, AES-256-GCM encryption |
| 7 | SDK Telemetry | SHA-256 hashing + Data Minimization |
| 8 | WebView | Whitelist-based Default Deny filter |
| 9 | Domain Layer | Class-level toString(), ToMask<T>, Memory Scrubbing |
Total: 9 security groups Β· 24 individual controls