App Hardening Tutorial for Android With Kotlin
In this App Hardening Tutorial for Android with Kotlin, you’ll learn how to code securely to mitigate security vulnerabilities. By Kolin Stürt.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
App Hardening Tutorial for Android With Kotlin
30 mins
Avoiding SQL Injection
The SQL language uses quotes to terminate strings, slashes to escape strings and semicolons to end a line of code. Attackers use this to terminate the string early to add commands. For example, you could bypass a login by entering ') OR 1=1 OR (password LIKE '*
into the text field. That code translates to “where password is like anything”, which bypasses the authentication altogether!
One solution is to escape, encode or add your double quotes in code. That way, the server sees quotes from the user as part of the input string instead of a terminating character. Another way is to strip out those characters. That’s what you’re going to do next.
Find sendReportPressed()
in ReportDetailActivity.kt. Replace the line that declares reportString
with this:
var reportString = details_edtxtview.text.toString()
reportString = reportString.replace("\\", "")
.replace(";", "").replace("%", "")
.replace("\"", "").replace("\'", "")
You’ve now stripped the vulnerable characters from the string.
If you change the LIKE
clause to ==
, the string has to literally match a*
.
LIKE
and CONTAINS
allow wild cards that you should avoid. It prevents attackers from getting a list of accounts when they enter a*
as the account name.
If you change the LIKE
clause to ==
, the string has to literally match a*
.
Only you will know what the expected input and output should be, given the design requirements, but here’s a few more points about sanitization:
- Dots and slashes may be harmful if passed to file management code. A directory traversal attack is when a user enters
../
, i.e. to view the parent directory of the path instead of the intended sub-directory. - If you’re interfacing with C, one special character is the
NULL
terminating byte. Pointers to C strings require it. Because of this, you can manipulate the string by introducing aNULL
byte. The attacker might want to terminate the string early if there was a flag such as needs_auth=1. - HTML, XML and JSON strings have their own special characters. Make sure to encode the URL and escape special characters from the user input so attackers can’t instruct the interpreter:
< must become <.
> should be replaced with >.
& should become &.
Inside attribute values, any “ or ‘ need to become " and &apos, respectively.
Just as it’s important to sanitize data before sending it out, you shouldn’t blindly trust the data you receive. The best practice is to validate all input to your app.
Validating Input
Subconsciously, animals are constantly validating their environment for danger, sometimes in better ways than our minimal human instincts. While we may not be great at validating danger in the wild, at least we can add validation to our programs.
Besides removing special characters for the platform you’re connecting with, you should only allow characters for the type of input required. Right now, users can enter anything into the email field.
For your next step, you’ll fix this by adding the following to the MainActivity.kt file, under the variable declarations for the class:
import java.util.regex.Pattern
private fun isValidEmailString(emailString: String): Boolean {
return emailString.isNotEmpty() && Pattern.compile(EMAIL_REGEX).matcher(emailString).matches()
}
This creates a method that verifies an email address via regular expressions. Now, add this to the companion object that’s at the bottom of the file:
private const val EMAIL_REGEX = "^[A-Za-z0-9._%+\\-]+@[A-Za-z0-9.\\-]+\\.[A-Za-z]{2,4}$"
That regular expression makes sure emails have a format of test@example.com.
Finally, navigate to // TODO: Replace below line with check for email field
inside the loginPressed()
method. Replace the // TODO
and success = true
line with the following:
val email = login_email.text.toString()
if (isValidEmailString(email)) {
createDataSource("users.xml", it)
success = true
} else {
toast("Please enter a valid email.")
}
Test it out by deleting the app to remove the previous login, then building and running it again. Enter an invalid email such as my.invalid.email and press SIGN UP. You’ll see that you’re restricted from doing so:
You’ve hardened the text inputs of your app, but it’s a good idea to make an inventory of all input to your app.
The app allows the user to upload a photo. Right now, you could attach a photo containing malware that would get delivered right to the organization! You’ll fix that now.
Add the following to the end of the class in the ReportDetailActivity.kt
file:
import java.io.IOException
import java.io.RandomAccessFile
@Throws(IOException::class)
private fun isValidJPEGAtPath(pathString: String?): Boolean {
var randomAccessFile: RandomAccessFile? = null
try {
randomAccessFile = RandomAccessFile(pathString, "r")
val length = randomAccessFile.length()
if (length < 10L) {
return false
}
val start = ByteArray(2)
randomAccessFile.readFully(start)
randomAccessFile.seek(length - 2)
val end = ByteArray(2)
randomAccessFile.readFully(end)
return start[0].toInt() == -1 && start[1].toInt() == -40 &&
end[0].toInt() == -1 && end[1].toInt() == -39
} finally {
randomAccessFile?.close()
}
}
For the JPEG format, the first two bytes and last two bytes are always FF D8 and FF D9. This method checks for that.
To implement it, find the getFilename()
method and replace its complete implementation with the following:
// Validate image
val isValid = isValidJPEGAtPath(decodableImageString)
if (isValid) {
//get filename
val fileNameColumn = arrayOf(MediaStore.Images.Media.DISPLAY_NAME)
val nameCursor = contentResolver.query(selectedImage, fileNameColumn, null,
null, null)
nameCursor?.moveToFirst()
val nameIndex = nameCursor?.getColumnIndex(fileNameColumn[0])
var filename = ""
nameIndex?.let {
filename = nameCursor.getString(it)
}
nameCursor?.close()
//update UI with filename
upload_status_textview?.text = filename
} else {
toast("Please choose a JPEG image")
}
This calls the photo check when the user imports a photo, validating if it’s a valid JPEG image file.
Speaking of files, developers often overlook serialized and archived data from storage.
Checking Stored Data
Open MainActivity.kt and take a look at createDataSource
. Notice the code makes assumptions about the stored data. Next, you’ll change that.
Replace the declaration of users
inside the createDataSource
method with the following:
val users = try { serializer.read(Users::class.java, inputStream) } catch (e: Exception) {null}
You caught exceptions during the reading of data into User
. To prevent overuse, Kotlin discourages exceptions in favor of better flow control. For the most part, using safety checks is a better approach because it makes a method resilient to errors. It contains the failure instead of propagating it outside the method, which can become an app-wide failure.
In your next step, you’ll implement safety checks. Start by replacing everything after the try
/catch
you just added with this:
users?.list?.let { //1
val userList = ArrayList(it) as? ArrayList
if (userList is ArrayList<User>) { //2
val firstUser = userList.first() as? User
if (firstUser is User) { //3
firstUser.password = login_password.text.toString()
val fileOutputStream = FileOutputStream(outFile)
val objectOutputStream = ObjectOutputStream(fileOutputStream)
objectOutputStream.writeObject(userList)
objectOutputStream.close()
fileOutputStream.close()
}
}
}
Here:
- You added null checks for the user list.
- You made sure the
ArrayList
containsUser
objects. - Added an extra check that ensures the
firstUser
is really aUser
object.
Adding safety checks around your code is Defensive Programming — the process of making sure your app still functions under unexpected conditions.
Next, navigate to // Todo: Implement safety checks
in the loginPressed()
method and implement safety checks as below :
if (list is ArrayList<User>) { //1
val firstUser = list.first() as? User
if (firstUser is User) { //2
if (firstUser.password == password) {
success = true
}
}
}
Here, you only set success
to true
when:
-
list
is anArrayList
. -
firsUser
is really aUser
object.