Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions ruby/ql/lib/codeql/ruby/Concepts.qll
Original file line number Diff line number Diff line change
Expand Up @@ -1184,3 +1184,69 @@ module TemplateRendering {
abstract DataFlow::Node getTemplate();
}
}

/**
* A data-flow node that constructs a LDAP query.
*
* Often, it is worthy of an alert if an LDAP query is constructed such that
* executing it would be a security risk.
*
* If it is important that the query is executed, use `LdapExecution`.
*
* Extend this class to refine existing API models. If you want to model new APIs,
* extend `LdapConstruction::Range` instead.
*/
class LdapConstruction extends DataFlow::Node instanceof LdapConstruction::Range {
/** Gets the argument that specifies the query to be constructed. */
DataFlow::Node getQuery() { result = super.getQuery() }
}

/** Provides a class for modeling new LDAP query construction APIs. */
module LdapConstruction {
/**
* A data-flow node that constructs a LDAP query.
*
* Often, it is worthy of an alert if an LDAP query is constructed such that
* executing it would be a security risk.
*
* If it is important that the query is executed, use `LdapExecution`.
*
* Extend this class to model new APIs. If you want to refine existing API models,
* extend `LdapConstruction` instead.
*/
abstract class Range extends DataFlow::Node {
/** Gets the argument that specifies the query to be constructed. */
abstract DataFlow::Node getQuery();
}
}

/**
* A data-flow node that executes LDAP queries.
*
* If the context of interest is such that merely constructing a LDAP query
* would be valuable to report, consider using `LdapConstruction`.
*
* Extend this class to refine existing API models. If you want to model new APIs,
* extend `LdapExecution::Range` instead.
*/
class LdapExecution extends DataFlow::Node instanceof LdapExecution::Range {
/** Gets the argument that specifies the query to be executed. */
DataFlow::Node getQuery() { result = super.getQuery() }
}

/** Provides a class for modeling new LDAP query execution APIs. */
module LdapExecution {
/**
* A data-flow node that executes LDAP queries.
*
* If the context of interest is such that merely constructing a LDAP query
* would be valuable to report, consider using `LdapConstruction`.
*
* Extend this class to model new APIs. If you want to refine existing API models,
* extend `LdapExecution` instead.
*/
abstract class Range extends DataFlow::Node {
/** Gets the argument that specifies the query to be executed. */
abstract DataFlow::Node getQuery();
}
}
1 change: 1 addition & 0 deletions ruby/ql/lib/codeql/ruby/Frameworks.qll
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,4 @@ private import codeql.ruby.frameworks.Mysql2
private import codeql.ruby.frameworks.Pg
private import codeql.ruby.frameworks.Yaml
private import codeql.ruby.frameworks.Sequel
private import codeql.ruby.frameworks.Ldap
70 changes: 70 additions & 0 deletions ruby/ql/lib/codeql/ruby/frameworks/Ldap.qll
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/**
* Provides modeling for `net-ldap` a ruby library for LDAP.
*/

private import ruby
private import codeql.ruby.ApiGraphs
private import codeql.ruby.dataflow.FlowSummary
private import codeql.ruby.Concepts

/**
* Provides modeling for `net-ldap` a ruby library for LDAP.
*/
module NetLdap {
/**
* Flow summary for `Net::LDAP.new`. This method establishes a connection to a LDAP server.
*/
private class LdapConnSummary extends SummarizedCallable {
LdapConnSummary() { this = "Net::LDAP.new" }

override MethodCall getACall() { result = any(NetLdapConnection l).asExpr().getExpr() }

override predicate propagatesFlowExt(string input, string output, boolean preservesValue) {
input = "Argument[0]" and output = "ReturnValue" and preservesValue = false
}
}

/**
* Flow summary for `Net::LDAP.Filter`.
*/
private class LdapFilterSummary extends SummarizedCallable {
LdapFilterSummary() { this = "Net::LDAP::Filter" }

override MethodCall getACall() { result = any(NetLdapFilter l).asExpr().getExpr() }

override predicate propagatesFlowExt(string input, string output, boolean preservesValue) {
input = ["Argument[0]", "Argument[1]"] and output = "ReturnValue" and preservesValue = false
}
}

/** Net LDAP Api Node */
private API::Node ldap() { result = API::getTopLevelMember("Net").getMember("LDAP") }

/** A call that establishes a LDAP Connection */
private class NetLdapConnection extends DataFlow::CallNode {
NetLdapConnection() { this in [ldap().getAnInstantiation(), ldap().getAMethodCall("open")] }
}

/** A call that constructs a LDAP query */
private class NetLdapFilter extends LdapConstruction::Range, DataFlow::CallNode {
NetLdapFilter() {
this =
any(ldap()
.getMember("Filter")
.getAMethodCall([
"begins", "bineq", "contains", "ends", "eq", "equals", "ex", "ge", "le", "ne",
"present"
])
)
}

override DataFlow::Node getQuery() { result = this.getArgument([0, 1]) }
}

/** A call considered as a LDAP execution. */
private class NetLdapExecution extends LdapExecution::Range, DataFlow::CallNode {
NetLdapExecution() { this = any(NetLdapConnection l).getAMethodCall("search") }

override DataFlow::Node getQuery() { result = this.getKeywordArgument(_) }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/**
* Provides default sources, sinks and sanitizers for detecting
* LDAP Injections, 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
private import codeql.ruby.ApiGraphs

/**
* Provides default sources, sinks and sanitizers for detecting
* LDAP Injections, as well as extension points for adding your own
*/
module LdapInjection {
/** A data flow source for LDAP Injection vulnerabilities */
abstract class Source extends DataFlow::Node { }

/** A data flow sink for LDAP Injection vulnerabilities */
abstract class Sink extends DataFlow::Node { }

/** A sanitizer for LDAP Injection vulnerabilities. */
abstract class Sanitizer extends DataFlow::Node { }

/**
* Additional taint steps for "LDAP Injection" vulnerabilities.
*/
predicate isAdditionalFlowStep(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) {
attributeArrayTaintStep(nodeFrom, nodeTo)
}

/**
* Additional taint step to handle elements inside an array,
* specifically in the context of the following LDAP search function:
*
* ldap.search(base: "", filter: "", attributes: [name])
*/
private predicate attributeArrayTaintStep(DataFlow::Node n1, DataFlow::Node n2) {
exists(DataFlow::CallNode filterCall |
filterCall.getMethodName() = "[]" and
n1 = filterCall.getArgument(_) and
n2 = filterCall
)
}

/**
* 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 LdapExecutionAsSink extends Sink {
LdapExecutionAsSink() { this = any(LdapExecution l).getQuery() }
}

/**
* 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
{ }

/**
* A call to `Net::LDAP::Filter.escape`, considered as a sanitizer.
*/
class NetLdapFilterEscapeSanitization extends Sanitizer {
NetLdapFilterEscapeSanitization() {
this =
API::getTopLevelMember("Net").getMember("LDAP").getMember("Filter").getAMethodCall("escape")
}
}
}
29 changes: 29 additions & 0 deletions ruby/ql/lib/codeql/ruby/security/LdapInjectionQuery.qll
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* Provides default sources, sinks and sanitizers for detecting
* LDAP Injections, as well as extension points for adding your own
*/

private import codeql.ruby.DataFlow
private import codeql.ruby.TaintTracking

/** Provides a taint-tracking configuration for detecting LDAP Injections vulnerabilities. */
module LdapInjection {
import LdapInjectionCustomizations::LdapInjection

/**
* A taint-tracking configuration for detecting LDAP Injections vulnerabilities.
*/
private module Config implements DataFlow::ConfigSig {
predicate isSource(DataFlow::Node source) { source instanceof Source }

predicate isSink(DataFlow::Node sink) { sink instanceof Sink }

predicate isBarrier(DataFlow::Node node) { node instanceof Sanitizer }

predicate isAdditionalFlowStep(DataFlow::Node node1, DataFlow::Node node2) {
LdapInjection::isAdditionalFlowStep(node1, node2)
}
}

import TaintTracking::Global<Config>
}
4 changes: 4 additions & 0 deletions ruby/ql/src/change-notes/2023-05-28-ldap-injection-query.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
category: newQuery
---
* Added a new experimental query, `rb/ldap-injection`, to detect cases where user input is incorporated into LDAP queries without proper validation or sanitization, potentially leading to LDAP injection vulnerabilities.
53 changes: 53 additions & 0 deletions ruby/ql/src/experimental/ldap-injection/LdapInjection.qhelp
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>

<overview>
<p>
If an LDAP query or DN is built using string concatenation or string formatting, and the
components of the concatenation include user input without any proper sanitization, a user
is likely to be able to run malicious LDAP queries.
</p>
</overview>

<recommendation>
<p>
If user input must be included in an LDAP query or DN, it should be escaped to
avoid a malicious user providing special characters that change the meaning
of the query.
</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>
The user and dc is specified using a parameter, <code>user_name</code> and <code>dc</code> provided by
the client which it then uses to build a LDAP query and DN. This value is accessible using the <code>params</code> method.
</p>

<p>The first example uses the unsanitized user input directly
in the search filter and DN for the LDAP query.
A malicious user could provide special characters to change the meaning of these
components, and search for a completely different set of values.</p>

<sample src="examples/LdapInjectionBad.rb" />

<p>In the second example, the input provided by the user is sanitized before it is included in the search filter or DN.
This ensures the meaning of the query cannot be changed by a malicious user.</p>

<sample src="examples/LdapInjectionGood.rb" />
</example>

<references>
<li>OWASP: <a href="https://cheatsheetseries.owasp.org/cheatsheets/LDAP_Injection_Prevention_Cheat_Sheet.html">LDAP Injection Prevention Cheat Sheet</a>.</li>
<li>OWASP: <a href="https://owasp.org/www-community/attacks/LDAP_Injection">LDAP Injection</a>.</li>
<li>Wikipedia: <a href="https://en.wikipedia.org/wiki/LDAP_injection">LDAP injection</a>.</li>
<li>BlackHat: <a href="https://www.blackhat.com/presentations/bh-europe-08/Alonso-Parada/Whitepaper/bh-eu-08-alonso-parada-WP.pdf">LDAP Injection and Blind LDAP Injection</a>.</li>
<li>LDAP: <a href="https://ldap.com/2018/05/04/understanding-and-defending-against-ldap-injection-attacks/">Understanding and Defending Against LDAP Injection Attacks</a>.</li>
</references>
</qhelp>
21 changes: 21 additions & 0 deletions ruby/ql/src/experimental/ldap-injection/LdapInjection.ql
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* @name LDAP Injection
* @description Building an LDAP query from user-controlled sources is vulnerable to insertion of
* malicious LDAP code by the user.
* @kind path-problem
* @problem.severity error
* @security-severity 9.8
* @precision high
* @id rb/ldap-injection
* @tags security
* external/cwe/cwe-090
*/

import codeql.ruby.DataFlow
import codeql.ruby.security.LdapInjectionQuery
import LdapInjection::PathGraph

from LdapInjection::PathNode source, LdapInjection::PathNode sink
where LdapInjection::flowPath(source, sink)
select sink.getNode(), source, sink, "This LDAP query depends on a $@.", source.getNode(),
"user-provided value"
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
require 'net/ldap'

class BadLdapController < ActionController::Base
def ldap_handler
name = params[:user_name]
dc = params[:dc]
ldap = Net::LDAP.new(
host: 'ldap.example.com',
port: 636,
encryption: :simple_tls,
auth: {
method: :simple,
username: 'uid=admin,dc=example,dc=com',
password: 'adminpassword'
}
)
filter = Net::LDAP::Filter.eq('foo', name)
attrs = [name]
result = ldap.search(base: "ou=people,dc=#{dc},dc=com", filter: filter, attributes: attrs)
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
require 'net/ldap'

class GoodLdapController < ActionController::Base
def ldap_handler
name = params[:user_name]
ldap = Net::LDAP.new(
host: 'ldap.example.com',
port: 636,
encryption: :simple_tls,
auth: {
method: :simple,
username: 'uid=admin,dc=example,dc=com',
password: 'adminpassword'
}
)

name = if ["admin", "guest"].include? name
name
else
name = "none"
end

filter = Net::LDAP::Filter.eq('foo', name)
attrs = ['dn']
result = ldap.search(base: 'ou=people,dc=example,dc=com', filter: filter, attributes: attrs)
end
end
Loading