IOS Mobile App Security: Best Practices for iOS Mobile Developers.

Munendra Pratap Singh
6 min readFeb 10, 2024

iOS developers must prioritize code security, data storage, and communication security based on OWASP Top 10. Although iOS is less vulnerable than Android, it still faces security issues. It is the responsibility of the developer/organization to secure code and data communication to prevent tampering and unauthorized code access.

To ensure caution on the developer's end, every iOS developer should take care of the following preliminary-level threats:

1. Screen Recording and Screen Capturing:

An attacker could record sensitive screens such as login pages and capture entered usernames and passwords. In video streaming applications, it is possible for users to stream and record paid video content.
There is a high risk of sensitive transaction details being compromised if a screenshot or screen recording is taken in a banking application.

We fix that we can observe the UIScreen.capturedDidChangeNotification and check for UIScreen.main.isCaptured

Observe changes to isCaptured with NotificationCenter:

NotificationCenter.default.addObserver(self, selector: #selector(screenCaptureDidChange),
name: UIScreen.capturedDidChangeNotification,
object: nil)

Handle the notification:

@objc func screenCaptureDidChange() {
print("screenCaptureDidChange.. isCapturing: \(UIScreen.main.isCaptured)")

if UIScreen.main.isCaptured {
//TODO: They started capturing..
print("screenCaptureDidChange - is recording screen")
} else {
//TODO: They stopped capturing..
print("screenCaptureDidChange - screen recording stoped")
}
}

2. Weak Jail Break Detection:

It's important to be aware that application logic and behaviour can be compromised on JailBroken devices, which can expose the application to attacks. However, it's worth noting that with some effort, any hacker can bypass these basic security checks. Therefore, relying solely on jailbreak detection methods may not be sufficient to ensure the safety of your application.

The following text seems to be instructions or guidelines related to identifying whether a device is jailbroken or not. It contains three tests that can help with this identification:

  1. One way to identify whether a device is jailbroken or not is by checking for unique files and applications that are installed on jailbroken devices. Developers can use this test to look for these files in the file system.
private var filesPathToCheck: [String] {

return ["/private/var/lib/apt",
"/Applications/Cydia.app",
"/private/var/lib/cydia",
"/private/var/tmp/cydia.log",
"/Applications/RockApp.app",
"/Applications/Icy.app",
"/Applications/WinterBoard.app",
"/Applications/SBSetttings.app",
"/Applications/blackra1n.app",
"/Applications/IntelliScreen.app",
"/Applications/Snoop-itConfig.app",
"/usr/libexec/cydia/",
"/usr/sbin/frida-server",
"/usr/bin/cycript",
"/usr/local/bin/cycript",
"/usr/lib/libcycript.dylib",
"/bin/sh",
"/usr/libexec/sftp-server",
"/usr/libexec/ssh-keysign",
"/Library/MobileSubstrate/MobileSubstrate.dylib",
"/bin/bash",
"/usr/sbin/sshd",
"/etc/apt",
"/usr/bin/ssh",
"/bin.sh",
"/var/checkra1n.dmg",
"/System/Library/LaunchDaemons/com.saurik.Cydia.Startup.plist",
"/System/Library/LaunchDaemons/com.ikey.bbot.plist",
"/Library/MobileSubstrate/DynamicLibraries/LiveClock.plist",
"/Library/MobileSubstrate/DynamicLibraries/Veency.plist"]
}

func isJailBrokenFilesPresentInTheDirectory() -> Bool{
var checkFileIfExist: Bool = false
filesPathToCheck.forEach {
checkFileIfExist = fm.fileExists(atPath: $0) ? true : false
if checkFileIfExist{
return
}
}

return checkFileIfExist
}

2. Another way to check whether an application is jailbroken or not is to see if it follows sandboxing rules. Checking if a file can be modified outside the application bundle can help identify this. Developers can use this test to check if their application follows sandboxing rules.

func canEditSandboxFilesForJailBreakDetection() -> Bool {
let jailBreakTestText = "Test for JailBreak"
do {
try jailBreakTestText.write(toFile:"/private/jailBreakTestText.txt", atomically:true, encoding:String.Encoding.utf8)
return true
} catch {
let resultJailBroken = isJailBrokenFilesPresentInTheDirectory()
return resultJailBroken
}
}

3. If calling the Cydia URL scheme (Cydia://) from an application results in success, it means the device is jailbroken. This test checks if the developer has performed this check.



// Protocol function extended for JailBreak detection
func assignJailBreakCheckType() -> Bool {
// If it is run on simulator follow the regular flow of the app
if !isSimulator {
// Check if Cydia app is installed on the device
guard UIApplication.shared.canOpenURL(URL(string: "cydia://")!) else {
return false
}
return true
}
return true
}

3. KeyChain Data Protection:

On JailBroken devices, a keychain item with a vulnerable accessibility option can be easily exposed to other applications or attackers with physical access. However, developers have multiple actions at their disposal to mitigate this security risk:

  1. kSecAttrAccessibleWhenUnlocked
  2. kSecAttrAccessibleAfterFirstUnlock
  3. kSecAttrAccessibleAlways
  4. kSecAttrAccessibleWhenUnlockedThisDeviceOnly
  5. kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
  6. kSecAttrAccessibleAlwaysThisDeviceOnly

Choosing the easiest or more prone to vulnerable options like, ‘kSecAttrAccessibleWhenUnlocked’ may lead to potential security risk.

The 'kSecAttrAccessibleAfterFirstUnlock' or 'kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly' modes should only be used when the application requires a KeyChain item for background processing.

4. File Data Protection:

When a new file is to be saved, a developer can choose from these options to better use of data protection:

  1. atomic:
    An option to write data to an auxiliary file first and then replace the original file with the auxiliary file when the write completes.
  2. withoutOverwriting:
    An option that attempts to write data to a file and fails with an error if the destination file already exists.
  3. noFileProtection:
    An option to not encrypt the file when writing it out.
  4. completeFileProtection:
    An option to make the file accessible only while the device is unlocked.
  5. completeFileProtectionUnlessOpen:
    An option to allow the file to be accessible while the device is unlocked or the file is already open.
  6. completeFileProtectionUntilFirstUserAuthentication:
    An option to allow the file to be accessible after a user first unlocks the device.
  7. fileProtectionMask:
    An option the system uses when determining the file protection options that the system assigns to the data.

Choosing the easiest or more prone to vulnerable options like, ‘NSFileProtectionNone’ may lead to potential security risks.

It is advisable to use ‘completeFileProtectionUnlessOpen’ and ‘completeFileProtectionUntilFirstUserAuthentication’ to have data protection on all files.

Encrypting a file on the first write

do {
try data.write(to: fileURL, options: .completeFileProtection)
}
catch {
// Handle errors.
}

For an existing file, you can use either NSFileManager/FileManager or NSURL:

try FileManager.default.setAttributes([.protectionKey: .completeFileProtection], ofItemAtPath: fileURL.path)
// Or
// cast as `NSURL` because the `fileProtection` property of `URLResourceValues` is read-only.
try (fileURL as NSURL).setResourceValue(URLFileProtection.complete, forKey: .fileProtectionKey)

With Core Data, you can pass the protection type when adding the persistent store:

try persistentStoreCoordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: storeURL, options: [NSPersistentStoreFileProtectionKey: .completeFileProtection])

5. Show/Hide password field:

Application password is exposed while recording the login screen and other sensitive information screens.

When you identify screen is being recorded, use ‘mask’ over the password text field (or) any sensitive text field and protect data security.
Hide/Dismiss Keyboard while the app is being recorded.

if(isRecording){
let maskView = UIView(frame: CGRect(x: 64, y: 0, width: 128, height: 128))
maskView.backgroundColor = .blue
maskView.layer.cornerRadius = 64
yourView.mask = maskView
}

6. SSL Certificate Expiry:

Usually, a certificate is valid for one year and requires renewal to continue working. If the renewal is not completed on time, the app will stop working.

It is recommended to renew the certificate before the expiry date and update the IPA to ensure seamless app functionality. Additionally, it is important to check the certificate expiry at every launch and request a new one a few days before the expiry date.

7. Lack of Certificate Pinning:

If an attacker is able to create a legitimate certificate for the domain they are targeting, they can trick the user into thinking that they are visiting a secure website, when in reality, they are being directed to an imposter website. This attack, known as a 'Man-in-the-Middle attack', can allow the attacker to intercept and decrypt traffic between the user and the website, potentially exposing sensitive information and taking advantage of other vulnerabilities.

The delegate must implement:

URLSession:task:didReceiveChallenge:completionHandler:
and handle all connections via certificate authentication.

func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
if let trust = challenge.protectionSpace.serverTrust, SecTrustGetCertificateCount(trust) > 0 {
if let certificate = SecTrustGetCertificateAtIndex(trust, 0) {
let data = SecCertificateCopyData(certificate) as Data
if certificates.contains(data) {
completionHandler(.useCredential, URLCredential(trust: trust))
return
} else {
//TODO: Throw SSL Certificate Mismatch
}
}
}
completionHandler(.cancelAuthenticationChallenge, nil)
}

8. Usage of HTTP Request

The application uses an insecure communication channel (HTTP). This means that an attacker on the same network as the victim could carry out a man-in-the-middle (MITM) attack by injecting a 301 HTTP redirection response to an attacker-controlled server.

9. Privacy Resources Access:

The application has the ability to access private devices and/or user resources like contacts, location, Bluetooth device ID, camera, and microphone. However, this could lead to a data leak if the data is transmitted insecurely (for example, sending the data as plaintext over HTTP). To prevent this, developers must ensure that access to these resources follows a secure policy, such as encrypting the data before transmitting it to the server. Additionally, it is important to verify that there are no third-party libraries in use that access resources insecurely. Validate all access to private resources and force compliance with a security policy. Like while using advertising identifiers, ABAddressBookRef and so on.

Validate all access to private resources and force compliance with a security policy. Like while using advertising identifiers, ABAddressBookRef and so on.

10. Debug Logs Enabling:

Debug Logs that print method completion details and sensitive information should be avoided in release builds.

Either use #ifDef DEBUG or enable debug=1 only on debug builds. NSLog to be strictly avoided.

#ifdef DEBUG
print("Your log statement")
#endif

--

--