iOS Code Quality and Build Settings¶
Overview¶
App Signing¶
Code signing your app assures users that the app has a known source and hasn't been modified since it was last signed. Before your app can integrate app services, be installed on a non-jailbroken device, or be submitted to the App Store, it must be signed with a certificate issued by Apple. For more information on how to request certificates and code sign your apps, review the App Distribution Guide.
Third-Party Libraries¶
iOS applications often make use of third party libraries which accelerate development as the developer has to write less code in order to solve a problem. However, third party libraries may contain vulnerabilities, incompatible licensing, or malicious content. Additionally, it is difficult for organizations and developers to manage application dependencies, including monitoring library releases and applying available security patches.
There are three widely used package management tools Swift Package Manager, Carthage, and CocoaPods:
- The Swift Package Manager is open source, included with the Swift language, integrated into Xcode (since Xcode 11) and supports Swift, Objective-C, Objective-C++, C, and C++ packages. It is written in Swift, decentralized and uses the Package.swift file to document and manage project dependencies.
- Carthage is open source and can be used for Swift and Objective-C packages. It is written in Swift, decentralized and uses the Cartfile file to document and manage project dependencies.
- CocoaPods is open source and can be used for Swift and Objective-C packages. It is written in Ruby, utilizes a centralized package registry for public and private packages and uses the Podfile file to document and manage project dependencies.
There are two categories of libraries:
- Libraries that are not (or should not) be packed within the actual production application, such as
OHHTTPStubs
used for testing. - Libraries that are packed within the actual production application, such as
Alamofire
.
These libraries can lead to unwanted side-effects:
- A library can contain a vulnerability, which will make the application vulnerable. A good example is
AFNetworking
version 2.5.1, which contained a bug that disabled certificate validation. This vulnerability would allow attackers to execute man-in-the-middle attacks against apps that are using the library to connect to their APIs. - A library can no longer be maintained or hardly be used, which is why no vulnerabilities are reported and/or fixed. This can lead to having bad and/or vulnerable code in your application through the library.
- A library can use a license, such as LGPL2.1, which requires the application author to provide access to the source code for those who use the application and request insight in its sources. In fact the application should then be allowed to be redistributed with modifications to its source code. This can endanger the intellectual property (IP) of the application.
Please note that this issue can hold on multiple levels: When you use webviews with JavaScript running in the webview, the JavaScript libraries can have these issues as well. The same holds for plugins/libraries for Cordova, React-native and Xamarin apps.
Memory Corruption Bugs¶
iOS applications have various ways to run into memory corruption bugs: first there are the native code issues which have been mentioned in the general Memory Corruption Bugs section. Next, there are various unsafe operations with both Objective-C and Swift to actually wrap around native code which can create issues. Last, both Swift and Objective-C implementations can result in memory leaks due to retaining objects which are no longer in use.
Learn more:
- https://developer.ibm.com/tutorials/mo-ios-memory/
- https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/MemoryMgmt/Articles/MemoryMgmt.html
- https://medium.com/zendesk-engineering/ios-identifying-memory-leaks-using-the-xcode-memory-graph-debugger-e84f097b9d15
Binary Protection Mechanisms¶
Detecting the presence of binary protection mechanisms heavily depend on the language used for developing the application.
Although Xcode enables all binary security features by default, it may be relevant to verify this for old applications or to check for compiler flag misconfigurations. The following features are applicable:
- PIE (Position Independent Executable):
- PIE applies to executable binaries (Mach-O type
MH_EXECUTE
). - However it's not applicable for libraries (Mach-O type
MH_DYLIB
).
- PIE applies to executable binaries (Mach-O type
- Memory management:
- Both pure Objective-C, Swift and hybrid binaries should have ARC (Automatic Reference Counting) enabled.
- For C/C++ libraries, the developer is responsible for doing proper manual memory management. See "Memory Corruption Bugs".
- Stack Smashing Protection: For pure Objective-C binaries, this should always be enabled. Since Swift is designed to be memory safe, if a library is purely written in Swift, and stack canaries weren’t enabled, the risk will be minimal.
Learn more:
- OS X ABI Mach-O File Format Reference
- On iOS Binary Protections
- Security of runtime process in iOS and iPadOS
- Mach-O Programming Topics - Position-Independent Code
Tests to detect the presence of these protection mechanisms heavily depend on the language used for developing the application. For example, existing techniques for detecting the presence of stack canaries do not work for pure Swift apps.
Xcode Project Settings¶
Stack Canary protection¶
Steps for enabling stack canary protection in an iOS application:
- In Xcode, select your target in the "Targets" section, then click the "Build Settings" tab to view the target's settings.
- Make sure that the "-fstack-protector-all" option is selected in the "Other C Flags" section.
- Make sure that Position Independent Executables (PIE) support is enabled.
PIE protection¶
Steps for building an iOS application as PIE:
- In Xcode, select your target in the "Targets" section, then click the "Build Settings" tab to view the target's settings.
- Set the iOS Deployment Target to iOS 4.3 or later.
- Make sure that "Generate Position-Dependent Code" (section "Apple Clang - Code Generation") is set to its default value ("NO").
- Make sure that "Generate Position-Dependent Executable" (section "Linking") is set to its default value ("NO").
ARC protection¶
ARC is automatically enabled for Swift apps by the swiftc
compiler. However, for Objective-C apps you'll have ensure that it's enabled by following these steps:
- In Xcode, select your target in the "Targets" section, then click the "Build Settings" tab to view the target's settings.
- Make sure that "Objective-C Automatic Reference Counting" is set to its default value ("YES").
See the Technical Q&A QA1788 Building a Position Independent Executable.
Debuggable Apps¶
Apps can be made debuggable ( Debugging) by adding the get-task-allow
key to the app entitlements file and setting it to true
.
While debugging is a useful feature when developing an app, it has to be turned off before releasing apps to the App Store or within an enterprise program. To do that you need to determine the mode in which your app is to be generated to check the flags in the environment:
- Select the build settings of the project
- Under 'Apple LVM - Preprocessing' and 'Preprocessor Macros', make sure 'DEBUG' or 'DEBUG_MODE' is not selected (Objective-C)
- Make sure that the "Debug executable" option is not selected.
- Or in the 'Swift Compiler - Custom Flags' section / 'Other Swift Flags', make sure the '-D DEBUG' entry does not exist.
Debugging Symbols¶
As a good practice, as little explanatory information as possible should be provided with a compiled binary. The presence of additional metadata such as debug symbols might provide valuable information about the code, e.g. function names leaking information about what a function does. This metadata is not required to execute the binary and thus it is safe to discard it for the release build, which can be done by using proper compiler configurations. As a tester you should inspect all binaries delivered with the app and ensure that no debugging symbols are present (at least those revealing any valuable information about the code).
When an iOS application is compiled, the compiler generates a list of debug symbols for each binary file in an app (the main app executable, frameworks, and app extensions). These symbols include class names, global variables, and method and function names which are mapped to specific files and line numbers where they're defined. Debug builds of an app place the debug symbols in a compiled binary by default, while release builds of an app place them in a companion Debug Symbol file (dSYM) to reduce the size of the distributed app.
Debugging Code and Error Logging¶
To speed up verification and get a better understanding of errors, developers often include debugging code, such as verbose logging statements (using NSLog
, println
, print
, dump
, and debugPrint
) about responses from their APIs and about their application's progress and/or state. Furthermore, there may be debugging code for "management-functionality", which is used by developers to set the application's state or mock responses from an API. Reverse engineers can easily use this information to track what's happening with the application. Therefore, debugging code should be removed from the application's release version.
Exception Handling¶
Exceptions often occur after an application enters an abnormal or erroneous state. Testing exception handling is about making sure that the application will handle the exception and get into a safe state without exposing any sensitive information via its logging mechanisms or the UI.
Bear in mind that exception handling in Objective-C is quite different from exception handling in Swift. Bridging the two approaches in an application that is written in both legacy Objective-C code and Swift code can be problematic.
Exception Handling in Objective-C¶
Objective-C has two types of errors:
NSException:
NSException
is used to handle programming and low-level errors (e.g., division by 0 and out-of-bounds array access).
An NSException
can either be raised by raise
or thrown with @throw
. Unless caught, this exception will invoke the unhandled exception handler, with which you can log the statement (logging will halt the program). @catch
allows you to recover from the exception if you're using a @try
-@catch
-block:
@try {
//do work here
}
@catch (NSException *e) {
//recover from exception
}
@finally {
//cleanup
Bear in mind that using NSException
comes with memory management pitfalls: you need to clean up allocations from the try block that are in the finally block. Note that you can promote NSException
objects to NSError
by instantiating an NSError
in the @catch
block.
NSError:
NSError
is used for all other types of errors. Some Cocoa framework APIs provide errors as objects in their failure callback in case something goes wrong; those that don't provide them pass a pointer to an NSError
object by reference. It is a good practice to provide a BOOL
return type to the method that takes a pointer to an NSError
object to indicate success or failure. If there's a return type, make sure to return nil
for errors. If NO
or nil
is returned, it allows you to inspect the error/reason for failure.
Exception Handling in Swift¶
Exception handing in Swift (2 - 5) is quite different. The try-catch block is not there to handle NSException
. The block is used to handle errors that conform to the Error
(Swift 3) or ErrorType
(Swift 2) protocol. This can be challenging when Objective-C and Swift code are combined in an application. Therefore, NSError
is preferable to NSException
for programs written in both languages. Furthermore, error-handling is opt-in in Objective-C, but throws
must be explicitly handled in Swift. To convert error-throwing, look at the Apple documentation.
Methods that can throw errors use the throws
keyword. The Result
type represents a success or failure, see Result, How to use Result in Swift 5 and The power of Result types in Swift. There are four ways to handle errors in Swift:
- Propagate the error from a function to the code that calls that function. In this situation, there's no
do-catch
; there's only athrow
throwing the actual error or atry
to execute the method that throws. The method containing thetry
also requires thethrows
keyword:
func dosomething(argumentx:TypeX) throws {
try functionThatThrows(argumentx: argumentx)
}
- Handle the error with a
do-catch
statement. You can use the following pattern:
func doTryExample() {
do {
try functionThatThrows(number: 203)
} catch NumberError.lessThanZero {
// Handle number is less than zero
} catch let NumberError.tooLarge(delta) {
// Handle number is too large (with delta value)
} catch {
// Handle any other errors
}
}
enum NumberError: Error {
case lessThanZero
case tooLarge(Int)
case tooSmall(Int)
}
func functionThatThrows(number: Int) throws -> Bool {
if number < 0 {
throw NumberError.lessThanZero
} else if number < 10 {
throw NumberError.tooSmall(10 - number)
} else if number > 100 {
throw NumberError.tooLarge(100 - number)
} else {
return true
}
}
- Handle the error as an optional value:
let x = try? functionThatThrows()
// In this case the value of x is nil in case of an error.
- Use the
try!
expression to assert that the error won't occur. - Handle the generic error as a
Result
return:
enum ErrorType: Error {
case typeOne
case typeTwo
}
func functionWithResult(param: String?) -> Result<String, ErrorType> {
guard let value = param else {
return .failure(.typeOne)
}
return .success(value)
}
func callResultFunction() {
let result = functionWithResult(param: "OWASP")
switch result {
case let .success(value):
// Handle success
case let .failure(error):
// Handle failure (with error)
}
}
- Handle network and JSON decoding errors with a
Result
type:
struct MSTG: Codable {
var root: String
var plugins: [String]
var structure: MSTGStructure
var title: String
var language: String
var description: String
}
struct MSTGStructure: Codable {
var readme: String
}
enum RequestError: Error {
case requestError(Error)
case noData
case jsonError
}
func getMSTGInfo() {
guard let url = URL(string: "https://raw.githubusercontent.com/OWASP/owasp-mastg/master/book.json") else {
return
}
request(url: url) { result in
switch result {
case let .success(data):
// Handle success with MSTG data
let mstgTitle = data.title
let mstgDescription = data.description
case let .failure(error):
// Handle failure
switch error {
case let .requestError(error):
// Handle request error (with error)
case .noData:
// Handle no data received in response
case .jsonError:
// Handle error parsing JSON
}
}
}
}
func request(url: URL, completion: @escaping (Result<MSTG, RequestError>) -> Void) {
let task = URLSession.shared.dataTask(with: url) { data, _, error in
if let error = error {
return completion(.failure(.requestError(error)))
} else {
if let data = data {
let decoder = JSONDecoder()
guard let response = try? decoder.decode(MSTG.self, from: data) else {
return completion(.failure(.jsonError))
}
return completion(.success(response))
}
}
}
task.resume()
}