/**
 * @name LDAP query built from user-controlled sources
 * @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
 * @id py/ldap-injection
 * @tags experimental
 *       security
 *       external/cwe/cwe-090
 */

import python
import experimental.semmle.python.security.injection.LDAP
import DataFlow::PathGraph

from LDAPInjectionFlowConfig config, DataFlow::PathNode source, DataFlow::PathNode sink
where config.hasFlowPath(source, sink)
select sink.getNode(), source, sink, "$@ LDAP query parameter comes from $@.", sink.getNode(),
  "This", source.getNode(), "a user-provided value"

// a concept

module LDAPEscape {
  abstract class Range extends DataFlow::Node {
    abstract DataFlow::Node getAnInput();
  }
}

class LDAPEscape extends DataFlow::Node {
  LDAPEscape::Range range;

  LDAPEscape() { this = range }

  DataFlow::Node getAnInput() { result = range.getAnInput() }
}

// a library modeling

private module LDAP2 {
  private class LDAP2QueryMethods extends string {
    LDAP2QueryMethods() {
      this in ["search", "search_s", "search_st", "search_ext", "search_ext_s"]
    }
  }

  private class LDAP2Query extends DataFlow::CallCfgNode, LDAPQuery::Range {
    DataFlow::Node ldapQuery;

    LDAP2Query() {
      exists(DataFlow::AttrRead searchMethod |
        this.getFunction() = searchMethod and
        API::moduleImport("ldap").getMember("initialize").getACall() =
          searchMethod.getObject().getALocalSource() and
        searchMethod.getAttributeName() instanceof LDAP2QueryMethods and
        (
          ldapQuery = this.getArg(0)
          or
          (
            ldapQuery = this.getArg(2) or
            ldapQuery = this.getArgByName("filterstr")
          )
        )
      )
    }

    override DataFlow::Node getQuery() { result = ldapQuery }
  }

  private class LDAP2EscapeDNCall extends DataFlow::CallCfgNode, LDAPEscape::Range {
    LDAP2EscapeDNCall() {
      this = API::moduleImport("ldap").getMember("dn").getMember("escape_dn_chars").getACall()
    }

    override DataFlow::Node getAnInput() { result = this.getArg(0) }
  }

  private class LDAP2EscapeFilterCall extends DataFlow::CallCfgNode, LDAPEscape::Range {
    LDAP2EscapeFilterCall() {
      this =
        API::moduleImport("ldap").getMember("filter").getMember("escape_filter_chars").getACall()
    }

    override DataFlow::Node getAnInput() { result = this.getArg(0) }
  }
}

// a taint flow config

class LDAPInjectionFlowConfig extends TaintTracking::Configuration {
  LDAPInjectionFlowConfig() { this = "LDAPInjectionFlowConfig" }

  override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }

  override predicate isSink(DataFlow::Node sink) { sink = any(LDAPQuery ldapQuery).getQuery() }

  override predicate isSanitizer(DataFlow::Node sanitizer) {
    sanitizer = any(LDAPEscape ldapEsc).getAnInput()
  }
}

// From https://github.com/github/codeql/pull/5443/files
