Advertisement
Scroll to top

From minimizing pointer use to strong type checking at compile time, Swift is a great language for secure development. But that means it's tempting to forget about security altogether. There are still vulnerabilities, and Swift is also enticing to new developers who haven't yet learned about security. 

This tutorial is a secure coding guide that will address changes in Swift 4 as well as the new tooling options available in Xcode 9 that will help you mitigate security vulnerabilities.

Pointers and Overflows

Many security vulnerabilities have revolved around C and its use of pointers. This is because pointers allow you to access raw memory locations, making it easier to read and write to the wrong area. It has been a major way for attackers to maliciously change a program. 

Swift mostly does away with pointers, but it still allows you to interface with C. Many APIs, including Apple's entire Core Foundation API, are based entirely in C, so it's very easy to introduce the use of pointers back into Swift. 

Fortunately, Apple has named the pointer types appropriately: UnsafePointer<T>, UnsafeRawPointer<T>UnsafeBufferPointer<T>, and UnsafeRawBufferPointer. There will come a time when the API you are interfacing with will return these types, and the main rule when using them is don't store or return pointers for later use. For example:

1
let myString = "Hello World!"
2
var unsafePointer  : UnsafePointer<CChar>? = nil
3
myString.withCString { myStringPointer in
4
    unsafePointer = myStringPointer
5
}
6
//sometime later...

7
print(unsafePointer?.pointee)

Because we accessed the pointer outside of the closure, we don't know for sure if the pointer still points to the expected memory contents. The safe way to use the pointer in this example would be to keep it, along with the print statement, within the closure. 

Pointers to strings and arrays also have no bounds checking. This means it's easy to use an unsafe pointer on an array but accidentally access beyond its boundary—a buffer overflow.

1
var numbers = [1, 2, 3, 4, 5]
2
numbers.withUnsafeMutableBufferPointer { buffer in
3
            
4
    //ok

5
    buffer[0] = 5
6
    print(buffer[0])
7
                
8
    //bad

9
    buffer[5] = 0
10
    print(buffer[5])
11
}

The good news is that Swift 4 attempts to crash the app instead of continuing with what would be called undefined behavior. We don't know what buffer[5] points to! However, Swift won't catch every case. Set a breakpoint after the following code and look at variables a and c. They will be set to 999.

1
func getAddress(pointer:UnsafeMutablePointer<Int>) -> UnsafeMutablePointer<Int>
2
{
3
    return pointer
4
}
5
var a = 111
6
var b = 222
7
var c = 333
8
let pointer : UnsafeMutablePointer<Int> = getAddress(pointer: &b)
9
pointer.successor().initialize(to: 999)
10
pointer.predecessor().initialize(to: 999)

This demonstrates a stack overflow because without an explicit allocation, variables are generally stored on the stack. 

In the next example, we make an allocation with a capacity of only a single Int8. Allocations are stored on the heap, so the next line will overflow the heap. For this example, Xcode only warns you with a note in the console that gets is unsafe.

1
let buffer = UnsafeMutablePointer<Int8>.allocate(capacity:1)
2
gets(buffer) 

So what's the best way to avoid overflows? It's extremely important when interfacing with C to do bounds checking on the input to make sure it's within range. 

You might be thinking that it's pretty hard to remember and find all of the different cases. So to help you out, Xcode comes with a very useful tool called Address Sanitizer. 

Address Sanitizer has been improved in Xcode 9. It is a tool that helps you catch invalid memory access such as the examples we have just seen. If you will be working with the Unsafe* types, it's a good idea to use the Address Sanitizer tool. It is not enabled by default, so to enable it, go to Product > Scheme > Edit Scheme > Diagnostics, and check Address Sanitizer. In Xcode 9 there is a new sub-option, Detect use of stack after return. This new option detects the use-after-scope and use-after-return vulnerabilities from our first example.

Sometimes overlooked is the integer overflow. This is because integer overflows are security holes only when used as an index or size of a buffer, or if the unexpected value of the overflow changes the flow of critical security code. Swift 4 catches most obvious integer overflows at compile time, such as when the number is clearly larger than the max value of the integer. 

For example, the following will not compile.

1
var someInteger : CInt = 2147483647
2
someInteger += 1

But a lot of the times the number will arrive dynamically at runtime, such as when a user enters information in a UITextField. Undefined Behavior Sanitizer is a new tool in Xcode 9 which detects signed integer overflow and other type-mismatch bugs. To enable it, go to Product > Scheme > Edit Scheme > Diagnostics, and turn on Undefined Behavior Sanitizer. Then in Build Settings > Undefined Behavior Sanitizer, set Enable Extra Integer Checks to Yes.

There's another thing worth mentioning about undefined behavior. Even though pure Swift hides pointers, references and copies of buffers are still used behind the scenes, so it's possible to run into behavior that you did not expect. For example, when you start iterating over collection indices, the indices could accidentally be modified by you during iteration.

1
var numbers = [1, 2, 3]
2
for number in numbers
3
{
4
    print(number)
5
    numbers = [4, 5, 6] //<- accident ???

6
}
7
        
8
for number in numbers
9
{
10
    print(number)
11
}

Here we just caused the numbers array to point to a new array inside the loop. Then what does number point to? This would be normally be called a dangling reference, but in this case Swift implicitly creates a reference to a copy of the buffer of your array for the duration of the loop. That means that the print statement will actually print out 1, 2, and 3 instead of 1, 4, 5.... This is good! Swift is saving you from undefined behavior or an app crash, although you might not have expected that output either. Your peer developers won't expect your collection to be mutated during enumeration, so in general, be extra careful during enumeration that you are not altering the collection.

So Swift 4 has great security enforcement at compile time to catch these security vulnerabilities. There are many situations where the vulnerability doesn't exist until run time when there is user interaction. Swift also includes dynamic checking, which can catch many of the issues at run time too, but it's too expensive to do across threads so it's not performed for multithreaded code. Dynamic checking will catch many but not all violations, so it's still important to write secure code in the first place! 

With that, let's turn to another very common area for vulnerabilities—code injection attacks.

Injection and Format String Attacks

Format string attacks happen when an input string is parsed in your app as a command that you did not intend. While pure Swift strings are not susceptible to format string attacks, the Objective-C NSString and Core Foundation CFString classes are, and they are available from Swift. Both of these classes have methods such as stringWithFormat.

Let's say the user can enter arbitrary text from a UITextField.

1
let inputString = "String from a textfield %@ %d %p %ld %@ %@" as NSString

This could be a security hole if the format string is handled directly.

1
let textFieldString = NSString.init(format: inputString) //bad

2
let textFieldString = NSString.init(format: "%@", inputString) //good

Swift 4 tries to handle missing format string arguments by returning 0 or NULL, but it is especially a concern if the string will get passed back to the Objective-C runtime.

1
NSLog(textFieldString); //bad

2
NSLog("%@", textFieldString); //good

While most of the time the incorrect way will just cause a crash, an attacker can carefully craft a format string to write data to specific memory locations on the stack to alter your app behavior (such as changing an isAuthenticated variable). 

Another big culprit is NSPredicate, which can accept a format string that is used to specify what data is retrieved from Core Data. Clauses such as LIKE and CONTAINS allow wildcards and should be avoided, or at least only used for searches. The idea is to avoid enumeration of accounts, for example, where the attacker enters "a*" as the account name. If you change the LIKE clause to ==, this means the string literally has to match “a*”. 

Other common attacks happen by terminating the input string early with a single-quote character so additional commands can be entered. For instance, a login could be bypassed by entering ') OR 1=1 OR (password LIKE '* into the UITextField. That line translates to "where password is like anything”, which bypasses the authentication altogether. The solution is to fully escape any attempts at injection by adding your own double quotes in code. That way, any additional quotes from the user are seen as part of the input string instead of being a special terminating character:

1
let query = NSPredicate.init(format: "password == \"%@\"", name)

One more way to safeguard against these attacks is to simply search for and exclude specific characters that you know could be harmful in the string. Examples would include quotes, or even dots and slashes. For instance, it is possible to do a directory traversal attack when input gets passed directly to the FileManager class. In this example, the user enters "../" to view the parent directory of the path instead of the intended sub-directory.

1
let userControllerString = "../" as NSString
2
let sourcePath = NSString.init(format: "%@/%@", Bundle.main.resourcePath! , userControllerString)
3
NSLog("%@", sourcePath)
4
        
5
//Instead of Build/Products/Debug/Swift4.app/Contents/Resources, it will be Build/Products/Debug/Swift4.app/Contents

6
let filemanager:FileManager = FileManager()
7
let files = filemanager.enumerator(atPath: sourcePath as String)
8
while let file = files?.nextObject()
9
{
10
    print(file)
11
}

Other special characters might include a NULL terminating byte if the string gets used as a C string. Pointers to C strings require a NULL terminating byte. Because of this, it is possible to manipulate the string simply by introducing a NULL byte. The attacker might want to terminate the string early if there was a flag such as needs_auth=1, or when access is on by default and turned off explicitly such as with is_subscriber=0.

1
let userInputString = "username=Ralph\0" as NSString
2
let commandString = NSString.init(format: "subscribe_user:%@&needs_authorization=1", userInputString)
3
NSLog("%s", commandString.utf8String!)
4
// prints subscribe_user:username=Ralph instead of subscribe_user:username=Ralph&needs_authorization=1

Parsing HTML, XML, and JSON strings requires special attention as well. The safest way to work with them is to use Foundation's native libraries that provide objects for each node, such as the NSXMLParser class. Swift 4 introduces type-safe serialization to external formats such as JSON. But if you are reading XML or HTML using a custom system, be sure that special characters from the user input cannot be used to instruct the interpreter.

  • < must become &lt.
  • > should get replaced with &gt.
  • & should become &amp.
  • Inside attribute values, any or ' need to become &quot and &apos, respectively.

Here is an example of a quick way to remove or replace specific characters:

1
var myString = "string to sanitize;"
2
myString = myString.replacingOccurrences(of: ";", with: "")

A final area for injection attacks is inside URL handlers. Check to make sure user input is not used directly inside the custom URL handlers openURL and didReceiveRemoteNotification. Verify the URL is what you are expecting and that it doesn't allow a user to arbitrarily enter info to manipulate your logic. For example, instead of letting the user choose which screen in the stack to navigate to by index, allow only specific screens using an opaque identifier, such as t=es84jg5urw

If you are using WKWebViews in your app, it might be good to check the URLs that will be loaded there as well. You can override decidePolicyFor navigationAction, which lets you choose if you want to continue with the URL request. 

Some known webview tricks include loading custom URL schemes the developer did not intend, such as an app-id: to launch an entirely different app or sms: to send a text. Note that embedded webviews don't show a bar with the URL address or SSL status (the lock icon), so the user is not able to determine if the connection is trusted. 

If the webview is full screen, for example, the URL could be hijacked with a webpage that looks just like your login screen except directing the credentials to a malicious domain instead. Other attacks in the past have included cross-site scripting attacks that have leaked cookies and even the entire filesystem. 

The best prevention for all of the mentioned attacks is to take the time to design your interface using native UI controls instead of simply displaying a web-based version within your app.

So far, we've been looking at relatively straightforward kinds of attacks. But let's finish off with a more advanced attack that can happen in the runtime.

Runtime Hacking

Just as Swift becomes more vulnerable when you interface with C, interfacing with Objective-C brings separate vulnerabilities to the table. 

We have already seen the issues with NSString and format string attacks. Another point is that Objective-C is much more dynamic as a language, allowing loose types and methods to be passed around. If your Swift class inherits from NSObject, then it becomes open to Objective-C runtime attacks. 

The most common vulnerability involves dynamically swapping an important security method for another method. For example, a method that returns if a user is validated could be swapped for another method that will almost always return true, such as isRetinaDisplay. Minimizing the use of Objective-C will make your app more robust against this type of attack.

In Swift 4, methods on classes that inherit from an Objective-C class are only exposed to the Objective-C runtime if those methods or the classes themselves are marked with @attribute. Often the Swift function is called instead, even if the @objc attribute is used. This can happen when the method has an @objc attribute but is never actually called from Objective-C. 

In other words, Swift 4 introduces less @objc inference, so this limits the attack surface compared to previous versions. Still, to support the runtime features, Objective-C-based binaries need to retain a lot of class information that cannot be stripped away. This is enough for reverse engineers to rebuild the class interface to figure out what security sections to patch, for example. 

In Swift, there is less information exposed in the binary, and function names are mangled. However, the mangling can be undone by the Xcode tool swift-demangle. In fact, Swift functions have a consistent naming scheme, indicating if each one is a Swift function or not, part of a class, module name and length, class name and length, method name and length, attributes, parameters, and return type. 

These names are shorter in Swift 4. If you're concerned about reverse engineering, make sure the release version of your app strips symbols by going to Build Settings > Deployment > Strip Swift Symbols and setting the option to Yes.

Beyond obfuscating critical security code, you can also request it to be inline. This means that any place that the function is called in your code, the code will be repeated in that place instead of existing only in one location of the binary. 

This way, if an attacker manages to bypass a particular security check, it will not affect any other occurrences of that check situated in other places of your code. Each check has to be patched or hooked, making it much more difficult to successfully perform a crack. You can inline code like this:

1
@inline(__always) func myFunction() 
2
{
3
    //...

4
}

Conclusion

Thinking about security should be a big part of development. Merely expecting the language to be secure can lead to vulnerabilities that could have been avoided. Swift is popular for iOS development, but it is available for macOS desktop apps, tvOS, watchOS, and Linux (so you could use it for server-side components where the potential for code execution exploits is much higher). App sandboxing can be broken, such as in the case of jailbroken devices which allow unsigned code to run, so it's important to still think about security and pay attention to Xcode notices while you debug. 

A final tip is to treat compiler warnings as errors. You can force Xcode to do this by going to Build Settings and setting Treat Warnings as Errors to Yes. Don't forget to modernize your project settings when migrating to Xcode 9 to get improved warnings, and last but not least, make use of the new features available by adopting Swift 4 today!

Learn Swift

We've built a complete guide to help you learn Swift, whether you're just getting started with the basics or you want to explore more advanced topics.

For a primer on other aspects of secure coding for iOS, check out some of my other posts here on Envato Tuts+!

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.