<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Yehor Mamaiev</title>
    <description>The latest articles on DEV Community by Yehor Mamaiev (@yehor-mamaiev).</description>
    <link>https://dev.to/yehor-mamaiev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3919896%2Fa43fd2fe-efa1-4e0e-96ec-3ad82d820f89.png</url>
      <title>DEV Community: Yehor Mamaiev</title>
      <link>https://dev.to/yehor-mamaiev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yehor-mamaiev"/>
    <language>en</language>
    <item>
      <title>I Found SSL Validation Completely Disabled in an Open-Source Financial App — Here's How It Works</title>
      <dc:creator>Yehor Mamaiev</dc:creator>
      <pubDate>Tue, 26 May 2026 09:27:37 +0000</pubDate>
      <link>https://dev.to/yehor-mamaiev/i-found-ssl-validation-completely-disabled-in-an-open-source-financial-app-heres-how-it-works-28bc</link>
      <guid>https://dev.to/yehor-mamaiev/i-found-ssl-validation-completely-disabled-in-an-open-source-financial-app-heres-how-it-works-28bc</guid>
      <description>&lt;h2&gt;
  
  
  "It's open source — someone must have reviewed the security."
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;During one of these assessments, I found something that stopped me mid-scroll: a method literally called &lt;code&gt;sslTrustAllCertificates()&lt;/code&gt;. In a financial app. One that handles real money.&lt;/p&gt;

&lt;p&gt;Let me show you exactly what this vulnerability looks like, why it's dangerous, and how to fix it.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Found
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;But instead of using Android's standard TLS certificate validation, the developer created a custom &lt;code&gt;X509TrustManager&lt;/code&gt; that does... nothing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;X509TrustManager&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;checkClientTrusted&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;X509Certificate&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;authType&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Empty — accepts any client certificate&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;checkServerTrusted&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;X509Certificate&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;authType&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Empty — accepts ANY server certificate without validation&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;X509Certificate&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="nf"&gt;getAcceptedIssuers&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;X509Certificate&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;];&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;checkServerTrusted()&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;Here, it's completely empty. Every certificate is accepted. Self-signed, expired, revoked, belonging to a different domain — all silently trusted.&lt;/p&gt;




&lt;h2&gt;
  
  
  How the Vulnerable Code Works
&lt;/h2&gt;

&lt;p&gt;The trust-all TrustManager is stored as a static field — meaning it exists for the entire lifetime of the app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;X509TrustManager&lt;/span&gt; &lt;span class="no"&gt;TRUST_ALL_CERTIFICATES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;X509TrustManager&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="o"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then it's used to create an &lt;code&gt;SSLSocketFactory&lt;/code&gt; that produces sockets with no certificate validation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;SSLSocketFactory&lt;/span&gt; &lt;span class="nf"&gt;sslTrustAllCertificates&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;SSLContext&lt;/span&gt; &lt;span class="n"&gt;sslContext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SSLContext&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getInstance&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SSL"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;sslContext&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;init&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;                                          &lt;span class="c1"&gt;// No KeyManagers&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TrustManager&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="no"&gt;TRUST_ALL_CERTIFICATES&lt;/span&gt; &lt;span class="o"&gt;},&lt;/span&gt; &lt;span class="c1"&gt;// Trust-all manager&lt;/span&gt;
        &lt;span class="kc"&gt;null&lt;/span&gt;                                           &lt;span class="c1"&gt;// No SecureRandom&lt;/span&gt;
    &lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;sslContext&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getSocketFactory&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice another problem: &lt;code&gt;SSLContext.getInstance("SSL")&lt;/code&gt; instead of &lt;code&gt;"TLS"&lt;/code&gt;. The &lt;code&gt;"SSL"&lt;/code&gt; identifier refers to the deprecated SSLv3 protocol, which is vulnerable to the POODLE attack. Modern apps should always use &lt;code&gt;"TLSv1.2"&lt;/code&gt; or &lt;code&gt;"TLSv1.3"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This factory is then used every time the app connects to a server to query financial data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;Socket&lt;/span&gt; &lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Server&lt;/span&gt; &lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;throws&lt;/span&gt; &lt;span class="nc"&gt;IOException&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nc"&gt;Server&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;TLS&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;SSLSocketFactory&lt;/span&gt; &lt;span class="n"&gt;factory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sslTrustAllCertificates&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="nc"&gt;Socket&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;factory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createSocket&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;socketAddress&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getHostName&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;socketAddress&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getPort&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="c1"&gt;// Connection established — with zero certificate validation&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The Developer's Attempted Mitigation
&lt;/h2&gt;

&lt;p&gt;To be fair, the developer didn't completely ignore the problem. After establishing the TLS connection, the code performs a secondary check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// After connection is already established...&lt;/span&gt;
&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;actualFingerprint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sha256Fingerprint&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;peerCertificates&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;]);&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;certificateFingerprint&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;actualFingerprint&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;equals&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;certificateFingerprint&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;SSLPeerUnverifiedException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Fingerprint mismatch"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// No stored fingerprint — fall back to hostname verification only&lt;/span&gt;
    &lt;span class="nc"&gt;HostnameVerifier&lt;/span&gt; &lt;span class="n"&gt;verifier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;HttpsURLConnection&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getDefaultHostnameVerifier&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;verifier&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;verify&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;hostname&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sslSession&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;SSLPeerUnverifiedException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Hostname mismatch"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This looks reasonable at first glance. But it has three critical flaws:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. The TLS handshake already completed.&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The fingerprint is optional.&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Hostname verification without chain validation is meaningless.&lt;/strong&gt; The &lt;code&gt;HostnameVerifier&lt;/code&gt; 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.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Attack Scenario
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fku33izfz9406thgnnhpg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fku33izfz9406thgnnhpg.png" alt="The Attack Scenario" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prerequisites:&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What makes this especially dangerous:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;This app handles real money — not social media posts or game scores&lt;/li&gt;
&lt;li&gt;The vulnerable code is in the module responsible for initiating financial transfers&lt;/li&gt;
&lt;li&gt;Transactions in this domain are irreversible — if a user is tricked into acting on false balance data, the money is gone&lt;/li&gt;
&lt;li&gt;The custom SSL sockets bypass Android's Network Security Configuration entirely — the app's own &lt;code&gt;network_security_config.xml&lt;/code&gt; is irrelevant for these connections&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Why This Happens
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;The intent behind &lt;code&gt;sslTrustAllCertificates()&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;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."&lt;/p&gt;




&lt;h2&gt;
  
  
  The Correct Fix
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Option 1: Custom TrustManager with fingerprint validation during handshake
&lt;/h3&gt;

&lt;p&gt;Move the fingerprint check into the TrustManager itself, so it runs during the TLS handshake — not after:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FingerprintTrustManager&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;X509TrustManager&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;expectedFingerprint&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;FingerprintTrustManager&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;fingerprint&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;expectedFingerprint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fingerprint&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;checkServerTrusted&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;X509Certificate&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;authType&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; 
            &lt;span class="kd"&gt;throws&lt;/span&gt; &lt;span class="nc"&gt;CertificateException&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chain&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;length&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CertificateException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Empty certificate chain"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// Check basic validity (expiration)&lt;/span&gt;
        &lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;].&lt;/span&gt;&lt;span class="na"&gt;checkValidity&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

        &lt;span class="c1"&gt;// Verify fingerprint if we have one&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expectedFingerprint&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sha256Fingerprint&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;]);&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;equals&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expectedFingerprint&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CertificateException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
                    &lt;span class="s"&gt;"Certificate fingerprint mismatch. Expected: "&lt;/span&gt; 
                    &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;expectedFingerprint&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;", got: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;actual&lt;/span&gt;
                &lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// No fingerprint — require a valid CA-signed certificate&lt;/span&gt;
            &lt;span class="nc"&gt;TrustManagerFactory&lt;/span&gt; &lt;span class="n"&gt;tmf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;TrustManagerFactory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getInstance&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
                &lt;span class="nc"&gt;TrustManagerFactory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getDefaultAlgorithm&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;tmf&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;init&lt;/span&gt;&lt;span class="o"&gt;((&lt;/span&gt;&lt;span class="nc"&gt;KeyStore&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;TrustManager&lt;/span&gt; &lt;span class="n"&gt;tm&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tmf&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getTrustManagers&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tm&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nc"&gt;X509TrustManager&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                    &lt;span class="o"&gt;((&lt;/span&gt;&lt;span class="nc"&gt;X509TrustManager&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="n"&gt;tm&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;checkServerTrusted&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;authType&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
                    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
                &lt;span class="o"&gt;}&lt;/span&gt;
            &lt;span class="o"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;CertificateException&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"No system TrustManager available"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// ... other methods&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Option 2: Network Security Configuration with certificate pinning
&lt;/h3&gt;

&lt;p&gt;For apps that know their server endpoints in advance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;network-security-config&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;domain-config&lt;/span&gt; &lt;span class="na"&gt;cleartextTrafficPermitted=&lt;/span&gt;&lt;span class="s"&gt;"false"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;domain&lt;/span&gt; &lt;span class="na"&gt;includeSubdomains=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;server.example.com&lt;span class="nt"&gt;&amp;lt;/domain&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;pin-set&lt;/span&gt; &lt;span class="na"&gt;expiration=&lt;/span&gt;&lt;span class="s"&gt;"2027-01-01"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;pin&lt;/span&gt; &lt;span class="na"&gt;digest=&lt;/span&gt;&lt;span class="s"&gt;"SHA-256"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;base64EncodedPinHere=&lt;span class="nt"&gt;&amp;lt;/pin&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;pin&lt;/span&gt; &lt;span class="na"&gt;digest=&lt;/span&gt;&lt;span class="s"&gt;"SHA-256"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;backupPinHere=&lt;span class="nt"&gt;&amp;lt;/pin&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/pin-set&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/domain-config&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/network-security-config&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Option 3: OkHttp CertificatePinner
&lt;/h3&gt;

&lt;p&gt;If you're already using OkHttp:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;CertificatePinner&lt;/span&gt; &lt;span class="n"&gt;pinner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CertificatePinner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"server.example.com"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"sha256/base64EncodedPinHere="&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

&lt;span class="nc"&gt;OkHttpClient&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OkHttpClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Builder&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;certificatePinner&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pinner&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  How to Check If Your App Is Vulnerable
&lt;/h2&gt;

&lt;p&gt;Search your codebase for these patterns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Trust-all TrustManager&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;"checkServerTrusted"&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.java"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nb"&gt;test
grep&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;"X509TrustManager"&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.java"&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;"TrustAllCertificates&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;trustAll&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;TRUST_ALL"&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.java"&lt;/span&gt;

&lt;span class="c"&gt;# Deprecated SSL protocol&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'getInstance("SSL")'&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.java"&lt;/span&gt;

&lt;span class="c"&gt;# In smali (decompiled APKs)&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;"sslTrustAll&lt;/span&gt;&lt;span class="se"&gt;\|&lt;/span&gt;&lt;span class="s2"&gt;trustAllCert"&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.smali"&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;"checkServerTrusted"&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.smali"&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;checkServerTrusted&lt;/code&gt; has an empty body (just &lt;code&gt;return&lt;/code&gt; or &lt;code&gt;return-void&lt;/code&gt; in smali) — you have this vulnerability.&lt;/p&gt;




&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;An empty &lt;code&gt;checkServerTrusted()&lt;/code&gt; is equivalent to no TLS at all.&lt;/strong&gt; The encryption still happens, but without authentication — an attacker can decrypt everything.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Post-handshake validation doesn't work.&lt;/strong&gt; Certificate checks must happen during the TLS handshake, inside the TrustManager. Moving them to application code is fundamentally broken.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;This pattern exists because developers face real problems.&lt;/strong&gt; Self-signed certificates, community-run infrastructure, certificate rotation — these are legitimate challenges. But &lt;code&gt;trustAllCertificates()&lt;/code&gt; is never the right solution.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Open source means the code is available for review — not that it has been reviewed.&lt;/strong&gt; This vulnerability exists in a well-maintained, actively developed financial app with a large user base.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Automated scanners find the pattern. Manual verification confirms the impact.&lt;/strong&gt; My scanner flagged &lt;code&gt;sslTrustAllCertificates()&lt;/code&gt; — but understanding that this is in the money-transfer flow of a financial app required reading the code.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;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: &lt;a href="mailto:yehor.mamaiev@gmail.com"&gt;yehor.mamaiev@gmail.com&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>android</category>
      <category>security</category>
      <category>opensource</category>
      <category>mobile</category>
    </item>
    <item>
      <title>I Scanned 10 Popular F-Droid Apps With My Security Scanner — Open Source Secure</title>
      <dc:creator>Yehor Mamaiev</dc:creator>
      <pubDate>Sun, 24 May 2026 15:14:10 +0000</pubDate>
      <link>https://dev.to/yehor-mamaiev/i-scanned-10-popular-f-droid-apps-with-my-security-scanner-open-source-secure-1l99</link>
      <guid>https://dev.to/yehor-mamaiev/i-scanned-10-popular-f-droid-apps-with-my-security-scanner-open-source-secure-1l99</guid>
      <description>&lt;p&gt;"It's open source, so it's secure."&lt;/p&gt;

&lt;p&gt;I hear this all the time. The idea is simple: if the code is public, someone must have reviewed it. Vulnerabilities would be caught. The community would fix them.&lt;/p&gt;

&lt;p&gt;I decided to test this assumption with real data.&lt;/p&gt;

&lt;p&gt;I took 10 popular open-source Android apps from F-Droid — apps that millions of people use every day — and ran them through my static analysis security scanner. Then I manually verified every single finding against the decompiled APK code.&lt;/p&gt;

&lt;p&gt;The result: &lt;strong&gt;60 confirmed vulnerabilities across 10 apps.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Apps
&lt;/h2&gt;

&lt;p&gt;I intentionally picked well-known, actively maintained apps across different categories:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AntennaPod&lt;/strong&gt; — Podcast manager&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bitcoin Wallet&lt;/strong&gt; — Cryptocurrency wallet&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DAVx5&lt;/strong&gt; — CalDAV/CardDAV sync&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GnuCash Android&lt;/strong&gt; — Financial accounting&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;K-9 Mail&lt;/strong&gt; — Email client&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;KeePassDX&lt;/strong&gt; — Password manager&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NewPipe&lt;/strong&gt; — YouTube frontend&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nextcloud&lt;/strong&gt; — Cloud storage client&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Signal&lt;/strong&gt; — Encrypted messenger&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VLC&lt;/strong&gt; — Media player&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These aren't abandoned side projects. They're mature, popular apps with active developer communities. Some of them handle extremely sensitive data — passwords, cryptocurrency, private messages, financial records.&lt;/p&gt;




&lt;h2&gt;
  
  
  Methodology
&lt;/h2&gt;

&lt;p&gt;For each app I:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Downloaded the APK&lt;/strong&gt; from F-Droid&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ran my static analysis scanner&lt;/strong&gt; (automated SAST covering manifest analysis, smali code analysis, native library inspection, taint analysis, and cryptographic checks)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decompiled the APK&lt;/strong&gt; using apktool to get the smali code, manifest, and resources&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Manually verified every finding&lt;/strong&gt; against the actual decompiled code — checking whether the flagged code was truly vulnerable or a false positive&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This last step is critical. Automated scanners generate a lot of candidates. Without manual verification, you don't know what's real and what's noise.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Found: The 5 Most Common Vulnerabilities
&lt;/h2&gt;

&lt;p&gt;After manually verifying all findings across 10 apps, clear patterns emerged. The same mistakes appeared again and again — regardless of the app's popularity or the sensitivity of the data it handles.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Exported Components Without Permission Guards
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Found in: 9 out of 10 apps&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Android components (activities, services, receivers, content providers) that are declared as &lt;code&gt;exported="true"&lt;/code&gt; without requiring any permission to interact with them. This means any app installed on the same device can launch these components, send them data, or receive data from them.&lt;/p&gt;

&lt;p&gt;What makes this dangerous: in a financial app, this could mean a malicious app can launch the "send money" screen. In a password manager, it could trigger the database unlock flow. In an email client, it could launch the compose screen with pre-filled data.&lt;/p&gt;

&lt;p&gt;The fix is straightforward — either set &lt;code&gt;exported="false"&lt;/code&gt; for components that don't need external access, or add an &lt;code&gt;android:permission&lt;/code&gt; attribute to restrict who can interact with them.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Overly Broad FileProvider Paths
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Found in: 4 out of 10 apps&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Android's &lt;code&gt;FileProvider&lt;/code&gt; is designed to securely share files between apps. But when the provider's path configuration includes &lt;code&gt;&amp;lt;root-path path="." /&amp;gt;&lt;/code&gt; or &lt;code&gt;&amp;lt;external-path path="." /&amp;gt;&lt;/code&gt;, it effectively grants access to the entire filesystem or all of external storage through content URIs.&lt;/p&gt;

&lt;p&gt;I found apps where the FileProvider configuration would allow access to &lt;code&gt;/storage/&lt;/code&gt; (all external storage), the entire app internal directory, and in the worst case, the entire device filesystem.&lt;/p&gt;

&lt;p&gt;Even though these providers are typically non-exported, they use &lt;code&gt;grantUriPermissions="true"&lt;/code&gt; — so if any exported activity forwards URI permissions (a common Android pattern), the broad paths become exploitable.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Cleartext Traffic Permitted + User CA Trust
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Found in: 6 out of 10 apps&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Many apps had &lt;code&gt;cleartextTrafficPermitted="true"&lt;/code&gt; in their network security config, allowing unencrypted HTTP connections. Several also explicitly trusted user-installed CA certificates with &lt;code&gt;&amp;lt;certificates src="user" /&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For apps connecting to self-hosted servers (CalDAV, email, cloud storage), this is partially intentional — users need to connect to their own servers, which may use HTTP or self-signed certificates. But the configuration applies globally, not just to self-hosted endpoints. Every connection the app makes — including API calls, analytics, update checks — defaults to allowing interception.&lt;/p&gt;

&lt;p&gt;One particularly concerning case: a media player downloaded update files over plaintext HTTP. A man-in-the-middle attacker on the same network could serve a malicious APK as a legitimate update.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Weak Cryptographic Implementations
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Found in: 3 out of 10 apps&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;These ranged from the dangerous to the archaic:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SSL validation completely disabled&lt;/strong&gt; — one app called a method that literally accepts ALL certificates without any validation. In a financial app. Handling real money.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ECB cipher mode&lt;/strong&gt; — a media player's remote access feature used AES in ECB mode, where identical plaintext blocks produce identical ciphertext blocks. This is a textbook crypto mistake.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;java.util.Random&lt;/code&gt; in cryptographic context&lt;/strong&gt; — instead of &lt;code&gt;SecureRandom&lt;/code&gt;, making the output predictable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trust-On-First-Use (TOFU) without pinning&lt;/strong&gt; — once a server certificate is accepted, it's trusted forever, even if the certificate changes (which could indicate a MITM attack).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5. Missing Tapjacking (Overlay Attack) Protection
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Found in: 8 out of 10 apps&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Almost none of the apps set &lt;code&gt;filterTouchesWhenObscured="true"&lt;/code&gt; on sensitive UI elements. This means a malicious app with overlay permission could draw an invisible layer on top of the app and capture user taps — or trick users into tapping buttons they didn't intend to.&lt;/p&gt;

&lt;p&gt;This is especially concerning for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Password managers (capturing master password input)&lt;/li&gt;
&lt;li&gt;Cryptocurrency wallets (confirming transactions)&lt;/li&gt;
&lt;li&gt;Certificate trust dialogs (accepting malicious certificates)&lt;/li&gt;
&lt;li&gt;OAuth/login screens (authorizing access)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What This Means
&lt;/h2&gt;

&lt;p&gt;Open source doesn't automatically mean secure. It means the code is &lt;em&gt;available&lt;/em&gt; for review — not that it &lt;em&gt;has been&lt;/em&gt; reviewed. These are popular, well-maintained apps with active communities, and they still have security issues that a systematic review can catch.&lt;/p&gt;

&lt;p&gt;This isn't about shaming developers. Open-source maintainers are often volunteers doing incredible work. The point is that security requires dedicated, focused attention. It's a different skill set from building features, and it benefits from dedicated tooling and expertise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If your Android app handles sensitive data — financial information, credentials, personal communications, health data — it deserves a security review.&lt;/strong&gt; Not just an automated scan that generates hundreds of candidates and calls it a day, but a proper assessment where every finding is verified and contextualized.&lt;/p&gt;




&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Automated scanners find candidates, not confirmed vulnerabilities.&lt;/strong&gt; Without manual verification, you don't know what's real.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The same 5 vulnerability patterns appear across almost every app.&lt;/strong&gt; Exported components, broad file providers, cleartext traffic, weak crypto, and missing overlay protection. These are low-hanging fruit that any security review should catch.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Context matters enormously.&lt;/strong&gt; Cleartext traffic in a media player streaming public videos is a different risk than cleartext traffic in a banking app. A security finding without context is just a line in a report.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Open source enables security review — it doesn't replace it.&lt;/strong&gt; The code being public is a prerequisite for thorough analysis, not a guarantee that the analysis has happened.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;I'm a mobile security researcher specializing in Android application security assessments. If you'd like to discuss your app's security posture, feel free to reach out at &lt;a href="mailto:yehor.mamaiev@gmail.com"&gt;yehor.mamaiev@gmail.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>android</category>
      <category>security</category>
      <category>opensource</category>
      <category>mobile</category>
    </item>
  </channel>
</rss>
