"It's open source — someone must have reviewed the security."
That's what most people assume. I assumed it too, until I started scanning open-source Android apps with my security scanner and manually verifying every finding against the decompiled source code.
During one of these assessments, I found something that stopped me mid-scroll: a method literally called sslTrustAllCertificates(). In a financial app. One that handles real money.
Let me show you exactly what this vulnerability looks like, why it's dangerous, and how to fix it.
What I Found
The app connects to remote servers over TLS to query financial data — account balances, transaction history, unspent funds. This is the kind of data that absolutely must be encrypted and authenticated in transit.
But instead of using Android's standard TLS certificate validation, the developer created a custom X509TrustManager that does... nothing:
new X509TrustManager() {
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) {
// Empty — accepts any client certificate
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) {
// Empty — accepts ANY server certificate without validation
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
}
The checkServerTrusted() method is the one that matters here. It's called during the TLS handshake to verify that the server's certificate is legitimate — signed by a trusted CA, not expired, not revoked, and matching the expected hostname.
Here, it's completely empty. Every certificate is accepted. Self-signed, expired, revoked, belonging to a different domain — all silently trusted.
How the Vulnerable Code Works
The trust-all TrustManager is stored as a static field — meaning it exists for the entire lifetime of the app:
private static final X509TrustManager TRUST_ALL_CERTIFICATES = new X509TrustManager() { ... };
Then it's used to create an SSLSocketFactory that produces sockets with no certificate validation:
private SSLSocketFactory sslTrustAllCertificates() {
SSLContext sslContext = SSLContext.getInstance("SSL");
sslContext.init(
null, // No KeyManagers
new TrustManager[] { TRUST_ALL_CERTIFICATES }, // Trust-all manager
null // No SecureRandom
);
return sslContext.getSocketFactory();
}
Notice another problem: SSLContext.getInstance("SSL") instead of "TLS". The "SSL" identifier refers to the deprecated SSLv3 protocol, which is vulnerable to the POODLE attack. Modern apps should always use "TLSv1.2" or "TLSv1.3".
This factory is then used every time the app connects to a server to query financial data:
private Socket connect(Server server) throws IOException {
if (server.type == Server.Type.TLS) {
SSLSocketFactory factory = sslTrustAllCertificates();
Socket socket = factory.createSocket(
server.socketAddress.getHostName(),
server.socketAddress.getPort()
);
// Connection established — with zero certificate validation
}
}
The Developer's Attempted Mitigation
To be fair, the developer didn't completely ignore the problem. After establishing the TLS connection, the code performs a secondary check:
// After connection is already established...
String actualFingerprint = sha256Fingerprint(peerCertificates[0]);
if (server.certificateFingerprint != null) {
if (!actualFingerprint.equals(server.certificateFingerprint)) {
throw new SSLPeerUnverifiedException("Fingerprint mismatch");
}
} else {
// No stored fingerprint — fall back to hostname verification only
HostnameVerifier verifier = HttpsURLConnection.getDefaultHostnameVerifier();
if (!verifier.verify(server.hostname, sslSession)) {
throw new SSLPeerUnverifiedException("Hostname mismatch");
}
}
This looks reasonable at first glance. But it has three critical flaws:
1. The TLS handshake already completed. By the time the fingerprint check runs, the connection is established. The attacker's certificate was already accepted during the handshake. Data could have already been transmitted.
2. The fingerprint is optional. The server list format includes the fingerprint as an optional field. For servers without a stored fingerprint, the only check is hostname verification — which an attacker can trivially pass by generating a self-signed certificate with the matching Common Name or Subject Alternative Name.
3. Hostname verification without chain validation is meaningless. The HostnameVerifier checks that the certificate's CN/SAN matches the hostname, but it doesn't verify that the certificate is signed by a trusted CA. With the trust-all TrustManager, an attacker can create a certificate that says anything — and it will be accepted.
The Attack Scenario
Prerequisites: The attacker only needs to be on the same network as the victim. Coffee shop WiFi, hotel network, airport — anywhere with shared network infrastructure. Tools like mitmproxy or Burp Suite handle the interception automatically.
What makes this especially dangerous:
- This app handles real money — not social media posts or game scores
- The vulnerable code is in the module responsible for initiating financial transfers
- Transactions in this domain are irreversible — if a user is tricked into acting on false balance data, the money is gone
- The custom SSL sockets bypass Android's Network Security Configuration entirely — the app's own
network_security_config.xmlis irrelevant for these connections
Why This Happens
This isn't lazy development. The developer faced a real problem: the servers this app connects to are community-run. They frequently use self-signed certificates. Those certificates change regularly. Standard TLS validation would cause constant connection failures, making the app unusable.
The intent behind sslTrustAllCertificates() was to work around this reality. The fingerprint check was supposed to be the real validation layer. But moving validation out of the TLS handshake into application-level code fundamentally breaks the security model.
This is a pattern I see repeatedly in security assessments: developers solving a real operational problem in a way that creates a security vulnerability. It's never malice — it's the tension between "make it work" and "make it secure."
The Correct Fix
Option 1: Custom TrustManager with fingerprint validation during handshake
Move the fingerprint check into the TrustManager itself, so it runs during the TLS handshake — not after:
public class FingerprintTrustManager implements X509TrustManager {
private final String expectedFingerprint;
public FingerprintTrustManager(String fingerprint) {
this.expectedFingerprint = fingerprint;
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
if (chain == null || chain.length == 0) {
throw new CertificateException("Empty certificate chain");
}
// Check basic validity (expiration)
chain[0].checkValidity();
// Verify fingerprint if we have one
if (expectedFingerprint != null) {
String actual = sha256Fingerprint(chain[0]);
if (!actual.equals(expectedFingerprint)) {
throw new CertificateException(
"Certificate fingerprint mismatch. Expected: "
+ expectedFingerprint + ", got: " + actual
);
}
} else {
// No fingerprint — require a valid CA-signed certificate
TrustManagerFactory tmf = TrustManagerFactory.getInstance(
TrustManagerFactory.getDefaultAlgorithm()
);
tmf.init((KeyStore) null);
for (TrustManager tm : tmf.getTrustManagers()) {
if (tm instanceof X509TrustManager) {
((X509TrustManager) tm).checkServerTrusted(chain, authType);
return;
}
}
throw new CertificateException("No system TrustManager available");
}
}
// ... other methods
}
Option 2: Network Security Configuration with certificate pinning
For apps that know their server endpoints in advance:
<network-security-config>
<domain-config cleartextTrafficPermitted="false">
<domain includeSubdomains="true">server.example.com</domain>
<pin-set expiration="2027-01-01">
<pin digest="SHA-256">base64EncodedPinHere=</pin>
<pin digest="SHA-256">backupPinHere=</pin>
</pin-set>
</domain-config>
</network-security-config>
Option 3: OkHttp CertificatePinner
If you're already using OkHttp:
CertificatePinner pinner = new CertificatePinner.Builder()
.add("server.example.com", "sha256/base64EncodedPinHere=")
.build();
OkHttpClient client = new OkHttpClient.Builder()
.certificatePinner(pinner)
.build();
How to Check If Your App Is Vulnerable
Search your codebase for these patterns:
# Trust-all TrustManager
grep -r "checkServerTrusted" --include="*.java" | grep -v test
grep -r "X509TrustManager" --include="*.java"
grep -r "TrustAllCertificates\|trustAll\|TRUST_ALL" --include="*.java"
# Deprecated SSL protocol
grep -r 'getInstance("SSL")' --include="*.java"
# In smali (decompiled APKs)
grep -r "sslTrustAll\|trustAllCert" --include="*.smali"
grep -r "checkServerTrusted" --include="*.smali" -l
If checkServerTrusted has an empty body (just return or return-void in smali) — you have this vulnerability.
Key Takeaways
An empty
checkServerTrusted()is equivalent to no TLS at all. The encryption still happens, but without authentication — an attacker can decrypt everything.Post-handshake validation doesn't work. Certificate checks must happen during the TLS handshake, inside the TrustManager. Moving them to application code is fundamentally broken.
This pattern exists because developers face real problems. Self-signed certificates, community-run infrastructure, certificate rotation — these are legitimate challenges. But
trustAllCertificates()is never the right solution.Open source means the code is available for review — not that it has been reviewed. This vulnerability exists in a well-maintained, actively developed financial app with a large user base.
Automated scanners find the pattern. Manual verification confirms the impact. My scanner flagged
sslTrustAllCertificates()— but understanding that this is in the money-transfer flow of a financial app required reading the code.
I'm a mobile security researcher specializing in Android application security. If your app handles sensitive data and you want to know where you stand — reach out: yehor.mamaiev@gmail.com














