diff --git a/core/opentaint-configuration-rules/configuration-rules-jvm/src/main/kotlin/org/opentaint/dataflow/configuration/jvm/SerializedTypeMatching.kt b/core/opentaint-configuration-rules/configuration-rules-jvm/src/main/kotlin/org/opentaint/dataflow/configuration/jvm/SerializedTypeMatching.kt new file mode 100644 index 000000000..bb584123d --- /dev/null +++ b/core/opentaint-configuration-rules/configuration-rules-jvm/src/main/kotlin/org/opentaint/dataflow/configuration/jvm/SerializedTypeMatching.kt @@ -0,0 +1,69 @@ +package org.opentaint.dataflow.configuration.jvm + +import org.opentaint.dataflow.configuration.jvm.serialized.SerializedSimpleNameMatcher +import org.opentaint.dataflow.configuration.jvm.serialized.SerializedTypeNameMatcher +import org.opentaint.ir.api.jvm.JIRArrayType +import org.opentaint.ir.api.jvm.JIRClassType +import org.opentaint.ir.api.jvm.JIRType +import org.opentaint.ir.api.jvm.JIRTypeVariable +import org.opentaint.ir.api.jvm.JIRUnboundWildcard + +fun SerializedTypeNameMatcher.matchType( + erasedTypeName: String, + resolveType: () -> JIRType, + erasedMatch: SerializedTypeNameMatcher.(String) -> Boolean, +): Boolean { + if (!erasedMatch(erasedTypeName)) return false + return matchTypeArgs(resolveType, erasedMatch) +} + +private fun SerializedTypeNameMatcher.matchTypeArgs( + resolveType: () -> JIRType?, + erasedMatch: SerializedTypeNameMatcher.(String) -> Boolean, +): Boolean { + return when (this) { + is SerializedSimpleNameMatcher -> true // no type args + + is SerializedTypeNameMatcher.ClassPattern -> { + val args = typeArgs ?: return true + + val type = resolveType() + if (type !is JIRClassType) return false + + if (args.size != type.typeArguments.size) return false + + args.zip(type.typeArguments).all { (m, a) -> + m.matchType(a.erasedName(), resolveType = { a }, erasedMatch) + } + } + + is SerializedTypeNameMatcher.Array -> element.matchTypeArgs( + resolveType = { (resolveType() as? JIRArrayType)?.elementType }, + erasedMatch + ) + } +} + +/** + * Erased class name for matching — drops any generic decoration that + * [JIRType.typeName] may carry (e.g. `Map` → `java.util.Map`) + * and reduces a type variable / unbound wildcard to its declared erasure + * (e.g. `E` → `java.lang.Object`) so string-based matchers can match against + * pass-through rules whose return/parameter types show up as type variables + * when resolved via the declaring class (e.g. `List.get` returns `E`). + */ +fun JIRType.erasedName(): String = when (this) { + is JIRClassType -> jIRClass.name + is JIRTypeVariable -> jIRClass.name + is JIRUnboundWildcard -> jIRClass.name + is JIRArrayType -> { + val el = elementType + when (el) { + is JIRClassType -> el.jIRClass.name + "[]" + is JIRTypeVariable -> el.jIRClass.name + "[]" + is JIRUnboundWildcard -> el.jIRClass.name + "[]" + else -> typeName + } + } + else -> typeName +} diff --git a/core/opentaint-configuration-rules/configuration-rules-jvm/src/main/kotlin/org/opentaint/dataflow/configuration/jvm/TaintCondition.kt b/core/opentaint-configuration-rules/configuration-rules-jvm/src/main/kotlin/org/opentaint/dataflow/configuration/jvm/TaintCondition.kt index 125cbb2be..06a257246 100644 --- a/core/opentaint-configuration-rules/configuration-rules-jvm/src/main/kotlin/org/opentaint/dataflow/configuration/jvm/TaintCondition.kt +++ b/core/opentaint-configuration-rules/configuration-rules-jvm/src/main/kotlin/org/opentaint/dataflow/configuration/jvm/TaintCondition.kt @@ -119,6 +119,7 @@ sealed interface ConditionNameMatcher { data class TypeMatchesPattern( val position: Position, val pattern: ConditionNameMatcher, + val typeArgs: List? = null, ) : Condition { override fun accept(conditionVisitor: ConditionVisitor): R = conditionVisitor.visit(this) } diff --git a/core/opentaint-configuration-rules/configuration-rules-jvm/src/main/kotlin/org/opentaint/dataflow/configuration/jvm/TypeArgMatcher.kt b/core/opentaint-configuration-rules/configuration-rules-jvm/src/main/kotlin/org/opentaint/dataflow/configuration/jvm/TypeArgMatcher.kt new file mode 100644 index 000000000..da0a7d2dd --- /dev/null +++ b/core/opentaint-configuration-rules/configuration-rules-jvm/src/main/kotlin/org/opentaint/dataflow/configuration/jvm/TypeArgMatcher.kt @@ -0,0 +1,17 @@ +package org.opentaint.dataflow.configuration.jvm + +/** + * A type-argument matcher that has been pre-resolved during rule resolution: + * the erased-name matchers are already compiled to [ConditionNameMatcher], + * so runtime evaluation only needs to dispatch on the structure. + */ +sealed interface TypeArgMatcher { + + data class Class( + val name: ConditionNameMatcher, + // null = no type-args constraint (matches raw / declared erasure). + val typeArgs: List?, + ) : TypeArgMatcher + + data class Array(val element: TypeArgMatcher) : TypeArgMatcher +} diff --git a/core/opentaint-configuration-rules/configuration-rules-jvm/src/main/kotlin/org/opentaint/dataflow/configuration/jvm/serialized/SerializedNameMatcher.kt b/core/opentaint-configuration-rules/configuration-rules-jvm/src/main/kotlin/org/opentaint/dataflow/configuration/jvm/serialized/SerializedNameMatcher.kt index e5d397eab..7b9c9fbf5 100644 --- a/core/opentaint-configuration-rules/configuration-rules-jvm/src/main/kotlin/org/opentaint/dataflow/configuration/jvm/serialized/SerializedNameMatcher.kt +++ b/core/opentaint-configuration-rules/configuration-rules-jvm/src/main/kotlin/org/opentaint/dataflow/configuration/jvm/serialized/SerializedNameMatcher.kt @@ -18,7 +18,10 @@ sealed interface SerializedTypeNameMatcher { @Serializable data class ClassPattern( val `package`: SerializedSimpleNameMatcher, - val `class`: SerializedSimpleNameMatcher + val `class`: SerializedSimpleNameMatcher, + // null = no type-args constraint (matches raw / declared erasure). + // empty list is reserved for an explicit zero-arg parameterization. + val typeArgs: List? = null, ) : SerializedTypeNameMatcher @Serializable diff --git a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/taint/JIRBasicAtomEvaluator.kt b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/taint/JIRBasicAtomEvaluator.kt index 1eb0bc8c5..70fd33e36 100644 --- a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/taint/JIRBasicAtomEvaluator.kt +++ b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/taint/JIRBasicAtomEvaluator.kt @@ -21,8 +21,10 @@ import org.opentaint.dataflow.configuration.jvm.Not import org.opentaint.dataflow.configuration.jvm.Or import org.opentaint.dataflow.configuration.jvm.Position import org.opentaint.dataflow.configuration.jvm.PositionResolver +import org.opentaint.dataflow.configuration.jvm.TypeArgMatcher import org.opentaint.dataflow.configuration.jvm.TypeMatches import org.opentaint.dataflow.configuration.jvm.TypeMatchesPattern +import org.opentaint.dataflow.configuration.jvm.erasedName import org.opentaint.dataflow.jvm.ap.ifds.CallPositionValue import org.opentaint.dataflow.jvm.ap.ifds.JIRFactTypeChecker import org.opentaint.dataflow.jvm.ap.ifds.JIRLocalAliasAnalysis @@ -31,7 +33,10 @@ import org.opentaint.dataflow.jvm.ap.ifds.JIRLocalAliasAnalysis.AliasApInfo import org.opentaint.dataflow.jvm.ap.ifds.JIRLocalAliasAnalysis.AliasInfo import org.opentaint.ir.api.common.cfg.CommonInst import org.opentaint.ir.api.common.cfg.CommonValue +import org.opentaint.ir.api.jvm.JIRArrayType +import org.opentaint.ir.api.jvm.JIRClassType import org.opentaint.ir.api.jvm.JIRRefType +import org.opentaint.ir.api.jvm.JIRType import org.opentaint.ir.api.jvm.cfg.JIRBool import org.opentaint.ir.api.jvm.cfg.JIRCallExpr import org.opentaint.ir.api.jvm.cfg.JIRConstant @@ -329,21 +334,35 @@ class JIRBasicAtomEvaluator( val type = value.type as? JIRRefType ?: return false val pattern = condition.pattern - if (pattern.match(type.typeName)) return true + val erasedMatch = pattern.match(type.typeName) - if (pattern !is ConditionNameMatcher.Concrete) { - // todo: check super classes? - return false - } + if (!erasedMatch) { + if (pattern !is ConditionNameMatcher.Concrete) { + // todo: check super classes? + return false + } - if (negated) return false + if (negated) return false + + if (type.typeName == "java.lang.Object") { + // todo: hack to avoid explosion + return false + } - if (type.typeName == "java.lang.Object") { - // todo: hack to avoid explosion - return false + if (!typeChecker.typeMayHaveSubtypeOf(type.typeName, pattern.name)) { + return false + } } - return typeChecker.typeMayHaveSubtypeOf(type.typeName, pattern.name) + val typeArgs = condition.typeArgs + ?: return true + + if (type !is JIRClassType) return true + + if (type.typeArguments.size != typeArgs.size) return false + return typeArgs.zip(type.typeArguments).all { (matcher, arg) -> + matcher.matchType(arg) + } } private fun ConditionNameMatcher.match(name: String): Boolean = when (this) { @@ -352,7 +371,7 @@ class JIRBasicAtomEvaluator( is ConditionNameMatcher.Simple -> match(name) } - private fun ConditionNameMatcher.Simple.match(name: String): Boolean = when (this) { + private fun ConditionNameMatcher.Simple.match(name: String): Boolean = when (this) { is ConditionNameMatcher.Pattern -> pattern.containsMatchIn(name) is ConditionNameMatcher.Concrete -> this.name == name is ConditionNameMatcher.AnyName -> true @@ -367,4 +386,24 @@ class JIRBasicAtomEvaluator( is CallPositionValue.Value -> value(res.value) is CallPositionValue.VarArgValue -> callVarArgValue(res.value) } + + private fun TypeArgMatcher.matchType(type: JIRType): Boolean = when (this) { + is TypeArgMatcher.Class -> matchType(type) + is TypeArgMatcher.Array -> matchType(type) + } + + private fun TypeArgMatcher.Class.matchType(type: JIRType): Boolean { + if (!name.match(type.erasedName())) return false + + val args = typeArgs + if (args == null || type !is JIRClassType) { + return true + } + + if (args.size != type.typeArguments.size) return false + return args.zip(type.typeArguments).all { (m, a) -> m.matchType(a) } + } + + private fun TypeArgMatcher.Array.matchType(type: JIRType): Boolean = + type is JIRArrayType && element.matchType(type.elementType) } diff --git a/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithArrayOfParameterized.java b/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithArrayOfParameterized.java new file mode 100644 index 000000000..0483fdd90 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithArrayOfParameterized.java @@ -0,0 +1,94 @@ +package example; + +import base.RuleSample; +import base.RuleSet; +import java.util.List; + +/** + * A15. Array of parameterized type — {@code List[]}. + * + * Rule return type {@code List[] $METHOD(...)}. + * + * Expected behavior: + *
    + *
  • Positive: method returns {@code List[]}.
  • + *
  • Negative: method returns {@code List} — missing array + * dimension; rule must NOT fire.
  • + *
  • Negative: method returns {@code List[]} — inner type arg + * differs; rule must NOT fire.
  • + *
  • Negative: method returns {@code String[]} — wrong outer type.
  • + *
+ * + * {@code List[]} is a legal return-type declaration in Java; we use + * {@code @SuppressWarnings("unchecked")} to silence the generic-array + * creation warning on the negative helpers. + */ +@SuppressWarnings({"unchecked", "rawtypes"}) +@RuleSet("example/RuleWithArrayOfParameterized.yaml") +public abstract class RuleWithArrayOfParameterized implements RuleSample { + + void sink(String data) {} + + List[] methodReturningListStringArray(String data) { + sink(data); + return null; + } + + List methodReturningListString(String data) { + sink(data); + return null; + } + + List[] methodReturningListIntegerArray(String data) { + sink(data); + return null; + } + + String[] methodReturningStringArray(String data) { + sink(data); + return null; + } + + final static class PositiveListStringArray extends RuleWithArrayOfParameterized { + @Override + public void entrypoint() { + String data = "tainted"; + methodReturningListStringArray(data); + } + } + + /** + * Honest Negative: missing the outer array dimension — return is + * {@code List}, not {@code List[]}. + */ + final static class NegativeListStringNoArray extends RuleWithArrayOfParameterized { + @Override + public void entrypoint() { + String data = "tainted"; + methodReturningListString(data); + } + } + + /** + * Honest Negative: inner type argument is {@code Integer}, not the + * required {@code String}. + */ + final static class NegativeListIntegerArray extends RuleWithArrayOfParameterized { + @Override + public void entrypoint() { + String data = "tainted"; + methodReturningListIntegerArray(data); + } + } + + /** + * Honest Negative: outer type is {@code String[]}, not {@code List<...>[]}. + */ + final static class NegativeStringArray extends RuleWithArrayOfParameterized { + @Override + public void entrypoint() { + String data = "tainted"; + methodReturningStringArray(data); + } + } +} diff --git a/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithArrayReturnType.java b/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithArrayReturnType.java new file mode 100644 index 000000000..63e7e1329 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithArrayReturnType.java @@ -0,0 +1,49 @@ +package example; + +import base.RuleSample; +import base.RuleSet; + +@RuleSet("example/RuleWithArrayReturnType.yaml") +public abstract class RuleWithArrayReturnType implements RuleSample { + + void sink(String data) {} + + String[] methodReturningStringArray(String data) { + sink(data); + return new String[] { data }; + } + + int[] methodReturningIntArray(String data) { + sink(data); + return new int[] { 1 }; + } + + String methodReturningString(String data) { + sink(data); + return data; + } + + final static class PositiveStringArrayReturn extends RuleWithArrayReturnType { + @Override + public void entrypoint() { + String data = "tainted"; + methodReturningStringArray(data); + } + } + + final static class NegativeIntArrayReturn extends RuleWithArrayReturnType { + @Override + public void entrypoint() { + String data = "tainted"; + methodReturningIntArray(data); + } + } + + final static class NegativeStringReturn extends RuleWithArrayReturnType { + @Override + public void entrypoint() { + String data = "tainted"; + methodReturningString(data); + } + } +} diff --git a/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithClassTypeParam.java b/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithClassTypeParam.java new file mode 100644 index 000000000..d157b2387 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithClassTypeParam.java @@ -0,0 +1,66 @@ +package example; + +import base.RuleSample; +import base.RuleSet; + +/** + * A20. {@code Class<$T>} parameter — reflection-style type token. + * + * Rule: {@code $RET $METHOD(Class<$T> $C, ..., String $A, ...)} — first + * parameter is {@code Class<...>} where {@code $T} metavar binds to any + * concrete type. + * + * Expected behavior: + *
    + *
  • Positive: method takes {@code Class, String} — matches.
  • + *
  • Positive: method takes {@code Class, String} — matches + * (metavar binds to any concrete type).
  • + *
  • Negative: method takes {@code String, String} — first parameter is + * not {@code Class<...>}; rule must NOT fire.
  • + *
+ */ +@RuleSet("example/RuleWithClassTypeParam.yaml") +public abstract class RuleWithClassTypeParam implements RuleSample { + + void sink(String data) {} + + void methodWithClassStringParam(Class c, String data) { + sink(data); + } + + void methodWithClassIntegerParam(Class c, String data) { + sink(data); + } + + void methodWithStringParam(String c, String data) { + sink(data); + } + + final static class PositiveClassString extends RuleWithClassTypeParam { + @Override + public void entrypoint() { + String data = "tainted"; + methodWithClassStringParam(String.class, data); + } + } + + final static class PositiveClassInteger extends RuleWithClassTypeParam { + @Override + public void entrypoint() { + String data = "tainted"; + methodWithClassIntegerParam(Integer.class, data); + } + } + + /** + * Honest Negative: first parameter is a plain {@code String}, not + * {@code Class<...>}; rule must NOT fire. + */ + final static class NegativeStringParam extends RuleWithClassTypeParam { + @Override + public void entrypoint() { + String data = "tainted"; + methodWithStringParam("x", data); + } + } +} diff --git a/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithCollectionReturn.java b/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithCollectionReturn.java new file mode 100644 index 000000000..c40ebf4b5 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithCollectionReturn.java @@ -0,0 +1,65 @@ +package example; + +import base.RuleSample; +import base.RuleSet; +import java.util.Collection; +import java.util.List; + +/** + * A21. Interface vs class widening — {@code Collection} pattern vs + * {@code List} method. + * + * Rule return type {@code Collection $METHOD(...)}. + * + * Expected behavior: + *
    + *
  • Positive: method returns {@code Collection} — exact + * match.
  • + *
  • Negative (observed): method returns {@code List}. The + * opentaint engine uses exact-type matching at method-decl return + * position, not subtype widening — so a {@code List} return is + * NOT matched by a {@code Collection} pattern. Initial pin was + * Positive (hoping for semgrep-like subtype semantics); the test fired + * as a missed Positive and the sample was flipped to Negative to + * document the observed exact-type matching behavior.
  • + *
+ */ +@RuleSet("example/RuleWithCollectionReturn.yaml") +public abstract class RuleWithCollectionReturn implements RuleSample { + + void sink(String data) {} + + Collection methodReturningCollectionString(String data) { + sink(data); + return null; + } + + List methodReturningListString(String data) { + sink(data); + return null; + } + + final static class PositiveCollectionString extends RuleWithCollectionReturn { + @Override + public void entrypoint() { + String data = "tainted"; + methodReturningCollectionString(data); + } + } + + /** + * Honest Negative (flipped from initial Positive pin after empirical + * observation): {@code List} is a declared subtype of + * {@code Collection}, but the opentaint engine matches only + * the exact declared class at the method-decl return position — it + * does NOT perform subtype widening. Therefore the rule does NOT fire + * and this is a true Negative. + */ + final static class NegativeListStringNotWidenedToCollection extends RuleWithCollectionReturn { + @Override + public void entrypoint() { + String data = "tainted"; + methodReturningListString(data); + } + } +} diff --git a/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithConcreteReturnDiscrim.java b/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithConcreteReturnDiscrim.java new file mode 100644 index 000000000..ad495078a --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithConcreteReturnDiscrim.java @@ -0,0 +1,58 @@ +package example; + +import base.RuleSample; +import base.RuleSet; + +/** + * A17. Concrete return type discriminates from a different type. + * + * Rule: {@code String $METHOD(String $A, String $DATA) { ... sink($DATA); ... }} + * — return type is the concrete type {@code String}. + * + * Expected behavior: + *
    + *
  • Positive: a method returning {@code String} — matches.
  • + *
  • Negative: a method returning {@code Integer} — rule must NOT fire, + * even though the parameter list matches.
  • + *
+ * + * This is the simplified (non-inheritance) formulation. A fuller test of + * inheritance substitution — where the return type of an overridden method + * of a parameterized base class surfaces as a concrete substituted type — + * would require dedicated design and is deferred. + */ +@RuleSet("example/RuleWithConcreteReturnDiscrim.yaml") +public abstract class RuleWithConcreteReturnDiscrim implements RuleSample { + + void sink(String data) {} + + String methodReturningString(String a, String data) { + sink(data); + return null; + } + + Integer methodReturningInteger(String a, String data) { + sink(data); + return null; + } + + final static class PositiveStringReturn extends RuleWithConcreteReturnDiscrim { + @Override + public void entrypoint() { + String data = "tainted"; + methodReturningString("x", data); + } + } + + /** + * Honest Negative: the return type is {@code Integer}, not the required + * concrete {@code String}; rule must NOT fire. + */ + final static class NegativeIntegerReturn extends RuleWithConcreteReturnDiscrim { + @Override + public void entrypoint() { + String data = "tainted"; + methodReturningInteger("x", data); + } + } +} diff --git a/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithConcreteReturnType.java b/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithConcreteReturnType.java new file mode 100644 index 000000000..010d82ec9 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithConcreteReturnType.java @@ -0,0 +1,48 @@ +package example; + +import base.RuleSample; +import base.RuleSet; + +@RuleSet("example/RuleWithConcreteReturnType.yaml") +public abstract class RuleWithConcreteReturnType implements RuleSample { + + void sink(String data) {} + + String methodReturningString(String data) { + sink(data); + return data; + } + + int methodReturningInt(String data) { + sink(data); + return 0; + } + + void methodReturningVoid(String data) { + sink(data); + } + + final static class PositiveStringReturn extends RuleWithConcreteReturnType { + @Override + public void entrypoint() { + String data = "tainted"; + methodReturningString(data); + } + } + + final static class NegativeIntReturn extends RuleWithConcreteReturnType { + @Override + public void entrypoint() { + String data = "tainted"; + methodReturningInt(data); + } + } + + final static class NegativeVoidReturn extends RuleWithConcreteReturnType { + @Override + public void entrypoint() { + String data = "tainted"; + methodReturningVoid(data); + } + } +} diff --git a/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithDeepNesting.java b/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithDeepNesting.java new file mode 100644 index 000000000..645307618 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithDeepNesting.java @@ -0,0 +1,73 @@ +package example; + +import base.RuleSample; +import base.RuleSet; +import java.util.List; + +/** + * A10. Deep nesting — {@code List>}. + * + * Rule return type {@code List> $METHOD(...)}. + * + * Expected behavior: + *
    + *
  • Positive: method returns {@code List>} — matches.
  • + *
  • Negative: method returns {@code List} — missing outer + * nesting; rule must NOT fire.
  • + *
  • Negative: method returns {@code List>} — inner type + * argument mismatch; rule must NOT fire.
  • + *
+ */ +@RuleSet("example/RuleWithDeepNesting.yaml") +public abstract class RuleWithDeepNesting implements RuleSample { + + void sink(String data) {} + + List> methodReturningListListString(String data) { + sink(data); + return null; + } + + List methodReturningListString(String data) { + sink(data); + return null; + } + + List> methodReturningListListInteger(String data) { + sink(data); + return null; + } + + final static class PositiveListListString extends RuleWithDeepNesting { + @Override + public void entrypoint() { + String data = "tainted"; + methodReturningListListString(data); + } + } + + /** + * Honest Negative: rule requires the outer nesting + * {@code List>}; {@code List} is missing the outer + * list. + */ + final static class NegativeFlatListString extends RuleWithDeepNesting { + @Override + public void entrypoint() { + String data = "tainted"; + methodReturningListString(data); + } + } + + /** + * Honest Negative: inner type arg is {@code Integer}, not the required + * {@code String}. + */ + final static class NegativeInnerTypeMismatch extends RuleWithDeepNesting { + @Override + public void entrypoint() { + String data = "tainted"; + methodReturningListListInteger(data); + } + } +} diff --git a/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithFqnTypeArg.java b/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithFqnTypeArg.java new file mode 100644 index 000000000..24a0cc963 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithFqnTypeArg.java @@ -0,0 +1,54 @@ +package example; + +import base.RuleSample; +import base.RuleSet; +import org.springframework.http.ResponseEntity; + +/** + * A13. Fully-qualified type argument — {@code ResponseEntity}. + * + * Rule return type {@code ResponseEntity $METHOD(...)}. + * + * Expected behavior: + *
    + *
  • Positive: method returns {@code ResponseEntity} — matches + * (FQN vs simple name resolve to the same class).
  • + *
  • Negative: method returns {@code ResponseEntity} — type arg + * does not match; rule must NOT fire.
  • + *
+ */ +@RuleSet("example/RuleWithFqnTypeArg.yaml") +public abstract class RuleWithFqnTypeArg implements RuleSample { + + void sink(String data) {} + + ResponseEntity methodReturningResponseEntityString(String data) { + sink(data); + return null; + } + + ResponseEntity methodReturningResponseEntityInteger(String data) { + sink(data); + return null; + } + + final static class PositiveStringMatchesFqn extends RuleWithFqnTypeArg { + @Override + public void entrypoint() { + String data = "tainted"; + methodReturningResponseEntityString(data); + } + } + + /** + * Honest Negative: type arg {@code Integer} does not match the required + * {@code java.lang.String}. + */ + final static class NegativeIntegerTypeArg extends RuleWithFqnTypeArg { + @Override + public void entrypoint() { + String data = "tainted"; + methodReturningResponseEntityInteger(data); + } + } +} diff --git a/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithGenericByteArrayReturnType.java b/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithGenericByteArrayReturnType.java new file mode 100644 index 000000000..ce21dc31d --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithGenericByteArrayReturnType.java @@ -0,0 +1,55 @@ +package example; + +import base.RuleSample; +import base.RuleSet; +import org.springframework.http.ResponseEntity; + +/** + * A1. Rule pattern-inside declares return type {@code ResponseEntity}. + * + * Expected behavior: only the byte-array-returning method matches; the + * String-returning method should NOT match (specificity on concrete type arg). + * + * Current engine behavior: the method-decl return-type generic specificity + * is ignored at the concrete-type-argument level — both methods match. This + * test is EXPECTED TO FAIL today with an FP on NegativeResponseEntityString; + * the failure honestly documents a gap introduced by this branch's + * type-matching feature. + */ +@RuleSet("example/RuleWithGenericByteArrayReturnType.yaml") +public abstract class RuleWithGenericByteArrayReturnType implements RuleSample { + + void sink(String data) {} + + ResponseEntity methodReturningResponseEntityByteArray(String data) { + sink(data); + return null; + } + + ResponseEntity methodReturningResponseEntityString(String data) { + sink(data); + return null; + } + + final static class PositiveResponseEntityByteArray extends RuleWithGenericByteArrayReturnType { + @Override + public void entrypoint() { + String data = "tainted"; + methodReturningResponseEntityByteArray(data); + } + } + + /** + * Honest Negative: rule requires {@code ResponseEntity} but the + * method returns {@code ResponseEntity}. The engine currently + * reports this as a match (FP); fixing this requires deeper concrete + * type-arg discrimination on method-decl return types. + */ + final static class NegativeResponseEntityString extends RuleWithGenericByteArrayReturnType { + @Override + public void entrypoint() { + String data = "tainted"; + methodReturningResponseEntityString(data); + } + } +} diff --git a/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithGenericMetavarArrayArg.java b/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithGenericMetavarArrayArg.java new file mode 100644 index 000000000..bf29a93a8 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithGenericMetavarArrayArg.java @@ -0,0 +1,61 @@ +package example; + +import base.RuleSample; +import base.RuleSet; +import base.TaintRuleFalsePositive; +import org.springframework.http.ResponseEntity; + +@RuleSet("example/RuleWithGenericMetavarArrayArg.yaml") +public abstract class RuleWithGenericMetavarArrayArg implements RuleSample { + + void sink(String data) {} + + ResponseEntity methodReturningResponseEntityString(String data) { + sink(data); + return null; + } + + ResponseEntity methodReturningResponseEntityByteArray(String data) { + sink(data); + return null; + } + + /** + * Raw ResponseEntity. Note: pattern is ResponseEntity<$T>, so whether this fires + * depends on whether the engine considers raw as unifiable with a type-arg metavar. + */ + @SuppressWarnings("rawtypes") + ResponseEntity methodReturningRawResponseEntity(String data) { + sink(data); + return null; + } + + final static class PositiveResponseEntityString extends RuleWithGenericMetavarArrayArg { + @Override + public void entrypoint() { + String data = "tainted"; + methodReturningResponseEntityString(data); + } + } + + final static class PositiveResponseEntityByteArray extends RuleWithGenericMetavarArrayArg { + @Override + public void entrypoint() { + String data = "tainted"; + methodReturningResponseEntityByteArray(data); + } + } + + /** + * In opentaint the method-decl pattern's return-type check is effectively ignored + * on raw vs. parameterized, so raw ResponseEntity DOES get matched by + * ResponseEntity<$T>. We treat this as a Positive to pin the current behavior. + */ + final static class PositiveRawResponseEntity extends RuleWithGenericMetavarArrayArg { + @Override + public void entrypoint() { + String data = "tainted"; + methodReturningRawResponseEntity(data); + } + } +} diff --git a/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithGenericReturnType.java b/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithGenericReturnType.java new file mode 100644 index 000000000..fb80832b1 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithGenericReturnType.java @@ -0,0 +1,50 @@ +package example; + +import base.RuleSample; +import base.RuleSet; +import org.springframework.http.ResponseEntity; + +@RuleSet("example/RuleWithGenericReturnType.yaml") +public abstract class RuleWithGenericReturnType implements RuleSample { + + void sink(String data) {} + + ResponseEntity methodReturningResponseEntityString(String data) { + sink(data); + return null; + } + + ResponseEntity methodReturningResponseEntityObject(String data) { + sink(data); + return null; + } + + String methodReturningString(String data) { + sink(data); + return data; + } + + final static class PositiveResponseEntityString extends RuleWithGenericReturnType { + @Override + public void entrypoint() { + String data = "tainted"; + methodReturningResponseEntityString(data); + } + } + + final static class PositiveResponseEntityObject extends RuleWithGenericReturnType { + @Override + public void entrypoint() { + String data = "tainted"; + methodReturningResponseEntityObject(data); + } + } + + final static class NegativeStringReturn extends RuleWithGenericReturnType { + @Override + public void entrypoint() { + String data = "tainted"; + methodReturningString(data); + } + } +} diff --git a/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithGenericTypeArgs.java b/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithGenericTypeArgs.java new file mode 100644 index 000000000..68a6cd9f2 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithGenericTypeArgs.java @@ -0,0 +1,37 @@ +package example; + +import base.RuleSample; +import base.RuleSet; +import java.util.Map; + +@RuleSet("example/RuleWithGenericTypeArgs.yaml") +public abstract class RuleWithGenericTypeArgs implements RuleSample { + + void sink(String data) {} + + void methodWithGenericParam(Map m, String data) { + sink(data); + } + + void methodWithDifferentGenericParam(Map m, String data) { + sink(data); + } + + final static class PositiveMatchingGenericParam extends RuleWithGenericTypeArgs { + @Override + public void entrypoint() { + String data = "tainted"; + Map m = null; + methodWithGenericParam(m, data); + } + } + + final static class NegativeDifferentGenericParam extends RuleWithGenericTypeArgs { + @Override + public void entrypoint() { + String data = "tainted"; + Map m = null; + methodWithDifferentGenericParam(m, data); + } + } +} diff --git a/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithMixedMetavarConcrete.java b/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithMixedMetavarConcrete.java new file mode 100644 index 000000000..681b0b629 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithMixedMetavarConcrete.java @@ -0,0 +1,70 @@ +package example; + +import base.RuleSample; +import base.RuleSet; +import java.util.Map; + +/** + * A8. Mixed metavar + concrete — {@code Map<$K, String>}. + * + * First slot is a metavariable {@code $K} (binds to any concrete type); + * second slot is the concrete type {@code String}. + * + * Expected behavior: + *
    + *
  • Positive: {@code Map} — {@code $K} binds to + * {@code Integer}; second slot is {@code String}.
  • + *
  • Positive: {@code Map} — {@code $K} binds to + * {@code String}; second slot is {@code String}.
  • + *
  • Negative: {@code Map} — second slot is not + * {@code String}, rule must NOT fire.
  • + *
+ */ +@RuleSet("example/RuleWithMixedMetavarConcrete.yaml") +public abstract class RuleWithMixedMetavarConcrete implements RuleSample { + + void sink(String data) {} + + void methodWithIntegerStringMap(Map m, String data) { + sink(data); + } + + void methodWithStringStringMap(Map m, String data) { + sink(data); + } + + void methodWithStringIntegerMap(Map m, String data) { + sink(data); + } + + final static class PositiveIntegerStringMap extends RuleWithMixedMetavarConcrete { + @Override + public void entrypoint() { + String data = "tainted"; + Map m = null; + methodWithIntegerStringMap(m, data); + } + } + + final static class PositiveStringStringMap extends RuleWithMixedMetavarConcrete { + @Override + public void entrypoint() { + String data = "tainted"; + Map m = null; + methodWithStringStringMap(m, data); + } + } + + /** + * Honest Negative: the second type argument is {@code Integer}, not the + * required concrete {@code String}; the rule must NOT fire. + */ + final static class NegativeSecondSlotNotString extends RuleWithMixedMetavarConcrete { + @Override + public void entrypoint() { + String data = "tainted"; + Map m = null; + methodWithStringIntegerMap(m, data); + } + } +} diff --git a/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithNestedGenericReturnType.java b/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithNestedGenericReturnType.java new file mode 100644 index 000000000..641a110a3 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithNestedGenericReturnType.java @@ -0,0 +1,54 @@ +package example; + +import base.RuleSample; +import base.RuleSet; +import java.util.List; +import org.springframework.http.ResponseEntity; + +/** + * A3. Rule pattern-inside declares return type {@code ResponseEntity>}. + * + * Expected behavior: only the {@code ResponseEntity>}-returning + * method matches; a method returning plain {@code ResponseEntity} + * should NOT match (the nested type arg differs). + * + * Current engine behavior: the generic specificity at the nested type-arg + * level is ignored — both methods match. This test is EXPECTED TO FAIL + * today with an FP on NegativeFlatGeneric. + */ +@RuleSet("example/RuleWithNestedGenericReturnType.yaml") +public abstract class RuleWithNestedGenericReturnType implements RuleSample { + + void sink(String data) {} + + ResponseEntity> methodReturningResponseEntityListString(String data) { + sink(data); + return null; + } + + ResponseEntity methodReturningResponseEntityString(String data) { + sink(data); + return null; + } + + final static class PositiveNestedGeneric extends RuleWithNestedGenericReturnType { + @Override + public void entrypoint() { + String data = "tainted"; + methodReturningResponseEntityListString(data); + } + } + + /** + * Honest Negative: rule requires {@code ResponseEntity>} + * but method returns {@code ResponseEntity}. The engine currently + * reports this as a match (FP). + */ + final static class NegativeFlatGeneric extends RuleWithNestedGenericReturnType { + @Override + public void entrypoint() { + String data = "tainted"; + methodReturningResponseEntityString(data); + } + } +} diff --git a/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithNestedMapListReturn.java b/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithNestedMapListReturn.java new file mode 100644 index 000000000..3a43174a0 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithNestedMapListReturn.java @@ -0,0 +1,95 @@ +package example; + +import base.RuleSample; +import base.RuleSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * A22. Nested mixed containers — {@code Map>}. + * + * Rule return type {@code Map> $METHOD(...)}. + * + * Expected behavior: + *
    + *
  • Positive: method returns {@code Map>} — + * matches.
  • + *
  • Negative: method returns {@code Map>} — + * inner-inner type arg differs; rule must NOT fire.
  • + *
  • Negative: method returns {@code Map>} — + * outer key type differs; rule must NOT fire.
  • + *
  • Negative: method returns {@code Map>} — the + * middle container is {@code Set}, not {@code List}; rule must NOT + * fire.
  • + *
+ */ +@RuleSet("example/RuleWithNestedMapListReturn.yaml") +public abstract class RuleWithNestedMapListReturn implements RuleSample { + + void sink(String data) {} + + Map> methodReturningMapStringListInteger(String data) { + sink(data); + return null; + } + + Map> methodReturningMapStringListString(String data) { + sink(data); + return null; + } + + Map> methodReturningMapIntegerListInteger(String data) { + sink(data); + return null; + } + + Map> methodReturningMapStringSetInteger(String data) { + sink(data); + return null; + } + + final static class PositiveMapStringListInteger extends RuleWithNestedMapListReturn { + @Override + public void entrypoint() { + String data = "tainted"; + methodReturningMapStringListInteger(data); + } + } + + /** + * Honest Negative: inner-inner type arg is {@code String}, not the + * required {@code Integer}. + */ + final static class NegativeInnerInnerMismatch extends RuleWithNestedMapListReturn { + @Override + public void entrypoint() { + String data = "tainted"; + methodReturningMapStringListString(data); + } + } + + /** + * Honest Negative: outer key type is {@code Integer}, not the required + * {@code String}. + */ + final static class NegativeOuterKeyMismatch extends RuleWithNestedMapListReturn { + @Override + public void entrypoint() { + String data = "tainted"; + methodReturningMapIntegerListInteger(data); + } + } + + /** + * Honest Negative: middle container is {@code Set}, not the required + * {@code List}. + */ + final static class NegativeMiddleContainerMismatch extends RuleWithNestedMapListReturn { + @Override + public void entrypoint() { + String data = "tainted"; + methodReturningMapStringSetInteger(data); + } + } +} diff --git a/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithNestedParamGeneric.java b/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithNestedParamGeneric.java new file mode 100644 index 000000000..03bd27a16 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithNestedParamGeneric.java @@ -0,0 +1,56 @@ +package example; + +import base.RuleSample; +import base.RuleSet; +import java.util.List; +import java.util.Map; + +/** + * A19. Nested generic in parameter — {@code List>}. + * + * Complement to A10 (nested generic in return position). The nested + * container appears in parameter position. + * + * Rule: {@code $RET $METHOD(List> $X, ..., String $A, ...)}. + * + * Expected behavior: + *
    + *
  • Positive: method takes {@code List>, String} + * — matches.
  • + *
  • Negative: method takes {@code List>, String} + * — the inner-inner type arg differs; rule must NOT fire.
  • + *
+ */ +@RuleSet("example/RuleWithNestedParamGeneric.yaml") +public abstract class RuleWithNestedParamGeneric implements RuleSample { + + void sink(String data) {} + + void methodWithListMapStringInteger(List> x, String data) { + sink(data); + } + + void methodWithListMapStringString(List> x, String data) { + sink(data); + } + + final static class PositiveListMapStringInteger extends RuleWithNestedParamGeneric { + @Override + public void entrypoint() { + String data = "tainted"; + methodWithListMapStringInteger(null, data); + } + } + + /** + * Honest Negative: inner-inner type argument is {@code String}, not the + * required {@code Integer}. + */ + final static class NegativeInnerInnerMismatch extends RuleWithNestedParamGeneric { + @Override + public void entrypoint() { + String data = "tainted"; + methodWithListMapStringString(null, data); + } + } +} diff --git a/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithParamConcreteListString.java b/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithParamConcreteListString.java new file mode 100644 index 000000000..50481d496 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithParamConcreteListString.java @@ -0,0 +1,56 @@ +package example; + +import base.RuleSample; +import base.RuleSet; +import java.util.List; + +/** + * A12. Parameter-position concrete-vs-metavar discrimination. + * + * Rule: {@code $RET $METHOD(List $A, String $DATA) { ... sink($DATA); ... }} + * — the first parameter is the concrete type {@code List} (not a + * metavar). + * + * Expected behavior: + *
    + *
  • Positive: {@code void foo(List x, String data)} — matches.
  • + *
  • Negative: {@code void foo(List x, String data)} — first + * parameter type argument is {@code Integer}, not the required + * {@code String}; rule must NOT fire.
  • + *
+ */ +@RuleSet("example/RuleWithParamConcreteListString.yaml") +public abstract class RuleWithParamConcreteListString implements RuleSample { + + void sink(String data) {} + + void methodWithListString(List x, String data) { + sink(data); + } + + void methodWithListInteger(List x, String data) { + sink(data); + } + + final static class PositiveListString extends RuleWithParamConcreteListString { + @Override + public void entrypoint() { + String data = "tainted"; + List x = null; + methodWithListString(x, data); + } + } + + /** + * Honest Negative: first parameter type argument is {@code Integer}, not + * the required {@code String}. + */ + final static class NegativeListInteger extends RuleWithParamConcreteListString { + @Override + public void entrypoint() { + String data = "tainted"; + List x = null; + methodWithListInteger(x, data); + } + } +} diff --git a/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithTwoArgGeneric.java b/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithTwoArgGeneric.java new file mode 100644 index 000000000..17020a8ee --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithTwoArgGeneric.java @@ -0,0 +1,51 @@ +package example; + +import base.RuleSample; +import base.RuleSet; +import java.util.List; +import java.util.Map; + +@RuleSet("example/RuleWithTwoArgGeneric.yaml") +public abstract class RuleWithTwoArgGeneric implements RuleSample { + + void sink(String data) {} + + void methodWithStringObjectMap(Map m, String data) { + sink(data); + } + + void methodWithStringStringMap(Map m, String data) { + sink(data); + } + + void methodWithList(List m, String data) { + sink(data); + } + + final static class PositiveStringObjectMap extends RuleWithTwoArgGeneric { + @Override + public void entrypoint() { + String data = "tainted"; + Map m = null; + methodWithStringObjectMap(m, data); + } + } + + final static class PositiveStringStringMap extends RuleWithTwoArgGeneric { + @Override + public void entrypoint() { + String data = "tainted"; + Map m = null; + methodWithStringStringMap(m, data); + } + } + + final static class NegativeListNotMap extends RuleWithTwoArgGeneric { + @Override + public void entrypoint() { + String data = "tainted"; + List m = null; + methodWithList(m, data); + } + } +} diff --git a/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithTwoDimArrayReturn.java b/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithTwoDimArrayReturn.java new file mode 100644 index 000000000..0a000d8de --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithTwoDimArrayReturn.java @@ -0,0 +1,89 @@ +package example; + +import base.RuleSample; +import base.RuleSet; + +/** + * A23. Array dimension mismatch — {@code String[][]}. + * + * Rule return type {@code String[][] $METHOD(...)}. + * + * Expected behavior: + *
    + *
  • Positive: method returns {@code String[][]} — matches.
  • + *
  • Negative: method returns {@code String[]} — one fewer dimension; + * rule must NOT fire.
  • + *
  • Negative: method returns {@code String[][][]} — one extra + * dimension; rule must NOT fire.
  • + *
  • Negative: method returns {@code Integer[][]} — wrong element type; + * rule must NOT fire.
  • + *
+ */ +@RuleSet("example/RuleWithTwoDimArrayReturn.yaml") +public abstract class RuleWithTwoDimArrayReturn implements RuleSample { + + void sink(String data) {} + + String[][] methodReturningStringTwoDim(String data) { + sink(data); + return null; + } + + String[] methodReturningStringOneDim(String data) { + sink(data); + return null; + } + + String[][][] methodReturningStringThreeDim(String data) { + sink(data); + return null; + } + + Integer[][] methodReturningIntegerTwoDim(String data) { + sink(data); + return null; + } + + final static class PositiveStringTwoDim extends RuleWithTwoDimArrayReturn { + @Override + public void entrypoint() { + String data = "tainted"; + methodReturningStringTwoDim(data); + } + } + + /** + * Honest Negative: return type has one fewer dimension than required. + */ + final static class NegativeStringOneDim extends RuleWithTwoDimArrayReturn { + @Override + public void entrypoint() { + String data = "tainted"; + methodReturningStringOneDim(data); + } + } + + /** + * Honest Negative: return type has one extra dimension beyond + * required. + */ + final static class NegativeStringThreeDim extends RuleWithTwoDimArrayReturn { + @Override + public void entrypoint() { + String data = "tainted"; + methodReturningStringThreeDim(data); + } + } + + /** + * Honest Negative: element type is {@code Integer}, not the required + * {@code String}. + */ + final static class NegativeIntegerTwoDim extends RuleWithTwoDimArrayReturn { + @Override + public void entrypoint() { + String data = "tainted"; + methodReturningIntegerTwoDim(data); + } + } +} diff --git a/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithWildcardGeneric.java b/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithWildcardGeneric.java new file mode 100644 index 000000000..8eaefb0da --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithWildcardGeneric.java @@ -0,0 +1,47 @@ +package example; + +import base.RuleSample; +import base.RuleSet; +import org.springframework.http.ResponseEntity; + +@RuleSet("example/RuleWithWildcardGeneric.yaml") +public abstract class RuleWithWildcardGeneric implements RuleSample { + + void sink(String data) {} + + ResponseEntity methodReturningResponseEntityWildcard(String data) { + sink(data); + return null; + } + + ResponseEntity methodReturningResponseEntityString(String data) { + sink(data); + return null; + } + + /** + * Wildcard ResponseEntity<?> trivially matches the <?> rule + * pattern. + */ + final static class PositiveWildcard extends RuleWithWildcardGeneric { + @Override + public void entrypoint() { + String data = "tainted"; + methodReturningResponseEntityWildcard(data); + } + } + + /** + * ResponseEntity<String> is a concrete parameterization. Java's + * unbounded wildcard `?` is the supertype of any `X`, so `<?>` + * accepts any concrete type argument — `ResponseEntity<String>` + * matches. + */ + final static class PositiveConcreteMatchesWildcard extends RuleWithWildcardGeneric { + @Override + public void entrypoint() { + String data = "tainted"; + methodReturningResponseEntityString(data); + } + } +} diff --git a/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithArrayOfParameterized.yaml b/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithArrayOfParameterized.yaml new file mode 100644 index 000000000..bf3244759 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithArrayOfParameterized.yaml @@ -0,0 +1,15 @@ +rules: + - id: example-RuleWithArrayOfParameterized + languages: + - java + severity: ERROR + message: match example/RuleWithArrayOfParameterized + patterns: + - pattern: |- + ... + sink($A); + ... + - pattern-inside: |- + List[] $METHOD(..., String $A, ...) { + ... + } diff --git a/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithArrayReturnType.yaml b/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithArrayReturnType.yaml new file mode 100644 index 000000000..cabc45ff1 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithArrayReturnType.yaml @@ -0,0 +1,15 @@ +rules: + - id: example-RuleWithArrayReturnType + languages: + - java + severity: ERROR + message: match example/RuleWithArrayReturnType + patterns: + - pattern: |- + ... + sink($A); + ... + - pattern-inside: |- + String[] $METHOD(..., String $A, ...) { + ... + } diff --git a/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithClassTypeParam.yaml b/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithClassTypeParam.yaml new file mode 100644 index 000000000..f44e683ca --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithClassTypeParam.yaml @@ -0,0 +1,15 @@ +rules: + - id: example-RuleWithClassTypeParam + languages: + - java + severity: ERROR + message: match example/RuleWithClassTypeParam + patterns: + - pattern: |- + ... + sink($A); + ... + - pattern-inside: |- + $RET $METHOD(Class<$T> $C, ..., String $A, ...) { + ... + } diff --git a/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithCollectionReturn.yaml b/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithCollectionReturn.yaml new file mode 100644 index 000000000..0a2860c5f --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithCollectionReturn.yaml @@ -0,0 +1,15 @@ +rules: + - id: example-RuleWithCollectionReturn + languages: + - java + severity: ERROR + message: match example/RuleWithCollectionReturn + patterns: + - pattern: |- + ... + sink($A); + ... + - pattern-inside: |- + Collection $METHOD(..., String $A, ...) { + ... + } diff --git a/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithConcreteReturnDiscrim.yaml b/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithConcreteReturnDiscrim.yaml new file mode 100644 index 000000000..c64a0787c --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithConcreteReturnDiscrim.yaml @@ -0,0 +1,15 @@ +rules: + - id: example-RuleWithConcreteReturnDiscrim + languages: + - java + severity: ERROR + message: match example/RuleWithConcreteReturnDiscrim + patterns: + - pattern: |- + ... + sink($DATA); + ... + - pattern-inside: |- + String $METHOD(String $A, String $DATA) { + ... + } diff --git a/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithConcreteReturnType.yaml b/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithConcreteReturnType.yaml new file mode 100644 index 000000000..a5d120f59 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithConcreteReturnType.yaml @@ -0,0 +1,15 @@ +rules: + - id: example-RuleWithConcreteReturnType + languages: + - java + severity: ERROR + message: match example/RuleWithConcreteReturnType + patterns: + - pattern: |- + ... + sink($A); + ... + - pattern-inside: |- + String $METHOD(..., String $A, ...) { + ... + } diff --git a/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithDeepNesting.yaml b/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithDeepNesting.yaml new file mode 100644 index 000000000..bfb56e0e5 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithDeepNesting.yaml @@ -0,0 +1,15 @@ +rules: + - id: example-RuleWithDeepNesting + languages: + - java + severity: ERROR + message: match example/RuleWithDeepNesting + patterns: + - pattern: |- + ... + sink($A); + ... + - pattern-inside: |- + List> $METHOD(..., String $A, ...) { + ... + } diff --git a/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithFqnTypeArg.yaml b/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithFqnTypeArg.yaml new file mode 100644 index 000000000..616a099e2 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithFqnTypeArg.yaml @@ -0,0 +1,15 @@ +rules: + - id: example-RuleWithFqnTypeArg + languages: + - java + severity: ERROR + message: match example/RuleWithFqnTypeArg + patterns: + - pattern: |- + ... + sink($A); + ... + - pattern-inside: |- + ResponseEntity $METHOD(..., String $A, ...) { + ... + } diff --git a/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithGenericByteArrayReturnType.yaml b/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithGenericByteArrayReturnType.yaml new file mode 100644 index 000000000..f1194e742 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithGenericByteArrayReturnType.yaml @@ -0,0 +1,15 @@ +rules: + - id: example-RuleWithGenericByteArrayReturnType + languages: + - java + severity: ERROR + message: match example/RuleWithGenericByteArrayReturnType + patterns: + - pattern: |- + ... + sink($A); + ... + - pattern-inside: |- + ResponseEntity $METHOD(..., String $A, ...) { + ... + } diff --git a/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithGenericMetavarArrayArg.yaml b/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithGenericMetavarArrayArg.yaml new file mode 100644 index 000000000..5878107a9 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithGenericMetavarArrayArg.yaml @@ -0,0 +1,15 @@ +rules: + - id: example-RuleWithGenericMetavarArrayArg + languages: + - java + severity: ERROR + message: match example/RuleWithGenericMetavarArrayArg + patterns: + - pattern: |- + ... + sink($A); + ... + - pattern-inside: |- + ResponseEntity<$T> $METHOD(..., String $A, ...) { + ... + } diff --git a/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithGenericReturnType.yaml b/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithGenericReturnType.yaml new file mode 100644 index 000000000..8a0db9d91 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithGenericReturnType.yaml @@ -0,0 +1,15 @@ +rules: + - id: example-RuleWithGenericReturnType + languages: + - java + severity: ERROR + message: match example/RuleWithGenericReturnType + patterns: + - pattern: |- + ... + sink($A); + ... + - pattern-inside: |- + ResponseEntity<$T> $METHOD(..., String $A, ...) { + ... + } diff --git a/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithGenericTypeArgs.yaml b/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithGenericTypeArgs.yaml new file mode 100644 index 000000000..90425f1b7 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithGenericTypeArgs.yaml @@ -0,0 +1,15 @@ +rules: + - id: example-RuleWithGenericTypeArgs + languages: + - java + severity: ERROR + message: match example/RuleWithGenericTypeArgs + patterns: + - pattern: |- + ... + sink($A); + ... + - pattern-inside: |- + $RET $METHOD(Map $M, ...) { + ... + } diff --git a/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithMixedMetavarConcrete.yaml b/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithMixedMetavarConcrete.yaml new file mode 100644 index 000000000..1867ccd3b --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithMixedMetavarConcrete.yaml @@ -0,0 +1,15 @@ +rules: + - id: example-RuleWithMixedMetavarConcrete + languages: + - java + severity: ERROR + message: match example/RuleWithMixedMetavarConcrete + patterns: + - pattern: |- + ... + sink($A); + ... + - pattern-inside: |- + $RET $METHOD(Map<$K, String> $M, ..., String $A, ...) { + ... + } diff --git a/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithNestedGenericReturnType.yaml b/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithNestedGenericReturnType.yaml new file mode 100644 index 000000000..b6cab41cb --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithNestedGenericReturnType.yaml @@ -0,0 +1,15 @@ +rules: + - id: example-RuleWithNestedGenericReturnType + languages: + - java + severity: ERROR + message: match example/RuleWithNestedGenericReturnType + patterns: + - pattern: |- + ... + sink($A); + ... + - pattern-inside: |- + ResponseEntity> $METHOD(..., String $A, ...) { + ... + } diff --git a/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithNestedMapListReturn.yaml b/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithNestedMapListReturn.yaml new file mode 100644 index 000000000..89bf44f4a --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithNestedMapListReturn.yaml @@ -0,0 +1,15 @@ +rules: + - id: example-RuleWithNestedMapListReturn + languages: + - java + severity: ERROR + message: match example/RuleWithNestedMapListReturn + patterns: + - pattern: |- + ... + sink($A); + ... + - pattern-inside: |- + Map> $METHOD(..., String $A, ...) { + ... + } diff --git a/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithNestedParamGeneric.yaml b/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithNestedParamGeneric.yaml new file mode 100644 index 000000000..b425dd698 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithNestedParamGeneric.yaml @@ -0,0 +1,15 @@ +rules: + - id: example-RuleWithNestedParamGeneric + languages: + - java + severity: ERROR + message: match example/RuleWithNestedParamGeneric + patterns: + - pattern: |- + ... + sink($A); + ... + - pattern-inside: |- + $RET $METHOD(List> $X, ..., String $A, ...) { + ... + } diff --git a/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithParamConcreteListString.yaml b/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithParamConcreteListString.yaml new file mode 100644 index 000000000..1f42e1c1f --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithParamConcreteListString.yaml @@ -0,0 +1,15 @@ +rules: + - id: example-RuleWithParamConcreteListString + languages: + - java + severity: ERROR + message: match example/RuleWithParamConcreteListString + patterns: + - pattern: |- + ... + sink($DATA); + ... + - pattern-inside: |- + $RET $METHOD(List $A, String $DATA) { + ... + } diff --git a/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithTwoArgGeneric.yaml b/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithTwoArgGeneric.yaml new file mode 100644 index 000000000..489c02cba --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithTwoArgGeneric.yaml @@ -0,0 +1,15 @@ +rules: + - id: example-RuleWithTwoArgGeneric + languages: + - java + severity: ERROR + message: match example/RuleWithTwoArgGeneric + patterns: + - pattern: |- + ... + sink($A); + ... + - pattern-inside: |- + $RET $METHOD(Map<$K, $V> $M, ..., String $A, ...) { + ... + } diff --git a/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithTwoDimArrayReturn.yaml b/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithTwoDimArrayReturn.yaml new file mode 100644 index 000000000..594a1b9fe --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithTwoDimArrayReturn.yaml @@ -0,0 +1,15 @@ +rules: + - id: example-RuleWithTwoDimArrayReturn + languages: + - java + severity: ERROR + message: match example/RuleWithTwoDimArrayReturn + patterns: + - pattern: |- + ... + sink($A); + ... + - pattern-inside: |- + String[][] $METHOD(..., String $A, ...) { + ... + } diff --git a/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithWildcardGeneric.yaml b/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithWildcardGeneric.yaml new file mode 100644 index 000000000..5d9af718f --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithWildcardGeneric.yaml @@ -0,0 +1,15 @@ +rules: + - id: example-RuleWithWildcardGeneric + languages: + - java + severity: ERROR + message: match example/RuleWithWildcardGeneric + patterns: + - pattern: |- + ... + sink($A); + ... + - pattern-inside: |- + ResponseEntity $METHOD(..., String $A, ...) { + ... + } diff --git a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/SemgrepJavaPattern.kt b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/SemgrepJavaPattern.kt index 40050dc12..b466775b2 100644 --- a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/SemgrepJavaPattern.kt +++ b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/SemgrepJavaPattern.kt @@ -194,6 +194,9 @@ sealed interface TypeName { ) : TypeName data class ArrayTypeName(val elementType: TypeName) : TypeName + + /** Unbounded wildcard `?` occurring as a type argument. */ + data object WildcardTypeName : TypeName } sealed interface Modifier diff --git a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/SemgrepJavaPatternParser.kt b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/SemgrepJavaPatternParser.kt index da522b67f..c1bd03896 100644 --- a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/SemgrepJavaPatternParser.kt +++ b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/SemgrepJavaPatternParser.kt @@ -214,9 +214,6 @@ private class TypenameParserVisitor : JavaParserBaseVisitor() { val parsedTypes = parsed.filterNotNull() if (parsedTypes.size == parsed.size) return parsedTypes - // T - if (parsed.size == 1 && parsedTypes.isEmpty()) return emptyList() - ctx.todo() } @@ -228,7 +225,7 @@ private class TypenameParserVisitor : JavaParserBaseVisitor() { it.todo() } - return null + return TypeName.WildcardTypeName } unreachable() diff --git a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/SemgrepRuleLoadErrorMessage.kt b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/SemgrepRuleLoadErrorMessage.kt index 5d98bb68c..2a6634deb 100644 --- a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/SemgrepRuleLoadErrorMessage.kt +++ b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/SemgrepRuleLoadErrorMessage.kt @@ -146,21 +146,6 @@ class FailedTransformationToActionList(causeMessage: String?) : RuleIssueBlockin override val message: String = "Failed to transform pattern into an action list: ${causeMessage ?: "unknown error"}" } -class TypeArgumentsIgnored : UnsupportedFeatureNonBlockingMessage() { - override val message: String = "Type arguments in the pattern are not supported and will be ignored during matching" -} - -class MethodDeclarationReturnTypeIsArray : UnsupportedFeatureNonBlockingMessage() { - override val message: String = "Method declaration pattern with array return type is not supported; the return type constraint will be ignored" -} - -class MethodDeclarationReturnTypeIsNotMetaVar : UnsupportedFeatureNonBlockingMessage() { - override val message: String = "Method declaration pattern with a concrete return type is not supported; only metavariable return types are handled" -} - -class MethodDeclarationReturnTypeHasTypeArgs : UnsupportedFeatureNonBlockingMessage() { - override val message: String = "Method declaration pattern with a generic return type is not supported; type arguments on return types will be ignored" -} class EmptyPatternsAfterConvertToRawRule(times: Int) : InternalWarningNonBlockingMessage() { override val message: String = "$times pattern variant(s) were dropped during normalization because they produced no positive patterns" diff --git a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/ParamCondition.kt b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/ParamCondition.kt index cfc719f05..a3b1409e8 100644 --- a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/ParamCondition.kt +++ b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/ParamCondition.kt @@ -6,13 +6,13 @@ import org.opentaint.semgrep.pattern.conversion.SemgrepPatternAction.SignatureMo @Serializable sealed interface TypeNamePattern { @Serializable - data class FullyQualified(val name: String) : TypeNamePattern { - override fun toString(): String = name + data class FullyQualified(val name: String, val typeArgs: List = emptyList()) : TypeNamePattern { + override fun toString(): String = if (typeArgs.isEmpty()) name else "$name<${typeArgs.joinToString(", ")}>" } @Serializable - data class ClassName(val name: String) : TypeNamePattern { - override fun toString(): String = "*.$name" + data class ClassName(val name: String, val typeArgs: List = emptyList()) : TypeNamePattern { + override fun toString(): String = if (typeArgs.isEmpty()) "*.$name" else "*.$name<${typeArgs.joinToString(", ")}>" } @Serializable diff --git a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/PatternRewriter.kt b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/PatternRewriter.kt index 40180b8a5..66922137d 100644 --- a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/PatternRewriter.kt +++ b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/PatternRewriter.kt @@ -262,6 +262,7 @@ interface PatternRewriter { fun TypeName.rewriteTypeName(): TypeName = when (this) { is TypeName.SimpleTypeName -> rewriteSimpleTypeName() is TypeName.ArrayTypeName -> rewriteArrayTypeName() + is TypeName.WildcardTypeName -> this } fun TypeName.SimpleTypeName.rewriteSimpleTypeName(): TypeName.SimpleTypeName = diff --git a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/PatternToActionListConverter.kt b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/PatternToActionListConverter.kt index 1a4fb128c..6b9d413f5 100644 --- a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/PatternToActionListConverter.kt +++ b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/PatternToActionListConverter.kt @@ -23,9 +23,6 @@ import org.opentaint.semgrep.pattern.Metavar import org.opentaint.semgrep.pattern.MetavarName import org.opentaint.semgrep.pattern.MethodArguments import org.opentaint.semgrep.pattern.MethodDeclaration -import org.opentaint.semgrep.pattern.MethodDeclarationReturnTypeHasTypeArgs -import org.opentaint.semgrep.pattern.MethodDeclarationReturnTypeIsArray -import org.opentaint.semgrep.pattern.MethodDeclarationReturnTypeIsNotMetaVar import org.opentaint.semgrep.pattern.MethodInvocation import org.opentaint.semgrep.pattern.Modifier import org.opentaint.semgrep.pattern.NamedValue @@ -39,7 +36,6 @@ import org.opentaint.semgrep.pattern.StaticFieldAccess import org.opentaint.semgrep.pattern.StringEllipsis import org.opentaint.semgrep.pattern.StringLiteral import org.opentaint.semgrep.pattern.ThisExpr -import org.opentaint.semgrep.pattern.TypeArgumentsIgnored import org.opentaint.semgrep.pattern.TypeName import org.opentaint.semgrep.pattern.TypedMetavar import org.opentaint.semgrep.pattern.VariableAssignment @@ -223,12 +219,11 @@ class PatternToActionListConverter: ActionListBuilder { val elementTypePattern = transformTypeName(typeName.elementType) TypeNamePattern.ArrayType(elementTypePattern) } + is TypeName.WildcardTypeName -> TypeNamePattern.AnyType } private fun transformSimpleTypeName(typeName: TypeName.SimpleTypeName): TypeNamePattern { - if (typeName.typeArgs.isNotEmpty()) { - semgrepTrace?.error(TypeArgumentsIgnored()) - } + val typeArgs = typeName.typeArgs.map { transformTypeName(it) } if (typeName.dotSeparatedParts.size == 1) { val name = typeName.dotSeparatedParts.single() @@ -240,7 +235,7 @@ class PatternToActionListConverter: ActionListBuilder { if (concreteNames.size == 1) { val className = concreteNames.single().name if (className.first().isUpperCase()) { - return TypeNamePattern.ClassName(className) + return TypeNamePattern.ClassName(className, typeArgs) } if (className in primitiveTypeNames) { @@ -251,7 +246,7 @@ class PatternToActionListConverter: ActionListBuilder { } val fqn = concreteNames.joinToString(".") { it.name } - return TypeNamePattern.FullyQualified(fqn) + return TypeNamePattern.FullyQualified(fqn, typeArgs) } transformationFailed("TypeName_non_concrete_unsupported") @@ -553,24 +548,7 @@ class PatternToActionListConverter: ActionListBuilder { is MetavarName -> SignatureName.MetaVar(name.metavarName) } - val retType = pattern.returnType - if (retType != null) { - run { - if (retType !is TypeName.SimpleTypeName) { - semgrepTrace?.error(MethodDeclarationReturnTypeIsArray()) - return@run - } - - val retTypeMetaVar = retType.dotSeparatedParts.singleOrNull() as? MetavarName - if (retTypeMetaVar == null) { - semgrepTrace?.error(MethodDeclarationReturnTypeIsNotMetaVar()) - } - - if (retType.typeArgs.isNotEmpty()) { - semgrepTrace?.error(MethodDeclarationReturnTypeHasTypeArgs()) - } - } - } + val returnTypePattern: TypeNamePattern? = pattern.returnType?.let { transformTypeName(it) } val paramConditions = mutableListOf() @@ -613,6 +591,7 @@ class PatternToActionListConverter: ActionListBuilder { val signature = SemgrepPatternAction.MethodSignature( methodName, ParamConstraint.Partial(paramConditions), + returnType = returnTypePattern, modifiers = modifiers, enclosingClassMetavar = null, enclosingClassConstraints = emptyList(), diff --git a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/SemgrepPatternAction.kt b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/SemgrepPatternAction.kt index d9d7b2645..5bc22259a 100644 --- a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/SemgrepPatternAction.kt +++ b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/SemgrepPatternAction.kt @@ -103,6 +103,7 @@ sealed interface SemgrepPatternAction { data class MethodSignature( val methodName: SignatureName, val params: ParamConstraint.Partial, + val returnType: TypeNamePattern? = null, val modifiers: List, val enclosingClassMetavar: String?, val enclosingClassConstraints: List, diff --git a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/automata/ActionListToAutomata.kt b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/automata/ActionListToAutomata.kt index aac0331e4..1a44cf421 100644 --- a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/automata/ActionListToAutomata.kt +++ b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/automata/ActionListToAutomata.kt @@ -284,6 +284,7 @@ private fun constructSignatureFormula( val signature = MethodSignature( methodName = methodName, enclosingClassName = MethodEnclosingClassName.anyClassName, + returnType = action.returnType, ) builder.addSignature(signature) diff --git a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/automata/Predicate.kt b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/automata/Predicate.kt index b0202cfc5..d13c56426 100644 --- a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/automata/Predicate.kt +++ b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/automata/Predicate.kt @@ -19,6 +19,7 @@ data class Predicate( data class MethodSignature( val methodName: MethodName, val enclosingClassName: MethodEnclosingClassName, + val returnType: TypeNamePattern? = null, ) @Serializable diff --git a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/AutomataToTaintRuleConversion.kt b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/AutomataToTaintRuleConversion.kt index d18c37652..202439905 100644 --- a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/AutomataToTaintRuleConversion.kt +++ b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/AutomataToTaintRuleConversion.kt @@ -11,33 +11,33 @@ import org.opentaint.dataflow.configuration.jvm.serialized.SerializedCondition.C import org.opentaint.dataflow.configuration.jvm.serialized.SerializedFieldRule import org.opentaint.dataflow.configuration.jvm.serialized.SerializedFunctionNameMatcher import org.opentaint.dataflow.configuration.jvm.serialized.SerializedItem -import org.opentaint.dataflow.configuration.jvm.serialized.SerializedSimpleNameMatcher -import org.opentaint.dataflow.configuration.jvm.serialized.SerializedTypeNameMatcher import org.opentaint.dataflow.configuration.jvm.serialized.SerializedRule +import org.opentaint.dataflow.configuration.jvm.serialized.SerializedSimpleNameMatcher import org.opentaint.dataflow.configuration.jvm.serialized.SerializedSimpleNameMatcher.Pattern import org.opentaint.dataflow.configuration.jvm.serialized.SerializedSimpleNameMatcher.Simple import org.opentaint.dataflow.configuration.jvm.serialized.SerializedTaintAssignAction import org.opentaint.dataflow.configuration.jvm.serialized.SerializedTaintCleanAction +import org.opentaint.dataflow.configuration.jvm.serialized.SerializedTypeNameMatcher import org.opentaint.dataflow.configuration.jvm.serialized.SinkMetaData import org.opentaint.dataflow.configuration.jvm.serialized.SinkRule +import org.opentaint.semgrep.pattern.FailedToConvertToTaintRule +import org.opentaint.semgrep.pattern.IgnoredMetavarConstraint import org.opentaint.semgrep.pattern.Mark.RuleUniqueMarkPrefix import org.opentaint.semgrep.pattern.MetaVarConstraint import org.opentaint.semgrep.pattern.MetaVarConstraintFormula -import org.opentaint.semgrep.pattern.ResolvedMetaVarInfo -import org.opentaint.semgrep.pattern.RuleWithMetaVars -import org.opentaint.semgrep.pattern.FailedToConvertToTaintRule -import org.opentaint.semgrep.pattern.IgnoredMetavarConstraint import org.opentaint.semgrep.pattern.NonMethodCallCleaner import org.opentaint.semgrep.pattern.PlaceholderAnnotation import org.opentaint.semgrep.pattern.PlaceholderMethodName import org.opentaint.semgrep.pattern.PlaceholderStringValue import org.opentaint.semgrep.pattern.PlaceholderTypeName -import org.opentaint.semgrep.pattern.TaintRuleMatchAnything +import org.opentaint.semgrep.pattern.ResolvedMetaVarInfo +import org.opentaint.semgrep.pattern.RuleWithMetaVars import org.opentaint.semgrep.pattern.SemgrepMatchingRule import org.opentaint.semgrep.pattern.SemgrepRule import org.opentaint.semgrep.pattern.SemgrepRuleLoadStepTrace import org.opentaint.semgrep.pattern.SemgrepTaintRule import org.opentaint.semgrep.pattern.TaintRuleFromSemgrep +import org.opentaint.semgrep.pattern.TaintRuleMatchAnything import org.opentaint.semgrep.pattern.UserRuleFromSemgrepInfo import org.opentaint.semgrep.pattern.conversion.IsMetavar import org.opentaint.semgrep.pattern.conversion.MetavarAtom @@ -497,6 +497,11 @@ private fun MethodSignature.notEvaluatedSignature(evaluated: MethodSignature): M MethodEnclosingClassName.anyClassName } else { enclosingClassName + }, + returnType = if (returnType == evaluated.returnType) { + null + } else { + returnType } ) } @@ -556,6 +561,18 @@ private fun TaintRuleGenerationCtx.evaluateFormulaSignature( } } + val returnType = signature.returnType + if (returnType != null) { + val returnTypeFormula = typeMatcher(returnType, semgrepRuleTrace) + if (returnTypeFormula != null) { + for (builder in buildersWithMethodName) { + builder.conditions += returnTypeFormula.toSerializedCondition { typeNameMatcher -> + SerializedCondition.IsType(typeNameMatcher, PositionBase.Result) + } + } + } + } + val classSignatureMatcherFormula = typeMatcher(signature.enclosingClassName.name, semgrepRuleTrace) if (classSignatureMatcherFormula == null) return signature to buildersWithMethodName @@ -679,6 +696,12 @@ private fun classNameMatcherFromConcreteString(name: String): SerializedTypeName return SerializedTypeNameMatcher.ClassPattern(pkg, cls) } +private fun anyClassPattern(): SerializedTypeNameMatcher.ClassPattern = + SerializedTypeNameMatcher.ClassPattern( + `package` = anyName(), + `class` = anyName() + ) + private fun TaintRuleGenerationCtx.evaluateEdgePredicateConstraint( edgeKind: TaintEdgeKind, state: State, @@ -804,17 +827,33 @@ private fun TaintRuleGenerationCtx.typeMatcher( semgrepRuleTrace: SemgrepRuleLoadStepTrace ): MetaVarConstraintFormula? { return when (typeName) { - is TypeNamePattern.ClassName -> MetaVarConstraintFormula.Constraint( - SerializedTypeNameMatcher.ClassPattern( - `package` = anyName(), - `class` = Simple(typeName.name) + is TypeNamePattern.ClassName -> { + val serializedTypeArgs = typeArgsMatcher(typeName.typeArgs, semgrepRuleTrace) + MetaVarConstraintFormula.Constraint( + SerializedTypeNameMatcher.ClassPattern( + `package` = anyName(), + `class` = Simple(typeName.name), + typeArgs = serializedTypeArgs + ) ) - ) + } is TypeNamePattern.FullyQualified -> { - MetaVarConstraintFormula.Constraint( - Simple(typeName.name) - ) + val serializedTypeArgs = typeArgsMatcher(typeName.typeArgs, semgrepRuleTrace) + if (serializedTypeArgs == null) { + MetaVarConstraintFormula.Constraint( + Simple(typeName.name) + ) + } else { + val (pkg, cls) = classNamePartsFromConcreteString(typeName.name) + MetaVarConstraintFormula.Constraint( + SerializedTypeNameMatcher.ClassPattern( + `package` = pkg, + `class` = cls, + typeArgs = serializedTypeArgs + ) + ) + } } is TypeNamePattern.PrimitiveName -> { @@ -891,6 +930,14 @@ private fun TaintRuleGenerationCtx.typeMatcher( } } +private fun TaintRuleGenerationCtx.typeArgsMatcher( + typeArgs: List, + semgrepRuleTrace: SemgrepRuleLoadStepTrace +): List? = typeArgs.takeIf { it.isNotEmpty() }?.map { + (typeMatcher(it, semgrepRuleTrace) as? MetaVarConstraintFormula.Constraint)?.constraint + ?: anyClassPattern() +} + private fun String.patternCanMatchDot(): Boolean = '.' in this || '-' in this // [A-Z] diff --git a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/MethodFormulaSimplifier.kt b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/MethodFormulaSimplifier.kt index e2ea527be..25c76c1aa 100644 --- a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/MethodFormulaSimplifier.kt +++ b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/MethodFormulaSimplifier.kt @@ -5,8 +5,6 @@ import org.opentaint.dataflow.util.filter import org.opentaint.dataflow.util.forEach import org.opentaint.dataflow.util.map import org.opentaint.dataflow.util.toSet -import org.opentaint.semgrep.pattern.conversion.automata.OperationCancelation -import org.opentaint.semgrep.pattern.conversion.generatedMethodClassName import org.opentaint.semgrep.pattern.MetaVarConstraint import org.opentaint.semgrep.pattern.MetaVarConstraintFormula import org.opentaint.semgrep.pattern.MetaVarConstraints @@ -27,10 +25,12 @@ import org.opentaint.semgrep.pattern.conversion.automata.MethodModifierConstrain import org.opentaint.semgrep.pattern.conversion.automata.MethodName import org.opentaint.semgrep.pattern.conversion.automata.MethodSignature import org.opentaint.semgrep.pattern.conversion.automata.NumberOfArgsConstraint +import org.opentaint.semgrep.pattern.conversion.automata.OperationCancelation import org.opentaint.semgrep.pattern.conversion.automata.ParamConstraint import org.opentaint.semgrep.pattern.conversion.automata.Position import org.opentaint.semgrep.pattern.conversion.automata.Predicate import org.opentaint.semgrep.pattern.conversion.generatedAnyValueGeneratorMethodName +import org.opentaint.semgrep.pattern.conversion.generatedMethodClassName import org.opentaint.semgrep.pattern.conversion.generatedStringConcatMethodName import java.util.BitSet @@ -709,9 +709,15 @@ private fun MethodSignature?.unify( metaVarInfo: ResolvedMetaVarInfo, ): MethodSignature? { if (this == null) return other + val unifiedReturnType = when { + this.returnType == null -> other.returnType + other.returnType == null -> this.returnType + else -> unifyTypeName(this.returnType, other.returnType, metaVarInfo) + } return MethodSignature( methodName.unify(other.methodName, metaVarInfo) ?: return null, enclosingClassName.unify(other.enclosingClassName, metaVarInfo) ?: return null, + returnType = unifiedReturnType, ) } @@ -766,6 +772,18 @@ private fun MethodEnclosingClassName.unify( unifyTypeName(this.name, other.name, metaVarInfo) ?.let { MethodEnclosingClassName(it) } +private fun unifyTypeArgs( + left: List, + right: List, + metaVarInfo: ResolvedMetaVarInfo +): List? { + if (left.isEmpty()) return right + if (right.isEmpty()) return left + if (left.size != right.size) return null + val unified = left.zip(right).map { (l, r) -> unifyTypeName(l, r, metaVarInfo) ?: return null } + return unified +} + private fun unifyTypeName( left: TypeNamePattern, right: TypeNamePattern, @@ -782,11 +800,19 @@ private fun unifyTypeName( TypeNamePattern.AnyType -> return left is TypeNamePattern.ArrayType, - is TypeNamePattern.ClassName, is TypeNamePattern.PrimitiveName -> return null + is TypeNamePattern.ClassName -> { + if (left.name != right.name) return null + val args = unifyTypeArgs(left.typeArgs, right.typeArgs, metaVarInfo) ?: return null + return TypeNamePattern.ClassName(left.name, args) + } + is TypeNamePattern.FullyQualified -> { - if (right.name.endsWith(left.name)) return right + if (right.name.endsWith(left.name)) { + val args = unifyTypeArgs(left.typeArgs, right.typeArgs, metaVarInfo) ?: return null + return TypeNamePattern.FullyQualified(right.name, args) + } return null } @@ -803,11 +829,18 @@ private fun unifyTypeName( is TypeNamePattern.PrimitiveName -> return null is TypeNamePattern.ClassName -> { - if (left.name.endsWith(right.name)) return left + if (left.name.endsWith(right.name)) { + val args = unifyTypeArgs(left.typeArgs, right.typeArgs, metaVarInfo) ?: return null + return TypeNamePattern.FullyQualified(left.name, args) + } return null } - is TypeNamePattern.FullyQualified -> return null + is TypeNamePattern.FullyQualified -> { + if (left.name != right.name) return null + val args = unifyTypeArgs(left.typeArgs, right.typeArgs, metaVarInfo) ?: return null + return TypeNamePattern.FullyQualified(left.name, args) + } is TypeNamePattern.MetaVar -> { if (left.name == generatedMethodClassName) return null diff --git a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/TaintEdgesGeneration.kt b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/TaintEdgesGeneration.kt index 287afb4a0..25c463ea5 100644 --- a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/TaintEdgesGeneration.kt +++ b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/TaintEdgesGeneration.kt @@ -12,9 +12,7 @@ import org.opentaint.semgrep.pattern.conversion.SemgrepPatternAction.ClassConstr import org.opentaint.semgrep.pattern.conversion.SemgrepPatternAction.SignatureModifier import org.opentaint.semgrep.pattern.conversion.SemgrepPatternAction.SignatureModifierValue import org.opentaint.semgrep.pattern.conversion.SemgrepPatternAction.SignatureName -import org.opentaint.semgrep.pattern.conversion.SpecificBoolValue import org.opentaint.semgrep.pattern.conversion.SpecificConstantValue -import org.opentaint.semgrep.pattern.conversion.SpecificStringValue import org.opentaint.semgrep.pattern.conversion.TypeNamePattern import org.opentaint.semgrep.pattern.conversion.automata.ClassModifierConstraint import org.opentaint.semgrep.pattern.conversion.automata.MethodConstraint @@ -363,10 +361,16 @@ private fun MetaVarCtx.typeNameMetaVars(typeName: TypeNamePattern, metaVars: Bit } TypeNamePattern.AnyType, - is TypeNamePattern.ClassName, - is TypeNamePattern.PrimitiveName, - is TypeNamePattern.FullyQualified -> { + is TypeNamePattern.PrimitiveName -> { // no metavars } + + is TypeNamePattern.ClassName -> { + typeName.typeArgs.forEach { typeNameMetaVars(it, metaVars) } + } + + is TypeNamePattern.FullyQualified -> { + typeName.typeArgs.forEach { typeNameMetaVars(it, metaVars) } + } } } diff --git a/core/opentaint-java-querylang/src/test/kotlin/org/opentaint/semgrep/TypeAwarePatternTest.kt b/core/opentaint-java-querylang/src/test/kotlin/org/opentaint/semgrep/TypeAwarePatternTest.kt new file mode 100644 index 000000000..8eb7e0d87 --- /dev/null +++ b/core/opentaint-java-querylang/src/test/kotlin/org/opentaint/semgrep/TypeAwarePatternTest.kt @@ -0,0 +1,117 @@ +package org.opentaint.semgrep + +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS +import org.opentaint.semgrep.util.SampleBasedTest +import kotlin.test.Test + +@TestInstance(PER_CLASS) +class TypeAwarePatternTest : SampleBasedTest() { + @Test + fun `test generic type args in method parameter`() = runTest(EXPECT_STATE_VAR) + + @Test + fun `test array return type matching`() = runTest() + + @Test + fun `test concrete return type matching`() = runTest() + + @Test + fun `test generic return type with metavar type arg`() = runTest() + + // A1. ResponseEntity — array type as a concrete type argument. + @Test + fun `A1 - ResponseEntity of byte array return type`() = runTest() + + // A2. ResponseEntity<$T> — metavar type arg resolving to any concrete type, + // including arrays and the raw form. All three method-decl forms are expected + // to match. + @Test + fun `A2 - ResponseEntity metavar matches parameterized string, byte array, and raw`() = + runTest() + + // A3. Nested generic: ResponseEntity>. + @Test + fun `A3 - nested generic ResponseEntity of List of String return type`() = + runTest() + + // A4. Two-arg generic: Map<$K, $V>. + @Test + fun `A4 - two-arg generic Map of K V in parameter`() = runTest() + + // A5. Wildcard type argument: ResponseEntity. Java's `?` is the + // supertype of any concrete parameterization, so `` accepts both + // ResponseEntity and ResponseEntity. + @Test + fun `A5 - wildcard type argument ResponseEntity of question mark`() = + runTest() + + // A8. Mixed metavar + concrete: Map<$K, String> — $K is a metavar, second + // slot is concrete String. + @Test + fun `A8 - mixed metavar and concrete Map of K String`() = + runTest() + + // A10. Deep nesting: List> — Negatives are List + // (missing outer) and List> (inner mismatch). + @Test + fun `A10 - deep nesting List of List of String`() = runTest() + + // A12. Parameter-position concrete-vs-metavar discrimination: first + // parameter is concrete List, not a metavar. + @Test + fun `A12 - parameter position concrete List of String`() = + runTest() + + // A13. Fully-qualified type argument: ResponseEntity. + @Test + fun `A13 - fully-qualified type argument ResponseEntity of java lang String`() = + runTest() + + // A15. Array of parameterized type: List[] return. + @Test + fun `A15 - array of parameterized type List of String array`() = + runTest() + + // A17. Concrete return type discriminates a different concrete return. + // Rule return is String; Negative method returns Integer. + @Test + fun `A17 - concrete return String discriminates from Integer`() = + runTest() + + // A19. Nested generic in parameter position: + // List> — complement to A10 (nested generic in + // return position). + @Test + fun `A19 - nested generic in parameter List of Map of String Integer`() = + runTest() + + // A20. Class<$T> reflection-style parameter. + @Test + fun `A20 - Class of T parameter`() = runTest() + + // A21. Interface vs class widening: Collection pattern vs + // List method. Observed behavior: engine uses exact-type + // matching at the method-decl return position — no subtype widening. + // The List sample was flipped from Positive to Negative to + // match the engine's actual semantics. + @Test + fun `A21 - Collection of String return exact type match no subtype widening`() = + runTest() + + // A22. Nested mixed containers: Map>. + @Test + fun `A22 - nested mixed containers Map of String List of Integer`() = + runTest() + + // A23. Array dimension mismatch: String[][] return. + @Test + fun `A23 - array dimension mismatch String two dim`() = + runTest() + + @AfterAll + fun close() { + closeRunner() + } +} diff --git a/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/jvm/sast/dataflow/rules/ClassNameUtils.kt b/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/jvm/sast/dataflow/rules/ClassNameUtils.kt index 882278bba..f3b8633a7 100644 --- a/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/jvm/sast/dataflow/rules/ClassNameUtils.kt +++ b/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/jvm/sast/dataflow/rules/ClassNameUtils.kt @@ -14,7 +14,7 @@ fun Pattern.isAny(): Boolean = pattern == ".*" fun SerializedTypeNameMatcher.normalizeAnyName(): SerializedTypeNameMatcher = when (this) { is SerializedSimpleNameMatcher -> normalizeAnyName() - is ClassPattern -> ClassPattern(`package`.normalizeAnyName(), `class`.normalizeAnyName()) + is ClassPattern -> ClassPattern(`package`.normalizeAnyName(), `class`.normalizeAnyName(), typeArgs?.map { it.normalizeAnyName() }) is SerializedTypeNameMatcher.Array -> SerializedTypeNameMatcher.Array(element.normalizeAnyName()) } diff --git a/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/jvm/sast/dataflow/rules/TaintConfiguration.kt b/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/jvm/sast/dataflow/rules/TaintConfiguration.kt index a59802d8d..7427fdc78 100644 --- a/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/jvm/sast/dataflow/rules/TaintConfiguration.kt +++ b/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/jvm/sast/dataflow/rules/TaintConfiguration.kt @@ -45,6 +45,7 @@ import org.opentaint.dataflow.configuration.jvm.TaintStaticFieldSource import org.opentaint.dataflow.configuration.jvm.This import org.opentaint.dataflow.configuration.jvm.TypeMatchesPattern import org.opentaint.dataflow.configuration.jvm.isFalse +import org.opentaint.dataflow.configuration.jvm.matchType import org.opentaint.dataflow.configuration.jvm.mkAnd import org.opentaint.dataflow.configuration.jvm.mkFalse import org.opentaint.dataflow.configuration.jvm.mkOr @@ -76,9 +77,12 @@ import org.opentaint.dataflow.configuration.jvm.simplify import org.opentaint.dataflow.jvm.util.JIRHierarchyInfo import org.opentaint.ir.api.jvm.JIRAnnotated import org.opentaint.ir.api.jvm.JIRAnnotation +import org.opentaint.ir.api.jvm.JIRClassType import org.opentaint.ir.api.jvm.JIRClasspath import org.opentaint.ir.api.jvm.JIRField import org.opentaint.ir.api.jvm.JIRMethod +import org.opentaint.ir.api.jvm.JIRType +import org.opentaint.ir.api.jvm.JIRTypedMethod import org.opentaint.ir.api.jvm.PredefinedPrimitives import org.opentaint.ir.api.jvm.TypeName import org.opentaint.ir.api.jvm.ext.allSuperHierarchySequence @@ -89,7 +93,7 @@ import org.opentaint.jvm.sast.dataflow.matchedAnnotations import org.opentaint.jvm.util.typename import java.util.concurrent.atomic.AtomicInteger -class TaintConfiguration(cp: JIRClasspath) { +class TaintConfiguration(private val cp: JIRClasspath) { private val patternManager = PatternManager() private val hierarchyInfo = JIRHierarchyInfo(cp) private val objectTypeName = cp.objectClass.typename @@ -279,26 +283,34 @@ class TaintConfiguration(cp: JIRClasspath) { } private fun SerializedSignatureMatcher.matchFunctionSignature(method: JIRMethod): Boolean { + val typedMethod by lazy { resolveTypedMethod(method) } + when (this) { is SerializedSignatureMatcher.Simple -> { if (method.parameters.size != args.size) return false + if (!`return`.matchTypedOrErased(method.returnType.typeName) { typedMethod?.returnType }) return false - if (!`return`.match(method.returnType.typeName)) return false - - return args.zip(method.parameters).all { (matcher, param) -> - matcher.match(param.type.typeName) + return args.withIndex().all { (idx, matcher) -> + matcher.matchTypedOrErased(method.parameters[idx].type.typeName) { + typedMethod?.parameters?.getOrNull(idx)?.type + } } } is SerializedSignatureMatcher.Partial -> { val ret = `return` - if (ret != null && !ret.match(method.returnType.typeName)) return false + if (ret != null) { + if (!ret.matchTypedOrErased(method.returnType.typeName) { typedMethod?.returnType }) return false + } - val params = params - if (params != null) { - for (param in params) { + val paramList = params + if (paramList != null) { + for (param in paramList) { val methodParam = method.parameters.getOrNull(param.index) ?: return false - if (!param.type.match(methodParam.type.typeName)) return false + val paramTypeMatched = param.type.matchTypedOrErased(methodParam.type.typeName) { + typedMethod?.parameters?.getOrNull(param.index)?.type + } + if (!paramTypeMatched) return false } } @@ -307,6 +319,22 @@ class TaintConfiguration(cp: JIRClasspath) { } } + private fun SerializedTypeNameMatcher.matchTypedOrErased(erased: String, resolveType: () -> JIRType?): Boolean { + return withTypeResolutionFailureHandling(onFailure = { true }) { + matchType(erased, { resolveType() ?: throw TypeResolutionFailed() }, { name -> match(name) }) + } + } + + private inline fun withTypeResolutionFailureHandling(onFailure: () -> T, body: () -> T): T = try { + body() + } catch (e: TypeResolutionFailed) { + onFailure() + } + + private class TypeResolutionFailed : Exception() { + override fun fillInStackTrace(): Throwable = this + } + private fun SerializedFieldRule.resolveFieldRule(field: JIRField): List { when (this) { is SerializedFieldRule.SerializedStaticFieldSource -> { @@ -678,21 +706,27 @@ class TaintConfiguration(cp: JIRClasspath) { val falsePositions = hashSetOf() val normalizedTypeIs = typeIs.normalizeAnyName() + + val typedMethod by lazy { resolveTypedMethod(method) } + for (pos in position) { val posTypeName = when (pos) { is Argument -> method.parameters[pos.index].type.typeName - Result -> method.returnType.typeName - This -> method.enclosingClass.name + is Result -> method.returnType.typeName + is This -> method.enclosingClass.name is PositionWithAccess, is ClassStatic -> continue } - if (normalizedTypeIs.match(posTypeName)) return mkTrue() + if (normalizedTypeIs.matchTypedOrErased(posTypeName) { typedMethod?.positionType(pos) }) { + return mkTrue() + } if (pos is This) { - if (method.enclosingClass.allSuperHierarchySequence.any { normalizedTypeIs.match(it.name) }) { - return mkTrue() + val anySuperTypeMatch = method.enclosingClass.allSuperHierarchySequence.any { + normalizedTypeIs.matchTypedOrErased(it.name) { typedMethod?.positionType(This) } } + if (anySuperTypeMatch) return mkTrue() if (method.isConstructor || method.isFinal) { falsePositions.add(pos) @@ -704,7 +738,22 @@ class TaintConfiguration(cp: JIRClasspath) { ?: return mkTrue() val nonFalsePositions = position.filter { it !in falsePositions } - return mkOr(nonFalsePositions.map { TypeMatchesPattern(it, matcher) }) + val typeArgs = (normalizedTypeIs as? ClassPattern)?.typeArgs + ?.map { it.toTypeArgMatcher(patternManager) } + + return mkOr(nonFalsePositions.map { TypeMatchesPattern(it, matcher, typeArgs) }) + } + + private fun JIRTypedMethod.positionType(pos: Position): JIRType? = when (pos) { + is Argument -> parameters.getOrNull(pos.index)?.type + is Result -> returnType + is This -> enclosingType + else -> null + } + + private fun resolveTypedMethod(method: JIRMethod): JIRTypedMethod? { + val classType = cp.typeOf(method.enclosingClass) as? JIRClassType ?: return null + return classType.declaredMethods.find { it.method == method } } private fun SerializedTaintAssignAction.resolveWithArray(method: JIRMethod, ctx: AnyArgSpecializationCtx): List = diff --git a/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/jvm/sast/dataflow/rules/TypeMatcherCondition.kt b/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/jvm/sast/dataflow/rules/TypeMatcherCondition.kt index 03782e6c7..b11319b6e 100644 --- a/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/jvm/sast/dataflow/rules/TypeMatcherCondition.kt +++ b/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/jvm/sast/dataflow/rules/TypeMatcherCondition.kt @@ -1,6 +1,7 @@ package org.opentaint.jvm.sast.dataflow.rules import org.opentaint.dataflow.configuration.jvm.ConditionNameMatcher +import org.opentaint.dataflow.configuration.jvm.TypeArgMatcher import org.opentaint.dataflow.configuration.jvm.serialized.SerializedSimpleNameMatcher import org.opentaint.dataflow.configuration.jvm.serialized.SerializedTypeNameMatcher import org.opentaint.dataflow.configuration.jvm.serialized.SerializedTypeNameMatcher.ClassPattern @@ -19,6 +20,18 @@ fun SerializedSimpleNameMatcher.toConditionNameMatcher(patternManager: PatternMa } } +fun SerializedTypeNameMatcher.toTypeArgMatcher(patternManager: PatternManager): TypeArgMatcher = when (this) { + is SerializedTypeNameMatcher.Array -> TypeArgMatcher.Array(element.toTypeArgMatcher(patternManager)) + is ClassPattern -> { + val name = toConditionNameMatcher(patternManager) ?: ConditionNameMatcher.AnyName + TypeArgMatcher.Class(name, typeArgs?.map { it.toTypeArgMatcher(patternManager) }) + } + is SerializedSimpleNameMatcher -> { + val name = toConditionNameMatcher(patternManager) ?: ConditionNameMatcher.AnyName + TypeArgMatcher.Class(name, typeArgs = null) + } +} + fun SerializedTypeNameMatcher.toConditionNameMatcher(patternManager: PatternManager): ConditionNameMatcher? { return when (this) { is Simple -> ConditionNameMatcher.Concrete(value)