Weak Biometric Authentication
ID |
swift.weak_biometric_authentication |
Severity |
high |
Resource |
Authentication |
Language |
Swift |
Tags |
CWE:288, CWE:303, MASVS:auth-8, MASWE:0014, NIST.SP.800-53, OWASP:2021:A07 |
Description
The LocalAuthentication framework provides APIs for implementing biometric authentication (Face ID, Touch ID) in iOS applications. However, using LAContext.evaluatePolicy() creates a critical security vulnerability: it returns a simple boolean value that can be trivially bypassed by runtime instrumentation tools like Frida.
When authentication logic relies on checking a boolean return value in application code, an attacker can use runtime manipulation to force that boolean to always return true, completely bypassing the authentication mechanism. This happens because the authentication result validation occurs in userspace application code, not in the hardware-backed Secure Enclave.
Vulnerable patterns include:
-
LAContext()- Creating a LocalAuthentication context to evaluate biometric policies -
evaluatePolicy()- Evaluating biometric authentication with a boolean result -
.deviceOwnerAuthentication- Authentication policy that returns a boolean
Rationale
The following example demonstrates vulnerable code that relies on boolean-based biometric authentication:
import LocalAuthentication
class BiometricAuthManager {
// VULNERABLE: Boolean-based authentication can be bypassed
func authenticateUser_vulnerable(completion: @escaping (Bool) -> Void) {
let context = LAContext() // WEAK! Creates bypassable auth context
var error: NSError?
// Check if biometric authentication is available
guard context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) else {
print("Biometric authentication not available")
completion(false)
return
}
// VULNERABLE: evaluatePolicy returns a boolean that can be hooked
context.evaluatePolicy(
.deviceOwnerAuthenticationWithBiometrics,
localizedReason: "Authenticate to access your account"
) { success, error in
if success {
// Attacker can force 'success' to true using Frida!
print("✅ Authentication successful")
completion(true)
} else {
print("❌ Authentication failed: \(error?.localizedDescription ?? "Unknown error")")
completion(false)
}
}
}
// VULNERABLE: Using boolean result to gate access to sensitive operations
func accessSensitiveData_vulnerable() {
authenticateUser_vulnerable { authenticated in
if authenticated {
// WEAK! This check can be bypassed
self.loadSensitiveUserData()
self.displayAccountBalance()
} else {
print("Access denied")
}
}
}
private func loadSensitiveUserData() {
// Load sensitive data...
}
private func displayAccountBalance() {
// Display financial information...
}
}
// VULNERABLE: Simple boolean check for authentication
class LoginViewController {
let authManager = BiometricAuthManager()
func loginWithBiometrics_vulnerable() {
authManager.authenticateUser_vulnerable { success in
if success {
// WEAK! Attacker can bypass this by hooking evaluatePolicy
self.transitionToHomeScreen()
} else {
self.showErrorAlert()
}
}
}
private func transitionToHomeScreen() {
// Navigate to authenticated area...
}
private func showErrorAlert() {
// Show error message...
}
}
This code has several critical security problems:
-
Trivial Frida bypass: An attacker can write a simple Frida script to hook
evaluatePolicyand force it to returntrue:// Frida script to bypass biometric authentication Interceptor.attach( Module.findExportByName( "LocalAuthentication", "evaluatePolicy:localizedReason:reply:" ), { onLeave: function(retval) { // Force authentication to always succeed retval.replace(ptr(1)); // true } } ); -
No hardware-backed verification: The authentication result is validated in userspace application code, not by the Secure Enclave.
-
Boolean manipulation: Runtime tools can manipulate boolean values anywhere in the execution flow, not just at the framework boundary.
-
No cryptographic binding: There’s no cryptographic proof that authentication actually occurred - just a boolean flag that can be set arbitrarily.
-
Persistent bypass: Once bypassed, the attacker has complete access to all "protected" functionality.
Remediation
Use Keychain with biometric access control instead of checking boolean return values:
Recommended Approach: Keychain + Biometric ACL
Store sensitive data in the Keychain with .biometryCurrentSet access control. The Secure Enclave will enforce authentication:
import Security
import LocalAuthentication
enum KeychainError: Error {
case accessControlCreationFailed
case keyGenerationFailed
case dataRetrievalFailed
case authenticationRequired
}
class SecureBiometricAuth {
// SECURE: Store encryption key in Keychain with biometric ACL
func createBiometricProtectedKey() throws {
// Create access control with biometric requirement
guard let accessControl = SecAccessControlCreateWithFlags(
kCFAllocatorDefault,
kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
.biometryCurrentSet, // Secure Enclave enforces this
nil
) else {
throw KeychainError.accessControlCreationFailed
}
let privateKeyAttrs: [String: Any] = [
kSecAttrIsPermanent as String: true,
kSecAttrApplicationTag as String: "com.example.authkey".data(using: .utf8)!,
kSecAttrAccessControl as String: accessControl
]
let attributes: [String: Any] = [
kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
kSecAttrKeySizeInBits as String: 256,
kSecPrivateKeyAttrs as String: privateKeyAttrs
]
var error: Unmanaged<CFError>?
guard let privateKey = SecKeyCreateRandomKey(attributes as CFDictionary, &error) else {
throw KeychainError.keyGenerationFailed
}
print("✅ Biometric-protected key created in Secure Enclave")
print(" • Authentication enforced by hardware, not app code")
print(" • Cannot be bypassed by Frida or similar tools")
}
// SECURE: Access to key automatically requires biometric authentication
func accessProtectedKey() throws -> SecKey {
let query: [String: Any] = [
kSecClass as String: kSecClassKey,
kSecAttrApplicationTag as String: "com.example.authkey".data(using: .utf8)!,
kSecReturnRef as String: true
]
var item: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &item)
guard status == errSecSuccess else {
if status == errSecInteractionNotAllowed {
// Biometric authentication required but not provided
throw KeychainError.authenticationRequired
}
throw KeychainError.dataRetrievalFailed
}
guard let key = item else {
throw KeychainError.dataRetrievalFailed
}
print("✅ Key accessed - Secure Enclave verified biometric authentication")
return (key as! SecKey)
}
// SECURE: Store sensitive data with biometric protection
func storeSensitiveData(data: Data) throws {
guard let accessControl = SecAccessControlCreateWithFlags(
kCFAllocatorDefault,
kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
.biometryCurrentSet,
nil
) else {
throw KeychainError.accessControlCreationFailed
}
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: "sensitive_data",
kSecValueData as String: data,
kSecAttrAccessControl as String: accessControl
]
// Delete existing item
SecItemDelete(query as CFDictionary)
let status = SecItemAdd(query as CFDictionary, nil)
guard status == errSecSuccess else {
throw KeychainError.accessControlCreationFailed
}
print("✅ Sensitive data stored with biometric protection")
}
// SECURE: Retrieve sensitive data (biometric auth enforced by Secure Enclave)
func retrieveSensitiveData() throws -> Data {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: "sensitive_data",
kSecReturnData as String: true
]
var item: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &item)
guard status == errSecSuccess else {
if status == errSecInteractionNotAllowed {
throw KeychainError.authenticationRequired
}
throw KeychainError.dataRetrievalFailed
}
guard let data = item as? Data else {
throw KeychainError.dataRetrievalFailed
}
print("✅ Data retrieved after Secure Enclave verified authentication")
return data
}
}
Alternative: LAContext with explicit prompt (still weaker)
If you must use LAContext for specific UI requirements, understand its limitations:
class LAContextWithWarning {
// ACCEPTABLE but WEAKER: Using LAContext with explicit UI
// NOTE: This is still vulnerable to Frida bypass!
func authenticateForExplicitAction() {
let context = LAContext()
context.localizedReason = "Authenticate to delete your account"
var error: NSError?
guard context.canEvaluatePolicy(
.deviceOwnerAuthenticationWithBiometrics,
error: &error
) else {
print("Biometrics not available")
return
}
// This boolean can still be bypassed by Frida
context.evaluatePolicy(
.deviceOwnerAuthenticationWithBiometrics,
localizedReason: "Confirm account deletion"
) { success, error in
if success {
// ⚠️ WARNING: Still bypassable!
// For critical operations, use Keychain + biometric ACL instead
print("User authenticated for action")
}
}
}
}
Comparison: Boolean Auth vs. Keychain + Biometric ACL:
| Approach | Security Level | Bypassable by Frida? |
|---|---|---|
|
⚠️ Weak |
✅ Yes - trivial to bypass |
Boolean check in app code |
⚠️ Weak |
✅ Yes - can hook any boolean |
Keychain with |
✅ Strong |
❌ No - Secure Enclave enforced |
Keychain with hardware-backed key |
✅ Strong |
❌ No - cryptographic proof required |
Best Practices:
-
Never rely on boolean return values for authentication: Boolean checks in application code can always be bypassed by runtime instrumentation.
-
Use Keychain + Biometric ACL: Store sensitive data or cryptographic keys in the Keychain with
.biometryCurrentSetaccess control. -
Let Secure Enclave handle authentication: The Secure Enclave performs hardware-backed authentication that cannot be bypassed by userspace tools.
-
Understand the threat model:
LAContextis appropriate for:-
Convenience authentication (auto-fill passwords)
-
Non-critical UI gating
-
User preference "shortcuts"
But NOT for: - Protecting sensitive data - Financial transactions - Account security operations - Access control to confidential information
-
-
Implement defense in depth: Even with Keychain protection, implement:
-
Jailbreak detection
-
Runtime integrity checks
-
Certificate pinning for network communications
-
Secure coding practices throughout the app
-
-
Document security architecture: Clearly document which authentication mechanisms are used where and why.
-
Test with Frida: During security testing, verify that your authentication cannot be bypassed with simple Frida scripts.
Attack Scenario: Frida Bypass
Understanding how easily LAContext can be bypassed:
Step 1: Normal App Flow
// App code
context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics) { success, error in
if success {
self.showSensitiveData() // Should require biometric
}
}
Step 2: Attacker’s Frida Script
// Frida script - attacker's device
Java.perform(function() {
var LAContext = ObjC.classes.LAContext;
Interceptor.attach(LAContext['- evaluatePolicy:localizedReason:reply:'].implementation, {
onEnter: function(args) {
console.log("[+] LAContext.evaluatePolicy called");
},
onLeave: function(retval) {
console.log("[+] Forcing authentication to succeed");
// Force the reply block to receive true
// This bypasses ALL biometric checks
}
});
});
Step 3: Result
- All evaluatePolicy calls return success
- No actual biometric authentication occurs
- Attacker gains full access to "protected" features
Step 4: Why Keychain + Biometric ACL Cannot Be Bypassed
// With Keychain + biometric ACL
let key = try accessProtectedKey() // Secure Enclave checks biometric
Even if Frida hooks this method and forces it to return a fake key object, the key won’t work for cryptographic operations because: - The real key never leaves the Secure Enclave - Cryptographic operations fail without valid hardware authentication - There’s no boolean to manipulate - either you have the key or you don’t
When LAContext is Acceptable
LAContext.evaluatePolicy() is appropriate for:
Non-Critical UI Convenience
// OK: Convenience for auto-filling username
func autofillUsername() {
let context = LAContext()
context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics,
localizedReason: "Autofill your username") { success, _ in
if success {
// OK: Just a convenience, not security-critical
self.usernameField.text = self.savedUsername
}
}
}
User Preference Shortcuts
// OK: Shortcut to user preferences
func quickAccessSettings() {
let context = LAContext()
context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics,
localizedReason: "Quick access to settings") { success, _ in
if success {
// OK: Just a UX shortcut, settings aren't sensitive
self.showSettingsScreen()
}
}
}
NOT Acceptable for: - Protecting financial data - Authorizing transactions - Accessing user credentials - Viewing confidential information - Any security-critical operation
Configuration
This detector identifies all uses of LAContext and evaluatePolicy to raise awareness of the security implications. In some cases, these APIs are used appropriately for non-critical convenience features.
Review each detection to determine if: 1. The authentication protects sensitive/critical functionality → Should use Keychain + biometric ACL 2. The authentication is only for convenience/UX → Current implementation may be acceptable
References
-
CWE-303: Incorrect Implementation of Authentication Algorithm.
-
CWE-288: Authentication Bypass Using an Alternate Path or Channel.
-
OWASP - Top 10 2021 Category A07 : Identification and Authentication Failures.
-
Apple Developer: Accessing Keychain Items with Face ID or Touch ID.
-
MASWE-0014: Cryptographic Keys Not Properly Protected at Rest.