Kotlin
Recommendations for OWASP Mobile TOP 10 2016
Authored by: Abhilash Nigam
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
orMODE_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()
, anddelete()
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 theNSURL
class methodsetAllowsAnyHTTPSCertificate
. - 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
orSSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER
. These are equivalent to trusting all certificates. - If using a class which extends
SSLSocketFactory
, make sure thecheckServerTrusted
method is properly implemented so that server certificate is correctly checked.
- Remove all code after the development cycle that may allow the application to accept all certificates such as
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
- https://github.com/Checkmarx/Kotlin-SCP
- https://www.raywenderlich.com/6294778-app-hardening-tutorial-for-android-with-kotlin
- https://owasp.org/www-project-mobile-top-10/
- https://mobile-security.gitbook.io/mobile-security-testing-guide/android-testing-guide/
- https://www.veracode.com/sites/default/files/pdf/resources/guides/secure-coding-best-practices-handbook-veracode-guide.pdf
- https://rules.sonarsource.com/kotlin/
- https://phauer.com/2017/idiomatic-kotlin-best-practices/