Chapters

Hide chapters

Real-World Android by Tutorials

Second Edition · Android 12 · Kotlin 1.6+ · Android Studio Chipmunk

Section I: Developing Real World Apps

Section 1: 7 chapters
Show chapters Hide chapters

18. App Hardening
Written by Antonio Roa-Valverde

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

As network communications and OSs become more secure, hackers have shifted their focus from basic eavesdropping to attacking devices and apps. In the previous chapters, you’ve secured your data in transit and at rest. Now, to protect your app from these additional kinds of attacks, you need to understand and use app hardening effectively.

From minimizing pointer use to null safety and type checks, Kotlin is a great language for secure development. So much so that it’s tempting to forget about secure coding altogether. However, even Kotlin has vulnerabilities that you need to protect your app against.

In this chapter, you’ll learn how to:

  • Avoid code vulnerabilities.
  • Validate input and sanitize output.
  • Perform integrity checking.

Right now, the app has an overflow of code vulnerabilities which you’ll eventually fix!

Introducing Overflows

In a language like C, hackers exploit security vulnerabilities by causing an app to write data to an area it’s not supposed to, such as beyond an expected boundary and into adjacent memory locations. That’s called an overflow, and it can overwrite important data.

In certain environments, this can be an area that contains code the device executes, giving attackers a way to maliciously change a program. Bug bounty hunters refer to it as “gaining arbitrary code execution”. It’s a very important preoccupation for them.

Figure 18.1 — Overflow Explained
Figure 18.1 — Overflow Explained

One example of an overflow in Kotlin is when a recursive function ends up in an infinite loop. Because the size of the stack runs out, you’ll get a StackOverflow exception.

Note: You can read more about stacks at https://www.programmerinterview.com/data-structures/difference-between-stack-and-heap/.

Kotlin provides safety modifiers, such as tailrec, which help avoid the chances of a stack overflow by adding rules and throwing an error if you break them. The rules are:

  • The last operation of the function can only call itself.
  • There cannot be more code after a recursive call.
  • Use within try/catch/finally blocks is prohibited.

These rules are especially helpful when your implementation changes later and you forget to check that it’s still safe.

To implement this, open Timing.kt and add tailrec, right after the private modifier in the method definition of factorial. Your modified method definition should look like this:

private tailrec fun factorial(number: Int, accumulator: Int = 1) : Int {

You’ve just added a safety modifier, but Android Studio also provides important security warnings for potential overflows.

Paying Attention to Warnings

Exceptions and crashes are obvious indicators that something is wrong, but a worse problem is an incorrect value that goes undetected for some time. This is what happens with an integer overflow. Kotlin doesn’t throw an exception for a signed integer overflow. Instead, the app continues with the wrong values!

Figure 18.2 — Warnings Are Important
Wotihi 49.4 — Gilrovcz Ise Edkupsorz

private const val REPORT_APP_ID = 46341L
private const val REPORT_PROVIDER_ID = 46341L

Sanitizing Data

You should always sanitize your pet’s output, especially when it happens indoors. If your app sends the data in text fields to a server, then sanitizing it reduces the potential for an attack. The most basic technique is to limit the amount of input that you can enter into your fields. This reduces the likelihood that a specific code snippet or payload can get through.

android:maxLength="254"
android:maxLength="512"
android:maxLength="32"

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 and then add commands.

Stripping Out Dangerous Characters

Find sendReportPressed() in ReportDetailFragment.kt, then add the following below the line that reads //TODO: Sanitize string here:

reportString = reportString.replace("\\", "")
    .replace(";", "").replace("%", "")
    .replace("\"", "").replace("\'", "")

More Sanitization Tips

Only you will know what the expected input and output should be, given the design requirements, but here are a few more points about sanitization:

Validating Input

Subconsciously, pets are constantly validating their environment for danger, sometimes in better ways than humans. While we may not be as equipped to validate danger in the wild, at least we can add validation to our apps.

Validating Emails

To fix this, navigate to DataValidator and add a regular expression definition just after the companion object { line:

private const val EMAIL_REGEX = "^[A-Za-z0-9._%+\\-]+@[A-Za-z0-9.\\-]+\\.[A-Za-z]{2,4}$"
fun isValidEmailString(emailString: String): Boolean {
  return emailString.isNotEmpty() && Pattern.compile(EMAIL_REGEX).matcher(emailString).matches()
}
import com.realworld.android.petsave.core.utils.DataValidator.Companion.isValidEmailString
var success = false
val email = login_email.text.toString()
if (isSignedUp || isValidEmailString(email)) {
  success = true
} else {
  toast("Please enter a valid email.")
}
Figure 18.3 — Invalid Email
Pehadi 95.6 — Omcaked Ocauy

Designing by Contract

If you’re expecting specific kinds of characters, such as numbers, you should check for this. Some methods that are helpful include:

Validating Photos

Add the following to the end of the companion object in DataValidator:

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()
  }
}
import com.realworld.android.petsave.core.utils.DataValidator.Companion.isValidJPEGAtPath
val isValid = isValidJPEGAtPath(decodableImageString)
if (isValid) {
  //get filename
  val fileNameColumn = arrayOf(MediaStore.Images.Media.DISPLAY_NAME)
  val nameCursor = activity?.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 {
  val toast = Toast.makeText(context, "Please choose a JPEG image", Toast
      .LENGTH_LONG)
  toast.show()
}

More About Validating Input

Here are a few more tips for validating input:

Nullability and Safety Checks

Does nothing exist? Or does it exist only in reference to something tangible? How can you divide several things among no things? These are the concepts that our pets surely contemplate while we’re away working. Okay, well, maybe not since nothing is a concept tied to language, and in the Kotlin language, the closest relative is null. To write solid code, it’s important to understand the concept of null.

Understanding Null

In Java, all variables except primitive variables actually store references to memory addresses. Because they’re references, you can set the variables to null.

Checking Stored Data

Open UserRepository.kt and look at createDataSource. Notice the code assumes that the stored data exists and is uncorrupted. You’ll change that now.

val users = try { serializer.read(Users::class.java, inputStream) } catch (e: Exception) {null}
users?.list?.let { // 1
  val userList = ArrayList(it) as? ArrayList // 2
  if (userList is ArrayList<User>) { // 3
    val firstUser = userList.first() as? User
    if (firstUser is User) { // 4
      firstUser.password = Base64.encodeToString(password, Base64.NO_WRAP)
      val fileOutputStream = FileOutputStream(outFile)
      val objectOutputStream = ObjectOutputStream(fileOutputStream)
      objectOutputStream.writeObject(userList)
      // 5
      objectOutputStream.close()
      fileOutputStream.close()
    }
  }
}
inputStream.close()

More Tips for Using Nullability and Safety Checks

Here are a few other best practices to keep in mind:

Nullability in Java

There are no null safety checks for types you declare in Java. Types coming from Java subvert the checking system!

Nullability in C++

For code that’s performance-sensitive or portable, it’s common to use C++ as the preferred language. C++ is powerful because it allows you to work with memory pointers. Here are a few points about pointers:

Concurrency

As soon as you have more than one thread that needs to write data to the same memory location at the same time, a race condition can occur. Race conditions cause data corruption.

Using Mutual Exclusion

But say this callback happens on a separate thread. The way to avoid those race conditions is to synchronize the data. Synchronizing data means locking it so only one thread can access that part of the code at a time, called mutual exclusion.

@Volatile

Making Variables Thread-safe

Find the reportNumber definition and replace it with the following:

var reportNumber = AtomicInteger()
synchronized(this) {
  ReportTracker.reportNumber.incrementAndGet()
}
synchronized(this) { //Locked.
  report = "Report: ${ReportTracker.reportNumber.get()}"
}

More About Synchronization

Here are a few more tips about synchronization:

Checking App Integrity

Users that try to crack your app need to use debuggers and emulators. You can often detect these states and monitor or reject those users, which is known as integrity checking. Since spammers use these tools, it helps keep them out of your app too!

Key Points

In this chapter, you covered all the major areas for hardening your app. Here’s a summary of the most important points:

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now