Certificate pinning bypass in 2026 is a layered problem. Start with Objection's one-liner - it handles NSURLSession and most third-party libraries. When it fails, identify the pinning implementation (TrustKit, Alamofire, custom SecTrustEvaluate, Network.framework) and write a targeted Frida script. On A15+ chips, rootless jailbreaks (Dopamine, Palera1n) still work. For hardened apps with jailbreak detection, use Frida gadget injection on a re-signed IPA. Always extract KeychainItem secrets post-bypass - they're often more valuable than the traffic.
// 01 What certificate pinning does - and why apps get it wrong
Certificate pinning is a mechanism that ties an app to a specific TLS certificate or public key, preventing a man-in-the-middle attacker (or pen tester) from substituting their own certificate, even if that certificate is signed by a trusted CA. The goal is to ensure traffic can only be intercepted by the intended server - not by a corporate proxy, a rogue CA, or a security researcher with Burp Suite.
For mobile pen testing, this is the gating challenge. Before you can intercept and manipulate API traffic, you need to bypass pinning. In 2021, this was often trivial - Objection's ios sslpinning disable handled the majority of applications. In 2026, the landscape is more varied: some apps pin correctly and deeply; others pin inconsistently across API endpoints; many use third-party SDKs that add their own layer of pinning logic on top of the OS implementation.
// 02 Jailbreak landscape in 2026: rootless is the standard
The shift to rootless jailbreaks (introduced with Dopamine and Palera1n in 2022-2023) changed the mobile pen testing workflow significantly. Rootless jailbreaks don't write to the root filesystem - instead they store all modifications in a dedicated /var/jb path. This has two implications: jailbreak detection evasion is harder (the jailbreak leaves fewer filesystem traces), and not all Cydia packages have been ported to rootless.
Recommended 2026 jailbreak setup
Checkra1n / unc0ver
Checkm8 bootrom exploit-based. Persistent, full filesystem access. Most Cydia packages work. Preferred for A9-A14 if available. No longer updated but remains functional for supported chips.
Dopamine / Palera1n
Rootless architecture. Requires Sileo or Zebra as package manager. Frida, SSL Kill Switch 3, and most pen testing tools have been ported. iOS 16.x-17.x coverage is good; 18.x is still in progress as of 2026.
Frida gadget (re-signed IPA)
Inject FridaGadget.dylib into the app binary, re-sign with your developer certificate, install via AltStore or Sideloadly. Works on any device including non-jailbroken stock iOS. Best for apps that detect jailbreaks.
iOS Simulator + Frida
For apps without device-attestation or biometric requirements, the simulator is often the fastest path. Frida attaches directly - no jailbreak, no re-signing. Not suitable for apps that refuse to run in simulator.
// 03 Start here: Objection's SSL pinning disable
Objection wraps Frida with a CLI interface. For roughly 60% of production iOS apps, the following workflow is all you need:
SHELL# 1. Ensure Frida server is running on the device (via jailbreak or gadget)
frida-ps -Ua # list running apps
# 2. Attach Objection to the target app
objection --gadget "com.example.BankingApp" explore
# 3. Inside Objection REPL - disable SSL pinning
ios sslpinning disable
# 4. Verify - route traffic through Burp with your CA installed
# If HTTPS traffic is now visible → bypass successful
# 5. If not working, check what Objection is hooking:
ios sslpinning disable --quiet # suppress verbose - sometimes fixes hook conflicts
Under the hood, Objection hooks a broad set of iOS SSL/TLS delegation methods: URLSession:didReceiveChallenge:completionHandler:, SecTrustEvaluate, SecTrustEvaluateWithError, and common third-party libraries including Alamofire, AFNetworking, and TrustKit. When it works, it works instantly. When it doesn't, you need to know why.
// 04 When Objection silently fails: identifying the pinning implementation
Objection's pinning disable can fail for several reasons: the app uses a pinning implementation Objection doesn't hook, jailbreak detection is killing Frida before the hook fires, the app is using Network.framework instead of URLSession, or a custom native implementation using Security framework calls directly.
Step 1: Identify the pinning mechanism
Run otool -L or use rabin2 -l on the decrypted IPA binary to check linked libraries:
SHELL# Decrypt the IPA first (if from App Store)
bagbak --output ~/Desktop/decrypted com.example.BankingApp
# Check linked frameworks
otool -L BankingApp.app/BankingApp | grep -i -E "trust|pin|ssl|network|alamofire"
# Common indicators:
# TrustKit.framework → TrustKit pinning
# Alamofire.framework → Alamofire + ServerTrustPolicy
# Network.framework → NWConnection / NWParameters (Network.framework)
# No external libs → custom SecTrustEvaluate or SecPolicyCreate
# Search for pinning-related strings in the binary
strings BankingApp.app/BankingApp | grep -i -E "pin|publicKey|certHash|spki"
Step 2: Trace with Frida
SHELL# Trace Security framework calls to see what's actually being invoked
frida-trace -U -n "BankingApp" \
-m "*[* SecTrustEvaluate*]" \
-m "*[* SecTrustCopyPublicKey*]" \
-m "*[* SecCertificate*]" \
-m "*[NSURLSession URLSession:*Challenge*]"
// 05 Frida scripts for common pinning implementations
Universal SecTrustEvaluate hook
This script intercepts the core Security framework function used by nearly all iOS pinning implementations and forces it to return success. It works for apps using the raw Security framework API directly, bypassing any wrapper library.
JAVASCRIPT (FRIDA)if (ObjC.available) {
// Hook SecTrustEvaluateWithError (iOS 12+)
const SecTrustEvaluateWithError = Module.findExportByName(
'Security', 'SecTrustEvaluateWithError'
);
if (SecTrustEvaluateWithError) {
Interceptor.replace(SecTrustEvaluateWithError,
new NativeCallback(function(trust, error) {
// Clear any error pointer and return true (trusted)
if (error.isNull() === false) {
error.writePointer(NULL);
}
return 1; // kSecTrustResultProceed
}, 'int', ['pointer', 'pointer'])
);
console.log('[+] SecTrustEvaluateWithError hooked');
}
// Hook legacy SecTrustEvaluate (pre iOS 12)
const SecTrustEvaluate = Module.findExportByName(
'Security', 'SecTrustEvaluate'
);
if (SecTrustEvaluate) {
Interceptor.replace(SecTrustEvaluate,
new NativeCallback(function(trust, result) {
result.writeU32(4); // kSecTrustResultProceed
return 0; // errSecSuccess
}, 'int', ['pointer', 'pointer'])
);
console.log('[+] SecTrustEvaluate hooked');
}
}
TrustKit bypass
TrustKit is a popular open-source pinning library that operates at a higher level - it intercepts NSURLSession challenges and compares the server certificate's SPKI hash against a bundled allowlist. Bypassing it requires hooking TrustKit's validation method, not the underlying Security framework:
JAVASCRIPT (FRIDA)if (ObjC.available) {
const TSKPinningValidator = ObjC.classes['TSKPinningValidator'];
if (TSKPinningValidator) {
// Hook the main validation method
Interceptor.attach(
TSKPinningValidator['- evaluateTrust:forHostname:'].implementation,
{
onLeave: function(retval) {
// TSKTrustDecision: 0=shouldBlockConnection, 1=shouldAllowConnection
retval.replace(ptr('0x1'));
console.log('[+] TrustKit validation bypassed');
}
}
);
} else {
console.log('[-] TrustKit not found in this app');
}
}
Network.framework (NWConnection) bypass
Apps using Apple's Network.framework directly set pinning in NWParameters via a sec_protocol_options_set_verify_block callback. This is the hardest pattern to bypass because it's set at connection-creation time using a closure, not a delegate method. The approach is to patch the verify block to always call the completion handler with true:
JAVASCRIPT (FRIDA)const sec_protocol_options_set_verify_block = Module.findExportByName(
'Security',
'sec_protocol_options_set_verify_block'
);
if (sec_protocol_options_set_verify_block) {
Interceptor.attach(sec_protocol_options_set_verify_block, {
onEnter: function(args) {
// args[1] is the verify_block (a Swift/ObjC closure)
// args[2] is the dispatch_queue
// Replace the verify block with one that always succeeds
const newBlock = new ObjC.Block({
retType: 'void',
argTypes: ['pointer', 'pointer', 'pointer'],
implementation: function(metadata, trust, completionHandler) {
// Invoke completion with true - always allow
const handler = new ObjC.Block(completionHandler);
handler.invoke(1); // bool true
}
});
args[1] = newBlock;
console.log('[+] NWConnection verify_block replaced');
}
});
}
// 06 SSL Kill Switch 3 - the tweak approach
On a fully jailbroken device with Sileo/Zebra, SSL Kill Switch 3 (available from the opa334 repo) installs as a system-wide tweak. Unlike Frida scripts that must be run per-session, SSL Kill Switch 3 hooks at the MobileSubstrate/libhooker layer and persists across app launches. It's the fastest workflow for testing multiple apps on the same device.
SHELL# Add repo and install (Sileo - rootless device)
# Repo: https://repo.opa334.me
# Package: SSLKillSwitch3
# After install, toggle via Settings → SSL Kill Switch 3
# Enable "Disable Certificate Validation" globally
# Confirm via Burp CA already in System Trust Store
# (export Burp CA → AirDrop → install via Settings → VPN & Device Mgmt)
# Limitation: does NOT bypass apps using Network.framework sec_protocol_options
# or apps with jailbreak detection that kill themselves before the tweak fires
// 07 KeychainItem extraction: what to do post-bypass
Once you're proxying traffic, pivot immediately to Keychain extraction. iOS applications store authentication tokens, refresh tokens, API keys, private certificates, and session cookies in the Keychain - often persisted across app reinstalls when the kSecAttrAccessibleAlways or kSecAttrAccessibleAfterFirstUnlock accessibility options are set. These artefacts are frequently more valuable than the intercepted traffic itself.
OBJECTION REPL# Dump all Keychain items for the current app
ios keychain dump
# Filter by type
ios keychain dump --json # output as JSON
ios keychain dump --show-protected # attempt to dump protected items
# Example output
{
"kSecAttrService": "com.example.BankingApp",
"kSecAttrAccount": "auth_token",
"kSecValueData": "eyJhbGciOiJSUzI1NiIsInR...", # live JWT
"kSecAttrAccessible": "kSecAttrAccessibleAfterFirstUnlock"
}
A Keychain item stored with kSecAttrAccessibleAlways or without a device-unlock requirement is typically a High severity finding - the token persists indefinitely and survives app reinstallation, meaning a physical device compromise or a backup extraction attack can recover valid credentials long after the user believes they've logged out.
// 08 Bypassing jailbreak detection when the app refuses to run
High-value targets - banking apps, healthcare apps, fintech platforms - frequently implement jailbreak detection that kills the app before your hooks can fire. The detection mechanisms typically check for: Cydia/Sileo existence, suspicious file paths (/var/jb, /usr/bin/frida-server), fork() success (only possible on jailbroken devices), dynamic library injection signatures, and process name enumeration.
The most reliable counter-technique in 2026 is Frida gadget injection with jailbreak check patching - no jailbreak, no Frida server process, nothing to detect:
SHELL# Step 1: Obtain decrypted IPA (sideloaded install, or use frida-ios-dump)
# Step 2: Inject FridaGadget.dylib into the main binary
pip install lief
python3 inject_gadget.py BankingApp.ipa FridaGadget.dylib
# Step 3: Re-sign with your Apple Developer certificate
codesign -f -s "iPhone Developer: You" \
--entitlements entitlements.plist \
BankingApp.app/BankingApp
# Step 4: Re-package and install via AltStore/Sideloadly
xcrun simctl install booted BankingApp.app
# Step 5: Attach Frida to the gadget
frida -U -n Gadget # or via Objection
objection --gadget Gadget explore
For applications that use iOS device attestation (App Attest / DeviceCheck), the gadget injection approach still allows you to hook SSL validation - it just won't bypass server-side integrity checks on the backend. In that case, you're testing what you can test and documenting the scope limitation.
// 09 iOS 17 / 18 considerations
Apple's continued hardening of the Secure Enclave and expanded use of pointer authentication codes (PAC) on A17 and M-series chips has made some memory-patching Frida techniques less reliable. The principal changes to be aware of in 2026 engagements:
- Pointer Authentication (PAC) on A17/M chips means function pointer patching requires authenticated pointers - use
Interceptor.attach(which handles PAC) rather thanMemory.patchCodefor reliability. - Lockdown Mode (optional user setting, iOS 16+) disables JIT, restricts Safari, and restricts USB device communication - Frida gadget injection still works, but device-attached Frida via USB may fail if Lockdown Mode is active.
- App Attest cryptographically proves the app has not been modified - any re-signed or patched binary will fail App Attest challenges. Document this as a scope limitation, not a bypass failure.
- The PrivateRelay iCloud relay intercepts traffic at the OS level and uses its own certificate chain - test with PrivateRelay disabled for the device under test.
// 10 What to report when pinning is bypassable
A successful pinning bypass is not itself a vulnerability to report - it's the prerequisite to testing. What you report depends on what you find in the decrypted traffic and the Keychain. However, the pinning implementation quality is worth noting in the report's security posture section. OWASP MASVS-NETWORK controls define three levels:
| MASVS Control | Requirement | Pass criteria in 2026 |
|---|---|---|
| MASVS-NETWORK-1 | TLS on all network communication | No HTTP, TLS 1.2+ minimum |
| MASVS-NETWORK-2 | TLS settings aligned with best practices | No TLS 1.0/1.1, no RC4/3DES, HSTS set |
| MASVS-RESILIENCE-4 | App implements certificate pinning | SPKI hash pinning, backup pins, not bypassable via sslpinning disable |
| MASVS-RESILIENCE-1 | Jailbreak detection present | Multiple layered checks - not a single flag check |
| MASVS-STORAGE-1 | Sensitive data not stored in Keychain with wrong accessibility | Auth tokens require user presence (kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly) |