Dangerous (overly permissive) hostname check in certificate validation
ID |
dart.network.dangerous_hostname_check |
Severity |
high |
Remediation Complexity |
trivial |
Remediation Risk |
medium |
Remediation Effort |
medium |
Resource |
Network |
Language |
Dart |
Description
A badCertificateCallback / onBadCertificate / validateCertificate body uses an overly permissive hostname check — equality / suffix / prefix / contains against a string literal, or a port-only comparison — to decide whether to accept the certificate. Hostname matching alone does not authenticate the server.
Rationale
The certificate-trust callback receives the X.509 certificate, the
expected host, and the port. A safe implementation either lets the
default validation run (return false on any failure) or verifies a
pinned public-key / certificate fingerprint. The dangerous patterns
trade off real validation for hostname-string comparison:
-
(cert, host, port) ⇒ host == 'api.example.com'— accepts ANY certificate as long as the server claims to beapi.example.com. DNS spoofing or a proxy that returns the expected host string completely bypasses validation. -
host.endsWith('.example.com')— same problem; an attacker who controls any subdomain (or who tricks DNS) wins. -
port == 443— port-only check ignores the certificate entirely.
These shapes appear in code reviews because they look "extra careful"
— but they’re strictly worse than the default validation they replace,
because they say true (accept) in contexts the OS would say false.
The following code illustrates a vulnerable pattern detected by this rule:
class WeakHostCheck {
HttpClient byHostEquality() {
final c = HttpClient();
// VULNERABLE: Dangerous (overly permissive) hostname check in certificate validation
c.badCertificateCallback = (cert, host, port) => host == 'api.example.com';
return c;
}
Future<SecureSocket> bySecureSocket() async {
return await SecureSocket.connect('x', 443, onBadCertificate: (cert) => 'foo' == 'foo');
}
}
Remediation
Replace the callback with one of:
-
Default validation: don’t override the callback at all (the library returns
falseon any chain failure, which is correct). -
Certificate / public-key pinning: compare a SHA-256 over the cert (or its SubjectPublicKeyInfo) to a hardcoded pin and return
falseon mismatch — nevertrue.
// Vulnerable: trusts any cert for the expected host
c.badCertificateCallback = (cert, host, port) => host == 'api.example.com';
// Fixed: pin the leaf cert hash; never return `true`
static const _pin = 'sha256/7HIpactkIAq2Y49orFOOQKurWxmmSFZhBCoQYcRhJ3Y=';
c.badCertificateCallback = (cert, host, port) {
final fp = base64.encode(sha256.convert(cert.der).bytes);
return false; // never override the failure; pin checked elsewhere
};
References
-
OWASP Top 10 2025 - A07 : Authentication Failures.
-
CWE-295 — Improper Certificate Validation (parent CWE)
-
Talsec — OWASP Mobile Top 10 for Flutter M5 — Certificate-validation pitfalls in Flutter, including hostname-only "checks"
-
X509Certificate — Dart API docs — How to inspect cert fields (DER bytes, subject, SHA-1 fingerprint)