Kotlin

Recommendations for OWASP Mobile TOP 10 2016

M1: Improper Platform Usage

  • Secure coding and configuration practices must be used on the server-side of the mobile application.
  • Components such as Intent, Container, e.t.c should not be exported.
  • Set android:exported=false in the manifest, for the components being used in the application.

M2: Insecure Data Storage

  • Do not store sensitive data where possible.
    • Transmit and display but do not persist to memory.
    • Store only in RAM and clear at application close
  • Data stored locally on the device should be encrypted.
  • Encrypting sensitive values in an SQLite database using SQLCipher, which encrypts the entire database using a PRAGMA key.
  • Do not use MODE_WORLD_WRITEABLE or MODE_WORLD_READABLE modes for IPC files because they do not provide the ability to limit data access to particular applications.
  • Avoid exclusively relying upon hardcoded encryption or decryption keys when storing sensitive information assets because those keys can be retrieved after decompiling the app.
  • If you are using components for sharing data between only your own apps, it is preferable to use the android:protectionLevel attribute set to “signature” protection.
  • When accessing a content provider, use parameterized query methods such as query(), update(), and delete() to avoid potential SQL injection from untrusted sources.
  • Sample code create secure database tables
    var secureDB = SQLiteDatabase.openOrCreateDatabase(database, "password123", null)
    secureDB.execSQL("CREATE TABLE IF NOT EXISTS Accounts(Username VARCHAR,Password VARCHAR);")
    secureDB.execSQL("INSERT INTO Accounts VALUES('admin','AdminPassEnc');")
    secureDB.close() 
  • Sample code create secure data when storing in files
    var fos: FileOutputStream? = null
    fos = openFileOutput("FILENAME", Context.MODE_PRIVATE)
    fos.write(test.toByteArray(Charsets.UTF_8))
    fos.close()

M3: Insecure Communication

  • Apply SSL/TLS to transport channels that the mobile app will use to transmit sensitive information, session tokens, or other sensitive data to a backend API or web service.
  • Use strong, industry standard cipher suites with appropriate key lengths.
  • Use certificates signed by a trusted CA provider.
  • Never allow self-signed certificates, and consider certificate pinning for security conscious applications.
  • Always require SSL chain verification.
  • Alert users through the UI if the mobile app detects an invalid certificate.
  • Do not send sensitive data over alternate channels (e.g, SMS, MMS, or notifications).
  • iOS Specific Best Practices
    • Ensure that certificates are valid and fail closed.
    • When using CFNetwork, consider using the Secure Transport API to designate trusted client certificates. In almost all situations, NSStreamSocketSecurityLevelTLSv1 should be used for higher standard cipher strength.
    • After development, ensure all NSURL calls (or wrappers of NSURL) do not allow self signed or invalid certificates such as the NSURL class method setAllowsAnyHTTPSCertificate.
    • Consider using certificate pinning by doing the following: export your certificate, include it in your app bundle, and anchor it to your trust object. Using the NSURL method connection:willSendRequestForAuthenticationChallenge: will now accept your cert.
  • Android Specific Best Practices
    • Remove all code after the development cycle that may allow the application to accept all certificates such as org.apache.http.conn.ssl.AllowAllHostnameVerifier or SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER. These are equivalent to trusting all certificates.
    • If using a class which extends SSLSocketFactory, make sure the checkServerTrusted method is properly implemented so that server certificate is correctly checked.

M4: Insecure Authentication

  • Avoid Weak Patterns
  • Ensure that all authentication requests are performed server-side.
  • Avoid authenticating users locally.
  • Client-Side stored data will need to be encrypted using an encryption key that is securely derived from the user’s login credentials.
  • User’s password should never be stored on the devices due to Persistent authentication (Remember Me) functionality implemented within mobile applications.
  • It is recommended to enable password complexities when storing the passwords locally on the device. The PasswordHelper Object class implements the basic complexity checks. We will then use this class during the Signup Activity to validate the complexity as shown below:
    package com.cx.goatlin
    // ...
    class SignupActivity : AppCompatActivity() {
        // ...
        private fun attemptSignup() {
            val name: String = this.name.text.toString()
            val email: String = this.email.text.toString()
            val password: String = this.password.text.toString()
            val confirmPassword: String = this.confirmPassword.text.toString()
 
            // test password strength
            if (!PasswordHelper.strength(password)) {
                this.password.error = """|Weak password. Please use:
                                      |* both upper and lower case letters
                                      |* numbers
                                      |* special characters (e.g. !"#$%&')
                                      |* from 10 to 128 characters sequence""".trimMargin()
                this.password.requestFocus()
                return;
            }
            // ...
        }
        // ...
    }
  • We then use OWASP recommended the following algorithms: bcrypt, PDKDF2, Argon2 and scrypt to enable hashing and salting passwords in a robust way. We can do this by making two small changes to the above signup code:
    package com.cx.goatlin
    // ...
    class SignupActivity : AppCompatActivity() {
        // ...
        /**
         * Attempts to create a new account on back-end
         */
        private fun attemptSignup() {
            //...
            // hashing password
            val hashedPassword: String = BCrypt.hashpw(password, BCrypt.gensalt())
            val account: Account = Account(name, email, hashedPassword)
            // ...
        }
    }
  • And the second one is the UserLoginTask doInBackground() method to compare a provided password with the stored one using Bcrypt.checkpw() method:
    package com.cx.goatlin
    // ...
    class LoginActivity : AppCompatActivity(), LoaderCallbacks<Cursor> {
        // ...
        inner class UserLoginTask internal constructor(private val mUsername: String, private val mPassword: String) : AsyncTask<Void, Void, Boolean>() {
            override fun doInBackground(vararg params: Void): Boolean? {
                if ((mUsername == "Supervisor") and (mPassword == "MySuperSecretPassword123!")){
                    return true
                }
                else {
                    val account:Account = DatabaseHelper(applicationContext).getAccount(mUsername)
                    if (BCrypt.checkpw(mPassword, account.password)) {
                        // ...
                    }
                    // ...
                }
            }
        }
    }

M5: Insufficient Cryptography

  • When storing any data locally on the device we need to encrypt the data. Use the key stored in the KeyStore to encrypt the data. To encrypt data use the below code
    #Get the key
    val keyStore = KeyStore.getInstance("AndroidKeyStore")
    keyStore.load(null)
 
    val secretKeyEntry =
        keyStore.getEntry("MyKeyAlias", null) as KeyStore.SecretKeyEntry
    val secretKey = secretKeyEntry.secretKey
 
    #Encrypt data
    val cipher = Cipher.getInstance("AES/GCM/NoPadding")
    cipher.init(Cipher.ENCRYPT_MODE, secretKey)
    val ivBytes = cipher.iv
    val encryptedBytes = cipher.doFinal(dataToEncrypt)
    
    map["iv"] = ivBytes
    map["encrypted"] = encryptedBytes
  • Ensure that passwords aren’t directly passed into an encryption function. Instead, the user-supplied password should be passed into a KDF to create a cryptographic key.
  • Use the secure random function to generate random numbers. Below is the code generate secure random numbers
    SecureRandom random = new SecureRandom();
       byte bytes[] = new byte[20];
       random.nextBytes(bytes);
  • Ensure that cryptographic algorithms are up to date and in-line with industry standards.
  • Avoid using below listed cryptographic algorithms as these are known to be weak
    • DES, 3DES
    • RC2
    • RC4
    • BLOWFISH
    • MD4
    • MD5
    • SHA1
  • The following algorithms are recommended:
    • Confidentiality algorithms: AES-GCM-256 or ChaCha20-Poly1305
    • Integrity algorithms: SHA-256, SHA-384, SHA-512, Blake2, the SHA-3 family
    • Digital signature algorithms: RSA (3072 bits and higher), ECDSA with NIST P-384
    • Key establishment algorithms: RSA (3072 bits and higher), DH (3072 bits or higher), ECDH with NIST P-384

M6: Insecure Authorization

  • When defining API routes it is necessary to include both authentication and authorization middlewares.
    • Non Compliant Code:
    router.put('/accounts/:username/notes/:note', auth, async (req, res, next) => {
        // ...
    });
 
    router.get('/accounts/:username/notes', auth, async (req, res, next) => {
        // ...
    });
  • The above code only includes the authentication middleware whereas the below code includes both authorization and authentication middleware.
    • Compliant Code
    router.put('/accounts/:username/notes/:note', [auth, ownership], async (req, res, next) => {
        // ...
    });
 
    router.get('/accounts/:username/notes', [auth, ownership], async (req, res, next) => {
        // ...
    });
  • Verify the roles and permissions of the authenticated user using only information contained in backend systems. Avoid relying on any roles or permission information that comes from the mobile device itself.
  • Backend code should independently verify that any incoming identifiers associated with a request (operands of a requested operation) that come along with the identifier match up and belong to the incoming identity.
  • It is recommended to use JWT Token with RS256 cryptographic algorithm.
  • Services that are accessible to multiple applications should be accessed using AccountManager.
  • Do not store username and password instead store a short term, service-specific authorization token.

M7: Client Code Quality

  • Maintain consistent coding patterns that everyone in the organization agrees upon;
  • Write code that is easy to read and well-documented;
  • When using buffers, always validate that the the lengths of any incoming buffer data will not exceed the length of the target buffer;
  • Via automation, identify buffer overflows and memory leaks through the use of third-party static analysis tools; and
  • Prioritize solving buffer overflows and memory leaks over other ‘code quality’ issues.
  • When using expressions:
  • Non Compliant Code
    fun getDefaultLocale(deliveryArea: String): Locale {
        val deliverAreaLower = deliveryArea.toLowerCase()
        if (deliverAreaLower == "germany" || deliverAreaLower == "austria") {
            return Locale.GERMAN
        }
        if (deliverAreaLower == "usa" || deliverAreaLower == "great britain") {
            return Locale.ENGLISH
        }
        if (deliverAreaLower == "france") {
            return Locale.FRENCH
        }
        return Locale.ENGLISH
    }
  • Compliant Code
    fun getDefaultLocale2(deliveryArea: String) = when (deliveryArea.toLowerCase()) {
        "germany", "austria" -> Locale.GERMAN
        "usa", "great britain" -> Locale.ENGLISH
        "france" -> Locale.FRENCH
        else -> Locale.ENGLISH
    }
  • When using Named Arguments
  • Non Compliant Code
    val config = SearchConfig()
       .setRoot("~/folder")
       .setTerm("game of thrones")
       .setRecursive(true)
       .setFollowSymlinks(true)
  • Compliant Code
    val config2 = SearchConfig2(
       root = "~/folder",
       term = "game of thrones",
       recursive = true,
       followSymlinks = true
    )
  • Don’t Overload for Default Arguments.
  • Avoid if-null Checks.
  • Avoid if-type Checks.
  • Avoid not-null Assertions !!.

M8: Code Tampering

  • It is recommended to implement an integrity check. This can be done using SafetyNet Verify Apps API provided by android.
  • Compliant Code to enable App verification
    SafetyNet.getClient(this)
        .isVerifyAppsEnabled
        .addOnCompleteListener { task ->
            if (task.isSuccessful) {
                if (task.result.isVerifyAppsEnabled) {
                    Log.d("MY_APP_TAG", "The Verify Apps feature is enabled.")
                } else {
                    Log.d("MY_APP_TAG", "The Verify Apps feature is disabled.")
                }
            } else {
                Log.e("MY_APP_TAG", "A general error occurred.")
            }
        }
  • Enable root detection in the application. Common ways to identify if a device is rooted is described below.
  • Whether the kernel was signed with custom keys generated by a third-party developer.
    private fun detectDeveloperBuild(): Boolean {
        val buildTags: String = Build.TAGS
 
        return buildTags.contains("test-keys")
    }
  • If OTA certificates are available.
    private fun detectOTACertificates(): Boolean {
        val otaCerts: File = File("/etc/security/otacerts.zip")
 
        return otaCerts.exists()
    }
  • Well-known applications to gain root access on Android devices are installed.
    private fun detectRootedAPKs(ctx: Context): Boolean {
        val knownRootedAPKs: Array<String> = arrayOf(
            "com.noshufou.android.su",
            "com.thirdparty.superuser",
            "eu.chainfire.supersu",
            "com.koushikdutta.superuser",
            "com.zachspong.temprootremovejb",
            "com.ramdroid.appquarantine"
        )
        val pm: PackageManager = ctx.packageManager
 
        for(uri in knownRootedAPKs) {
            try {
                pm.getPackageInfo(uri, PackageManager.GET_ACTIVITIES)
                return true
            } catch (e: PackageManager.NameNotFoundException) {
                // application is not installed
            }
        }
 
        return false
    }
  • If su binary is available on the device.
    private fun detectForSUBinaries(): Boolean {
        var suBinaries: Array<String> = arrayOf(
            "/system/bin/su",
            "/system/xbin/su",
            "/sbin/su",
            "/system/su",
            "/system/bin/.ext/.su",
            "/system/usr/we-need-root/su-backup",
            "/system/xbin/mu"
        )
 
        for (bin in suBinaries) {
            if (File(bin).exists()) {
                return true
            }
        }
 
        return false
    }
  • Prevent the application to run on a Rooted environment, the RootDetectionHelper.check() method, which combines all the above described techniques, is called on our main activity (Login).
    package com.cx.goatlin
    // ...
        class LoginActivity : AppCompatActivity(), LoaderCallbacks<Cursor> {
        // ...
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_login)
 
            if (RootDetectionHelper.check(applicationContext)) {
                forceCloseApp()
            }
            //  ...
        }
        // ...
        private fun forceCloseApp() {
            val dialog: AlertDialog.Builder = AlertDialog.Builder(this)
 
            dialog
                    .setMessage("The application can not run on rooted devices")
                    .setCancelable(false)
                    .setPositiveButton("Close Application", DialogInterface.OnClickListener {
                        _, _ -> finish()
                    })
 
            val alert: AlertDialog = dialog.create()
 
            alert.setTitle("Unsafe Device")
            alert.show()
        }
        //...
    }
  • Set android:exported=false in the manifest, for the components being used in the application.
  • Set android:backup=false in the manifest, for the components being used in the application.

M9: Reverse Engineering

  • It is recommended to obfuscate the ANdroid Application Code using an obfuscation tool such as Proguard and R8.
  • Identify what methods / code segments to obfuscate.
  • Tune the degree of obfuscation to balance performance impact.
  • Withstand de-obfuscation from tools like IDA Pro and Hopper.
  • Obfuscate string tables as well as methods.

M10: Extraneous Functionality

  • Verify that all test code is not included in the final production build of the application.
  • Examine all API endpoints accessed by the mobile app to verify that these endpoints are well documented and publicly available.
  • Examine all log statements to ensure nothing overly descriptive about the backend is being written to the logs.
  • Ensure no hardcoded sensitive data is present, such as API keys, account credentials, personal information, etc.
  • Ensure to remove all the hidden back-end endpoints.

References