|
| 1 | +/** |
| 2 | + * @name OpenSSLMissingCNCheck |
| 3 | + * @kind path-problem |
| 4 | + * @problem.severity recommendation |
| 5 | + * @id cpp/openssl-missing-verify-callback |
| 6 | + * @tags security |
| 7 | + * external/cwe/cwe-273 |
| 8 | + */ |
| 9 | + |
| 10 | +/* |
| 11 | + * The default `verify_callback` https://www.openssl.org/docs/man1.1.1/man3/SSL_CTX_set_verify.html |
| 12 | + * does not compare CN/hostnames so a TLS client can be MITMed. |
| 13 | + * |
| 14 | + * There are multiple stategies for verifying server certificates so this query assists with code |
| 15 | + * review but does not accurately find vulnerabilities, false positives are likely. |
| 16 | + * |
| 17 | + * False negatives include using verify_callback to ignore verification, such as always returning |
| 18 | + * a 1 without checking `preverify_ok`. |
| 19 | + */ |
| 20 | + |
| 21 | +import cpp |
| 22 | +import semmle.code.cpp.dataflow.TaintTracking |
| 23 | +import OpenSSLVerify |
| 24 | + |
| 25 | +class SslSet1HostFunctionCall extends FunctionCall { |
| 26 | + SslSet1HostFunctionCall() { this.getTarget().hasName("SSL_set1_host") } |
| 27 | +} |
| 28 | + |
| 29 | +class SslLikeCheckHostnameFunctionCall extends FunctionCall { |
| 30 | + SslLikeCheckHostnameFunctionCall() { |
| 31 | + this instanceof SslSet1HostFunctionCall or |
| 32 | + this instanceof SslCtxSetCertVerifyCallbackFunctionCall |
| 33 | + } |
| 34 | +} |
| 35 | + |
| 36 | +class SslPointerVariable extends Variable { |
| 37 | + SslPointerVariable() { |
| 38 | + this.hasDefinition() and this.getUnderlyingType().(PointerType).getBaseType().hasName("SSL") |
| 39 | + } |
| 40 | +} |
| 41 | + |
| 42 | +class SslCtxPointerVariable extends Variable { |
| 43 | + SslCtxPointerVariable() { |
| 44 | + this.hasDefinition() and this.getUnderlyingType().(PointerType).getBaseType().hasName("SSL_CTX") |
| 45 | + } |
| 46 | +} |
| 47 | + |
| 48 | +class SslLikePointerVariable extends Variable { |
| 49 | + SslLikePointerVariable() { |
| 50 | + this instanceof SslPointerVariable or |
| 51 | + this instanceof SslCtxPointerVariable |
| 52 | + } |
| 53 | +} |
| 54 | + |
| 55 | +class SslCtxNewClientFunctionCall extends FunctionCall { |
| 56 | + SslCtxNewClientFunctionCall() { |
| 57 | + this.getTarget().hasName("SSL_CTX_new") and |
| 58 | + exists(FunctionCall fc | |
| 59 | + fc.getTarget().getQualifiedName().matches("%_client_method") and |
| 60 | + TaintTracking::localTaint(DataFlow::exprNode(fc), DataFlow::exprNode(this.getArgument(0))) |
| 61 | + ) |
| 62 | + } |
| 63 | +} |
| 64 | + |
| 65 | +predicate sslPointerTaintStep(DataFlow::Node pred, DataFlow::Node succ) { |
| 66 | + // Propagate the taint from SSL <-> SSL_CTX. |
| 67 | + exists(SslLikePointerVariable ctx, FunctionCall fc | |
| 68 | + pred.asExpr() = ctx.getAnAccess() and |
| 69 | + pred.asExpr() = fc.getArgument(0) and |
| 70 | + succ.asExpr() = fc and |
| 71 | + // Simple to go back and forth. |
| 72 | + (fc.getTarget().hasName("SSL_new") or fc.getTarget().hasName("SSL_get_SSL_CTX")) |
| 73 | + ) |
| 74 | +} |
| 75 | + |
| 76 | +class SslCtxSetVerifyConfiguration extends TaintTracking::Configuration { |
| 77 | + SslCtxSetVerifyConfiguration() { this = "SslCtxSetVerifyConfiguration" } |
| 78 | + |
| 79 | + override predicate isSource(DataFlow::Node node) { |
| 80 | + node.asExpr() instanceof SslCtxNewClientFunctionCall |
| 81 | + } |
| 82 | + |
| 83 | + override predicate isSink(DataFlow::Node node) { |
| 84 | + // False positives can be reduced by checking that argument(1) is not VERIFY_NONE. |
| 85 | + // This may lead to broader false negatives where code mistakenly sets VERIFY_NONE. |
| 86 | + exists(SslLikeSetVerifyFunctionCall fc | node.asExpr() = fc.getArgument(0)) |
| 87 | + } |
| 88 | + |
| 89 | + override predicate isAdditionalTaintStep(DataFlow::Node pred, DataFlow::Node succ) { |
| 90 | + sslPointerTaintStep(pred, succ) |
| 91 | + } |
| 92 | +} |
| 93 | + |
| 94 | +class SslCtxCheckHostnameConfiguration extends TaintTracking::Configuration { |
| 95 | + SslCtxCheckHostnameConfiguration() { this = "SslCtxCheckHostnameConfiguration" } |
| 96 | + |
| 97 | + override predicate isSource(DataFlow::Node node) { |
| 98 | + node.asExpr() instanceof SslCtxNewClientFunctionCall |
| 99 | + } |
| 100 | + |
| 101 | + override predicate isSink(DataFlow::Node node) { |
| 102 | + exists(SslLikeCheckHostnameFunctionCall fc | node.asExpr() = fc.getArgument(0)) |
| 103 | + } |
| 104 | + |
| 105 | + override predicate isAdditionalTaintStep(DataFlow::Node pred, DataFlow::Node succ) { |
| 106 | + sslPointerTaintStep(pred, succ) |
| 107 | + } |
| 108 | +} |
| 109 | + |
| 110 | +predicate flowsToSetVerify(FunctionCall ctx, FunctionCall fc) { |
| 111 | + exists(SslCtxSetVerifyConfiguration conf | |
| 112 | + conf.hasFlow(DataFlow::exprNode(ctx), DataFlow::exprNode(fc.getArgument(0))) |
| 113 | + ) |
| 114 | +} |
| 115 | + |
| 116 | +predicate flowsToCheckHostname(FunctionCall ctx) { |
| 117 | + exists(SslCtxCheckHostnameConfiguration conf, FunctionCall fc | |
| 118 | + conf.hasFlow(DataFlow::exprNode(ctx), DataFlow::exprNode(fc.getArgument(0))) |
| 119 | + ) |
| 120 | +} |
| 121 | + |
| 122 | +from FunctionCall ctx, FunctionCall fc |
| 123 | +where |
| 124 | + flowsToSetVerify(ctx, fc) and |
| 125 | + not flowsToCheckHostname(ctx) and |
| 126 | + // Empty verify callback (this will not check CN). |
| 127 | + // False positives include intentional skipped verification and any |
| 128 | + // additional custom checking via accessing the peer certificate post-handshake. |
| 129 | + fc.getArgument(2).getValue() = "0" |
| 130 | +select ctx, ctx, fc, |
| 131 | + "may be a client context without hostname verification because SSL_set1_host " + |
| 132 | + "and other indicators of leaf cert hostname checking are missing" |
0 commit comments