From 5cdc230e3ba63a5d7af7423fb0fd52a390b0e32f Mon Sep 17 00:00:00 2001 From: Aleksandr Misonizhnik Date: Wed, 6 May 2026 07:12:28 +0200 Subject: [PATCH 1/6] fix(analyzer): Distinguish raw vs parameterized class types in pattern matching A pattern-inside `ResponseEntity` combined with a pattern-not-inside `ResponseEntity<$T>` was collapsing to an always-false condition: the negative method-decl predicate dropped its returnType when its signature differed from the unified positive signature, and JacoDB-resolved raw class types (whose `typeArguments` are the class's own declared type parameters) satisfied a parameterized `ResponseEntity<$T>` matcher. Add the missing return-type IsType clause for predicates that retain a distinct returnType, and treat a JIRClassType whose typeArguments are exactly its declared typeParameters as raw so a parameterized matcher no longer matches it. --- .../jvm/SerializedTypeMatching.kt | 19 +++++++++ .../ap/ifds/taint/JIRBasicAtomEvaluator.kt | 15 +++++++ .../RuleWithGenericMetavarArrayArg.java | 13 ++---- ...WithRawResponseEntityNotInsideGeneric.java | 41 +++++++++++++++++++ ...WithRawResponseEntityNotInsideGeneric.yaml | 21 ++++++++++ .../taint/AutomataToTaintRuleConversion.kt | 10 +++++ .../opentaint/semgrep/TypeAwarePatternTest.kt | 17 ++++++-- 7 files changed, 123 insertions(+), 13 deletions(-) create mode 100644 core/opentaint-java-querylang/samples/src/main/java/example/RuleWithRawResponseEntityNotInsideGeneric.java create mode 100644 core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithRawResponseEntityNotInsideGeneric.yaml 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 index bb584123d..7f5f37442 100644 --- 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 @@ -30,6 +30,8 @@ private fun SerializedTypeNameMatcher.matchTypeArgs( val type = resolveType() if (type !is JIRClassType) return false + if (type.isRawClassType()) return false + if (args.size != type.typeArguments.size) return false args.zip(type.typeArguments).all { (m, a) -> @@ -44,6 +46,23 @@ private fun SerializedTypeNameMatcher.matchTypeArgs( } } +/** + * A raw use of a generic class (e.g. `ResponseEntity` written without `<>`) is + * represented by JacoDB as a [JIRClassType] whose [JIRClassType.typeArguments] + * are the class's own declared type parameters (rather than empty). Detect this + * shape so that a parameterized pattern like `ResponseEntity<$T>` does not + * spuriously match a raw use site. + */ +private fun JIRClassType.isRawClassType(): Boolean { + val params = typeParameters + if (params.isEmpty()) return false + val args = typeArguments + if (args.size != params.size) return false + return args.zip(params).all { (arg, param) -> + arg is JIRTypeVariable && arg.symbol == param.symbol + } +} + /** * Erased class name for matching — drops any generic decoration that * [JIRType.typeName] may carry (e.g. `Map` → `java.util.Map`) 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 70fd33e36..03f6da72f 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 @@ -37,6 +37,7 @@ 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.JIRTypeVariable import org.opentaint.ir.api.jvm.cfg.JIRBool import org.opentaint.ir.api.jvm.cfg.JIRCallExpr import org.opentaint.ir.api.jvm.cfg.JIRConstant @@ -359,6 +360,8 @@ class JIRBasicAtomEvaluator( if (type !is JIRClassType) return true + if (type.isRawClassType()) return false + if (type.typeArguments.size != typeArgs.size) return false return typeArgs.zip(type.typeArguments).all { (matcher, arg) -> matcher.matchType(arg) @@ -400,10 +403,22 @@ class JIRBasicAtomEvaluator( return true } + if (type.isRawClassType()) return false + if (args.size != type.typeArguments.size) return false return args.zip(type.typeArguments).all { (m, a) -> m.matchType(a) } } + private fun JIRClassType.isRawClassType(): Boolean { + val params = typeParameters + if (params.isEmpty()) return false + val args = typeArguments + if (args.size != params.size) return false + return args.zip(params).all { (arg, param) -> + arg is JIRTypeVariable && arg.symbol == param.symbol + } + } + 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/RuleWithGenericMetavarArrayArg.java b/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithGenericMetavarArrayArg.java index bf29a93a8..26245ae3e 100644 --- a/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithGenericMetavarArrayArg.java +++ b/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithGenericMetavarArrayArg.java @@ -2,7 +2,6 @@ import base.RuleSample; import base.RuleSet; -import base.TaintRuleFalsePositive; import org.springframework.http.ResponseEntity; @RuleSet("example/RuleWithGenericMetavarArrayArg.yaml") @@ -20,10 +19,6 @@ ResponseEntity methodReturningResponseEntityByteArray(String 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); @@ -47,11 +42,11 @@ public void entrypoint() { } /** - * 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. + * The rule pattern is {@code ResponseEntity<$T>}, which requires a concrete + * type argument. A raw use of {@code ResponseEntity} (no type argument) + * therefore must NOT match. */ - final static class PositiveRawResponseEntity extends RuleWithGenericMetavarArrayArg { + final static class NegativeRawResponseEntity extends RuleWithGenericMetavarArrayArg { @Override public void entrypoint() { String data = "tainted"; diff --git a/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithRawResponseEntityNotInsideGeneric.java b/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithRawResponseEntityNotInsideGeneric.java new file mode 100644 index 000000000..37f2fc6f0 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithRawResponseEntityNotInsideGeneric.java @@ -0,0 +1,41 @@ +package example; + +import base.RuleSample; +import base.RuleSet; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; + +@RuleSet("example/RuleWithRawResponseEntityNotInsideGeneric.yaml") +public abstract class RuleWithRawResponseEntityNotInsideGeneric implements RuleSample { + + void sink(String data) {} + + @SuppressWarnings("rawtypes") + @GetMapping("/raw") + ResponseEntity rawResponseEntity(String data) { + sink(data); + return null; + } + + @GetMapping("/parametrized") + ResponseEntity parametrizedResponseEntity(String data) { + sink(data); + return null; + } + + final static class PositiveRawResponseEntity extends RuleWithRawResponseEntityNotInsideGeneric { + @Override + public void entrypoint() { + String data = "tainted"; + rawResponseEntity(data); + } + } + + final static class NegativeParametrizedResponseEntity extends RuleWithRawResponseEntityNotInsideGeneric { + @Override + public void entrypoint() { + String data = "tainted"; + parametrizedResponseEntity(data); + } + } +} diff --git a/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithRawResponseEntityNotInsideGeneric.yaml b/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithRawResponseEntityNotInsideGeneric.yaml new file mode 100644 index 000000000..89125ca45 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithRawResponseEntityNotInsideGeneric.yaml @@ -0,0 +1,21 @@ +rules: + - id: example-RuleWithRawResponseEntityNotInsideGeneric + languages: + - java + severity: ERROR + message: match example/RuleWithRawResponseEntityNotInsideGeneric + patterns: + - pattern: |- + ... + sink($A); + ... + - pattern-inside: |- + @$ANNOTATION(...) + ResponseEntity $METHOD(..., String $A, ...) { + ... + } + - pattern-not-inside: |- + @$ANNOTATION(...) + ResponseEntity<$T> $METHOD(..., String $A, ...) { + ... + } 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 202439905..298a2aa22 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 @@ -812,6 +812,16 @@ private fun TaintRuleGenerationCtx.evaluateMethodSignatureCondition( SerializedCondition.and(cond) } + + val returnType = signature.returnType + if (returnType != null) { + val returnTypeFormula = typeMatcher(returnType, semgrepRuleTrace) + if (returnTypeFormula != null) { + conditions += returnTypeFormula.toSerializedCondition { typeNameMatcher -> + SerializedCondition.IsType(typeNameMatcher, PositionBase.Result) + } + } + } } private fun findMetaVarPosition( 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 index 8eb7e0d87..8e170dc17 100644 --- 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 @@ -24,11 +24,11 @@ class TypeAwarePatternTest : SampleBasedTest() { @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. + // A2. ResponseEntity<$T> — metavar type arg requires a concrete type + // argument, so it matches parameterized forms (String, byte[]) but NOT + // the raw use (no type argument). @Test - fun `A2 - ResponseEntity metavar matches parameterized string, byte array, and raw`() = + fun `A2 - ResponseEntity metavar matches parameterized string and byte array but not raw`() = runTest() // A3. Nested generic: ResponseEntity>. @@ -110,6 +110,15 @@ class TypeAwarePatternTest : SampleBasedTest() { fun `A23 - array dimension mismatch String two dim`() = runTest() + // A24. pattern-not-inside discriminates raw vs. parameterized return type. + // pattern-inside requires raw `ResponseEntity` (no type args), and + // pattern-not-inside excludes `ResponseEntity<$T>` (parameterized). + // A method returning raw `ResponseEntity` should match (Positive); a + // method returning `ResponseEntity` should be excluded (Negative). + @Test + fun `A24 - pattern-not-inside discriminates raw vs parameterized ResponseEntity`() = + runTest() + @AfterAll fun close() { closeRunner() From ca17f2a8232a4aace83430d8bce8270f68d9b23e Mon Sep 17 00:00:00 2001 From: Aleksandr Misonizhnik Date: Thu, 7 May 2026 12:21:10 +0200 Subject: [PATCH 2/6] feat(analyzer): Treat raw and wildcard type args as "any type" in matching MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Generalize generic-type pattern matching so raw class uses, unbounded wildcards ``, and unconstrained metavars `<$T>` all denote the same set of types — "any parameterization". A concrete pattern arg like `` therefore matches raw `Foo` and `Foo` (the unknown could be `String`), while still rejecting other concrete args like ``. Concretely, the runtime matchers short-circuit when the actual type argument is a `JIRTypeVariable` (raw class form) or `JIRUnboundWildcard` (``), and the rule converter collapses an all-wildcard parameter list to a no-args constraint so `Foo` and raw `Foo` are indistinguishable downstream. The previously-introduced raw-vs-parameterized rejection is removed. Tests: add A25 (`` pattern) and A26 (`` pattern) covering the cross-product against ``, ``, raw, and other concrete types; flip A2's raw case from Negative to Positive; drop A24 (its raw-vs-`<$T>` discrimination premise no longer exists). --- .../jvm/SerializedTypeMatching.kt | 24 +---- .../ap/ifds/taint/JIRBasicAtomEvaluator.kt | 28 +++--- .../RuleWithGenericMetavarArrayArg.java | 8 +- .../java/example/RuleWithObjectTypeArg.java | 95 +++++++++++++++++++ ...WithRawResponseEntityNotInsideGeneric.java | 41 -------- ...ithStringTypeArgMatchesRawAndWildcard.java | 94 ++++++++++++++++++ .../java/example/RuleWithWildcardGeneric.java | 43 ++++++++- .../example/RuleWithObjectTypeArg.yaml | 15 +++ ...WithRawResponseEntityNotInsideGeneric.yaml | 21 ---- ...ithStringTypeArgMatchesRawAndWildcard.yaml | 15 +++ .../taint/AutomataToTaintRuleConversion.kt | 13 ++- .../opentaint/semgrep/TypeAwarePatternTest.kt | 37 +++++--- 12 files changed, 311 insertions(+), 123 deletions(-) create mode 100644 core/opentaint-java-querylang/samples/src/main/java/example/RuleWithObjectTypeArg.java delete mode 100644 core/opentaint-java-querylang/samples/src/main/java/example/RuleWithRawResponseEntityNotInsideGeneric.java create mode 100644 core/opentaint-java-querylang/samples/src/main/java/example/RuleWithStringTypeArgMatchesRawAndWildcard.java create mode 100644 core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithObjectTypeArg.yaml delete mode 100644 core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithRawResponseEntityNotInsideGeneric.yaml create mode 100644 core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithStringTypeArgMatchesRawAndWildcard.yaml 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 index 7f5f37442..4c563b1f1 100644 --- 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 @@ -30,11 +30,14 @@ private fun SerializedTypeNameMatcher.matchTypeArgs( val type = resolveType() if (type !is JIRClassType) return false - if (type.isRawClassType()) return false - if (args.size != type.typeArguments.size) return false args.zip(type.typeArguments).all { (m, a) -> + // Raw class type arguments (`JIRTypeVariable`) and unbounded + // wildcards (``) denote "any type", so any pattern matcher + // accepts them — the unknown type could be whatever the + // pattern requires. + if (a is JIRTypeVariable || a is JIRUnboundWildcard) return@all true m.matchType(a.erasedName(), resolveType = { a }, erasedMatch) } } @@ -46,23 +49,6 @@ private fun SerializedTypeNameMatcher.matchTypeArgs( } } -/** - * A raw use of a generic class (e.g. `ResponseEntity` written without `<>`) is - * represented by JacoDB as a [JIRClassType] whose [JIRClassType.typeArguments] - * are the class's own declared type parameters (rather than empty). Detect this - * shape so that a parameterized pattern like `ResponseEntity<$T>` does not - * spuriously match a raw use site. - */ -private fun JIRClassType.isRawClassType(): Boolean { - val params = typeParameters - if (params.isEmpty()) return false - val args = typeArguments - if (args.size != params.size) return false - return args.zip(params).all { (arg, param) -> - arg is JIRTypeVariable && arg.symbol == param.symbol - } -} - /** * Erased class name for matching — drops any generic decoration that * [JIRType.typeName] may carry (e.g. `Map` → `java.util.Map`) 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 03f6da72f..ed7b678f7 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 @@ -38,6 +38,7 @@ 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.JIRTypeVariable +import org.opentaint.ir.api.jvm.JIRUnboundWildcard import org.opentaint.ir.api.jvm.cfg.JIRBool import org.opentaint.ir.api.jvm.cfg.JIRCallExpr import org.opentaint.ir.api.jvm.cfg.JIRConstant @@ -360,8 +361,6 @@ class JIRBasicAtomEvaluator( if (type !is JIRClassType) return true - if (type.isRawClassType()) return false - if (type.typeArguments.size != typeArgs.size) return false return typeArgs.zip(type.typeArguments).all { (matcher, arg) -> matcher.matchType(arg) @@ -390,9 +389,16 @@ class JIRBasicAtomEvaluator( 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.matchType(type: JIRType): Boolean { + // Raw class type arguments (`JIRTypeVariable`) and unbounded + // wildcards (``) denote "any type", so any pattern matcher + // accepts them — the unknown type could be whatever the pattern + // requires. + if (type is JIRTypeVariable || type is JIRUnboundWildcard) return true + return when (this) { + is TypeArgMatcher.Class -> matchType(type) + is TypeArgMatcher.Array -> matchType(type) + } } private fun TypeArgMatcher.Class.matchType(type: JIRType): Boolean { @@ -403,22 +409,10 @@ class JIRBasicAtomEvaluator( return true } - if (type.isRawClassType()) return false - if (args.size != type.typeArguments.size) return false return args.zip(type.typeArguments).all { (m, a) -> m.matchType(a) } } - private fun JIRClassType.isRawClassType(): Boolean { - val params = typeParameters - if (params.isEmpty()) return false - val args = typeArguments - if (args.size != params.size) return false - return args.zip(params).all { (arg, param) -> - arg is JIRTypeVariable && arg.symbol == param.symbol - } - } - 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/RuleWithGenericMetavarArrayArg.java b/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithGenericMetavarArrayArg.java index 26245ae3e..1342b198a 100644 --- a/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithGenericMetavarArrayArg.java +++ b/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithGenericMetavarArrayArg.java @@ -42,11 +42,11 @@ public void entrypoint() { } /** - * The rule pattern is {@code ResponseEntity<$T>}, which requires a concrete - * type argument. A raw use of {@code ResponseEntity} (no type argument) - * therefore must NOT match. + * The metavar type-arg pattern {@code ResponseEntity<$T>} matches the same + * set of types as {@code ResponseEntity} and the raw form, so a raw use + * of {@code ResponseEntity} matches. */ - final static class NegativeRawResponseEntity extends RuleWithGenericMetavarArrayArg { + final static class PositiveRawResponseEntity extends RuleWithGenericMetavarArrayArg { @Override public void entrypoint() { String data = "tainted"; diff --git a/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithObjectTypeArg.java b/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithObjectTypeArg.java new file mode 100644 index 000000000..fa60e237b --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithObjectTypeArg.java @@ -0,0 +1,95 @@ +package example; + +import base.RuleSample; +import base.RuleSet; +import org.springframework.http.ResponseEntity; + +/** + * A25. Concrete-{@code Object} type argument — pattern + * {@code ResponseEntity $METHOD(...)}. + * + *

{@code Object} is the upper bound of the unbounded wildcard {@code ?}, + * so a method declaring its return as {@code ResponseEntity} satisfies + * the {@code ResponseEntity} pattern; the raw form is identical to + * {@code ResponseEntity} and matches as well. Other concrete type + * arguments such as {@code String} or {@code Integer} do NOT match.

+ */ +@RuleSet("example/RuleWithObjectTypeArg.yaml") +public abstract class RuleWithObjectTypeArg implements RuleSample { + + void sink(String data) {} + + ResponseEntity methodReturningResponseEntityObject(String data) { + sink(data); + return null; + } + + ResponseEntity methodReturningResponseEntityWildcard(String data) { + sink(data); + return null; + } + + @SuppressWarnings("rawtypes") + ResponseEntity methodReturningRawResponseEntity(String data) { + sink(data); + return null; + } + + ResponseEntity methodReturningResponseEntityString(String data) { + sink(data); + return null; + } + + ResponseEntity methodReturningResponseEntityInteger(String data) { + sink(data); + return null; + } + + final static class PositiveObjectMatchesObject extends RuleWithObjectTypeArg { + @Override + public void entrypoint() { + String data = "tainted"; + methodReturningResponseEntityObject(data); + } + } + + /** + * {@code ResponseEntity} matches the {@code } pattern because + * the unbounded wildcard's upper bound is {@code Object}. + */ + final static class PositiveWildcardMatchesObject extends RuleWithObjectTypeArg { + @Override + public void entrypoint() { + String data = "tainted"; + methodReturningResponseEntityWildcard(data); + } + } + + /** + * Raw {@code ResponseEntity} is identical to {@code ResponseEntity}, + * so it matches the {@code } pattern too. + */ + final static class PositiveRawMatchesObject extends RuleWithObjectTypeArg { + @Override + public void entrypoint() { + String data = "tainted"; + methodReturningRawResponseEntity(data); + } + } + + final static class NegativeStringTypeArg extends RuleWithObjectTypeArg { + @Override + public void entrypoint() { + String data = "tainted"; + methodReturningResponseEntityString(data); + } + } + + final static class NegativeIntegerTypeArg extends RuleWithObjectTypeArg { + @Override + public void entrypoint() { + String data = "tainted"; + methodReturningResponseEntityInteger(data); + } + } +} diff --git a/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithRawResponseEntityNotInsideGeneric.java b/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithRawResponseEntityNotInsideGeneric.java deleted file mode 100644 index 37f2fc6f0..000000000 --- a/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithRawResponseEntityNotInsideGeneric.java +++ /dev/null @@ -1,41 +0,0 @@ -package example; - -import base.RuleSample; -import base.RuleSet; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; - -@RuleSet("example/RuleWithRawResponseEntityNotInsideGeneric.yaml") -public abstract class RuleWithRawResponseEntityNotInsideGeneric implements RuleSample { - - void sink(String data) {} - - @SuppressWarnings("rawtypes") - @GetMapping("/raw") - ResponseEntity rawResponseEntity(String data) { - sink(data); - return null; - } - - @GetMapping("/parametrized") - ResponseEntity parametrizedResponseEntity(String data) { - sink(data); - return null; - } - - final static class PositiveRawResponseEntity extends RuleWithRawResponseEntityNotInsideGeneric { - @Override - public void entrypoint() { - String data = "tainted"; - rawResponseEntity(data); - } - } - - final static class NegativeParametrizedResponseEntity extends RuleWithRawResponseEntityNotInsideGeneric { - @Override - public void entrypoint() { - String data = "tainted"; - parametrizedResponseEntity(data); - } - } -} diff --git a/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithStringTypeArgMatchesRawAndWildcard.java b/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithStringTypeArgMatchesRawAndWildcard.java new file mode 100644 index 000000000..48277b878 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithStringTypeArgMatchesRawAndWildcard.java @@ -0,0 +1,94 @@ +package example; + +import base.RuleSample; +import base.RuleSet; +import org.springframework.http.ResponseEntity; + +/** + * A26. Concrete-{@code String} type argument — pattern + * {@code ResponseEntity $METHOD(...)}. + * + *

Raw {@code ResponseEntity} and {@code ResponseEntity} both denote + * "any type argument", so a concrete pattern like {@code } matches + * both — the unknown type argument could be {@code String}. Other concrete + * type arguments such as {@code Object} or {@code Integer} do NOT match.

+ */ +@RuleSet("example/RuleWithStringTypeArgMatchesRawAndWildcard.yaml") +public abstract class RuleWithStringTypeArgMatchesRawAndWildcard implements RuleSample { + + void sink(String data) {} + + ResponseEntity methodReturningResponseEntityString(String data) { + sink(data); + return null; + } + + ResponseEntity methodReturningResponseEntityWildcard(String data) { + sink(data); + return null; + } + + @SuppressWarnings("rawtypes") + ResponseEntity methodReturningRawResponseEntity(String data) { + sink(data); + return null; + } + + ResponseEntity methodReturningResponseEntityObject(String data) { + sink(data); + return null; + } + + ResponseEntity methodReturningResponseEntityInteger(String data) { + sink(data); + return null; + } + + final static class PositiveStringMatchesString extends RuleWithStringTypeArgMatchesRawAndWildcard { + @Override + public void entrypoint() { + String data = "tainted"; + methodReturningResponseEntityString(data); + } + } + + /** + * {@code ResponseEntity} could be parameterized with {@code String}, so + * the {@code } pattern matches it. + */ + final static class PositiveWildcardMatchesString extends RuleWithStringTypeArgMatchesRawAndWildcard { + @Override + public void entrypoint() { + String data = "tainted"; + methodReturningResponseEntityWildcard(data); + } + } + + /** + * Raw {@code ResponseEntity} is identical to {@code ResponseEntity}, so + * the {@code } pattern matches it for the same reason. + */ + final static class PositiveRawMatchesString extends RuleWithStringTypeArgMatchesRawAndWildcard { + @Override + public void entrypoint() { + String data = "tainted"; + methodReturningRawResponseEntity(data); + } + } + + final static class NegativeObjectTypeArg extends RuleWithStringTypeArgMatchesRawAndWildcard { + @Override + public void entrypoint() { + String data = "tainted"; + methodReturningResponseEntityObject(data); + } + } + + final static class NegativeIntegerTypeArg extends RuleWithStringTypeArgMatchesRawAndWildcard { + @Override + public void entrypoint() { + String data = "tainted"; + methodReturningResponseEntityInteger(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 index 8eaefb0da..7df8985f0 100644 --- a/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithWildcardGeneric.java +++ b/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithWildcardGeneric.java @@ -19,6 +19,17 @@ ResponseEntity methodReturningResponseEntityString(String data) { return null; } + ResponseEntity methodReturningResponseEntityObject(String data) { + sink(data); + return null; + } + + @SuppressWarnings("rawtypes") + ResponseEntity methodReturningRawResponseEntity(String data) { + sink(data); + return null; + } + /** * Wildcard ResponseEntity<?> trivially matches the <?> rule * pattern. @@ -32,10 +43,9 @@ public void entrypoint() { } /** - * 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. + * ResponseEntity<String> is a concrete parameterization. The + * unbounded wildcard pattern `<?>` matches every parameterization + * of {@code ResponseEntity}, so this method matches. */ final static class PositiveConcreteMatchesWildcard extends RuleWithWildcardGeneric { @Override @@ -44,4 +54,29 @@ public void entrypoint() { methodReturningResponseEntityString(data); } } + + /** + * ResponseEntity<Object> — the wildcard pattern `<?>` matches + * every parameterization, including {@code Object}. + */ + final static class PositiveObjectMatchesWildcard extends RuleWithWildcardGeneric { + @Override + public void entrypoint() { + String data = "tainted"; + methodReturningResponseEntityObject(data); + } + } + + /** + * Raw {@code ResponseEntity} — the pattern `ResponseEntity<?>` and + * the raw form {@code ResponseEntity} have identical meaning, so the raw + * use is matched by the wildcard pattern. + */ + final static class PositiveRawMatchesWildcard extends RuleWithWildcardGeneric { + @Override + public void entrypoint() { + String data = "tainted"; + methodReturningRawResponseEntity(data); + } + } } diff --git a/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithObjectTypeArg.yaml b/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithObjectTypeArg.yaml new file mode 100644 index 000000000..0bc78d0b7 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithObjectTypeArg.yaml @@ -0,0 +1,15 @@ +rules: + - id: example-RuleWithObjectTypeArg + languages: + - java + severity: ERROR + message: match example/RuleWithObjectTypeArg + patterns: + - pattern: |- + ... + sink($A); + ... + - pattern-inside: |- + ResponseEntity $METHOD(..., String $A, ...) { + ... + } diff --git a/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithRawResponseEntityNotInsideGeneric.yaml b/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithRawResponseEntityNotInsideGeneric.yaml deleted file mode 100644 index 89125ca45..000000000 --- a/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithRawResponseEntityNotInsideGeneric.yaml +++ /dev/null @@ -1,21 +0,0 @@ -rules: - - id: example-RuleWithRawResponseEntityNotInsideGeneric - languages: - - java - severity: ERROR - message: match example/RuleWithRawResponseEntityNotInsideGeneric - patterns: - - pattern: |- - ... - sink($A); - ... - - pattern-inside: |- - @$ANNOTATION(...) - ResponseEntity $METHOD(..., String $A, ...) { - ... - } - - pattern-not-inside: |- - @$ANNOTATION(...) - ResponseEntity<$T> $METHOD(..., String $A, ...) { - ... - } diff --git a/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithStringTypeArgMatchesRawAndWildcard.yaml b/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithStringTypeArgMatchesRawAndWildcard.yaml new file mode 100644 index 000000000..c317ada23 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithStringTypeArgMatchesRawAndWildcard.yaml @@ -0,0 +1,15 @@ +rules: + - id: example-RuleWithStringTypeArgMatchesRawAndWildcard + languages: + - java + severity: ERROR + message: match example/RuleWithStringTypeArgMatchesRawAndWildcard + patterns: + - pattern: |- + ... + sink($A); + ... + - pattern-inside: |- + ResponseEntity $METHOD(..., String $A, ...) { + ... + } 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 298a2aa22..1ee3dba8c 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 @@ -943,9 +943,16 @@ 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() +): List? { + if (typeArgs.isEmpty()) return null + // `Foo` (and any all-wildcard parameterization) denotes the same set + // of types as the raw form `Foo` — drop the type-args constraint so the + // two patterns are indistinguishable downstream. + if (typeArgs.all { it is TypeNamePattern.AnyType }) return null + return typeArgs.map { + (typeMatcher(it, semgrepRuleTrace) as? MetaVarConstraintFormula.Constraint)?.constraint + ?: anyClassPattern() + } } private fun String.patternCanMatchDot(): Boolean = 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 index 8e170dc17..727b136da 100644 --- 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 @@ -24,11 +24,11 @@ class TypeAwarePatternTest : SampleBasedTest() { @Test fun `A1 - ResponseEntity of byte array return type`() = runTest() - // A2. ResponseEntity<$T> — metavar type arg requires a concrete type - // argument, so it matches parameterized forms (String, byte[]) but NOT - // the raw use (no type argument). + // A2. ResponseEntity<$T> — metavar type arg matches the same set of types + // as `` and the raw form, so all three method-decl forms match + // (String, byte[], and raw). @Test - fun `A2 - ResponseEntity metavar matches parameterized string and byte array but not raw`() = + fun `A2 - ResponseEntity metavar matches parameterized string, byte array, and raw`() = runTest() // A3. Nested generic: ResponseEntity>. @@ -40,9 +40,9 @@ class TypeAwarePatternTest : SampleBasedTest() { @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. + // A5. Wildcard type argument: ResponseEntity. The pattern matches + // every parameterization of ResponseEntity, including , , + // , and the raw form (raw and `` denote the same set of types). @Test fun `A5 - wildcard type argument ResponseEntity of question mark`() = runTest() @@ -110,14 +110,23 @@ class TypeAwarePatternTest : SampleBasedTest() { fun `A23 - array dimension mismatch String two dim`() = runTest() - // A24. pattern-not-inside discriminates raw vs. parameterized return type. - // pattern-inside requires raw `ResponseEntity` (no type args), and - // pattern-not-inside excludes `ResponseEntity<$T>` (parameterized). - // A method returning raw `ResponseEntity` should match (Positive); a - // method returning `ResponseEntity` should be excluded (Negative). + // A25. Concrete-Object type argument: ResponseEntity. + // `Object` is the upper bound of `?`, so methods returning + // `ResponseEntity`, `ResponseEntity`, and the raw form match. + // Methods returning `ResponseEntity` or `ResponseEntity` + // do not. @Test - fun `A24 - pattern-not-inside discriminates raw vs parameterized ResponseEntity`() = - runTest() + fun `A25 - Object type argument matches Object wildcard and raw but not other concrete`() = + runTest() + + // A26. Concrete-String type argument: ResponseEntity. + // Raw `ResponseEntity` and `ResponseEntity` denote "any type + // argument", so the `` pattern matches both — the unknown type + // could be `String`. Other concrete type arguments such as `Object` or + // `Integer` do NOT match. + @Test + fun `A26 - String type argument matches String wildcard and raw but not other concrete`() = + runTest() @AfterAll fun close() { From f814269dedd20ab71be7811c5b84facccd77614a Mon Sep 17 00:00:00 2001 From: Aleksandr Misonizhnik Date: Thu, 7 May 2026 12:26:01 +0200 Subject: [PATCH 3/6] refactor(analyzer): Extract isAnyTypeArg helper, drop redundant collapse Extract the "type arg denotes any type" predicate (raw-class type variable or unbounded wildcard) to a single `JIRType.isAnyTypeArg()` extension in `configuration-rules-jvm`, alongside the existing `erasedName()`. Both runtime matchers now share this helper instead of inlining the same `JIRTypeVariable || JIRUnboundWildcard` check with duplicated comments. Drop the conversion-time all-wildcard collapse in `typeArgsMatcher`: it was redundant with the runtime short-circuit that already makes `Foo` and raw `Foo` match the same set of types. --- .../configuration/jvm/SerializedTypeMatching.kt | 14 +++++++++----- .../jvm/ap/ifds/taint/JIRBasicAtomEvaluator.kt | 9 ++------- .../taint/AutomataToTaintRuleConversion.kt | 13 +++---------- 3 files changed, 14 insertions(+), 22 deletions(-) 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 index 4c563b1f1..b549372f0 100644 --- 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 @@ -33,11 +33,7 @@ private fun SerializedTypeNameMatcher.matchTypeArgs( if (args.size != type.typeArguments.size) return false args.zip(type.typeArguments).all { (m, a) -> - // Raw class type arguments (`JIRTypeVariable`) and unbounded - // wildcards (``) denote "any type", so any pattern matcher - // accepts them — the unknown type could be whatever the - // pattern requires. - if (a is JIRTypeVariable || a is JIRUnboundWildcard) return@all true + if (a.isAnyTypeArg()) return@all true m.matchType(a.erasedName(), resolveType = { a }, erasedMatch) } } @@ -49,6 +45,14 @@ private fun SerializedTypeNameMatcher.matchTypeArgs( } } +/** + * `true` for type arguments that denote "any type" — a raw use's declared + * type variable (e.g. `E` of `List`) or an unbounded wildcard ``. Any + * pattern matcher accepts such a slot because the unknown could be whatever + * the pattern requires. + */ +fun JIRType.isAnyTypeArg(): Boolean = this is JIRTypeVariable || this is JIRUnboundWildcard + /** * Erased class name for matching — drops any generic decoration that * [JIRType.typeName] may carry (e.g. `Map` → `java.util.Map`) 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 ed7b678f7..4937fe8b1 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 @@ -25,6 +25,7 @@ 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.configuration.jvm.isAnyTypeArg import org.opentaint.dataflow.jvm.ap.ifds.CallPositionValue import org.opentaint.dataflow.jvm.ap.ifds.JIRFactTypeChecker import org.opentaint.dataflow.jvm.ap.ifds.JIRLocalAliasAnalysis @@ -37,8 +38,6 @@ 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.JIRTypeVariable -import org.opentaint.ir.api.jvm.JIRUnboundWildcard import org.opentaint.ir.api.jvm.cfg.JIRBool import org.opentaint.ir.api.jvm.cfg.JIRCallExpr import org.opentaint.ir.api.jvm.cfg.JIRConstant @@ -390,11 +389,7 @@ class JIRBasicAtomEvaluator( } private fun TypeArgMatcher.matchType(type: JIRType): Boolean { - // Raw class type arguments (`JIRTypeVariable`) and unbounded - // wildcards (``) denote "any type", so any pattern matcher - // accepts them — the unknown type could be whatever the pattern - // requires. - if (type is JIRTypeVariable || type is JIRUnboundWildcard) return true + if (type.isAnyTypeArg()) return true return when (this) { is TypeArgMatcher.Class -> matchType(type) is TypeArgMatcher.Array -> matchType(type) 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 1ee3dba8c..298a2aa22 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 @@ -943,16 +943,9 @@ private fun TaintRuleGenerationCtx.typeMatcher( private fun TaintRuleGenerationCtx.typeArgsMatcher( typeArgs: List, semgrepRuleTrace: SemgrepRuleLoadStepTrace -): List? { - if (typeArgs.isEmpty()) return null - // `Foo` (and any all-wildcard parameterization) denotes the same set - // of types as the raw form `Foo` — drop the type-args constraint so the - // two patterns are indistinguishable downstream. - if (typeArgs.all { it is TypeNamePattern.AnyType }) return null - return typeArgs.map { - (typeMatcher(it, semgrepRuleTrace) as? MetaVarConstraintFormula.Constraint)?.constraint - ?: anyClassPattern() - } +): List? = typeArgs.takeIf { it.isNotEmpty() }?.map { + (typeMatcher(it, semgrepRuleTrace) as? MetaVarConstraintFormula.Constraint)?.constraint + ?: anyClassPattern() } private fun String.patternCanMatchDot(): Boolean = From 34bde9ffa45ed33967be2aee1614b80300c4a76c Mon Sep 17 00:00:00 2001 From: Aleksandr Misonizhnik Date: Thu, 7 May 2026 12:46:14 +0200 Subject: [PATCH 4/6] test(analyzer): Cover nested generics and class-mismatch under `` pattern MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add A27 — pattern-inside `ResponseEntity` against: - `ResponseEntity>` and `ResponseEntity>` (Positive) to lock in nested-generic acceptance, - `List` and `String` returns (Negative) to lock in that the wildcard only loosens the type-argument slot, not the class portion. --- ...WildcardPatternNestedAndClassMismatch.java | 82 +++++++++++++++++++ ...WildcardPatternNestedAndClassMismatch.yaml | 15 ++++ .../opentaint/semgrep/TypeAwarePatternTest.kt | 9 ++ 3 files changed, 106 insertions(+) create mode 100644 core/opentaint-java-querylang/samples/src/main/java/example/RuleWithWildcardPatternNestedAndClassMismatch.java create mode 100644 core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithWildcardPatternNestedAndClassMismatch.yaml diff --git a/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithWildcardPatternNestedAndClassMismatch.java b/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithWildcardPatternNestedAndClassMismatch.java new file mode 100644 index 000000000..a4467b5ab --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithWildcardPatternNestedAndClassMismatch.java @@ -0,0 +1,82 @@ +package example; + +import base.RuleSample; +import base.RuleSet; +import org.springframework.http.ResponseEntity; + +import java.util.List; +import java.util.Map; + +/** + * A27. Wildcard rule pattern with nested-generic and class-mismatch coverage. + * + *

Rule pattern: {@code ResponseEntity $METHOD(..., String $A, ...)}. + * The wildcard accepts every parameterization of {@code ResponseEntity}, + * including nested generics like {@code ResponseEntity>} and + * {@code ResponseEntity>}. The class portion of the + * pattern still narrows: methods that return a non-{@code ResponseEntity} + * type ({@code List}, {@code String}) must NOT match.

+ */ +@RuleSet("example/RuleWithWildcardPatternNestedAndClassMismatch.yaml") +public abstract class RuleWithWildcardPatternNestedAndClassMismatch implements RuleSample { + + void sink(String data) {} + + ResponseEntity> methodReturningResponseEntityListString(String data) { + sink(data); + return null; + } + + ResponseEntity> methodReturningResponseEntityMapStringInteger(String data) { + sink(data); + return null; + } + + List methodReturningListString(String data) { + sink(data); + return null; + } + + String methodReturningString(String data) { + sink(data); + return null; + } + + final static class PositiveNestedListString extends RuleWithWildcardPatternNestedAndClassMismatch { + @Override + public void entrypoint() { + String data = "tainted"; + methodReturningResponseEntityListString(data); + } + } + + final static class PositiveNestedMapStringInteger extends RuleWithWildcardPatternNestedAndClassMismatch { + @Override + public void entrypoint() { + String data = "tainted"; + methodReturningResponseEntityMapStringInteger(data); + } + } + + /** + * The pattern's class portion is {@code ResponseEntity}; a method + * returning {@code List} has a different erased class name and + * must NOT match — the wildcard only loosens the type-argument slot, + * not the class name. + */ + final static class NegativeListReturn extends RuleWithWildcardPatternNestedAndClassMismatch { + @Override + public void entrypoint() { + String data = "tainted"; + methodReturningListString(data); + } + } + + final static class NegativeStringReturn extends RuleWithWildcardPatternNestedAndClassMismatch { + @Override + public void entrypoint() { + String data = "tainted"; + methodReturningString(data); + } + } +} diff --git a/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithWildcardPatternNestedAndClassMismatch.yaml b/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithWildcardPatternNestedAndClassMismatch.yaml new file mode 100644 index 000000000..9d7f13b5a --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithWildcardPatternNestedAndClassMismatch.yaml @@ -0,0 +1,15 @@ +rules: + - id: example-RuleWithWildcardPatternNestedAndClassMismatch + languages: + - java + severity: ERROR + message: match example/RuleWithWildcardPatternNestedAndClassMismatch + patterns: + - pattern: |- + ... + sink($A); + ... + - pattern-inside: |- + ResponseEntity $METHOD(..., String $A, ...) { + ... + } 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 index 727b136da..bd42b538a 100644 --- 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 @@ -128,6 +128,15 @@ class TypeAwarePatternTest : SampleBasedTest() { fun `A26 - String type argument matches String wildcard and raw but not other concrete`() = runTest() + // A27. Wildcard rule pattern: `ResponseEntity`. The wildcard accepts + // every parameterization including nested generics + // (`ResponseEntity>`, `ResponseEntity>`), + // but the class portion still narrows — methods returning a + // non-`ResponseEntity` type (`List`, `String`) must NOT match. + @Test + fun `A27 - wildcard pattern matches nested generic ResponseEntity but not other classes`() = + runTest() + @AfterAll fun close() { closeRunner() From d3a2dd3557dac416966af1f82c8e0cc1bc7e5555 Mon Sep 17 00:00:00 2001 From: Aleksandr Misonizhnik Date: Thu, 7 May 2026 14:07:34 +0200 Subject: [PATCH 5/6] test(analyzer): Cover return-type IsType emission for distinct-signature negatives MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add A28 — pattern-inside `ResponseEntity` combined with pattern-not-inside `ResponseEntity` (same parameter shape, different return type). Methods returning `` and `` must fire as Positives; the `` return is excluded. Verified by temporarily reverting the return-type `IsType` emission in `evaluateMethodSignatureCondition`: without it, the negative predicate drops its return type, matches every method sharing the parameter shape, and masks both Positives — exactly the regression A28 guards. --- .../RuleWithNotInsideDistinctReturnType.java | 72 +++++++++++++++++++ .../RuleWithNotInsideDistinctReturnType.yaml | 19 +++++ .../opentaint/semgrep/TypeAwarePatternTest.kt | 9 +++ 3 files changed, 100 insertions(+) create mode 100644 core/opentaint-java-querylang/samples/src/main/java/example/RuleWithNotInsideDistinctReturnType.java create mode 100644 core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithNotInsideDistinctReturnType.yaml diff --git a/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithNotInsideDistinctReturnType.java b/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithNotInsideDistinctReturnType.java new file mode 100644 index 000000000..81599726d --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/java/example/RuleWithNotInsideDistinctReturnType.java @@ -0,0 +1,72 @@ +package example; + +import base.RuleSample; +import base.RuleSet; +import org.springframework.http.ResponseEntity; + +/** + * A28. {@code pattern-not-inside} with a return type that differs from + * {@code pattern-inside} must filter on its own return type. + * + *

Rule: {@code pattern-inside ResponseEntity $METHOD(..., String $A, ...)} + * combined with {@code pattern-not-inside ResponseEntity $METHOD(...)}. + * The two method-decl signatures share parameter shape but differ on return + * type. The negative predicate must emit a return-type {@code IsType} clause + * for its own signature; otherwise it would drop the return-type constraint + * and exclude every method matching the parameter shape, masking real + * positives.

+ */ +@RuleSet("example/RuleWithNotInsideDistinctReturnType.yaml") +public abstract class RuleWithNotInsideDistinctReturnType implements RuleSample { + + void sink(String data) {} + + ResponseEntity methodReturningString(String data) { + sink(data); + return null; + } + + ResponseEntity methodReturningObject(String data) { + sink(data); + return null; + } + + ResponseEntity methodReturningInteger(String data) { + sink(data); + return null; + } + + /** + * {@code } return — the not-inside's return-type {@code } + * must NOT match here, so the rule fires. + */ + final static class PositiveStringReturn extends RuleWithNotInsideDistinctReturnType { + @Override + public void entrypoint() { + String data = "tainted"; + methodReturningString(data); + } + } + + /** + * {@code } return — same reasoning. + */ + final static class PositiveObjectReturn extends RuleWithNotInsideDistinctReturnType { + @Override + public void entrypoint() { + String data = "tainted"; + methodReturningObject(data); + } + } + + /** + * {@code } return — the not-inside excludes this method. + */ + final static class NegativeIntegerReturn extends RuleWithNotInsideDistinctReturnType { + @Override + public void entrypoint() { + String data = "tainted"; + methodReturningInteger(data); + } + } +} diff --git a/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithNotInsideDistinctReturnType.yaml b/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithNotInsideDistinctReturnType.yaml new file mode 100644 index 000000000..6d16d2c7a --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/resources/example/RuleWithNotInsideDistinctReturnType.yaml @@ -0,0 +1,19 @@ +rules: + - id: example-RuleWithNotInsideDistinctReturnType + languages: + - java + severity: ERROR + message: match example/RuleWithNotInsideDistinctReturnType + patterns: + - pattern: |- + ... + sink($A); + ... + - pattern-inside: |- + ResponseEntity $METHOD(..., String $A, ...) { + ... + } + - pattern-not-inside: |- + ResponseEntity $METHOD(..., String $A, ...) { + ... + } 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 index bd42b538a..be84b002d 100644 --- 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 @@ -137,6 +137,15 @@ class TypeAwarePatternTest : SampleBasedTest() { fun `A27 - wildcard pattern matches nested generic ResponseEntity but not other classes`() = runTest() + // A28. `pattern-not-inside` whose method-decl return type differs from + // `pattern-inside` must filter on its own return type. Without the + // return-type `IsType` clause, the negative predicate drops its return + // type and excludes every method sharing the parameter shape, masking + // real positives. + @Test + fun `A28 - pattern-not-inside with distinct return type filters on its own return`() = + runTest() + @AfterAll fun close() { closeRunner() From 73ae5bdd07bb7f24df33cb237b41713ed3c8afe9 Mon Sep 17 00:00:00 2001 From: Valentyn Sobol <8640896+Saloed@users.noreply.github.com> Date: Thu, 7 May 2026 15:54:59 +0300 Subject: [PATCH 6/6] Minor refactoring --- .../taint/AutomataToTaintRuleConversion.kt | 33 +++++++++---------- 1 file changed, 16 insertions(+), 17 deletions(-) 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 298a2aa22..d3f9444fd 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 @@ -561,15 +561,10 @@ 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) - } - } + signature.returnType?.let { returnType -> + val condition = evaluateFormulaSignatureReturnType(returnType, semgrepRuleTrace) + buildersWithMethodName.forEach { builder -> + builder.conditions += condition } } @@ -629,6 +624,16 @@ private fun TaintRuleGenerationCtx.evaluateFormulaSignature( return signature to buildersWithClass } +private fun TaintRuleGenerationCtx.evaluateFormulaSignatureReturnType( + returnType: TypeNamePattern, + semgrepRuleTrace: SemgrepRuleLoadStepTrace, +): SerializedCondition { + val returnTypeFormula = typeMatcher(returnType, semgrepRuleTrace) + return returnTypeFormula.toSerializedCondition { typeNameMatcher -> + SerializedCondition.IsType(typeNameMatcher, PositionBase.Result) + } +} + private fun TaintRuleGenerationCtx.evaluateFormulaSignatureMethodName( methodName: SignatureName, semgrepRuleTrace: SemgrepRuleLoadStepTrace, @@ -813,14 +818,8 @@ private fun TaintRuleGenerationCtx.evaluateMethodSignatureCondition( SerializedCondition.and(cond) } - val returnType = signature.returnType - if (returnType != null) { - val returnTypeFormula = typeMatcher(returnType, semgrepRuleTrace) - if (returnTypeFormula != null) { - conditions += returnTypeFormula.toSerializedCondition { typeNameMatcher -> - SerializedCondition.IsType(typeNameMatcher, PositionBase.Result) - } - } + signature.returnType?.let { + conditions += evaluateFormulaSignatureReturnType(it, semgrepRuleTrace) } }