Add Improper LDAP Authentication query (CWE-287)

This commit is contained in:
Maiky
2023-05-29 21:16:13 +02:00
parent 2d8318dc02
commit 62353122c0
12 changed files with 336 additions and 8 deletions

View File

@@ -212,8 +212,7 @@ module FileSystemWriteAccess {
* Extend this class to refine existing API models. If you want to model new APIs,
* extend `FileSystemPermissionModification::Range` instead.
*/
class FileSystemPermissionModification extends DataFlow::Node instanceof FileSystemPermissionModification::Range
{
class FileSystemPermissionModification extends DataFlow::Node instanceof FileSystemPermissionModification::Range {
/**
* Gets an argument to this permission modification that is interpreted as a
* set of permissions.
@@ -469,8 +468,7 @@ module Http {
}
}
private class RequestInputAccessAsRemoteFlowSource extends RemoteFlowSource::Range instanceof RequestInputAccess
{
private class RequestInputAccessAsRemoteFlowSource extends RemoteFlowSource::Range instanceof RequestInputAccess {
override string getSourceType() { result = this.(RequestInputAccess).getSourceType() }
}
@@ -959,8 +957,7 @@ module Path {
* Extend this class to refine existing API models. If you want to model new APIs,
* extend `CookieSecurityConfigurationSetting::Range` instead.
*/
class CookieSecurityConfigurationSetting extends DataFlow::Node instanceof CookieSecurityConfigurationSetting::Range
{
class CookieSecurityConfigurationSetting extends DataFlow::Node instanceof CookieSecurityConfigurationSetting::Range {
/**
* Gets a description of how this cookie setting may weaken application security.
* This predicate has no results if the setting is considered to be safe.
@@ -1040,8 +1037,7 @@ module Cryptography {
* Extend this class to refine existing API models. If you want to model new APIs,
* extend `CryptographicOperation::Range` instead.
*/
class CryptographicOperation extends SC::CryptographicOperation instanceof CryptographicOperation::Range
{
class CryptographicOperation extends SC::CryptographicOperation instanceof CryptographicOperation::Range {
/** DEPRECATED: Use `getAlgorithm().isWeak() or getBlockMode().isWeak()` instead */
deprecated predicate isWeak() { super.isWeak() }
}
@@ -1195,3 +1191,50 @@ module LdapExecution {
abstract DataFlow::Node getQuery();
}
}
/**
* A data-flow node that collects methods binding a LDAP connection.
*
* Extend this class to refine existing API models. If you want to model new APIs,
* extend `LdapBind::Range` instead.
*/
class LdapBind extends DataFlow::Node instanceof LdapBind::Range {
/** Gets the argument containing the binding host */
DataFlow::Node getHost() { result = super.getHost() }
/** Gets the argument containing the binding expression. */
DataFlow::Node getPassword() { result = super.getPassword() }
/** Holds if the binding process is anonymous. */
predicate isEmptyPassword() {
(
this.getPassword().getConstantValue().isStringlikeValue("")
or
this.getPassword().(DataFlow::ExprNode).getExprNode().getConstantValue().getValueType() =
"nil"
)
}
/** Holds if the binding process use SSL. */
predicate useSsl() { super.useSsl() }
}
/** Provides classes for modeling LDAP bind-related APIs. */
module LdapBind {
/**
* A data-flow node that collects methods binding a LDAP connection.
*
* Extend this class to model new APIs. If you want to refine existing API models,
* extend `LdapBind` instead.
*/
abstract class Range extends DataFlow::Node {
/** Gets the argument containing the binding host. */
abstract DataFlow::Node getHost();
/** Gets the argument containing the binding expression. */
abstract DataFlow::Node getPassword();
/** Holds if the binding process use SSL. */
abstract predicate useSsl();
}
}

View File

@@ -30,6 +30,24 @@ module NetLdap {
/** A call that establishes a LDAP Connection */
private class NetLdapConnection extends DataFlow::CallNode {
NetLdapConnection() { this in [ldap().getAnInstantiation(), ldap().getAMethodCall(["open"])] }
predicate useSsl() {
this.getKeywordArgument("encryption").getConstantValue().isStringlikeValue("simple_tls")
or
this.getAMethodCall("encryption")
.getArgument(0)
.getConstantValue()
.isStringlikeValue(":simple_tls")
or
this.getAMethodCall("encryption")
.getKeywordArgument("method")
.getConstantValue()
.isStringlikeValue("simple_tls")
}
DataFlow::Node getAuthValue(string arg) {
result = this.getKeywordArgument("auth").(DataFlow::CallNode).getKeywordArgument(arg)
}
}
/** A call that constructs a LDAP query */
@@ -45,4 +63,27 @@ module NetLdap {
override DataFlow::Node getQuery() { result = this.getKeywordArgument(_) }
}
/** A call considered as a LDAP bind. */
private class NetLdapBind extends LdapBind::Range, DataFlow::CallNode {
NetLdapConnection l;
NetLdapBind() { this = l.getAMethodCall("bind") }
override DataFlow::Node getHost() {
(
result = l.getKeywordArgument("encryption")
or
result = l.getAMethodCall("encryption").getArgument(0)
) and
result.getConstantValue().isStringlikeValue(":simple_tls")
}
override DataFlow::Node getPassword() {
result = l.getAuthValue("password") or
result = l.getAMethodCall("auth").getArgument(1)
}
override predicate useSsl() { l.useSsl() }
}
}

View File

@@ -0,0 +1,48 @@
/**
* Provides default sources, sinks and sanitizers for detecting
* improper LDAP authentication, as well as extension points for adding your own
*/
private import codeql.ruby.Concepts
private import codeql.ruby.DataFlow
private import codeql.ruby.dataflow.BarrierGuards
private import codeql.ruby.dataflow.RemoteFlowSources
/**
* Provides default sources, sinks and sanitizers for detecting
* improper LDAP authentication, as well as extension points for adding your own
*/
module ImproperLdapAuth {
/** A data flow source for improper LDAP authentication vulnerabilities */
abstract class Source extends DataFlow::Node { }
/** A data flow sink for improper LDAP authentication vulnerabilities */
abstract class Sink extends DataFlow::Node { }
/** A sanitizer for improper LDAP authentication vulnerabilities. */
abstract class Sanitizer extends DataFlow::Node { }
/**
* A source of remote user input, considered as a flow source.
*/
private class RemoteFlowSourceAsSource extends Source, RemoteFlowSource { }
/**
* An LDAP query execution considered as a flow sink.
*/
private class LdapBindAsSink extends Sink {
LdapBindAsSink() { this = any(LdapBind l).getPassword() }
}
/**
* A comparison with a constant string, considered as a sanitizer-guard.
*/
private class StringConstCompareAsSanitizerGuard extends Sanitizer, StringConstCompareBarrier { }
/**
* An inclusion check against an array of constant strings, considered as a
* sanitizer-guard.
*/
private class StringConstArrayInclusionCallAsSanitizer extends Sanitizer,
StringConstArrayInclusionCallBarrier { }
}

View File

@@ -0,0 +1,21 @@
/**
* Provides default sources, sinks and sanitizers for detecting
* improper LDAP authentication, as well as extension points for adding your own
*/
private import codeql.ruby.DataFlow
private import codeql.ruby.TaintTracking
private import ImproperLdapAuthCustomizations::ImproperLdapAuth
/**
* A taint-tracking configuration for detecting improper LDAP authentication vulnerabilities.
*/
class Configuration extends TaintTracking::Configuration {
Configuration() { this = "ImproperLdapAuth" }
override predicate isSource(DataFlow::Node source) { source instanceof Source }
override predicate isSink(DataFlow::Node sink) { sink instanceof Sink }
override predicate isSanitizer(DataFlow::Node node) { node instanceof Sanitizer }
}

View File

@@ -0,0 +1,4 @@
---
category: newQuery
---
* Added a new experimental query, `rb/improper-ldap-auth`, to detect cases where user input is used during LDAP authentication without proper validation or sanitization, potentially leading to authentication bypass.

View File

@@ -0,0 +1,39 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>
<overview>
<p>
If an LDAP connection uses user-supplied data as password, anonymous bind could be caused using an empty password
to result in a successful authentication.
</p>
</overview>
<recommendation>
<p>
Don't use user-supplied data as password while establishing an LDAP connection.
</p>
</recommendation>
<example>
<p>
In the following Rails example, an <code>ActionController</code> class
has a <code>ldap_handler</code> method to handle requests.
</p>
<p>
In the first example, the code builds a LDAP query whose authentication depends on user supplied data.
</p>
<sample src="examples/LdapAuthenticationBad.rb" />
<p>In the second example, the authentication is established using a default password.</p>
<sample src="examples/LdapAuthenticationGood.rb" />
</example>
<references>
<li>MITRE: <a href="https://cwe.mitre.org/data/definitions/287.html">CWE-287: Improper Authentication</a>.</li>
</references>
</qhelp>

View File

@@ -0,0 +1,20 @@
/**
* @name Improper LDAP Authentication
* @description A user-controlled query carries no authentication
* @kind path-problem
* @problem.severity warning
* @id rb/improper-ldap-auth
* @tags security
* experimental
* external/cwe/cwe-287
*/
import codeql.ruby.DataFlow
import codeql.ruby.security.ImproperLdapAuthQuery
import codeql.ruby.Concepts
import DataFlow::PathGraph
from Configuration config, DataFlow::PathNode source, DataFlow::PathNode sink
where config.hasFlowPath(source, sink)
select sink.getNode(), source, sink, "This LDAP authencation depends on a $@.", source.getNode(),
"user-provided value"

View File

@@ -0,0 +1,16 @@
class FooController < ActionController::Base
def some_request_handler
pass = params[:pass]
ldap = Net::LDAP.new(
host: 'ldap.example.com',
port: 636,
encryption: :simple_tls,
auth: {
method: :simple,
username: 'uid=admin,dc=example,dc=com',
password: pass
}
)
ldap.bind
end
end

View File

@@ -0,0 +1,16 @@
class FooController < ActionController::Base
def some_request_handler
pass = params[:pass]
ldap = Net::LDAP.new(
host: 'ldap.example.com',
port: 636,
encryption: :simple_tls,
auth: {
method: :simple,
username: 'uid=admin,dc=example,dc=com',
password: '$uper$password123'
}
)
ldap.bind
end
end

View File

@@ -0,0 +1,20 @@
edges
| ImproperLdapAuth.rb:5:5:5:8 | pass | ImproperLdapAuth.rb:15:23:15:26 | pass |
| ImproperLdapAuth.rb:5:12:5:17 | call to params | ImproperLdapAuth.rb:5:12:5:24 | ...[...] |
| ImproperLdapAuth.rb:5:12:5:24 | ...[...] | ImproperLdapAuth.rb:5:5:5:8 | pass |
| ImproperLdapAuth.rb:24:5:24:8 | pass | ImproperLdapAuth.rb:31:24:31:27 | pass |
| ImproperLdapAuth.rb:24:12:24:17 | call to params | ImproperLdapAuth.rb:24:12:24:24 | ...[...] |
| ImproperLdapAuth.rb:24:12:24:24 | ...[...] | ImproperLdapAuth.rb:24:5:24:8 | pass |
nodes
| ImproperLdapAuth.rb:5:5:5:8 | pass | semmle.label | pass |
| ImproperLdapAuth.rb:5:12:5:17 | call to params | semmle.label | call to params |
| ImproperLdapAuth.rb:5:12:5:24 | ...[...] | semmle.label | ...[...] |
| ImproperLdapAuth.rb:15:23:15:26 | pass | semmle.label | pass |
| ImproperLdapAuth.rb:24:5:24:8 | pass | semmle.label | pass |
| ImproperLdapAuth.rb:24:12:24:17 | call to params | semmle.label | call to params |
| ImproperLdapAuth.rb:24:12:24:24 | ...[...] | semmle.label | ...[...] |
| ImproperLdapAuth.rb:31:24:31:27 | pass | semmle.label | pass |
subpaths
#select
| ImproperLdapAuth.rb:15:23:15:26 | pass | ImproperLdapAuth.rb:5:12:5:17 | call to params | ImproperLdapAuth.rb:15:23:15:26 | pass | This LDAP authencation depends on a $@. | ImproperLdapAuth.rb:5:12:5:17 | call to params | user-provided value |
| ImproperLdapAuth.rb:31:24:31:27 | pass | ImproperLdapAuth.rb:24:12:24:17 | call to params | ImproperLdapAuth.rb:31:24:31:27 | pass | This LDAP authencation depends on a $@. | ImproperLdapAuth.rb:24:12:24:17 | call to params | user-provided value |

View File

@@ -0,0 +1 @@
experimental/ldap-improper-auth/ImproperLdapAuth.ql

View File

@@ -0,0 +1,59 @@
class FooController < ActionController::Base
def some_request_handler
# A string tainted by user input is used directly as password
# (i.e a remote flow source)
pass = params[:pass]
# BAD: user input is not sanetized
ldap = Net::LDAP.new(
host: 'ldap.example.com',
port: 636,
encryption: :simple_tls,
auth: {
method: :simple,
username: 'uid=admin,dc=example,dc=com',
password: pass
}
)
ldap.bind
end
def some_request_handler
# A string tainted by user input is used directly as password
# (i.e a remote flow source)
pass = params[:pass]
# BAD: user input is not sanetized
ldap = Net::LDAP.new
ldap.host = your_server_ip_address
ldap.encryption(:method => :simple_tls)
ldap.port = 639
ldap.auth "admin", pass
ldap.bind
end
end
class BarController < ApplicationController
def safe_paths
pass = params[:pass]
# GOOD: barrier guard prevents taint flow
if password.nil? || password.empty?
# protect against passwordless auth from ldap server
pass = "$uper$secure123"
else
pass
end
ldap = Net::LDAP.new(
host: 'ldap.example.com',
port: 636,
encryption: :simple_tls,
auth: {
method: :simple,
username: 'uid=admin,dc=example,dc=com',
password: pass
}
)
end
end