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
Hardening Code
Taking that technique further, another good programming practice is design by contract, where the inputs and outputs of your methods satisfy a contract that defines specific interface expectations.
To see how this works, navigate to the ReportViewModel.kt file. Replace the contents of getReportList
with the following:
if (file.exists() && password.isNotEmpty()) { // 1
if (reportList == null) {
loadReportList(file, password)
}
}
if (reportList is ArrayList) { // 2
return reportList ?: arrayListOf()
}
return arrayListOf()
Here’s what this code does:
- Makes sure that the
file
exists before accessing it and thatpassword
is not empty. - Adds type checks and a safety fallback for the return value.
This is Failsafe Programming — where you return a default or safe value that causes minimal harm if something goes wrong.
Now that you’ve hardened both the inputs and outputs of your app, here are a few more tips:
- If you’re expecting specific kinds of characters, such as numbers, you should check for this. Some methods that are helpful include:
–Char.isLetterOrDigit(): Boolean
–Char.isLetter(): Boolean
–Char.isDigit(): Boolean
–String
‘slength
method is also handy. For example, if your server expects a string of 32 characters or less, make sure that the interface will only return up to and including 32 characters. - Another overlooked area is inside deep link or URL handlers. Make sure user input fits expectations and that it’s not used directly. It shouldn’t allow a user to enter info to manipulate your logic. For example, instead of letting the user choose which screen in a stack to navigate to by index, allow only specific screens using an opaque identifier, such as t=qs91jz5urq.
- Be careful when displaying an error alert that directly shows a message from the server. Error messages could disclose private debugging or security-related information. The solution is to have the server send an error code that the app looks up to show a predefined message.
Now it’s time to take a look at another common source of vulnerabilities.
Working With 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. For instance, an attacker might be able to alter a shared resource to change the flow of security code on another thread.
In the case of authentication status, an attacker could take advantage of a time gap between the time of check and the time of use of a flag.
The way to avoid race conditions is to synchronize the data. Synchronizing data means to “lock” it so that only one thread can access that part of the code at a time, which is called a mutex — for mutual exclusion.
If the design of your app makes you work with multiple threads, a good way to protect yourself from the security issues of concurrency is to design your classes to be lock-free so that you don’t need any synchronization code in the first place. This requires some real thought about the design of your interface.
Open the ReportDetailActivity.kt file and find the sendReportPressed()
method. Notice the line that reads if (!isSendingReport) {
. It’s right after the line that spawns a thread. If the user repeatedly presses the SEND REPORT button, several threads will change the isSendingReport
variable at the same time.
Select the if (!isSendingReport) {
line and the line below it. Cut and paste them right above the Executors.newSingleThreadExecutor().execute {
statement.
Now, nothing bad will happen if the user taps the button multiple times. This is called Thread Confinement — where the logic doesn’t exist outside a single thread.
You fixed one problem without needing to add locks. But there’s another problem. One thread updates the reportNumber
variable while another displays it to the user at the same time.
Synchronizing Data
You can fix this by adding locks. However, modern techniques call a synchronized
method or mark variables atomic instead. An atomic variable is one where the load or store executes with a single instruction. It prevents an attacker slipping steps in between the save and load of a security flag.
Add the following right above the definition for the isSendingReport
variable:
@Volatile
In Kotlin, @Volatile
is an annotation for atomic. Keep in mind it only secures linear read/writes, not actions with a larger scope. Making a variable atomic does not make it thread-safe. You’ll do that now for the reportNumber
variable.
Find the reportNumber
definition and replace it with the following:
import java.util.concurrent.atomic.AtomicInteger
var reportNumber = AtomicInteger()
Navigate to the sendReportPressed()
method, find the line that reads ReportTracker.reportNumber++
and replace it with the following:
synchronized(this) {
ReportTracker.reportNumber.incrementAndGet()
}
Inside the runOnUiThread
block, replace everything up to the finish()
statement with this:
isSendingReport = false
var report : String
synchronized(this) { //Locked.
report = "Report: ${ReportTracker.reportNumber.get()}"
}
You’ve now synchronized reportNumber
between two threads! Here are a few more tips:
- If there are more than a few places to synchronize, scattering synchronization all over your code isn’t good practice. It’s hard to remember the places you’ve synchronized and easy to miss places that you should have synchronized. Instead, keep all that functionality in one place.
- Good design using accessor methods is the solution. Using getter and setter methods and only using them to access the data means you can synchronize in one place. This avoids having to update many parts of your code when you’re changing or refactoring it.
You’ve hardened your concurrent code. Still, the ability to spawn multiple tasks has risks when it comes to availability.
Denial of Service
Allowing more than one process at the same time opens the door for denial of service attacks — where an attacker purposely uses up resources to prevent your app from working. These vulnerabilities arise if the developer forgets to relinquish resources when finished with them.
Navigate to // Todo: Close streams here
under loginPressed()
in MainActivity.kt and replace it with this:
objectInputStream.close()
fileInputStream.close()
This releases all the acquired resources. When you find a software defect, it’s a good clue that the defect likely exists in other places as well. It’s a great opportunity to look at other places in the code for the same mistake. Can you find it?
Replace // Todo: Close streams here also
at the end of createDataSource
with the following:
inputStream.close()
You’ve been a good citizen by cleaning up after yourself!
Here are a few more points about concurrent programming:
- It’s pointless to have synchronization inside a class when its interface exposes a mutable object to the shared data. Any user of your class can do whatever they want with the data if it isn’t protected. Instead, return immutable variables or copies to the data.
- Make sure you mark synchronized variables as private. Good interface design and data encapsulation are important when designing concurrent programs to make sure you protect your shared data.
- It’s good for code readability to write your methods with only one entry and one exit point, especially if you’ll be adding locks later. It’s easy to miss a
return
hidden in the middle of a method that was supposed to lock your data later. That can cause a race condition. Instead ofreturn true
for example, you can declare aBoolean
, update it along the way and then return it at the end of the method. Then you can wrap synchronization code in the method without much work. - When a thread is waiting on another thread but the other thread requires something from the first thread, it’s called a Deadlock and the app will crash. Crashes are security vulnerabilities when temporary work files are never cleaned up before the thread terminates.
With all the changes you’ve made, it’s important to do thorough testing to make sure you didn’t break something that was working before. Developers call that Regression Testing.