← Back

M10:2016 Extraneous Functionality - Code Examples

Each pair below shows vulnerable code that ships development-only functionality, and the secure version that keeps it out of the release build. The examples span Android (Kotlin and Java), iOS (Swift), and the build configuration that actually enforces the separation.

Android — Kotlin

1. Hidden Debug Menu / God-Mode

Vulnerable

class SettingsActivity : AppCompatActivity() {
    override fun onCreate(b: Bundle?) {
        super.onCreate(b)
        // Secret trigger recovered by anyone who decompiles the app
        versionLabel.setOnLongClickListener {
            startActivity(Intent(this, InternalDebugMenu::class.java)) // toggles, raw API console
            true
        }
    }
}
// InternalDebugMenu is in src/main - it ships in the release APK

Secure

// Debug-only screen lives in src/debug/ and is NOT compiled into release.
// A no-op stub in src/release/ (or absence of the call) means there is nothing to find.

// src/debug/java/.../DebugHooks.kt
object DebugHooks {
    fun attach(activity: SettingsActivity) {
        activity.versionLabel.setOnLongClickListener {
            activity.startActivity(Intent(activity, InternalDebugMenu::class.java)); true
        }
    }
}

// src/release/java/.../DebugHooks.kt
object DebugHooks { fun attach(activity: SettingsActivity) { /* no-op in release */ } }

// src/main - calls the flavor-specific implementation
override fun onCreate(b: Bundle?) { super.onCreate(b); DebugHooks.attach(this) }

2. Test / Staging Endpoint and Credentials

Vulnerable

object Api {
    const val BASE_URL = "https://staging-api.internal.example.com/" // internal host leaked
    const val QA_USER  = "qa_admin"
    const val QA_PASS  = "Test1234!"                                  // usable creds in the binary
}

Secure

object Api {
    // Selected per build type; release cannot reach staging, and no creds are compiled in.
    val BASE_URL = BuildConfig.BASE_URL   // set via buildConfigField in build.gradle
}
// Test accounts are provisioned in the server's test environment only - never in the app.

3. Client-Side Feature Flag Hiding Shipped Code

Vulnerable

// Full feature compiled in; hidden only by a local boolean an attacker can flip
if (prefs.getBoolean("ff_new_payments", false)) {
    showUnreleasedPaymentsFlow()
}

Secure

// Authoritative decision comes from the server, tied to the authenticated user/entitlement.
// The client trusts a signed/server-checked response, not a local toggle.
if (entitlements.isEnabled(Feature.NEW_PAYMENTS)) {   // server-verified per request
    showNewPaymentsFlow()
}
// Truly unreleased code that must not leak stays behind a src/debug source set until launch.

Android — Java

Developer Backdoor and Verbose Logging

Vulnerable

public boolean isAuthorized(User u) {
    if (u.getId() == 0xDEADBEEFL) return true;              // backdoor: magic id skips checks
    Log.d("Auth", "check user=" + u.getEmail()
                  + " token=" + session.getJwt());          // leaks PII + token in release
    return acl.check(u);
}

Secure

public boolean isAuthorized(User u) {
    // No magic identifiers. Every user goes through the same access-control path.
    if (BuildConfig.DEBUG) {                                 // stripped from release by R8
        SafeLog.d("Auth", "checking user id=" + u.getId());  // no token, no PII, debug only
    }
    return acl.check(u);
}
// SafeLog.d no-ops when !BuildConfig.DEBUG; Log.* calls also removed via
// -assumenosideeffects in proguard-rules.pro.

iOS — Swift

1. Debug Menu and Environment Switcher

Vulnerable

final class RootViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        // Magic value opens an internal console on any stock device
        searchField.addTarget(self, action: #selector(check), for: .editingChanged)
    }
    @objc func check() {
        if searchField.text == "!!debug!!" {
            present(InternalDebugMenu(), animated: true) // ships in the Release binary
        }
    }
}

Secure

final class RootViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        #if DEBUG
        installDebugConsole()   // compiled ONLY into the Debug configuration
        #endif
    }

    #if DEBUG
    private func installDebugConsole() {
        searchField.addTarget(self, action: #selector(openDebug), for: .editingChanged)
    }
    @objc private func openDebug() {
        if searchField.text == "!!debug!!" { present(InternalDebugMenu(), animated: true) }
    }
    #endif
}
// InternalDebugMenu and its trigger are absent from the Release Mach-O entirely.

2. Base URL and Test Credentials

Vulnerable

enum API {
    static let baseURL = URL(string: "https://staging-api.internal.example.com/")!
    static let qaUser  = "qa_admin"
    static let qaPass  = "Test1234!"          // recovered with `strings` on the binary
}

Secure

enum API {
    static let baseURL: URL = {
        #if DEBUG
        return URL(string: "https://staging-api.example.com/")!  // Debug builds only
        #else
        return URL(string: "https://api.example.com/")!          // Release: production only
        #endif
    }()
    // No credentials in code. QA accounts exist only in the test back-end.
}

3. Logging

Vulnerable

print("login ok: \(user.email) token=\(session.jwt)")   // written in Release too

Secure

func debugLog(_ message: @autoclosure () -> String) {
    #if DEBUG
    print(message())        // argument is not even evaluated in Release
    #endif
}
debugLog("login ok id=\(user.id)")   // no token, no email; and gone from Release

// For production diagnostics use os.Logger with privacy redaction:
import os
let logger = Logger(subsystem: "com.example.app", category: "auth")
logger.info("login ok id=\(user.id, privacy: .public) token=\(session.jwt, privacy: .private)")

Build Configuration — Enforcing the Separation

1. Android: Build Types, Flavors, and debuggable

// build.gradle (app)
android {
    buildTypes {
        debug {
            buildConfigField "String", "BASE_URL", "\"https://staging-api.example.com/\""
            // debuggable is true for debug by default - fine, it never ships
        }
        release {
            buildConfigField "String", "BASE_URL", "\"https://api.example.com/\""
            debuggable false          // MUST be false; never set android:debuggable in the manifest
            minifyEnabled true        // R8 strips BuildConfig.DEBUG branches + Log.* no-ops
            shrinkResources true
        }
    }
}

// src layout - development-only code physically excluded from release
// app/src/debug/java/...   -> compiled into debug APK only
// app/src/release/java/... -> release stubs / overrides
// app/src/main/java/...    -> shared production code

2. iOS: Active Compilation Conditions

# Build Settings
# Debug   -> SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG
# Release -> SWIFT_ACTIVE_COMPILATION_CONDITIONS = (empty)
#
# Result: every `#if DEBUG ... #endif` block is absent from the Release binary.
# Use separate .xcconfig files / schemes so Release never inherits DEBUG.

3. CI Gate: Fail the Build on Debug Artifacts

#!/usr/bin/env bash
set -euo pipefail
APK=app/build/outputs/apk/release/app-release.apk

# Refuse a debuggable release build
aapt dump badging "$APK" | grep -qi "application-debuggable" \
  && { echo "FAIL: release APK is debuggable"; exit 1; }

# Refuse staging endpoints / test credentials that leaked into the artifact
unzip -p "$APK" classes.dex | strings \
  | grep -Eiq 'staging-api|qa_admin|Test1234|InternalDebugMenu|!!debug!!' \
  && { echo "FAIL: debug/staging artifact found in release"; exit 1; }

# Secret + debug-code static analysis on the source
gitleaks detect --source . --redact
echo "OK: no extraneous functionality detected in release artifact"

What Changed, and Why

LeftoverVulnerableSecure
Debug menu / god-modeIn src/main, ships in releaseIn src/debug / #if DEBUG, compiled out
Staging endpointHardcoded constant in binarySelected by BuildConfig/#if DEBUG
Test credentialsConstants in the appOnly in the server's test environment
Feature flagClient boolean hides shipped codeServer-authoritative entitlement
Verbose logsLog/print in releaseNo-op / stripped, privacy-redacted os.Logger
BackdoorMagic id skips access controlRemoved; single access-control path
debuggabletrue in shipped manifestfalse, verified in CI on the APK

Next Steps