DataAtRestIcon

Secure Coding in Swift 4

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, UnsafeRawPointerUnsafeBufferPointer,
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:

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.

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.

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.

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.

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.

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.

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

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.

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:

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.

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.

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 <.

  • > should get replaced with >.
  • & should become &.
  • Inside attribute values, any or ' need to become " and &apos, respectively.

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

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:

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!

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

  • iOS SDK
    Securing iOS Data at Rest: Protecting the User's Data
    Collin Stuart
  • iOS SDK
    Securing Communications on iOS
    Collin Stuart
  • Security
    Creating Digital Signatures With Swift
    Collin Stuart

Powered by WPeMatico

Leave a Comment

Scroll to Top