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.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

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.

Happy Database

If you change the LIKE clause to ==, the string has to literally match a*.

Note: If you’re also developing the server-side code, clauses such as 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 a NULL 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 &lt.
    > should be replaced with &gt.
    & should become &amp.
    Inside attribute values, any or need to become &quot 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:

Invalid email error

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:

  1. You added null checks for the user list.
  2. You made sure the ArrayList contains User objects.
  3. Added an extra check that ensures the firstUser is really a User 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:

  1. list is an ArrayList.
  2. firsUser is really a User object.