Answers & Feedback - GitHub Security Lab CTF 4: CodeQL and chill - The Java edition

This page includes a reference solution written by CTF reviewers during the contest. There are many correct ways to solve this challenge; this is one approach.

Note that several participants' solutions and their associated community feedback are also available in our forum.

Complete reference answer

/**
 * @name Insecure Bean validation
 * @description User-controlled data may be evaluated as Java EL expressions, leading to arbitrary code execution.
 * @kind path-problem
 * @problem.severity error
 * @precision high
 * @id java/ctf/unsafe-eval
 * @tags security
 *       external/cwe/cwe-094
 */

import java
import semmle.code.java.dataflow.TaintTracking
import semmle.code.java.dataflow.FlowSources
import DataFlow::PathGraph

// only for Step 1.4
// import DataFlow::PartialPathGraph
/* Step 1.1 */
/** The generic interface `javax.validation.ConstraintValidator`. */
class TypeConstraintValidator extends Interface {
  TypeConstraintValidator() { this.hasQualifiedName("javax.validation", "ConstraintValidator") }

  /** Gets a method named `isValid` declared in this interface. */
  Method getIsValidMethod() {
    result.getDeclaringType() = this and
    result.hasName("isValid")
  }
}

/**
 * An implementation of the method named `isValid` from the
 * interface `javax.validation.ConstraintValidator`.
 */
class ConstraintValidatorIsValidMethod extends Method {
  ConstraintValidatorIsValidMethod() {
    this.overridesOrInstantiates*(any(TypeConstraintValidator t).getIsValidMethod())
  }
}

/**
 * The first parameter of a method that implements `ConstraintValidator.isValid`,
 * where the method is used to validate a field whose value comes from user input.
 */
class BeanValidationSource extends DataFlow::Node {
  BeanValidationSource() {
    exists(ConstraintValidatorIsValidMethod isValidMethod |
      // This source is the first parameter of the `isValid` method
      this.asParameter() = isValidMethod.getParameter(0) and
      // which must be present in the source code
      isValidMethod.fromSource() and
      // Bonus step: and must be used to validate user-controlled data
      Bonus::validatesUserControlledBeanProperty(isValidMethod, _, _, _)
    )
  }
}

/* Logic used for the bonus task in step 1.1. */
module Bonus {
  /**
   * Holds if `isValidMethod` is declared on the constraint validator `validatorType`
   * and is used to validate the field/property `validatedField`,
   * whose value may be obtained from the user-controlled `remoteInput`.
   */
  predicate validatesUserControlledBeanProperty(
    ConstraintValidatorIsValidMethod isValidMethod, Field validatedField, RefType validatorType,
    RemoteFlowSource remoteInput
  ) {
    // This `isValid` method is used to validate a field, or the field's class.
    validatorType = isValidMethod.getDeclaringType() and
    (
      validatedConstraint(validatedField, _, _, validatorType) or
      validatedConstraint(validatedField.getDeclaringType(), _, _, validatorType)
    ) and
    // The value of the field is obtained from user input.
    any(UserInputToValidatedFieldConfig config)
        .hasFlow(remoteInput, DataFlow::exprNode(validatedField.getAnAssignedValue()))
  }

  /** The interface `javax.validation.Constraint`. */
  class TypeConstraint extends Interface {
    TypeConstraint() { this.hasQualifiedName("javax.validation", "Constraint") }
  }

  /** A `@Constraint` annotation. */
  class ConstraintAnnotation extends Annotation {
    ConstraintAnnotation() { this.getType() instanceof TypeConstraint }

    /** Holds if this constraint is validated by the class `validatorType`. */
    predicate isValidatedBy(RefType validatorType) {
      validatorType =
        this.getValue("validatedBy").(ArrayInit).getAnInit().(TypeLiteral).getTypeName().getType()
    }
  }

  /**
   * Holds if `validatedElement` is annotated with a validation constraint defined by `constraintType`,
   * which in turn is annotated with `constraintAnnotation` and validated by `validatorType`.
   */
  predicate validatedConstraint(
    Annotatable validatedElement, RefType constraintType, ConstraintAnnotation constraintAnnotation,
    RefType validatorType
  ) {
    validatedElement.getAnAnnotation().getType() = constraintType and
    constraintType.getAnAnnotation() = constraintAnnotation and
    constraintAnnotation.isValidatedBy(validatorType)
  }

  // This 
  import semmle.code.java.dataflow.TaintTracking2

  /**
   * Taint tracking configuration describing the flow of data
   * from user input to a field.
   * This is used in the bonus part of step 1.1, to detect user-controlled bean properties.
   * 
   * This analysis will be used to compute the sources of the original taint tracking configuration,
   * so we use a second copy of the taint tracking library to avoid compiler warnings due to
   * recursion through a negation.
   */
  class UserInputToValidatedFieldConfig extends TaintTracking2::Configuration {
    UserInputToValidatedFieldConfig() { this = "UserInputToValidatedFieldConfig" }

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

    override predicate isSink(DataFlow2::Node sink) {
      sink.asExpr() = any(Field field).getAnAssignedValue()
    }
  }

  /**
   * Tracks the flow of data from a protobuf message to a corresponding Java object,
   * created using a method called `toCore<Name>`.
   * This additional step is used to track from remote input parameters
   * to bean validation of their values.
   */
  class ProtoToCoreTaintStep extends TaintTracking::AdditionalTaintStep {
    override predicate step(DataFlow::Node n1, DataFlow::Node n2) {
      exists(MethodAccess ma |
        ma.getMethod().getName().matches("toCore%") and
        n2.asExpr() = ma and
        n1.asExpr() = ma.getArgument(0)
      )
    }
  }

  /**
   * Tracks the flow of data through utility methods in the class `CollectionsExt`.
   * This additional step is used to track from remote input parameters
   * to bean validation of their values.
   */
  class CollectionsExtTaintStep extends TaintTracking::AdditionalTaintStep {
    override predicate step(DataFlow::Node n1, DataFlow::Node n2) {
      exists(MethodAccess ma |
        ma.getMethod().getName() = ["nullableImmutableCopyOf", "nonNull"] and
        ma.getMethod().getDeclaringType().getName() = "CollectionsExt" and
        n2.asExpr() = ma and
        n1.asExpr() = ma.getArgument(0)
      )
    }
  }
}

/* Step 1.2 */
/** The type `javax.validation.ConstraintValidatorContext`. */
class TypeConstraintValidatorContext extends RefType {
  TypeConstraintValidatorContext() {
    this.hasQualifiedName("javax.validation", "ConstraintValidatorContext")
  }
}

/**
 * A method named `buildConstraintViolationWithTemplate` declared on a subtype
 * of `javax.validation.ConstraintValidatorContext`.
 */
class BuildConstraintViolationWithTemplateMethod extends Method {
  BuildConstraintViolationWithTemplateMethod() {
    this.getDeclaringType().getASupertype*() instanceof TypeConstraintValidatorContext and
    this.hasName("buildConstraintViolationWithTemplate")
  }
}

/* Step 1.3 */
class TaintConfig extends TaintTracking::Configuration {
  TaintConfig() { this = "TaintConfig" }

  /* Step 1.1 */
  override predicate isSource(DataFlow::Node source) { source instanceof BeanValidationSource }

  /* Step 1.2 */
  override predicate isSink(DataFlow::Node sink) {
    exists(MethodAccess ma |
      ma.getMethod() instanceof BuildConstraintViolationWithTemplateMethod and
      sink.asExpr() = ma.getArgument(0)
    )
  }

  /* step 2 */
  override int explorationLimit() { result = 4 }
}

/* Step 1.5, 1.6 */
/** Tracks the flow of taint through getter calls: `x -> x.get*(...)`. */
class GetterTaintStep extends TaintTracking::AdditionalTaintStep {
  override predicate step(DataFlow::Node n1, DataFlow::Node n2) {
    exists(MethodAccess ma |
      (
        // Two alternatives for identifying getters.
        ma.getMethod() instanceof GetterMethod or
        ma.getMethod().getName().matches("get%")
      ) and
      n1.asExpr() = ma.getQualifier() and
      n2.asExpr() = ma
    )
  }
}

/* Step 1.6 */
import semmle.code.java.Maps

/** A call to the method `keySet()` declared on a subtype of `java.util.Map`. */
class MapKeySetCall extends MethodAccess {
  MapKeySetCall() {
    this.getMethod().(MapMethod).getName() = "keySet"
    // Alternative, without using the `Maps` library:
    // this.getMethod().getDeclaringType().getSourceDeclaration().hasQualifiedName("java.util", "Map")
  }
}

/** Tracks the flow of taint from `x` to `x.keySet()`. */
class KeySetTaintStep extends TaintTracking::AdditionalTaintStep {
  override predicate step(DataFlow::Node n1, DataFlow::Node n2) {
    exists(MapKeySetCall call |
      n1.asExpr() = call.getQualifier() and
      n2.asExpr() = call
    )
  }
}

/* Step 1.7 */
/** A call to the constructor `java.util.HashSet<>(...)`. */
class HashSetConstructorCall extends Call {
  HashSetConstructorCall() {
    this
        .(ConstructorCall)
        .getConstructedType()
        .getSourceDeclaration()
        .hasQualifiedName("java.util", "HashSet")
  }
}

/** Tracks the flow of taint from `x` to `new HashSet(x)`. */
class HashSetTaintStep extends TaintTracking::AdditionalTaintStep {
  override predicate step(DataFlow::Node n1, DataFlow::Node n2) {
    exists(HashSetConstructorCall call |
      n1.asExpr() = call.getAnArgument() and
      n2.asExpr() = call
    )
  }
}

/* Step 1.8: no longer necessary in recent versions of the libraries */
import semmle.code.java.Collections

/** A call to the method `retainAll` declared in a collection type. */
class CollectionRetainAllCall extends MethodAccess {
  CollectionRetainAllCall() {
    this.getMethod().(CollectionMethod).getName() = "retainAll"
    // Alternative, without using the `Collections` library:
    // this.getMethod().getDeclaringType().getASourceSupertype+().hasQualifiedName("java.util", "Collection")
  }
}

/**
 * Tracks the flow of taint from `x` to `y.retainAll(x)`,
 * where `x` and `y` are both collections.
 */
class CollectionRetainAllTaintStep extends TaintTracking::AdditionalTaintStep {
  override predicate step(DataFlow::Node n1, DataFlow::Node n2) {
    exists(CollectionRetainAllCall ma |
      n1.asExpr() = ma.getAnArgument() and
      n2.asExpr() = ma.getQualifier()
    )
  }
}

/* Step 2 */
/** A call to the method `stream` declared in a collection type. */
class CollectionStreamCall extends MethodAccess {
  CollectionStreamCall() { this.getMethod().(CollectionMethod).getName() = "stream" }
}

/** Track taint from `x` to `x.stream()` where `x` is a collection. */
class CollectionStreamTaintStep extends TaintTracking::AdditionalTaintStep {
  override predicate step(DataFlow::Node n1, DataFlow::Node n2) {
    exists(CollectionStreamCall call |
      n1.asExpr() = call.getQualifier() and
      n2.asExpr() = call
    )
  }
}

/** The interface `java.util.stream.Stream`. */
class TypeStream extends Interface {
  TypeStream() { this.hasQualifiedName("java.util.stream", "Stream") }
}

/** A method declared in a stream type, that is, a subtype of `java.util.stream.Stream`. */
class StreamMethod extends Method {
  StreamMethod() { this.getDeclaringType().getASourceSupertype+() instanceof TypeStream }
}

/** A call to the method `map` declared in a stream type. */
class StreamMapCall extends MethodAccess {
  StreamMapCall() { this.getMethod().(StreamMethod).getName() = "map" }
}

/** Track taint from `stream` to `stream.map(lambda)`. */
class StreamMapTaintStep extends TaintTracking::AdditionalTaintStep {
  override predicate step(DataFlow::Node n1, DataFlow::Node n2) {
    exists(StreamMapCall call |
      n1.asExpr() = call.getQualifier() and
      n2.asExpr() = call
    )
  }
}

/** A call to the method `collect` declared in a stream type. */
class StreamCollectCall extends MethodAccess {
  StreamCollectCall() { this.getMethod().(StreamMethod).getName() = "collect" }
}

/** Track taint from `stream` to `stream.collect()`. */
class StreamCollectTaintStep extends TaintTracking::AdditionalTaintStep {
  override predicate step(DataFlow::Node n1, DataFlow::Node n2) {
    exists(StreamCollectCall call |
      n1.asExpr() = call.getQualifier() and
      n2.asExpr() = call
    )
  }
}

/* Step 3 */
/**
 * Track the flow of taint from the argument of a call to an exception message that may be thrown by that call.
 * For example, from `tainted` to `e.getMessage()` in:
 * ```java
 * try {
 *   call(tainted);
 * } catch(Exception e) {
 *   ... e.getMessage()
 * }
 * ```
 */
class ExceptionTaintStep extends TaintTracking::AdditionalTaintStep {
  override predicate step(DataFlow::Node n1, DataFlow::Node n2) {
    exists(Call call, TryStmt try, CatchClause catch, MethodAccess getMessageCall |
      // the call is within the `try` block, which has a corresponding `catch` clause
      call.getEnclosingStmt().getEnclosingStmt*() = try.getBlock() and
      try.getACatchClause() = catch and
      // the `catch` clause is likely to catch an exception thrown by the call
      (
        catch.getACaughtType().getASupertype*() = call.getCallee().getAThrownExceptionType() or
        catch.getACaughtType().getASupertype*() instanceof TypeRuntimeException
      ) and
      // the exception message is read by `getMessageCall` within the `catch` block
      catch.getVariable().getAnAccess() = getMessageCall.getQualifier() and
      getMessageCall.getMethod().getName().regexpMatch("get(Localized)?Message|toString") and
      // taint flows from any argument of the call to a place where the exception message is accessed
      n1.asExpr() = call.getAnArgument() and
      n2.asExpr() = getMessageCall
    )
  }
}

/* Step 1.3, 1.8, 2, 3 */
from TaintConfig cfg, DataFlow::PathNode source, DataFlow::PathNode sink
where cfg.hasFlowPath(source, sink)
select sink, source, sink, "Custom constraint error message contains unsanitized user data"
/* Step 1.4 */
// from TaintConfig cfg, DataFlow::PartialPathNode source, DataFlow::PartialPathNode sink
// where
//   cfg.hasPartialFlow(source, sink, _) and
//   source.getNode().getLocation().getFile().getBaseName() = "SchedulingConstraintValidator.java"
// select sink, source, sink, "Partial flow from unsanitized user data"