diff --git a/core/opentaint-configuration-rules/configuration-rules-go/src/main/kotlin/org/opentaint/dataflow/configuration/go/serialized/GoSerializedAction.kt b/core/opentaint-configuration-rules/configuration-rules-go/src/main/kotlin/org/opentaint/dataflow/configuration/go/serialized/GoSerializedAction.kt index e48f7baa4..8a34ee847 100644 --- a/core/opentaint-configuration-rules/configuration-rules-go/src/main/kotlin/org/opentaint/dataflow/configuration/go/serialized/GoSerializedAction.kt +++ b/core/opentaint-configuration-rules/configuration-rules-go/src/main/kotlin/org/opentaint/dataflow/configuration/go/serialized/GoSerializedAction.kt @@ -4,32 +4,10 @@ import org.opentaint.dataflow.configuration.jvm.serialized.PositionBaseWithModif sealed interface GoSerializedAction -sealed interface GoSerializedAssignAction : GoSerializedAction { - val kind: String - - fun rawPosition(): PositionBaseWithModifiers - fun changePos(newPos: PositionBaseWithModifiers): GoSerializedAssignAction - - data class Direct( - override val kind: String, - val pos: PositionBaseWithModifiers, - ) : GoSerializedAssignAction { - override fun rawPosition(): PositionBaseWithModifiers = pos - override fun changePos(newPos: PositionBaseWithModifiers) = copy(pos = newPos) - } - - data class AnyAccessor( - override val kind: String, - val pos: PositionBaseWithModifiers, - ) : GoSerializedAssignAction { - override fun rawPosition(): PositionBaseWithModifiers = pos - override fun changePos(newPos: PositionBaseWithModifiers) = copy(pos = newPos) - } - - companion object { - operator fun invoke(kind: String, pos: PositionBaseWithModifiers) = Direct(kind, pos) - } -} +data class GoSerializedAssignAction( + val kind: String, + val pos: PositionBaseWithModifiers, +) : GoSerializedAction data class GoSerializedCleanAction( val taintKind: String? = null, diff --git a/core/opentaint-configuration-rules/configuration-rules-go/src/main/kotlin/org/opentaint/dataflow/configuration/go/serialized/GoSerializedCondition.kt b/core/opentaint-configuration-rules/configuration-rules-go/src/main/kotlin/org/opentaint/dataflow/configuration/go/serialized/GoSerializedCondition.kt index ee9946072..cc4d57d27 100644 --- a/core/opentaint-configuration-rules/configuration-rules-go/src/main/kotlin/org/opentaint/dataflow/configuration/go/serialized/GoSerializedCondition.kt +++ b/core/opentaint-configuration-rules/configuration-rules-go/src/main/kotlin/org/opentaint/dataflow/configuration/go/serialized/GoSerializedCondition.kt @@ -12,8 +12,6 @@ sealed interface GoSerializedCondition { data class ContainsMark(val tainted: String, val pos: PositionBaseWithModifiers) : GoSerializedCondition - data class ContainsMarkOnAnyAccessor(val tainted: String, val pos: PositionBaseWithModifiers) : GoSerializedCondition - data class ConstantCmp(val pos: PositionBase, val value: ConstantValue, val cmp: ConstantCmpType) : GoSerializedCondition data class ConstantMatches(val pos: PositionBase, val pattern: String) : GoSerializedCondition diff --git a/core/opentaint-configuration-rules/configuration-rules-jvm/src/main/kotlin/org/opentaint/dataflow/configuration/jvm/Position.kt b/core/opentaint-configuration-rules/configuration-rules-jvm/src/main/kotlin/org/opentaint/dataflow/configuration/jvm/Position.kt index 4b107bd48..11071a6b8 100644 --- a/core/opentaint-configuration-rules/configuration-rules-jvm/src/main/kotlin/org/opentaint/dataflow/configuration/jvm/Position.kt +++ b/core/opentaint-configuration-rules/configuration-rules-jvm/src/main/kotlin/org/opentaint/dataflow/configuration/jvm/Position.kt @@ -23,10 +23,6 @@ sealed interface PositionAccessor { override fun toString(): String = javaClass.simpleName } - data object AnyFieldAccessor : PositionAccessor { - override fun toString(): String = javaClass.simpleName - } - data class FieldAccessor( val className: String, val fieldName: String, diff --git a/core/opentaint-configuration-rules/configuration-rules-jvm/src/main/kotlin/org/opentaint/dataflow/configuration/jvm/TaintAction.kt b/core/opentaint-configuration-rules/configuration-rules-jvm/src/main/kotlin/org/opentaint/dataflow/configuration/jvm/TaintAction.kt index d64749b3e..b3751f39b 100644 --- a/core/opentaint-configuration-rules/configuration-rules-jvm/src/main/kotlin/org/opentaint/dataflow/configuration/jvm/TaintAction.kt +++ b/core/opentaint-configuration-rules/configuration-rules-jvm/src/main/kotlin/org/opentaint/dataflow/configuration/jvm/TaintAction.kt @@ -2,32 +2,35 @@ package org.opentaint.dataflow.configuration.jvm import org.opentaint.dataflow.configuration.CommonTaintAction import org.opentaint.dataflow.configuration.CommonTaintAssignAction -import org.opentaint.dataflow.configuration.TaintCleanReach sealed interface Action: CommonTaintAction +sealed interface ActionPosition { + data class Exact(val position: Position) : ActionPosition + data class AnyAccessorAfter(val position: Position): ActionPosition +} + data class CopyAllMarks( - val from: Position, - val to: Position, + val from: ActionPosition, + val to: ActionPosition, ) : Action data class CopyMark( val mark: TaintMark, - val from: Position, - val to: Position, + val from: ActionPosition, + val to: ActionPosition, ) : Action data class AssignMark( val mark: TaintMark, - val position: Position, + val position: ActionPosition, ) : Action, CommonTaintAssignAction data class RemoveAllMarks( - val position: Position, + val position: ActionPosition, ) : Action data class RemoveMark( val mark: TaintMark, - val position: Position, - val reach: TaintCleanReach = TaintCleanReach.Exact, + val position: ActionPosition, ) : Action diff --git a/core/opentaint-configuration-rules/configuration-rules-jvm/src/main/kotlin/org/opentaint/dataflow/configuration/jvm/serialized/SerializedAction.kt b/core/opentaint-configuration-rules/configuration-rules-jvm/src/main/kotlin/org/opentaint/dataflow/configuration/jvm/serialized/SerializedAction.kt index 04e73ce3c..f76412aea 100644 --- a/core/opentaint-configuration-rules/configuration-rules-jvm/src/main/kotlin/org/opentaint/dataflow/configuration/jvm/serialized/SerializedAction.kt +++ b/core/opentaint-configuration-rules/configuration-rules-jvm/src/main/kotlin/org/opentaint/dataflow/configuration/jvm/serialized/SerializedAction.kt @@ -1,7 +1,6 @@ package org.opentaint.dataflow.configuration.jvm.serialized import kotlinx.serialization.Serializable -import org.opentaint.dataflow.configuration.TaintCleanReach sealed interface SerializedAction @@ -16,7 +15,6 @@ data class SerializedTaintAssignAction( data class SerializedTaintCleanAction( val taintKind: String? = null, val pos: PositionBaseWithModifiers, - val reach: TaintCleanReach = TaintCleanReach.Exact, ): SerializedAction @Serializable diff --git a/core/opentaint-configuration-rules/configuration-rules-jvm/src/main/kotlin/org/opentaint/dataflow/configuration/jvm/serialized/SerializedPosition.kt b/core/opentaint-configuration-rules/configuration-rules-jvm/src/main/kotlin/org/opentaint/dataflow/configuration/jvm/serialized/SerializedPosition.kt index 3357c0695..afc8a4390 100644 --- a/core/opentaint-configuration-rules/configuration-rules-jvm/src/main/kotlin/org/opentaint/dataflow/configuration/jvm/serialized/SerializedPosition.kt +++ b/core/opentaint-configuration-rules/configuration-rules-jvm/src/main/kotlin/org/opentaint/dataflow/configuration/jvm/serialized/SerializedPosition.kt @@ -88,6 +88,29 @@ sealed interface PositionBaseWithModifiers { PositionBaseWithModifiers } +data class PositionBeforeAnyField( + val position: PositionBaseWithModifiers, + val hasAnyField: Boolean, +) + +fun PositionBaseWithModifiers.beforeFirstAnyField(): PositionBeforeAnyField { + return when (this) { + is PositionBaseWithModifiers.BaseOnly -> PositionBeforeAnyField(this, hasAnyField = false) + is PositionBaseWithModifiers.WithModifiers -> { + val firstAnyField = modifiers.indexOfFirst { it == PositionModifier.AnyField } + if (firstAnyField < 0) return PositionBeforeAnyField(this, hasAnyField = false) + val retained = modifiers.take(firstAnyField) + + val position = if (retained.isEmpty()) { + PositionBaseWithModifiers.BaseOnly(base) + } else { + PositionBaseWithModifiers.WithModifiers(base, retained) + } + PositionBeforeAnyField(position, hasAnyField = true) + } + } +} + class PositionBaseWithModifiersSerializer : YamlContentPolymorphicSerializer(PositionBaseWithModifiers::class) { diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/taint/TaintFactAwareConditionEvaluator.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/taint/TaintFactAwareConditionEvaluator.kt index f253b8e61..de9b38e53 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/taint/TaintFactAwareConditionEvaluator.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/taint/TaintFactAwareConditionEvaluator.kt @@ -42,11 +42,41 @@ class TaintFactAwareConditionEvaluator( } } + private val anyFieldMarkEvalCache = hashMapOf, MarkEvaluationResult>() + private fun evalContainsMarkOnAnyField(positionAccess: PositionAccess, mark: TaintMarkAccessor): Boolean { val conditionBase = positionAccess.base() val relevantFacts = basedFacts[conditionBase] ?: return false + val result = anyFieldMarkEvalCache.computeIfAbsent(positionAccess to mark) { + val evaluatedFact = containsMarkOnAnyField(positionAccess, mark, relevantFacts) + + if (evaluatedFact != null) { + evaluatedFact + } else { + markAfterAnyAccessorResolver?.resolve(mark) + NoFact + } + } + + return when (result) { + is NoFact -> false + is EvaluatedFact -> { + hasEvaluatedContainsMark = true + evaluatedFacts += result + + true + } + } + } + + private fun containsMarkOnAnyField( + positionAccess: PositionAccess, + mark: TaintMarkAccessor, + relevantFacts: List + ): EvaluatedFact? { val requiredPosition = positionAccess.withSuffix(listOf(mark)) + for (reader in relevantFacts) { val positionWithTaintMark = reader.containsAnyPosition(requiredPosition) ?: continue @@ -54,16 +84,9 @@ class TaintFactAwareConditionEvaluator( if (!reader.containsPosition(finalPositionWithTaintMark)) continue val tmPosition = positionWithTaintMark.removeSuffix(listOf(mark)) - - hasEvaluatedContainsMark = true - evaluatedFacts += EvaluatedFact(reader, tmPosition, mark) - - return true + return EvaluatedFact(reader, tmPosition, mark) } - - markAfterAnyAccessorResolver?.resolve(mark) - - return false + return null } private val markEvalCache = hashMapOf, MarkEvaluationResult>() diff --git a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/taint/TaintUtil.kt b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/taint/TaintUtil.kt index 10408c49a..b9979319d 100644 --- a/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/taint/TaintUtil.kt +++ b/core/opentaint-dataflow-core/opentaint-dataflow/src/main/kotlin/org/opentaint/dataflow/taint/TaintUtil.kt @@ -21,10 +21,10 @@ abstract class TaintUtil(val apManager: ApManager) { abstract fun conditionFact(factReader: FinalFactReader): List - open fun patchSinkConditionFactReader(factReaders: List): List = factReaders - abstract fun handleReachedSink(rule: Sink, factReader: FinalFactReader?, evaluatedFacts: List) + open fun patchSinkConditionFactReader(factReaders: List): List = factReaders + fun applySinkRules( sinkRules: List>, factReader: FinalFactReader?, @@ -63,6 +63,7 @@ abstract class TaintUtil(val apManager: ApManager) { createFinalFact: (FinalFactAp, Trace) -> Unit, createEdge: (InitialFactAp, FinalFactAp, Trace) -> Unit, createNDEdge: (Set, FinalFactAp, Trace) -> Unit, + markAfterAnyFieldResolver: FactWithMarkAfterAnyAccessorResolver? = null, ) { if (sourceRules.isEmpty()) return @@ -76,7 +77,7 @@ abstract class TaintUtil(val apManager: ApManager) { apManager, initialFacts, conditionFactReaders, - markAfterAnyFieldResolver = null, // we don't expect such marks in source rules + markAfterAnyFieldResolver = markAfterAnyFieldResolver, assumptionsManager = sourceAssumptionsManager(), applyRule = { rule, evaluatedFacts -> // unconditional sources handled with zero fact diff --git a/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/GoCallExpr.kt b/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/GoCallExpr.kt index 009b576cd..6d4a913f5 100644 --- a/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/GoCallExpr.kt +++ b/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/GoCallExpr.kt @@ -65,5 +65,15 @@ fun GoCallExpr.signature(): GoFunctionSignature? { val receiverType = effectiveReceiver?.type val paramTypes = explicitArgs.map { it.type } val resultType = callInfo.resultType - return GoFunctionSignature(name, receiverType, paramTypes, resultType, resolvedCallee?.pkg?.name) + val variadicArgumentIndexes = resolvedCallee + ?.signature + ?.takeIf { it.isVariadic && it.params.isNotEmpty() } + ?.let { signature -> + val firstVariadicIndex = signature.params.lastIndex + (firstVariadicIndex until explicitArgs.size).toSet() + } + .orEmpty() + return GoFunctionSignature( + name, receiverType, paramTypes, resultType, resolvedCallee?.pkg?.name, variadicArgumentIndexes + ) } diff --git a/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/GoFlowFunctionUtils.kt b/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/GoFlowFunctionUtils.kt index 83066c2ef..7627a6797 100644 --- a/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/GoFlowFunctionUtils.kt +++ b/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/GoFlowFunctionUtils.kt @@ -8,6 +8,7 @@ import org.opentaint.dataflow.ap.ifds.ElementAccessor import org.opentaint.dataflow.ap.ifds.FieldAccessor import org.opentaint.dataflow.go.GoFlowFunctionUtils.Access.RefAccess import org.opentaint.dataflow.go.GoFlowFunctionUtils.Access.Simple +import org.opentaint.dataflow.go.rules.ActionPosition import org.opentaint.dataflow.go.rules.Position import org.opentaint.dataflow.go.rules.PositionAccessor import org.opentaint.dataflow.go.rules.PositionWithAccess @@ -289,24 +290,29 @@ object GoFlowFunctionUtils { return type is GoIRBasicType && type.kind == GoIRBasicTypeKind.STRING } + fun ActionPosition.resolvePosAccess(): PositionAccess = when (this) { + is ActionPosition.Exact -> position.resolvePosAccess() + is ActionPosition.AnyAccessorAfter -> PositionAccess.Complex(position.resolvePosAccess(), AnyAccessor) + } + fun Position.resolvePosAccess(): PositionAccess = when (this) { is Position.Simple -> resolvePosAccess() is PositionWithAccess -> PositionAccess.Complex(base.resolvePosAccess(), access.resolvePosAccess()) } - fun Position.Simple.resolvePosAccess(): PositionAccess.Simple { - val base = when (this) { - is Position.Argument -> AccessPathBase.Argument(index) - is Position.Result -> AccessPathBase.Return - is Position.This -> AccessPathBase.This - } - return PositionAccess.Simple(base) + fun Position.Simple.resolvePosAccess(): PositionAccess = when (this) { + is Position.Argument -> PositionAccess.Simple(AccessPathBase.Argument(index)) + is Position.Result -> PositionAccess.Simple(AccessPathBase.Return) + is Position.This -> PositionAccess.Simple(AccessPathBase.This) + is Position.ClassStatic -> PositionAccess.Complex( + PositionAccess.Simple(AccessPathBase.ClassStatic), + ClassStaticAccessor(className) + ) } fun PositionAccessor.resolvePosAccess(): Accessor = when (this) { is PositionAccessor.ElementAccessor -> ElementAccessor is PositionAccessor.FieldAccessor -> createFieldAccessor(className, fieldName) - is PositionAccessor.AnyAccessor -> AnyAccessor } fun detectGlobalReadName(inst: GoIRAssignInst): GoGlobalFieldSignature? { diff --git a/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/GoFunctionSignature.kt b/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/GoFunctionSignature.kt index 5f7b800d5..4adfcad22 100644 --- a/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/GoFunctionSignature.kt +++ b/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/GoFunctionSignature.kt @@ -8,6 +8,7 @@ data class GoFunctionSignature( val paramTypes: List, val resultType: GoIRType, val pkgName: String? = null, + val variadicArgumentIndexes: Set = emptySet(), ) { val arity: Int get() = paramTypes.size val hasReceiver: Boolean get() = receiverType != null diff --git a/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/analysis/GoCallRuleBasedSummaryRewriter.kt b/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/analysis/GoCallRuleBasedSummaryRewriter.kt index f07eec3f5..2efc8af91 100644 --- a/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/analysis/GoCallRuleBasedSummaryRewriter.kt +++ b/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/analysis/GoCallRuleBasedSummaryRewriter.kt @@ -1,5 +1,6 @@ package org.opentaint.dataflow.go.analysis +import org.opentaint.dataflow.ap.ifds.AccessPathBase import org.opentaint.dataflow.ap.ifds.TaintMarkAccessor import org.opentaint.dataflow.ap.ifds.access.ApManager import org.opentaint.dataflow.ap.ifds.access.FinalFactAp @@ -8,7 +9,7 @@ import org.opentaint.dataflow.configuration.go.serialized.GoUserDefinedRuleInfo import org.opentaint.dataflow.go.GoCallExpr import org.opentaint.dataflow.go.GoFlowFunctionUtils.resolvePosAccess import org.opentaint.dataflow.go.GoFunctionSignature -import org.opentaint.dataflow.go.rules.Position +import org.opentaint.dataflow.go.rules.ActionPosition import org.opentaint.dataflow.go.rules.RemoveMark import org.opentaint.dataflow.go.rules.TaintRule import org.opentaint.dataflow.go.signature @@ -31,9 +32,14 @@ class GoCallRuleBasedSummaryRewriter( private val callSignature: GoFunctionSignature? get() = callExpr.signature() + private fun ActionPosition.cleanReach(): TaintCleanReach = when (this) { + is ActionPosition.Exact -> TaintCleanReach.Exact + is ActionPosition.AnyAccessorAfter -> TaintCleanReach.ExactAndAnyField + } + private data class UserRuleDefinedAction( val rule: TaintRule, - val positions: Set, + val positions: Set, val controlledMarks: Set ) @@ -47,7 +53,7 @@ class GoCallRuleBasedSummaryRewriter( if (sourceRuleWithCond.condition.isFalse) continue - val positions = sourceRule.actionsAfter.mapTo(hashSetOf()) { it.rawPosition() } + val positions = sourceRule.actionsAfter.mapTo(hashSetOf()) { it.pos } result += UserRuleDefinedAction(sourceRule, positions, ruleInfo.relevantTaintMarks) } @@ -72,7 +78,7 @@ class GoCallRuleBasedSummaryRewriter( val cleanedFact = userRuleDefinedActions.applyCleanerActions( evalAction = { f, rule, action -> val pos = action.pos.resolvePosAccess() - cleanEvaluator.removeFinalFact(f, pos, TaintMarkAccessor(action.mark), rule, action, TaintCleanReach.Exact) + cleanEvaluator.removeFinalFact(f, pos, TaintMarkAccessor(action.mark), rule, action, action.pos.cleanReach()) }, itemRule = { it.rule }, itemActions = { action -> diff --git a/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/analysis/GoSequentTaintUtil.kt b/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/analysis/GoSequentTaintUtil.kt index a9e35bbcd..0b0f25f3c 100644 --- a/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/analysis/GoSequentTaintUtil.kt +++ b/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/analysis/GoSequentTaintUtil.kt @@ -8,6 +8,7 @@ import org.opentaint.dataflow.configuration.CommonTaintConfigurationSource import org.opentaint.dataflow.configuration.isTrue import org.opentaint.dataflow.go.GoFlowFunctionUtils import org.opentaint.dataflow.go.GoFlowFunctionUtils.resolvePosAccess +import org.opentaint.dataflow.go.rules.ActionPosition import org.opentaint.dataflow.go.rules.GoAssignAction import org.opentaint.dataflow.go.rules.TaintRule import org.opentaint.dataflow.taint.PositionAccess @@ -59,7 +60,7 @@ inline fun applyGlobalOrFieldReadSourceRules( } } -fun GoAssignAction.resolvePosAccess(): PositionAccess = when (this) { - is GoAssignAction.Direct -> pos.resolvePosAccess() - is GoAssignAction.AnyAccessor -> PositionAccess.Complex(pos.resolvePosAccess(), AnyAccessor) +fun GoAssignAction.resolvePosAccess(): PositionAccess = when (val actionPos = pos) { + is ActionPosition.Exact -> actionPos.position.resolvePosAccess() + is ActionPosition.AnyAccessorAfter -> PositionAccess.Complex(actionPos.position.resolvePosAccess(), AnyAccessor) } diff --git a/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/rules/GoBasicAtomEvaluator.kt b/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/rules/GoBasicAtomEvaluator.kt index b7153d831..b46d30f68 100644 --- a/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/rules/GoBasicAtomEvaluator.kt +++ b/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/rules/GoBasicAtomEvaluator.kt @@ -57,6 +57,7 @@ class GoBasicAtomEvaluator( is Position.Argument -> callExpr.explicitArgs.getOrNull(pos.index) is Position.Result -> returnValue is Position.This -> callExpr.effectiveReceiver + is Position.ClassStatic -> null } private inline fun cmpConstant( diff --git a/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/rules/GoConditionResolver.kt b/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/rules/GoConditionResolver.kt index 8ca0a6318..1a7f8d3bc 100644 --- a/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/rules/GoConditionResolver.kt +++ b/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/rules/GoConditionResolver.kt @@ -5,6 +5,7 @@ import org.opentaint.dataflow.configuration.go.serialized.GoSerializedCondition import org.opentaint.dataflow.configuration.jvm.serialized.PositionBase import org.opentaint.dataflow.configuration.jvm.serialized.PositionBaseWithModifiers import org.opentaint.dataflow.configuration.jvm.serialized.PositionModifier +import org.opentaint.dataflow.configuration.jvm.serialized.beforeFirstAnyField import org.opentaint.dataflow.configuration.mkAnd import org.opentaint.dataflow.configuration.mkFalse import org.opentaint.dataflow.configuration.mkOr @@ -33,12 +34,15 @@ private fun GoSerializedCondition.resolveImpl(signature: GoFunctionSignature): C is GoSerializedCondition.Or -> mkOr(anyOf.map { it.resolveImpl(signature) }) is GoSerializedCondition.Not -> CommonCondition.Not(not.resolveImpl(signature)) - is GoSerializedCondition.ContainsMark -> pos.resolveAny(signature, PositionBaseWithModifiers::resolve) { - GoRuleCondition.ContainsMark(it, tainted) - } - - is GoSerializedCondition.ContainsMarkOnAnyAccessor -> pos.resolveAny(signature, PositionBaseWithModifiers::resolve) { - GoRuleCondition.ContainsMarkOnAnyAccessor(it, tainted) + is GoSerializedCondition.ContainsMark -> { + val (position, hasAnyField) = pos.beforeFirstAnyField() + position.resolveAny(signature, PositionBaseWithModifiers::resolve) { + if (hasAnyField) { + GoRuleCondition.ContainsMarkOnAnyAccessor(it, tainted) + } else { + GoRuleCondition.ContainsMark(it, tainted) + } + } } is GoSerializedCondition.ConstantCmp -> { @@ -109,7 +113,7 @@ fun PositionBase.resolve(signature: GoFunctionSignature): List } } - is PositionBase.ClassStatic -> error("Unused") + is PositionBase.ClassStatic -> listOf(Position.ClassStatic(className)) is PositionBase.Result -> listOf(Position.Result) is PositionBase.This -> if (signature.hasReceiver) listOf(Position.This) else emptyList() } @@ -141,6 +145,7 @@ fun PositionBaseWithModifiers.resolve(signature: GoFunctionSignature): List = when (pos) { is Position.Argument -> listOfNotNull(paramTypes.getOrNull(pos.index)) + is Position.ClassStatic -> emptyList() is Position.Result -> { val types = mutableListOf(resultType) if (resultType is GoIRTupleType) { @@ -178,7 +183,7 @@ private fun List.resolveUntyped(): List? { PositionAccessor.FieldAccessor(mod.className, mod.fieldName, mod.fieldType) } - is PositionModifier.AnyField -> PositionAccessor.AnyAccessor + is PositionModifier.AnyField -> error("AnyField must be specialized before resolving Position") } } } diff --git a/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/rules/GoTaintAction.kt b/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/rules/GoTaintAction.kt index 4e37b904b..a96b16719 100644 --- a/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/rules/GoTaintAction.kt +++ b/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/rules/GoTaintAction.kt @@ -5,35 +5,32 @@ import org.opentaint.dataflow.configuration.CommonTaintAssignAction sealed interface GoTaintAction : CommonTaintAction +sealed interface ActionPosition { + data class Exact(val position: Position) : ActionPosition + data class AnyAccessorAfter(val position: Position) : ActionPosition +} + data class CopyTaintMark( val mark: String, - val from: Position, - val to: Position, + val from: ActionPosition, + val to: ActionPosition, ) : GoTaintAction data class CopyData( - val from: Position, - val to: Position, + val from: ActionPosition, + val to: ActionPosition, ) : GoTaintAction data class RemoveMark( val mark: String, - val pos: Position, + val pos: ActionPosition, ) : GoTaintAction data class RemoveAllMarks( - val pos: Position, + val pos: ActionPosition, ) : GoTaintAction -sealed interface GoAssignAction : GoTaintAction, CommonTaintAssignAction { - val mark: String - fun rawPosition(): Position - - data class Direct(override val mark: String, val pos: Position) : GoAssignAction { - override fun rawPosition(): Position = pos - } - - data class AnyAccessor(override val mark: String, val pos: Position) : GoAssignAction { - override fun rawPosition(): Position = pos - } -} +data class GoAssignAction( + val mark: String, + val pos: ActionPosition, +) : GoTaintAction, CommonTaintAssignAction diff --git a/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/rules/GoTaintConfiguration.kt b/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/rules/GoTaintConfiguration.kt index 03c38dfdf..af624f225 100644 --- a/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/rules/GoTaintConfiguration.kt +++ b/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/rules/GoTaintConfiguration.kt @@ -14,6 +14,7 @@ import org.opentaint.dataflow.configuration.go.serialized.GoSerializedTaintConfi import org.opentaint.dataflow.configuration.isFalse import org.opentaint.dataflow.configuration.jvm.serialized.PositionBase import org.opentaint.dataflow.configuration.jvm.serialized.PositionBaseWithModifiers +import org.opentaint.dataflow.configuration.jvm.serialized.beforeFirstAnyField import org.opentaint.dataflow.go.GoFieldSignature import org.opentaint.dataflow.go.GoFunctionSignature import org.opentaint.dataflow.go.GoGlobalFieldSignature @@ -238,19 +239,8 @@ class GoTaintConfiguration : GoTaintRulesProvider { return TaintRule.Sink(signature.name, condition, trackFacts, id, meta, rule.info, rule.serializedId) } - private fun List.specialize(signature: GoFunctionSignature) = flatMap { t -> - when (t) { - is GoSerializedAssignAction.Direct -> t.pos.resolve(signature) - .map { GoAssignAction.Direct(t.kind, it) } - - is GoSerializedAssignAction.AnyAccessor -> t.pos.resolve(signature) - .flatMap { - listOf( - GoAssignAction.AnyAccessor(t.kind, it), - GoAssignAction.Direct(t.kind, it), // todo: remove this hack after fact fix - ) - } - } + private fun List.specialize(signature: GoFunctionSignature) = flatMap { action -> + action.pos.resolveActionPosition(signature).map { GoAssignAction(action.kind, it) } } private fun specialize(rule: GoSerializedRule.PassThrough, signature: GoFunctionSignature): TaintRule.PassThrough? { @@ -267,19 +257,36 @@ class GoTaintConfiguration : GoTaintRulesProvider { } private fun GoSerializedPassAction.toTaintAction(signature: GoFunctionSignature): List = - from.resolve(signature).flatMap { f -> - to.resolve(signature).map { t -> - val kind = taintKind - if (kind == null) CopyData(f, t) else CopyTaintMark(kind, f, t) + from.resolveActionPosition(signature).flatMap { source -> + val sources = buildList { + add(source) + val exact = source as? ActionPosition.Exact + val argument = exact?.position as? Position.Argument + if (argument != null && argument.index in signature.variadicArgumentIndexes) { + add(ActionPosition.Exact(PositionWithAccess(argument, PositionAccessor.ElementAccessor))) + } + } + sources.flatMap { f -> + to.resolveActionPosition(signature).map { t -> + val kind = taintKind + if (kind == null) CopyData(f, t) else CopyTaintMark(kind, f, t) + } } } private fun GoSerializedCleanAction.toTaintAction(signature: GoFunctionSignature): List = - pos.resolve(signature).map { + pos.resolveActionPosition(signature).map { val kind = taintKind if (kind == null) RemoveAllMarks(it) else RemoveMark(kind, it) } + private fun PositionBaseWithModifiers.resolveActionPosition(signature: GoFunctionSignature): List { + val (position, hasAnyField) = beforeFirstAnyField() + return position.resolve(signature).map { + if (hasAnyField) ActionPosition.AnyAccessorAfter(it) else ActionPosition.Exact(it) + } + } + private fun generateRuleId(rule: GoSerializedRule.Sink): String { rule.meta?.cwe?.firstOrNull()?.let { return "CWE-$it" } return "go-generated-id-${ruleIdGen.incrementAndGet()}" @@ -297,7 +304,6 @@ class GoTaintConfiguration : GoTaintRulesProvider { is GoSerializedCondition.Not -> validateConditionForFieldSource(condition.not) is GoSerializedCondition.ContainsMark -> validatePositionWithModifiersForFieldSource(condition.pos) - is GoSerializedCondition.ContainsMarkOnAnyAccessor -> validatePositionWithModifiersForFieldSource(condition.pos) is GoSerializedCondition.ConstantCmp -> validatePositionBaseForFieldSource(condition.pos) is GoSerializedCondition.ConstantMatches -> validatePositionBaseForFieldSource(condition.pos) @@ -308,11 +314,7 @@ class GoTaintConfiguration : GoTaintRulesProvider { } private fun validateAssignActionForFieldSource(action: GoSerializedAssignAction) { - val pos = when (action) { - is GoSerializedAssignAction.AnyAccessor -> action.pos - is GoSerializedAssignAction.Direct -> action.pos - } - check(pos.base is PositionBase.Result) { "Unsupported field-source taint target: ${pos.base}" } + check(action.pos.base is PositionBase.Result) { "Unsupported field-source taint target: ${action.pos.base}" } } private fun validatePositionWithModifiersForFieldSource(pos: PositionBaseWithModifiers) { diff --git a/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/rules/Position.kt b/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/rules/Position.kt index 2359a5586..b2d43ec58 100644 --- a/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/rules/Position.kt +++ b/core/opentaint-dataflow-core/opentaint-go-dataflow/src/main/kotlin/org/opentaint/dataflow/go/rules/Position.kt @@ -12,6 +12,8 @@ sealed interface Position { data object Result : Simple { override fun toString(): String = javaClass.simpleName } + + data class ClassStatic(val className: String) : Simple } sealed interface PositionAccessor { @@ -24,8 +26,6 @@ sealed interface PositionAccessor { val fieldName: String, val fieldType: String ) : PositionAccessor - - data object AnyAccessor : PositionAccessor } data class PositionWithAccess( diff --git a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/JIRLocalAliasAnalysis.kt b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/JIRLocalAliasAnalysis.kt index 23cba41c6..300baa4a7 100644 --- a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/JIRLocalAliasAnalysis.kt +++ b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/JIRLocalAliasAnalysis.kt @@ -6,6 +6,7 @@ import org.opentaint.dataflow.ap.ifds.analysis.alias.AAInfo import org.opentaint.dataflow.ap.ifds.analysis.alias.AnalysisResult import org.opentaint.dataflow.ap.ifds.analysis.alias.ContextInfo import org.opentaint.dataflow.ap.ifds.analysis.alias.LocalAliasAnalysis +import org.opentaint.dataflow.configuration.jvm.ActionPosition import org.opentaint.dataflow.configuration.jvm.Argument import org.opentaint.dataflow.configuration.jvm.ClassStatic import org.opentaint.dataflow.configuration.jvm.CopyAllMarks @@ -87,6 +88,11 @@ class JIRLocalAliasAnalysis( return externalAssigns } + private fun ActionPosition.toExternalObject(): ExternalObject? = when (this) { + is ActionPosition.Exact -> position.toExternalObject() + is ActionPosition.AnyAccessorAfter -> null + } + private fun Position.toExternalObject(): ExternalObject? { val base = when (this) { is Argument -> ExternalCallModelProvider.Position.Arg(index) @@ -105,7 +111,6 @@ class JIRLocalAliasAnalysis( } private fun PositionAccessor.toAaAccessor(): AAHeapAccessor? = when (this) { - is PositionAccessor.AnyFieldAccessor -> null is PositionAccessor.ElementAccessor -> ArrayAlias is PositionAccessor.FieldAccessor -> FieldAlias( AliasAccessor.Field(className, fieldName, fieldType), diff --git a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/analysis/JIRMethodCallFlowFunction.kt b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/analysis/JIRMethodCallFlowFunction.kt index aa590a7af..5e537cfd4 100644 --- a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/analysis/JIRMethodCallFlowFunction.kt +++ b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/analysis/JIRMethodCallFlowFunction.kt @@ -121,7 +121,8 @@ class JIRMethodCallFlowFunction( final.forEachSourceFactWithAliases { addUnchecked(CallToReturnNonDistributiveFact(initial, it, trace)) } - } + }, + markAfterAnyFieldResolver = markAfterAnyFieldResolver, ) JIRMethodCallFactMapper.mapMethodCallToStartFlowFact( @@ -239,6 +240,7 @@ class JIRMethodCallFlowFunction( createFinalFact: (FinalFactAp, TraceInfo) -> Unit, createEdge: (InitialFactAp, FinalFactAp, TraceInfo) -> Unit, createNDEdge: (Set, FinalFactAp, TraceInfo) -> Unit, + markAfterAnyFieldResolver: FactWithMarkAfterAnyAccessorResolver? = null, ) { val sourceRules = taintCtx.sourceRulesForCallStatement(statement, callExpr, returnValue, factReader?.factAp) if (sourceRules.isEmpty()) return @@ -246,7 +248,8 @@ class JIRMethodCallFlowFunction( val taintUtil = JIRMethodCallTaintUtil(apManager, statement, callExpr, analysisContext, generateTrace) taintUtil.applySourceRules( sourceRules, initialFacts, factReader, exclusion, - createFinalFact, createEdge, createNDEdge + createFinalFact, createEdge, createNDEdge, + markAfterAnyFieldResolver ) } diff --git a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/analysis/JIRMethodCallRuleBasedSummaryRewriter.kt b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/analysis/JIRMethodCallRuleBasedSummaryRewriter.kt index 48db87b26..1b2697e67 100644 --- a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/analysis/JIRMethodCallRuleBasedSummaryRewriter.kt +++ b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/analysis/JIRMethodCallRuleBasedSummaryRewriter.kt @@ -3,8 +3,7 @@ package org.opentaint.dataflow.jvm.ap.ifds.analysis import org.opentaint.dataflow.ap.ifds.AccessPathBase import org.opentaint.dataflow.ap.ifds.access.ApManager import org.opentaint.dataflow.ap.ifds.access.FinalFactAp -import org.opentaint.dataflow.configuration.TaintCleanReach -import org.opentaint.dataflow.configuration.jvm.Position +import org.opentaint.dataflow.configuration.jvm.ActionPosition import org.opentaint.dataflow.configuration.jvm.RemoveMark import org.opentaint.dataflow.configuration.jvm.TaintConfigurationItem import org.opentaint.dataflow.configuration.jvm.TaintMark @@ -48,13 +47,13 @@ class JIRMethodCallRuleBasedSummaryRewriter( private data class UserRuleDefinedAction( val rule: TaintConfigurationItem, - val positions: Set, + val positions: Set, ) private val userRuleDefinedActions: Map>> by lazy { val result = hashMapOf>>() - fun indexRule(rule: TaintConfigurationItem, positions: Set, marks: Set) { + fun indexRule(rule: TaintConfigurationItem, positions: Set, marks: Set) { positions.groupBy { it.resolveBaseAp() }.forEach { (base, basePositions) -> val actionsByMark = result.computeIfAbsent(base) { hashMapOf() } val action = UserRuleDefinedAction(rule, basePositions.toSet()) @@ -101,9 +100,7 @@ class JIRMethodCallRuleBasedSummaryRewriter( itemRule = { it.rule }, itemActions = { ruleDefinedAction -> val taintMark = TaintMark(mark) - ruleDefinedAction.positions.map { - RemoveMark(taintMark, it, TaintCleanReach.Exact) - } + ruleDefinedAction.positions.map { RemoveMark(taintMark, it) } }, initial = current ) diff --git a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/analysis/JIRMethodGetDefault.kt b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/analysis/JIRMethodGetDefault.kt index 2767c6482..606e19d62 100644 --- a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/analysis/JIRMethodGetDefault.kt +++ b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/analysis/JIRMethodGetDefault.kt @@ -1,6 +1,7 @@ package org.opentaint.dataflow.jvm.ap.ifds.analysis import org.opentaint.dataflow.ap.ifds.taint.TaintAnalysisContext.RuleWithCondition +import org.opentaint.dataflow.configuration.jvm.ActionPosition.Exact import org.opentaint.dataflow.configuration.jvm.CopyAllMarks import org.opentaint.dataflow.configuration.jvm.PositionAccessor import org.opentaint.dataflow.configuration.jvm.PositionWithAccess @@ -27,11 +28,11 @@ class JIRMethodGetDefault( private fun TypeName.mayBeArray(): Boolean = isArray || this == objectTypeName private val getDefaultActions = listOf( - CopyAllMarks(from = This, to = Result) + CopyAllMarks(from = Exact(This), to = Exact(Result)) ) private val getDefaultArrayActions = listOf( - CopyAllMarks(from = This, to = PositionWithAccess(Result, PositionAccessor.ElementAccessor)) + CopyAllMarks(from = Exact(This), to = Exact(PositionWithAccess(Result, PositionAccessor.ElementAccessor))) ) fun defaultPropagationRules(method: JIRMethod): List> { diff --git a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/taint/TaintEvaluator.kt b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/taint/TaintEvaluator.kt index b2c0119b8..60ee38d3a 100644 --- a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/taint/TaintEvaluator.kt +++ b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/main/kotlin/org/opentaint/dataflow/jvm/ap/ifds/taint/TaintEvaluator.kt @@ -7,6 +7,8 @@ import org.opentaint.dataflow.ap.ifds.ElementAccessor import org.opentaint.dataflow.ap.ifds.FieldAccessor import org.opentaint.dataflow.ap.ifds.TaintMarkAccessor import org.opentaint.dataflow.configuration.CommonTaintConfigurationItem +import org.opentaint.dataflow.configuration.TaintCleanReach +import org.opentaint.dataflow.configuration.jvm.ActionPosition import org.opentaint.dataflow.configuration.jvm.Argument import org.opentaint.dataflow.configuration.jvm.ClassStatic import org.opentaint.dataflow.configuration.jvm.Condition @@ -47,16 +49,17 @@ class JIRTaintCleanActionEvaluator( ): List { val variable = action.position.resolveAp() val mark = TaintMarkAccessor(action.mark.name) - val cleaned = evaluator.removeFinalFact(initialFact, variable, mark, rule, action, action.reach) + val cleaned = evaluator.removeFinalFact(initialFact, variable, mark, rule, action, action.position.cleanReach()) val positionType = positionTypeResolver.resolve(variable) if (positionType?.typeName != STRING) { return cleaned } - val stringBytesVar = PositionWithAccess(action.position, stringBytes).resolveAp() + val stringBytesPosition = action.position.append(stringBytes) + val stringBytesVar = stringBytesPosition.resolveAp() return cleaned.flatMap { f -> - evaluator.removeFinalFact(f, stringBytesVar, mark, rule, action, action.reach) + evaluator.removeFinalFact(f, stringBytesVar, mark, rule, action, stringBytesPosition.cleanReach()) } } @@ -70,6 +73,11 @@ class JIRTaintCleanActionEvaluator( } } +fun ActionPosition.resolveBaseAp(): AccessPathBase = when (this) { + is ActionPosition.Exact -> position.resolveBaseAp() + is ActionPosition.AnyAccessorAfter -> position.resolveBaseAp() +} + fun Position.resolveBaseAp(): AccessPathBase = when (this) { is Argument -> AccessPathBase.Argument(index) is This -> AccessPathBase.This @@ -78,6 +86,21 @@ fun Position.resolveBaseAp(): AccessPathBase = when (this) { is PositionWithAccess -> base.resolveBaseAp() } +fun ActionPosition.resolveAp(): PositionAccess = when (this) { + is ActionPosition.Exact -> position.resolveAp() + is ActionPosition.AnyAccessorAfter -> PositionAccess.Complex(position.resolveAp(), AnyAccessor) +} + +fun ActionPosition.cleanReach(): TaintCleanReach = when (this) { + is ActionPosition.Exact -> TaintCleanReach.Exact + is ActionPosition.AnyAccessorAfter -> TaintCleanReach.ExactAndAnyField +} + +private fun ActionPosition.append(accessor: PositionAccessor): ActionPosition = when (this) { + is ActionPosition.Exact -> ActionPosition.Exact(PositionWithAccess(position, accessor)) + is ActionPosition.AnyAccessorAfter -> ActionPosition.AnyAccessorAfter(PositionWithAccess(position, accessor)) +} + fun Position.resolveAp(): PositionAccess = resolveAp(resolveBaseAp()) fun Position.resolveAp(baseAp: AccessPathBase): PositionAccess { @@ -101,7 +124,6 @@ fun Position.resolveAp(baseAp: AccessPathBase): PositionAccess { } fun PositionAccessor.toApAccessor() = when(this) { - PositionAccessor.AnyFieldAccessor -> AnyAccessor PositionAccessor.ElementAccessor -> ElementAccessor is PositionAccessor.FieldAccessor -> FieldAccessor(className, fieldName, fieldType) } diff --git a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/test/kotlin/org/opentaint/dataflow/jvm/ap/ifds/alias/DSUAliasAnalysisInvalidateOuterHeapAliasesTest.kt b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/test/kotlin/org/opentaint/dataflow/jvm/ap/ifds/alias/DSUAliasAnalysisInvalidateOuterHeapAliasesTest.kt index aefa6cc53..65d1ef1e9 100644 --- a/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/test/kotlin/org/opentaint/dataflow/jvm/ap/ifds/alias/DSUAliasAnalysisInvalidateOuterHeapAliasesTest.kt +++ b/core/opentaint-dataflow-core/opentaint-jvm-dataflow/src/test/kotlin/org/opentaint/dataflow/jvm/ap/ifds/alias/DSUAliasAnalysisInvalidateOuterHeapAliasesTest.kt @@ -39,6 +39,69 @@ class DSUAliasAnalysisInvalidateOuterHeapAliasesTest { private fun State.invalidate(builder: StateBuilder, start: Set): State = with(analysis) { invalidateOuterHeapAliases(builder.infoIds(start)) } + /** + * KNOWN FALSE NEGATIVE: invalidation drops a still-live heap alias entirely. + * + * Shape of `o = build(); x = o.f; ; x.v = tainted; sink(o)`: o's group holds an + * outer element (the call return), and x holds the value loaded from the slot. The opaque call + * may reassign `o.f`, so the LIVE slot link must break — a later load of `o.f` must not rejoin + * x's set. But the pair itself should survive in some form, so a store through `x` can still be + * rebased onto `o.f` as a may-alias. + * + * The DSU cannot represent a singleton set, so breaking the link removes the whole pair and the + * `%tmp ~ o.f` relation is lost. The tainted store through the temp is never rebased onto + * `o.f.v`, which is the depth-2 false negative parked as + * `taint.StarDeepSink.KnownFnDepth2`. Depth >= 3 escapes it only by accident: the intermediate + * temps are dead at the call, so dead-local cleanup has already turned the chain into orphaned + * elements that the invalidation cascade cannot see through. + * + * This test pins the current, lossy behaviour. When the underlying representation gains a way + * to keep the relation (without letting a post-call load rejoin the pre-call set), flip the + * expectation to keep `x ~ o.f` and unpark `KnownFnDepth2`. + */ + @Test + fun invalidateDropsLiveHeapAliasLosingPathRelation() { + val builder = fillState { + val o = local(0) + val outer = outerThis() + merge(setOf(o, outer)) + val x = local(2) + val f = fieldAlias(o, "f", isImmutable = false) + merge(setOf(x, f)) + } + val state = builder.build() + + val result = state.invalidate(builder, emptySet()) + + val expected = buildState { + // x's set is gone entirely: the o.f element was removed and x was never merged into + // the DSU on its own, so nothing records that x held the value of o.f. + val o2 = local(0) + val outer2 = outerThis() + merge(setOf(o2, outer2)) + } + + assertEquals(expected, result) + } + + @Test + fun invalidateIsIdempotent() { + val builder = fillState { + val o = local(0) + val outer = outerThis() + merge(setOf(o, outer)) + val x = local(2) + val f = fieldAlias(o, "f", isImmutable = false) + merge(setOf(x, f)) + } + val state = builder.build() + + val once = state.invalidate(builder, emptySet()) + val twice = once.invalidate(builder, emptySet()) + + assertEquals(once, twice) + } + @Test fun invalidateEmptyStartSetIsNoop() { val builder = fillState { diff --git a/core/opentaint-go-querylang/grammar/semgrep-extensions.patch b/core/opentaint-go-querylang/grammar/semgrep-extensions.patch index 01e47f202..13b715037 100644 --- a/core/opentaint-go-querylang/grammar/semgrep-extensions.patch +++ b/core/opentaint-go-querylang/grammar/semgrep-extensions.patch @@ -1,15 +1,16 @@ --- a/GoLexer.g4 +++ b/GoLexer.g4 -@@ -69,6 +69,15 @@ +@@ -69,6 +69,16 @@ TYPE : 'type'; VAR : 'var'; - + + +// --- Semgrep extensions --- +LDOTS : '<...'; +RDOTS : '...>' -> mode(NLSEMI); +METAVAR_ELLIPSIS : '$...' [A-Z_] [A-Z_0-9]* -> mode(NLSEMI); +ANONYMOUS_METAVAR : '$_' -> mode(NLSEMI); ++METAVAR_STAR_IDENT : '$*' [A-Z_] [A-Z_0-9]* -> mode(NLSEMI); +METAVAR_IDENT : '$' [A-Z_] [A-Z_0-9]* -> mode(NLSEMI); +// --- end semgrep extensions --- + @@ -45,15 +46,16 @@ // Hidden tokens -@@ -210,6 +229,16 @@ +@@ -210,6 +230,17 @@ fragment UNICODE_LETTER: [\p{L}]; - + mode NLSEMI; +// --- Semgrep extensions in NLSEMI mode --- +LDOTS_NLSEMI : '<...' -> type(LDOTS), mode(DEFAULT_MODE); +RDOTS_NLSEMI : '...>' -> type(RDOTS); +METAVAR_ELLIPSIS_NLSEMI : '$...' [A-Z_] [A-Z_0-9]* -> type(METAVAR_ELLIPSIS); +ANONYMOUS_METAVAR_NLSEMI : '$_' -> type(ANONYMOUS_METAVAR); ++METAVAR_STAR_IDENT_NLSEMI : '$*' [A-Z_] [A-Z_0-9]* -> type(METAVAR_STAR_IDENT); +METAVAR_IDENT_NLSEMI : '$' [A-Z_] [A-Z_0-9]* -> type(METAVAR_IDENT); +METAVAR_LITERAL_NLSEMI : '"' '$' [A-Z_] [A-Z_0-9]* '"' -> type(METAVAR_LITERAL); +ELLIPSIS_LITERAL_NLSEMI : '"' '...' '"' -> type(ELLIPSIS_LITERAL); @@ -67,7 +69,7 @@ @@ -39,10 +39,52 @@ superClass = GoParserBase; } - + +@parser::members { + // Semgrep: enable the `operand` (composite-literal) alternative for a qualified + // type literal `pkg.T{...}`. Without imports the base predicate routes `pkg.T` @@ -122,7 +124,7 @@ ; -identifier : IDENTIFIER ; -+identifier : IDENTIFIER | METAVAR_IDENT | METAVAR_ELLIPSIS | ANONYMOUS_METAVAR ; ++identifier : IDENTIFIER | METAVAR_IDENT | METAVAR_STAR_IDENT | METAVAR_ELLIPSIS | ANONYMOUS_METAVAR ; importDecl - : IMPORT (importSpec | L_PAREN (importSpec eos)* R_PAREN) @@ -264,17 +266,18 @@ ; functionType -@@ -391,6 +440,9 @@ +@@ -391,6 +440,10 @@ parameterDecl : identifierList? ELLIPSIS? type_ + | ELLIPSIS + | METAVAR_ELLIPSIS + | METAVAR_IDENT ++ | METAVAR_STAR_IDENT ; expression -@@ -408,13 +460,15 @@ +@@ -408,13 +461,15 @@ ) expression | expression LOGICAL_AND expression | expression LOGICAL_OR expression @@ -291,19 +294,23 @@ ; conversion -@@ -424,6 +478,7 @@ +@@ -424,6 +479,9 @@ operand - : literal +- : literal ++ : METAVAR_STAR_IDENT ++ | literal | operandName typeArgs? ++ | L_PAREN METAVAR_STAR_IDENT COLON type_ R_PAREN + | L_PAREN METAVAR_IDENT COLON type_ R_PAREN | L_PAREN expression R_PAREN ; - -@@ -451,11 +506,17 @@ + +@@ -451,11 +507,18 @@ operandName : IDENTIFIER + | METAVAR_IDENT ++ | METAVAR_STAR_IDENT + | ANONYMOUS_METAVAR + | METAVAR_ELLIPSIS | qualifiedIdent @@ -317,7 +324,7 @@ ; compositeLit -@@ -472,7 +533,7 @@ +@@ -472,7 +535,7 @@ ; literalValue @@ -326,7 +333,7 @@ ; elementList -@@ -491,6 +552,7 @@ +@@ -491,6 +554,7 @@ element : expression | literalValue @@ -334,7 +341,7 @@ ; structType -@@ -499,11 +561,14 @@ +@@ -499,11 +563,14 @@ fieldDecl : (identifierList type_ | embeddedField) tag = string_? @@ -349,7 +356,7 @@ ; embeddedField -@@ -532,6 +597,7 @@ +@@ -532,6 +599,7 @@ methodExpr : type_ DOT IDENTIFIER diff --git a/core/opentaint-go-querylang/samples-go-massive/loginj_01_env_logprintf/rule.yaml b/core/opentaint-go-querylang/samples-go-massive/loginj_01_env_logprintf/rule.yaml index e53e50f14..3a42b1962 100644 --- a/core/opentaint-go-querylang/samples-go-massive/loginj_01_env_logprintf/rule.yaml +++ b/core/opentaint-go-querylang/samples-go-massive/loginj_01_env_logprintf/rule.yaml @@ -7,4 +7,6 @@ rules: pattern-sources: - pattern: os.Getenv("USER_NAME") pattern-sinks: - - pattern: log.Printf($FMT, $X) + - patterns: + - pattern: log.Printf($FMT, $*X) + - focus-metavariable: $X diff --git a/core/opentaint-go-querylang/samples-go-massive/loginj_02_env_logprintf_concat/rule.yaml b/core/opentaint-go-querylang/samples-go-massive/loginj_02_env_logprintf_concat/rule.yaml index 141d7c782..be32d4e75 100644 --- a/core/opentaint-go-querylang/samples-go-massive/loginj_02_env_logprintf_concat/rule.yaml +++ b/core/opentaint-go-querylang/samples-go-massive/loginj_02_env_logprintf_concat/rule.yaml @@ -7,4 +7,6 @@ rules: pattern-sources: - pattern: os.Getenv("REQ_ID") pattern-sinks: - - pattern: log.Printf($FMT, $X) + - patterns: + - pattern: log.Printf($FMT, $*X) + - focus-metavariable: $X diff --git a/core/opentaint-go-querylang/samples-go-massive/loginj_03_form_logprint/rule.yaml b/core/opentaint-go-querylang/samples-go-massive/loginj_03_form_logprint/rule.yaml index a0684aa05..da4129b5c 100644 --- a/core/opentaint-go-querylang/samples-go-massive/loginj_03_form_logprint/rule.yaml +++ b/core/opentaint-go-querylang/samples-go-massive/loginj_03_form_logprint/rule.yaml @@ -7,4 +7,6 @@ rules: pattern-sources: - pattern: $R.FormValue($K) pattern-sinks: - - pattern: log.Print($X) + - patterns: + - pattern: log.Print($*X) + - focus-metavariable: $X diff --git a/core/opentaint-go-querylang/samples-go-massive/loginj_04_postform_logprint/rule.yaml b/core/opentaint-go-querylang/samples-go-massive/loginj_04_postform_logprint/rule.yaml index d1f1f91a9..6b5c466fa 100644 --- a/core/opentaint-go-querylang/samples-go-massive/loginj_04_postform_logprint/rule.yaml +++ b/core/opentaint-go-querylang/samples-go-massive/loginj_04_postform_logprint/rule.yaml @@ -7,4 +7,6 @@ rules: pattern-sources: - pattern: $R.PostFormValue($K) pattern-sinks: - - pattern: log.Print($X) + - patterns: + - pattern: log.Print($*X) + - focus-metavariable: $X diff --git a/core/opentaint-go-querylang/samples-go-massive/loginj_07_env_tolower_logprintf/rule.yaml b/core/opentaint-go-querylang/samples-go-massive/loginj_07_env_tolower_logprintf/rule.yaml index 8addc0c73..053a1d25b 100644 --- a/core/opentaint-go-querylang/samples-go-massive/loginj_07_env_tolower_logprintf/rule.yaml +++ b/core/opentaint-go-querylang/samples-go-massive/loginj_07_env_tolower_logprintf/rule.yaml @@ -7,4 +7,6 @@ rules: pattern-sources: - pattern: os.Getenv("AUDIT") pattern-sinks: - - pattern: log.Printf($FMT, $X) + - patterns: + - pattern: log.Printf($FMT, $*X) + - focus-metavariable: $X diff --git a/core/opentaint-go-querylang/samples-go-massive/loginj_08_env_escape_sanitizer/rule.yaml b/core/opentaint-go-querylang/samples-go-massive/loginj_08_env_escape_sanitizer/rule.yaml index ac29c69f9..d0a393927 100644 --- a/core/opentaint-go-querylang/samples-go-massive/loginj_08_env_escape_sanitizer/rule.yaml +++ b/core/opentaint-go-querylang/samples-go-massive/loginj_08_env_escape_sanitizer/rule.yaml @@ -7,6 +7,8 @@ rules: pattern-sources: - pattern: os.Getenv("TRACE_ID") pattern-sinks: - - pattern: log.Printf($FMT, $X) + - patterns: + - pattern: log.Printf($FMT, $*X) + - focus-metavariable: $X pattern-sanitizers: - pattern: escapeNewlines($V) diff --git a/core/opentaint-go-querylang/samples-go-massive/star_01_sink_field/rule.yaml b/core/opentaint-go-querylang/samples-go-massive/star_01_sink_field/rule.yaml new file mode 100644 index 000000000..db459c55e --- /dev/null +++ b/core/opentaint-go-querylang/samples-go-massive/star_01_sink_field/rule.yaml @@ -0,0 +1,12 @@ +rules: + - id: star-01-sink-field + languages: [go] + severity: WARNING + message: Tainted struct field reaches a starred whole-object sink + mode: taint + pattern-sources: + - pattern: star_01_sink_field.Source(...) + pattern-sinks: + - patterns: + - pattern: star_01_sink_field.Sink_Box($*Y) + - focus-metavariable: $Y diff --git a/core/opentaint-go-querylang/samples-go-massive/star_01_sink_field/sample.go b/core/opentaint-go-querylang/samples-go-massive/star_01_sink_field/sample.go new file mode 100644 index 000000000..7eeec9174 --- /dev/null +++ b/core/opentaint-go-querylang/samples-go-massive/star_01_sink_field/sample.go @@ -0,0 +1,26 @@ +package util + +// Box carries a single string field; the star operator lets a whole-object sink +// observe taint that lives on a nested field rather than the object's base value. +type Box struct { + Value string +} + +func Source() string { return "tainted" } + +func Sink_Box(b Box) { _ = b } + +// Positive_tainted_field: a source-tainted value is written into b.Value (a nested +// field). The starred sink Sink_Box($*Y) matches the field taint on the whole object. +func Positive_tainted_field() { + var b Box + b.Value = Source() + Sink_Box(b) +} + +// Negative_clean_object: the field is never tainted, so the starred sink stays silent. +func Negative_clean_object() { + var b Box + b.Value = "safe" + Sink_Box(b) +} diff --git a/core/opentaint-go-querylang/samples-go-massive/star_02_source_field/rule.yaml b/core/opentaint-go-querylang/samples-go-massive/star_02_source_field/rule.yaml new file mode 100644 index 000000000..1909a1491 --- /dev/null +++ b/core/opentaint-go-querylang/samples-go-massive/star_02_source_field/rule.yaml @@ -0,0 +1,10 @@ +rules: + - id: star-02-source-field + languages: [go] + severity: WARNING + message: A starred whole-object source taints every field; a field read reaches the sink + mode: taint + pattern-sources: + - pattern: $*X = star_02_source_field.Source() + pattern-sinks: + - pattern: star_02_source_field.Sink($Y) diff --git a/core/opentaint-go-querylang/samples-go-massive/star_02_source_field/sample.go b/core/opentaint-go-querylang/samples-go-massive/star_02_source_field/sample.go new file mode 100644 index 000000000..d09ce770c --- /dev/null +++ b/core/opentaint-go-querylang/samples-go-massive/star_02_source_field/sample.go @@ -0,0 +1,25 @@ +package util + +// Data is the whole object tainted by the starred source; every nested field +// inherits the taint, so a later field read is tainted too. +type Data struct { + Field string +} + +func Source() Data { return Data{Field: "tainted"} } + +func Sink(s string) { _ = s } + +// Positive_field_read: the starred source ($*X = Source()) taints the whole object +// AND all its fields; the field read d.Field then reaches the plain sink. +func Positive_field_read() { + d := Source() + Sink(d.Field) +} + +// Negative_untainted: the object is built from a constant, so no field is tainted. +func Negative_untainted() { + var d Data + d.Field = "safe" + Sink(d.Field) +} diff --git a/core/opentaint-go-querylang/samples-go-massive/star_03_sanitizer_field/rule.yaml b/core/opentaint-go-querylang/samples-go-massive/star_03_sanitizer_field/rule.yaml new file mode 100644 index 000000000..e74839e91 --- /dev/null +++ b/core/opentaint-go-querylang/samples-go-massive/star_03_sanitizer_field/rule.yaml @@ -0,0 +1,14 @@ +rules: + - id: star-03-sanitizer-field + languages: [go] + severity: WARNING + message: Tainted struct field reaches the sink unless a starred sanitizer clears the whole object + mode: taint + pattern-sources: + - pattern: star_03_sanitizer_field.Source(...) + pattern-sanitizers: + - patterns: + - pattern: star_03_sanitizer_field.Clean($*C) + - focus-metavariable: $C + pattern-sinks: + - pattern: star_03_sanitizer_field.Sink($Y) diff --git a/core/opentaint-go-querylang/samples-go-massive/star_03_sanitizer_field/sample.go b/core/opentaint-go-querylang/samples-go-massive/star_03_sanitizer_field/sample.go new file mode 100644 index 000000000..ef6ce4154 --- /dev/null +++ b/core/opentaint-go-querylang/samples-go-massive/star_03_sanitizer_field/sample.go @@ -0,0 +1,30 @@ +package util + +// Box carries the tainted field. The starred sanitizer Clean($*C) must clear the +// taint on the whole object INCLUDING the nested field, so a later field read is clean. +type Box struct { + Value string +} + +func Source() string { return "tainted" } + +// Clean is the $*C sanitizer: it clears the argument object and all of its fields. +func Clean(b Box) Box { return b } + +func Sink(s string) { _ = s } + +// Positive_unsanitized: field taint reaches the sink with no sanitizer in between. +func Positive_unsanitized() { + var b Box + b.Value = Source() + Sink(b.Value) +} + +// Negative_sanitized: the starred sanitizer sits between source and sink; if $*C truly +// clears the concrete nested-field taint, the field read must be clean and nothing reports. +func Negative_sanitized() { + var b Box + b.Value = Source() + cleaned := Clean(b) + Sink(cleaned.Value) +} diff --git a/core/opentaint-go-querylang/samples-go-massive/star_04_deep_sink_field/rule.yaml b/core/opentaint-go-querylang/samples-go-massive/star_04_deep_sink_field/rule.yaml new file mode 100644 index 000000000..d9ce0b3c8 --- /dev/null +++ b/core/opentaint-go-querylang/samples-go-massive/star_04_deep_sink_field/rule.yaml @@ -0,0 +1,12 @@ +rules: + - id: star-04-deep-sink-field + languages: [go] + severity: WARNING + message: Taint hidden 5 fields deep reaches a starred whole-object sink + mode: taint + pattern-sources: + - pattern: star_04_deep_sink_field.Source(...) + pattern-sinks: + - patterns: + - pattern: star_04_deep_sink_field.Sink_L0($*Y) + - focus-metavariable: $Y diff --git a/core/opentaint-go-querylang/samples-go-massive/star_04_deep_sink_field/sample.go b/core/opentaint-go-querylang/samples-go-massive/star_04_deep_sink_field/sample.go new file mode 100644 index 000000000..06e8bb916 --- /dev/null +++ b/core/opentaint-go-querylang/samples-go-massive/star_04_deep_sink_field/sample.go @@ -0,0 +1,50 @@ +package util + +// L0..L4 nest a string field 5 levels deep. The starred whole-object sink must observe +// taint that lives on a nested field. +type L0 struct { + V0 string + F *L1 +} +type L1 struct { + V1 string + F *L2 +} +type L2 struct { + V2 string + F *L3 +} +type L3 struct { + V3 string + F *L4 +} +type L4 struct{ V string } + +func Source() string { return "tainted" } + +func Sink_L0(b L0) { _ = b } + +func build() L0 { + return L0{F: &L1{F: &L2{F: &L3{F: &L4{}}}}} +} + +// Positive_depth1: taint at field depth 1; the starred sink observes it. +func Positive_depth1() { + o := build() + o.V0 = Source() + Sink_L0(o) +} + +// Positive_depth5: taint hidden 5 fields deep; the starred sink must still match. +func Positive_depth5() { + o := build() + o.F.F.F.F.V = Source() + Sink_L0(o) +} + +// Negative_clean_object: no field ever tainted, so the starred sink stays silent. +func Negative_clean_object() { + o := build() + o.F.F.F.F.V = "safe" + Sink_L0(o) +} diff --git a/core/opentaint-go-querylang/samples-go-massive/star_05_deep_source_field/rule.yaml b/core/opentaint-go-querylang/samples-go-massive/star_05_deep_source_field/rule.yaml new file mode 100644 index 000000000..d7a88202a --- /dev/null +++ b/core/opentaint-go-querylang/samples-go-massive/star_05_deep_source_field/rule.yaml @@ -0,0 +1,10 @@ +rules: + - id: star-05-deep-source-field + languages: [go] + severity: WARNING + message: A starred whole-object source taints every nested field; a deep field read reaches the sink + mode: taint + pattern-sources: + - pattern: $*X = star_05_deep_source_field.Source() + pattern-sinks: + - pattern: star_05_deep_source_field.Sink($Y) diff --git a/core/opentaint-go-querylang/samples-go-massive/star_05_deep_source_field/sample.go b/core/opentaint-go-querylang/samples-go-massive/star_05_deep_source_field/sample.go new file mode 100644 index 000000000..26ebfc9ec --- /dev/null +++ b/core/opentaint-go-querylang/samples-go-massive/star_05_deep_source_field/sample.go @@ -0,0 +1,27 @@ +package util + +// The starred whole-object source taints every nested field at every depth; a 5-level +// field read must therefore be tainted too. +type L0 struct{ F L1 } +type L1 struct{ F L2 } +type L2 struct{ F L3 } +type L3 struct{ F L4 } +type L4 struct{ V string } + +func Source() L0 { return L0{} } + +func Sink(s string) { _ = s } + +// Positive_deep_field_read: the starred source ($*X = Source()) taints the whole object AND +// all nested fields; the depth-5 field read o.F.F.F.F.V then reaches the plain sink. +func Positive_deep_field_read() { + o := Source() + Sink(o.F.F.F.F.V) +} + +// Negative_untainted: the object is built from constants, so no field is tainted. +func Negative_untainted() { + var o L0 + o.F.F.F.F.V = "safe" + Sink(o.F.F.F.F.V) +} diff --git a/core/opentaint-go-querylang/samples-go-massive/star_06_deep_sanitizer_field/rule.yaml b/core/opentaint-go-querylang/samples-go-massive/star_06_deep_sanitizer_field/rule.yaml new file mode 100644 index 000000000..91ddd7bd4 --- /dev/null +++ b/core/opentaint-go-querylang/samples-go-massive/star_06_deep_sanitizer_field/rule.yaml @@ -0,0 +1,14 @@ +rules: + - id: star-06-deep-sanitizer-field + languages: [go] + severity: WARNING + message: Deep struct-field taint reaches the sink unless a starred sanitizer clears the whole object + mode: taint + pattern-sources: + - pattern: star_06_deep_sanitizer_field.Source(...) + pattern-sanitizers: + - patterns: + - pattern: star_06_deep_sanitizer_field.Clean($*C) + - focus-metavariable: $C + pattern-sinks: + - pattern: star_06_deep_sanitizer_field.Sink($Y) diff --git a/core/opentaint-go-querylang/samples-go-massive/star_06_deep_sanitizer_field/sample.go b/core/opentaint-go-querylang/samples-go-massive/star_06_deep_sanitizer_field/sample.go new file mode 100644 index 000000000..9873f03cc --- /dev/null +++ b/core/opentaint-go-querylang/samples-go-massive/star_06_deep_sanitizer_field/sample.go @@ -0,0 +1,35 @@ +package util + +// L0..L3 nest a string field 4 levels deep. The starred sanitizer Clean($*C) must clear the +// taint on the whole object INCLUDING the nested field, so a later deep field read is clean. +type L0 struct{ F *L1 } +type L1 struct{ F *L2 } +type L2 struct{ F *L3 } +type L3 struct{ V string } + +func Source() string { return "tainted" } + +// Clean is the $*C sanitizer: it clears the argument object and all of its nested fields. +func Clean(b L0) L0 { return b } + +func Sink(s string) { _ = s } + +func build() L0 { + return L0{F: &L1{F: &L2{F: &L3{}}}} +} + +// Positive_unsanitized: deep field taint reaches the sink with no sanitizer in between. +func Positive_unsanitized() { + o := build() + o.F.F.F.V = Source() + Sink(o.F.F.F.V) +} + +// Negative_sanitized: the starred sanitizer sits between source and sink; if $*C truly clears +// the concrete deep-field taint, the field read must be clean and nothing reports. +func Negative_sanitized() { + o := build() + o.F.F.F.V = Source() + cleaned := Clean(o) + Sink(cleaned.F.F.F.V) +} diff --git a/core/opentaint-go-querylang/samples-go-massive/star_07_interproc_chain/rule.yaml b/core/opentaint-go-querylang/samples-go-massive/star_07_interproc_chain/rule.yaml new file mode 100644 index 000000000..0468b6730 --- /dev/null +++ b/core/opentaint-go-querylang/samples-go-massive/star_07_interproc_chain/rule.yaml @@ -0,0 +1,10 @@ +rules: + - id: star-07-interproc-chain + languages: [go] + severity: WARNING + message: A starred whole-object source survives a 5+ hop hide/expose interprocedural chain + mode: taint + pattern-sources: + - pattern: $*X = star_07_interproc_chain.Source() + pattern-sinks: + - pattern: star_07_interproc_chain.Sink($Y) diff --git a/core/opentaint-go-querylang/samples-go-massive/star_07_interproc_chain/sample.go b/core/opentaint-go-querylang/samples-go-massive/star_07_interproc_chain/sample.go new file mode 100644 index 000000000..64616a110 --- /dev/null +++ b/core/opentaint-go-querylang/samples-go-massive/star_07_interproc_chain/sample.go @@ -0,0 +1,52 @@ +package util + +// Box carries a single string field. A starred whole-object source is threaded through a 5+ +// hop interprocedural chain that alternately hides taint inside the object and exposes it. +type Box struct{ V string } + +func Source() Box { return Box{} } + +func Sink(s string) { _ = s } + +// step1..step5: 5 interprocedural hops. Alternation: +// step1 pass object -> step2 EXPOSE field to scalar -> step3 HIDE scalar in a new Box +// -> step4 pass object -> step5 EXPOSE the field again, reaching the sink. +func step1(b Box) Box { return b } +func step2(b Box) string { return b.V } +func step3(s string) Box { return Box{V: s} } +func step4(b Box) Box { return b } +func step5(b Box) string { return b.V } + +// Positive_alternating_chain: taint survives 5 hops of hide/expose alternation from a starred +// source ($*X = Source()). +func Positive_alternating_chain() { + b := Source() + b1 := step1(b) + s2 := step2(b1) + b3 := step3(s2) + b4 := step4(b3) + s5 := step5(b4) + Sink(s5) +} + +// Positive_passthrough_chain: simplest 5-hop pass-through, field exposed only at the end. +func Positive_passthrough_chain() { + b := Source() + b1 := step1(b) + b2 := step1(b1) + b3 := step1(b2) + b4 := step4(b3) + s := step5(b4) + Sink(s) +} + +// Negative_clean_chain: a fresh untainted Box threaded through the same chain. +func Negative_clean_chain() { + b := Box{V: "safe"} + b1 := step1(b) + s2 := step2(b1) + b3 := step3(s2) + b4 := step4(b3) + s5 := step5(b4) + Sink(s5) +} diff --git a/core/opentaint-go-querylang/samples-go-massive/star_08_source_and_sink/rule.yaml b/core/opentaint-go-querylang/samples-go-massive/star_08_source_and_sink/rule.yaml new file mode 100644 index 000000000..431aa6f74 --- /dev/null +++ b/core/opentaint-go-querylang/samples-go-massive/star_08_source_and_sink/rule.yaml @@ -0,0 +1,12 @@ +rules: + - id: star-08-source-and-sink + languages: [go] + severity: WARNING + message: A whole-object source and a whole-object sink compose across a nested extraction + mode: taint + pattern-sources: + - pattern: $*X = star_08_source_and_sink.Source() + pattern-sinks: + - patterns: + - pattern: star_08_source_and_sink.Sink_Inner($*Y) + - focus-metavariable: $Y diff --git a/core/opentaint-go-querylang/samples-go-massive/star_08_source_and_sink/sample.go b/core/opentaint-go-querylang/samples-go-massive/star_08_source_and_sink/sample.go new file mode 100644 index 000000000..e4fd5582f --- /dev/null +++ b/core/opentaint-go-querylang/samples-go-massive/star_08_source_and_sink/sample.go @@ -0,0 +1,26 @@ +package util + +// Both ends starred: a whole-object source ($*X = Source()) taints every nested field, and a +// whole-object sink (Sink_Inner($*Y)) observes a nested sub-object pulled out in between. +type Outer struct{ F Mid } +type Mid struct{ F Inner } +type Inner struct{ V string } + +func Source() Outer { return Outer{} } + +func Sink_Inner(i Inner) { _ = i } + +// Positive_nested_object_to_star_sink: the whole-object source taint reaches a nested +// sub-object handed to the starred sink. +func Positive_nested_object_to_star_sink() { + o := Source() + inner := o.F.F + Sink_Inner(inner) +} + +// Negative_clean_nested: locally-built object, nothing tainted. +func Negative_clean_nested() { + var o Outer + o.F.F.V = "safe" + Sink_Inner(o.F.F) +} diff --git a/core/opentaint-go-querylang/samples-go-massive/star_09_matrix_source/rule.yaml b/core/opentaint-go-querylang/samples-go-massive/star_09_matrix_source/rule.yaml new file mode 100644 index 000000000..32866b91d --- /dev/null +++ b/core/opentaint-go-querylang/samples-go-massive/star_09_matrix_source/rule.yaml @@ -0,0 +1,10 @@ +rules: + - id: star-09-matrix-source + languages: [go] + severity: WARNING + message: Starred source 5 calls deep survives per-hop field unwrapping into a 5-deep sink chain + mode: taint + pattern-sources: + - pattern: $*X = star_09_matrix_source.Source() + pattern-sinks: + - pattern: star_09_matrix_source.Sink($Y) diff --git a/core/opentaint-go-querylang/samples-go-massive/star_09_matrix_source/sample.go b/core/opentaint-go-querylang/samples-go-massive/star_09_matrix_source/sample.go new file mode 100644 index 000000000..8d2ac9f29 --- /dev/null +++ b/core/opentaint-go-querylang/samples-go-massive/star_09_matrix_source/sample.go @@ -0,0 +1,53 @@ +package util + +// Starred SOURCE, 5+ interprocedural depth x 5+ field depth combined (Go port of +// StarMatrixSource). The source statement `$*X = Source()` sits FIVE calls deep +// (src1..src5); the tainted whole object then climbs back up and is unwrapped ONE field +// level per hop across five more calls (u1..u5, L0->..->string), and the scalar finally +// travels five calls down a sink chain (k1..k5) to a plain sink. The 5-level field taint +// is carried by the $* source's abstract any-field mark. +type L0 struct{ F L1 } +type L1 struct{ F L2 } +type L2 struct{ F L3 } +type L3 struct{ F L4 } +type L4 struct{ V string } + +func Source() L0 { return L0{} } + +func Sink(s string) { _ = s } + +// Source five calls deep: the starred source statement is inside src1. +func src5() L0 { return src4() } +func src4() L0 { return src3() } +func src3() L0 { return src2() } +func src2() L0 { return src1() } +func src1() L0 { o := Source(); return o } // $*X = Source() matches HERE, depth 5 + +// Five hops, each unwrapping exactly one field level: interproc depth x field depth. +func u1(o L0) L1 { return o.F } +func u2(o L1) L2 { return o.F } +func u3(o L2) L3 { return o.F } +func u4(o L3) L4 { return o.F } +func u5(o L4) string { return o.V } + +// Sink five calls deep. +func k1(s string) { k2(s) } +func k2(s string) { k3(s) } +func k3(s string) { k4(s) } +func k4(s string) { k5(s) } +func k5(s string) { Sink(s) } // Sink() called HERE, depth 5 + +// Positive_deep_chain: deep source -> 5x1-field unwrap hops -> deep sink. +func Positive_deep_chain() { + o := src5() + s := u5(u4(u3(u2(u1(o))))) + k1(s) +} + +// Negative_clean_chain: an untainted object through the identical chains. +func Negative_clean_chain() { + var o L0 + o.F.F.F.F.V = "safe" + s := u5(u4(u3(u2(u1(o))))) + k1(s) +} diff --git a/core/opentaint-go-querylang/samples-go-massive/star_10_matrix_sink/rule.yaml b/core/opentaint-go-querylang/samples-go-massive/star_10_matrix_sink/rule.yaml new file mode 100644 index 000000000..0831cf79a --- /dev/null +++ b/core/opentaint-go-querylang/samples-go-massive/star_10_matrix_sink/rule.yaml @@ -0,0 +1,12 @@ +rules: + - id: star-10-matrix-sink + languages: [go] + severity: WARNING + message: Whole-object taint wrapped 5 field levels deep reaches a starred sink 5 calls deep + mode: taint + pattern-sources: + - pattern: $*X = star_10_matrix_sink.Source() + pattern-sinks: + - patterns: + - pattern: star_10_matrix_sink.Sink_L0($*Y) + - focus-metavariable: $Y diff --git a/core/opentaint-go-querylang/samples-go-massive/star_10_matrix_sink/sample.go b/core/opentaint-go-querylang/samples-go-massive/star_10_matrix_sink/sample.go new file mode 100644 index 000000000..aa7952eef --- /dev/null +++ b/core/opentaint-go-querylang/samples-go-massive/star_10_matrix_sink/sample.go @@ -0,0 +1,53 @@ +package util + +// Starred SINK, 5+ interprocedural depth x 5+ field depth combined (Go port of +// StarMatrixSink). A starred source taints the INNERMOST object (L4) five calls deep; +// five hops then each WRAP it one level deeper (L4->L3->..->L0), and the outermost object +// travels five calls down a sink chain to `Sink_L0($*Y)` — the starred sink must observe +// the whole-object taint buried five field levels down the wrapped object. +type L0 struct{ F L1 } +type L1 struct{ F L2 } +type L2 struct{ F L3 } +type L3 struct{ F L4 } +type L4 struct{ V string } + +func Source() L4 { return L4{} } + +func Sink_L0(o L0) { _ = o } + +// Source five calls deep: the starred source statement is inside src1. +func src5() L4 { return src4() } +func src4() L4 { return src3() } +func src3() L4 { return src2() } +func src2() L4 { return src1() } +func src1() L4 { o := Source(); return o } // $*X = Source() matches HERE, depth 5 + +// Five hops, each WRAPPING one field level (hide direction). +func w1(o L4) L3 { var n L3; n.F = o; return n } +func w2(o L3) L2 { var n L2; n.F = o; return n } +func w3(o L2) L1 { var n L1; n.F = o; return n } +func w4(o L1) L0 { var n L0; n.F = o; return n } +func w5(o L0) L0 { return o } + +// Sink five calls deep. +func k1(o L0) { k2(o) } +func k2(o L0) { k3(o) } +func k3(o L0) { k4(o) } +func k4(o L0) { k5(o) } +func k5(o L0) { Sink_L0(o) } // Sink_L0($*Y) matches HERE, depth 5 + +// Positive_wrapped_deep: the tainted L4 is wrapped five levels deep; the starred sink +// observes it. +func Positive_wrapped_deep() { + t := src5() + o := w5(w4(w3(w2(w1(t))))) + k1(o) +} + +// Negative_clean_wrapped: an untainted L4 wrapped and threaded through the identical chains. +func Negative_clean_wrapped() { + var t L4 + t.V = "safe" + o := w5(w4(w3(w2(w1(t))))) + k1(o) +} diff --git a/core/opentaint-go-querylang/samples-go-massive/star_11_matrix_propagator/rule.yaml b/core/opentaint-go-querylang/samples-go-massive/star_11_matrix_propagator/rule.yaml new file mode 100644 index 000000000..24fb53d4b --- /dev/null +++ b/core/opentaint-go-querylang/samples-go-massive/star_11_matrix_propagator/rule.yaml @@ -0,0 +1,14 @@ +rules: + - id: star-11-matrix-propagator + languages: [go] + severity: WARNING + message: A doubly-starred propagator moves whole-object taint into a fresh object mid-chain + mode: taint + pattern-sources: + - pattern: $*X = star_11_matrix_propagator.Source() + pattern-propagators: + - pattern: $*T = star_11_matrix_propagator.Pass($*F) + from: $F + to: $T + pattern-sinks: + - pattern: star_11_matrix_propagator.Sink($Y) diff --git a/core/opentaint-go-querylang/samples-go-massive/star_11_matrix_propagator/sample.go b/core/opentaint-go-querylang/samples-go-massive/star_11_matrix_propagator/sample.go new file mode 100644 index 000000000..cadfad9ed --- /dev/null +++ b/core/opentaint-go-querylang/samples-go-massive/star_11_matrix_propagator/sample.go @@ -0,0 +1,74 @@ +package util + +// Starred PROPAGATOR — BOTH occurrences starred (`$*T = Pass($*F)`) — at 5+ interprocedural +// depth x 5+ field depth (Go port of StarMatrixPropagator). A starred source five calls deep +// taints a whole L0; the object travels five pass-hops to the propagator call, whose starred +// FROM observes the any-field taint of the whole argument and whose starred TO assigns +// whole-object taint to the fresh M0 result. The M0 is then unwrapped ONE field level per hop +// across five calls (M0->..->string) — only possible if the TO really carries any-field taint +// — and the scalar travels five calls down a sink chain to a plain sink. +type L0 struct{ F L1 } +type L1 struct{ F L2 } +type L2 struct{ F L3 } +type L3 struct{ F L4 } +type L4 struct{ V string } + +type M0 struct{ F M1 } +type M1 struct{ F M2 } +type M2 struct{ F M3 } +type M3 struct{ F M4 } +type M4 struct{ V string } + +func Source() L0 { return L0{} } + +func Pass(o L0) M0 { _ = o; return M0{} } + +func Sink(s string) { _ = s } + +// Source five calls deep. +func src5() L0 { return src4() } +func src4() L0 { return src3() } +func src3() L0 { return src2() } +func src2() L0 { return src1() } +func src1() L0 { o := Source(); return o } // $*X = Source() matches HERE, depth 5 + +// Five object pass-hops before the propagator. +func p1(o L0) L0 { return o } +func p2(o L0) L0 { return o } +func p3(o L0) L0 { return o } +func p4(o L0) L0 { return o } +func p5(o L0) L0 { return o } + +// Five hops, each unwrapping one field level of the PROPAGATED object: taint reaches the +// scalar only if the starred TO assigned any-field taint to the M0. +func u1(o M0) M1 { return o.F } +func u2(o M1) M2 { return o.F } +func u3(o M2) M3 { return o.F } +func u4(o M3) M4 { return o.F } +func u5(o M4) string { return o.V } + +// Sink five calls deep. +func k1(s string) { k2(s) } +func k2(s string) { k3(s) } +func k3(s string) { k4(s) } +func k4(s string) { k5(s) } +func k5(s string) { Sink(s) } // Sink() called HERE, depth 5 + +// Positive_propagated_deep: deep source -> 5 hops -> starred propagator -> per-hop unwrap +// -> deep sink. +func Positive_propagated_deep() { + o := src5() + o5 := p5(p4(p3(p2(p1(o))))) + t := Pass(o5) // $*T = Pass($*F): whole object in, whole object out + s := u5(u4(u3(u2(u1(t))))) + k1(s) +} + +// Negative_clean_propagated: an untainted object through the identical propagator and chains. +func Negative_clean_propagated() { + var o L0 + o5 := p5(p4(p3(p2(p1(o))))) + t := Pass(o5) + s := u5(u4(u3(u2(u1(t))))) + k1(s) +} diff --git a/core/opentaint-go-querylang/samples-go-massive/star_12_matrix_sanitizer/rule.yaml b/core/opentaint-go-querylang/samples-go-massive/star_12_matrix_sanitizer/rule.yaml new file mode 100644 index 000000000..1dbb1b054 --- /dev/null +++ b/core/opentaint-go-querylang/samples-go-massive/star_12_matrix_sanitizer/rule.yaml @@ -0,0 +1,14 @@ +rules: + - id: star-12-matrix-sanitizer + languages: [go] + severity: WARNING + message: Deep whole-object taint reaches the sink unless a starred sanitizer inside a wrapper clears it + mode: taint + pattern-sources: + - pattern: $*X = star_12_matrix_sanitizer.Source() + pattern-sanitizers: + - patterns: + - pattern: star_12_matrix_sanitizer.Clean($*C) + - focus-metavariable: $C + pattern-sinks: + - pattern: star_12_matrix_sanitizer.Sink($Y) diff --git a/core/opentaint-go-querylang/samples-go-massive/star_12_matrix_sanitizer/sample.go b/core/opentaint-go-querylang/samples-go-massive/star_12_matrix_sanitizer/sample.go new file mode 100644 index 000000000..2b427622b --- /dev/null +++ b/core/opentaint-go-querylang/samples-go-massive/star_12_matrix_sanitizer/sample.go @@ -0,0 +1,61 @@ +package util + +// Starred SANITIZER, 5+ interprocedural depth x 5+ field depth combined (Go port of +// StarMatrixSanitizer). A starred source five calls deep taints a whole L0. On the sanitized +// path the object goes through `Sanitize()` — a HELPER whose body calls the starred-clean +// `Clean()` (the wrapper shape behind the OWASP escapeHtml FPs, i.e. the deep-mark-exclusion +// fix's sample-level regression test). Afterwards five hops unwrap one field level each and +// the scalar travels five calls down to the sink; the whole-object clean must have removed +// the any-field taint at every depth. +type L0 struct{ F L1 } +type L1 struct{ F L2 } +type L2 struct{ F L3 } +type L3 struct{ F L4 } +type L4 struct{ V string } + +func Source() L0 { return L0{} } + +func Clean(o L0) L0 { return o } + +func Sink(s string) { _ = s } + +// Source five calls deep. +func src5() L0 { return src4() } +func src4() L0 { return src3() } +func src3() L0 { return src2() } +func src2() L0 { return src1() } +func src1() L0 { o := Source(); return o } // $*X = Source() matches HERE, depth 5 + +// The starred clean sits INSIDE a wrapper: its whole-object effect must survive the +// wrapper's interprocedural summary (deep mark exclusions). +func Sanitize(o L0) L0 { return Clean(o) } + +// Five hops, each unwrapping exactly one field level. +func u1(o L0) L1 { return o.F } +func u2(o L1) L2 { return o.F } +func u3(o L2) L3 { return o.F } +func u4(o L3) L4 { return o.F } +func u5(o L4) string { return o.V } + +// Sink five calls deep. +func k1(s string) { k2(s) } +func k2(s string) { k3(s) } +func k3(s string) { k4(s) } +func k4(s string) { k5(s) } +func k5(s string) { Sink(s) } // Sink() called HERE, depth 5 + +// Positive_unsanitized_deep: the unsanitized path flags. +func Positive_unsanitized_deep() { + o := src5() + s := u5(u4(u3(u2(u1(o))))) + k1(s) +} + +// Negative_sanitized_deep: the wrapped whole-object clean clears the taint at every field +// depth. +func Negative_sanitized_deep() { + o := src5() + c := Sanitize(o) + s := u5(u4(u3(u2(u1(c))))) + k1(s) +} diff --git a/core/opentaint-go-querylang/samples-go-massive/star_13_matrix_pattern_not/rule.yaml b/core/opentaint-go-querylang/samples-go-massive/star_13_matrix_pattern_not/rule.yaml new file mode 100644 index 000000000..3b95b9938 --- /dev/null +++ b/core/opentaint-go-querylang/samples-go-massive/star_13_matrix_pattern_not/rule.yaml @@ -0,0 +1,13 @@ +rules: + - id: star-13-matrix-pattern-not + languages: [go] + severity: WARNING + message: Starred sink with a starred pattern-not exclusion — flagged mode fires, safe mode is excluded + mode: taint + pattern-sources: + - pattern: $*X = star_13_matrix_pattern_not.Source() + pattern-sinks: + - patterns: + - pattern: star_13_matrix_pattern_not.Emit($*Y, $MODE) + - pattern-not: star_13_matrix_pattern_not.Emit($*Y, "safe") + - focus-metavariable: $Y diff --git a/core/opentaint-go-querylang/samples-go-massive/star_13_matrix_pattern_not/sample.go b/core/opentaint-go-querylang/samples-go-massive/star_13_matrix_pattern_not/sample.go new file mode 100644 index 000000000..399db76e5 --- /dev/null +++ b/core/opentaint-go-querylang/samples-go-massive/star_13_matrix_pattern_not/sample.go @@ -0,0 +1,57 @@ +package util + +// Starred PATTERN-NOT sink, 5+ interprocedural depth x 5+ field depth combined (Go port of +// StarMatrixPatternNot). The sink is `Emit($*Y, $MODE)` with +// `pattern-not: Emit($*Y, "safe")` — the starred metavar occurrence appears in BOTH the +// pattern and the pattern-not (the constraint solver keeps $Y and $*Y distinct, so the forms +// must agree). A starred source five calls deep taints a whole L0; the object travels five +// hops and is emitted five calls deep — flagged in "html" mode, excluded by the pattern-not +// in "safe" mode. +type L0 struct{ F L1 } +type L1 struct{ F L2 } +type L2 struct{ F L3 } +type L3 struct{ F L4 } +type L4 struct{ V string } + +func Source() L0 { return L0{} } + +func Emit(o L0, mode string) { _, _ = o, mode } + +// Source five calls deep. +func src5() L0 { return src4() } +func src4() L0 { return src3() } +func src3() L0 { return src2() } +func src2() L0 { return src1() } +func src1() L0 { o := Source(); return o } // $*X = Source() matches HERE, depth 5 + +// Five object pass-hops. +func p1(o L0) L0 { return o } +func p2(o L0) L0 { return o } +func p3(o L0) L0 { return o } +func p4(o L0) L0 { return o } +func p5(o L0) L0 { return o } + +// Two sink chains five calls deep: one emits in a flagged mode, one in the excluded mode. +func k1(o L0) { k2(o) } +func k2(o L0) { k3(o) } +func k3(o L0) { k4(o) } +func k4(o L0) { k5(o) } +func k5(o L0) { Emit(o, "html") } // matches the sink, depth 5 + +func j1(o L0) { j2(o) } +func j2(o L0) { j3(o) } +func j3(o L0) { j4(o) } +func j4(o L0) { j5(o) } +func j5(o L0) { Emit(o, "safe") } // excluded by pattern-not, depth 5 + +// Positive_emit_html: tainted object emitted in a non-excluded mode. +func Positive_emit_html() { + o := src5() + k1(p5(p4(p3(p2(p1(o)))))) +} + +// Negative_emit_safe: same tainted object, but the emit call matches the pattern-not. +func Negative_emit_safe() { + o := src5() + j1(p5(p4(p3(p2(p1(o)))))) +} diff --git a/core/opentaint-go-querylang/samples-go-massive/star_14_matrix_pattern_inside/rule.yaml b/core/opentaint-go-querylang/samples-go-massive/star_14_matrix_pattern_inside/rule.yaml new file mode 100644 index 000000000..5ac635e22 --- /dev/null +++ b/core/opentaint-go-querylang/samples-go-massive/star_14_matrix_pattern_inside/rule.yaml @@ -0,0 +1,15 @@ +rules: + - id: star-14-matrix-pattern-inside + languages: [go] + severity: WARNING + message: Starred sink gated by a pattern-inside-introduced receiver + mode: taint + pattern-sources: + - pattern: $*X = star_14_matrix_pattern_inside.Source() + pattern-sinks: + - patterns: + - pattern-inside: | + $R = star_14_matrix_pattern_inside.OpenSink() + ... + - pattern: $R.Consume($*Y) + - focus-metavariable: $Y diff --git a/core/opentaint-go-querylang/samples-go-massive/star_14_matrix_pattern_inside/sample.go b/core/opentaint-go-querylang/samples-go-massive/star_14_matrix_pattern_inside/sample.go new file mode 100644 index 000000000..653d217c9 --- /dev/null +++ b/core/opentaint-go-querylang/samples-go-massive/star_14_matrix_pattern_inside/sample.go @@ -0,0 +1,70 @@ +package util + +// Starred sink gated by PATTERN-INSIDE, 5+ interprocedural depth x 5+ field depth combined +// (Go port of StarMatrixPatternInside). The sink `$R.Consume($*Y)` only counts when the +// receiver comes from `OpenSink()` in the same function (pattern-inside). A starred source +// five calls deep taints a whole L0; the object travels five hops; the Consume call sits five +// calls deep. The gated function uses OpenSink() (flagged); the ungated one obtains its +// receiver elsewhere (not a sink at all). +type Out struct{} + +func (r Out) Consume(o L0) { _ = o } + +type L0 struct{ F L1 } +type L1 struct{ F L2 } +type L2 struct{ F L3 } +type L3 struct{ F L4 } +type L4 struct{ V string } + +func Source() L0 { return L0{} } + +func OpenSink() Out { return Out{} } + +func PlainOut() Out { return Out{} } + +// Source five calls deep. +func src5() L0 { return src4() } +func src4() L0 { return src3() } +func src3() L0 { return src2() } +func src2() L0 { return src1() } +func src1() L0 { o := Source(); return o } // $*X = Source() matches HERE, depth 5 + +// Five object pass-hops. +func p1(o L0) L0 { return o } +func p2(o L0) L0 { return o } +func p3(o L0) L0 { return o } +func p4(o L0) L0 { return o } +func p5(o L0) L0 { return o } + +// Sink chain five calls deep, ending in the pattern-inside-gated Consume. +func k1(o L0) { k2(o) } +func k2(o L0) { k3(o) } +func k3(o L0) { k4(o) } +func k4(o L0) { k5(o) } +func k5(o L0) { + r := OpenSink() // pattern-inside context + r.Consume(o) // starred sink matches HERE, depth 5 +} + +// Same-depth chain whose Consume receiver does NOT come from OpenSink(). +func j1(o L0) { j2(o) } +func j2(o L0) { j3(o) } +func j3(o L0) { j4(o) } +func j4(o L0) { j5(o) } +func j5(o L0) { + r := PlainOut() // no pattern-inside context + r.Consume(o) +} + +// Positive_gated_consume: tainted object consumed inside the gated context. +func Positive_gated_consume() { + o := src5() + k1(p5(p4(p3(p2(p1(o)))))) +} + +// Negative_ungated_consume: same tainted object, but the Consume call lacks the +// pattern-inside context. +func Negative_ungated_consume() { + o := src5() + j1(p5(p4(p3(p2(p1(o)))))) +} diff --git a/core/opentaint-go-querylang/samples-go-massive/star_15_matrix_pattern_not_inside/rule.yaml b/core/opentaint-go-querylang/samples-go-massive/star_15_matrix_pattern_not_inside/rule.yaml new file mode 100644 index 000000000..620a4a9c8 --- /dev/null +++ b/core/opentaint-go-querylang/samples-go-massive/star_15_matrix_pattern_not_inside/rule.yaml @@ -0,0 +1,18 @@ +rules: + - id: star-15-matrix-pattern-not-inside + languages: [go] + severity: WARNING + message: Starred sink suppressed by a pattern-not-inside guard wired through the pattern-inside + mode: taint + pattern-sources: + - pattern: $*X = star_15_matrix_pattern_not_inside.Source() + pattern-sinks: + - patterns: + - pattern-inside: | + $G = star_15_matrix_pattern_not_inside.NewChecker() + ... + - pattern: star_15_matrix_pattern_not_inside.Use($*Y) + - pattern-not-inside: | + $G.Check($*Y) + ... + - focus-metavariable: $Y diff --git a/core/opentaint-go-querylang/samples-go-massive/star_15_matrix_pattern_not_inside/sample.go b/core/opentaint-go-querylang/samples-go-massive/star_15_matrix_pattern_not_inside/sample.go new file mode 100644 index 000000000..fb96a301b --- /dev/null +++ b/core/opentaint-go-querylang/samples-go-massive/star_15_matrix_pattern_not_inside/sample.go @@ -0,0 +1,73 @@ +package util + +// Starred sink guarded by PATTERN-NOT-INSIDE, 5+ interprocedural depth x 5+ field depth +// combined (Go port of StarMatrixPatternNotInside). The sink `Use($*Y)` sits in a +// `pattern-inside` context that INTRODUCES the guard receiver (`$G = NewChecker(); ...`), +// and `pattern-not-inside: $G.Check($*Y); ...` suppresses it — every not-inside metavar must +// be introduced and wired by the pattern-inside/sink patterns (a not-inside with unbound +// metavars is dropped during automata-to-taint-rule conversion). A starred source five calls +// deep taints a whole L0; the object travels five hops; the Use call sits five calls deep — +// flagged in the unguarded function, suppressed in the guarded one. +type Checker struct{} + +func (c Checker) Check(o L0) { _ = o } + +type L0 struct{ F L1 } +type L1 struct{ F L2 } +type L2 struct{ F L3 } +type L3 struct{ F L4 } +type L4 struct{ V string } + +func Source() L0 { return L0{} } + +func NewChecker() Checker { return Checker{} } + +func Use(o L0) { _ = o } + +// Source five calls deep. +func src5() L0 { return src4() } +func src4() L0 { return src3() } +func src3() L0 { return src2() } +func src2() L0 { return src1() } +func src1() L0 { o := Source(); return o } // $*X = Source() matches HERE, depth 5 + +// Five object pass-hops. +func p1(o L0) L0 { return o } +func p2(o L0) L0 { return o } +func p3(o L0) L0 { return o } +func p4(o L0) L0 { return o } +func p5(o L0) L0 { return o } + +// Unguarded sink chain five calls deep. +func k1(o L0) { k2(o) } +func k2(o L0) { k3(o) } +func k3(o L0) { k4(o) } +func k4(o L0) { k5(o) } +func k5(o L0) { + g := NewChecker() // pattern-inside context (binds $G), no Check() -> flagged + _ = g + Use(o) // starred sink matches HERE, depth 5 +} + +// Guarded sink chain five calls deep: Check() precedes the Use in the same function. +func j1(o L0) { j2(o) } +func j2(o L0) { j3(o) } +func j3(o L0) { j4(o) } +func j4(o L0) { j5(o) } +func j5(o L0) { + g := NewChecker() // pattern-inside context (binds $G) + g.Check(o) // pattern-not-inside: $G.Check($*Y) precedes -> suppressed + Use(o) +} + +// Positive_unguarded_use: tainted object used without the guard. +func Positive_unguarded_use() { + o := src5() + k1(p5(p4(p3(p2(p1(o)))))) +} + +// Negative_guarded_use: same tainted object, but the Use is preceded by Check(). +func Negative_guarded_use() { + o := src5() + j1(p5(p4(p3(p2(p1(o)))))) +} diff --git a/core/opentaint-go-querylang/samples-go-massive/xss_07_json_field_write/rule.yaml b/core/opentaint-go-querylang/samples-go-massive/xss_07_json_field_write/rule.yaml index b5ce924cb..887887221 100644 --- a/core/opentaint-go-querylang/samples-go-massive/xss_07_json_field_write/rule.yaml +++ b/core/opentaint-go-querylang/samples-go-massive/xss_07_json_field_write/rule.yaml @@ -7,4 +7,6 @@ rules: pattern-sources: - pattern: $R.FormValue($K) pattern-sinks: - - pattern: $W.Write($B) + - patterns: + - pattern: $W.Write($*B) + - focus-metavariable: $B diff --git a/core/opentaint-go-querylang/samples-go-massive/xss_18_template_struct_data/rule.yaml b/core/opentaint-go-querylang/samples-go-massive/xss_18_template_struct_data/rule.yaml index 79540c97f..ede34dcc2 100644 --- a/core/opentaint-go-querylang/samples-go-massive/xss_18_template_struct_data/rule.yaml +++ b/core/opentaint-go-querylang/samples-go-massive/xss_18_template_struct_data/rule.yaml @@ -8,5 +8,5 @@ rules: - pattern: os.Getenv($K) pattern-sinks: - patterns: - - pattern: $T.Execute($W, $D) + - pattern: $T.Execute($W, $*D) - focus-metavariable: $D diff --git a/core/opentaint-go-querylang/samples-go/CmdInjEnvSink/rule.yaml b/core/opentaint-go-querylang/samples-go/CmdInjEnvSink/rule.yaml index 04a3c1bb7..e705e5fd8 100644 --- a/core/opentaint-go-querylang/samples-go/CmdInjEnvSink/rule.yaml +++ b/core/opentaint-go-querylang/samples-go/CmdInjEnvSink/rule.yaml @@ -9,7 +9,15 @@ rules: - pattern: os.Getenv($K) pattern-sinks: - pattern-either: - - pattern: $C.CombinedOutput() - - pattern: $C.Run() - - pattern: $C.Output() - - pattern: $C.Start() + - patterns: + - pattern: $*C.CombinedOutput() + - focus-metavariable: $C + - patterns: + - pattern: $*C.Run() + - focus-metavariable: $C + - patterns: + - pattern: $*C.Output() + - focus-metavariable: $C + - patterns: + - pattern: $*C.Start() + - focus-metavariable: $C diff --git a/core/opentaint-go-querylang/samples-go/CmdStringShellSink/rule.yaml b/core/opentaint-go-querylang/samples-go/CmdStringShellSink/rule.yaml index 7130091ed..35e6edd98 100644 --- a/core/opentaint-go-querylang/samples-go/CmdStringShellSink/rule.yaml +++ b/core/opentaint-go-querylang/samples-go/CmdStringShellSink/rule.yaml @@ -11,7 +11,7 @@ rules: - pattern-inside: | import "os/exec" ... - - pattern: exec.Command("$NAME", ..., $UNTRUSTED, ...) + - pattern: exec.Command("$NAME", ..., $*UNTRUSTED, ...) - metavariable-regex: metavariable: $NAME regex: sh diff --git a/core/opentaint-go-querylang/samples-go/CmdTypedReceiverSink/rule.yaml b/core/opentaint-go-querylang/samples-go/CmdTypedReceiverSink/rule.yaml index 4bd5388b7..ba427405e 100644 --- a/core/opentaint-go-querylang/samples-go/CmdTypedReceiverSink/rule.yaml +++ b/core/opentaint-go-querylang/samples-go/CmdTypedReceiverSink/rule.yaml @@ -2,16 +2,16 @@ rules: - id: cmd-typed-receiver-sink languages: [go] severity: ERROR - message: Tainted user input reaches OS command execution via a typed *exec.Cmd receiver + message: Tainted user input reaches OS command execution via an *exec.Cmd receiver mode: taint pattern-sources: - pattern-either: - pattern: os.Getenv($K) pattern-sinks: - pattern-either: - - pattern: | - import "os/exec" - ($C : *exec.Cmd).Run() - - pattern: | - import "os/exec" - ($C : *exec.Cmd).CombinedOutput() + - patterns: + - pattern: "($*C : *exec.Cmd).Run()" + - focus-metavariable: $C + - patterns: + - pattern: "($*C : *exec.Cmd).CombinedOutput()" + - focus-metavariable: $C diff --git a/core/opentaint-go-querylang/samples-go/MapValueToReceiver/rule.yaml b/core/opentaint-go-querylang/samples-go/MapValueToReceiver/rule.yaml index 31b446ce8..7d5c29f3b 100644 --- a/core/opentaint-go-querylang/samples-go/MapValueToReceiver/rule.yaml +++ b/core/opentaint-go-querylang/samples-go/MapValueToReceiver/rule.yaml @@ -7,4 +7,6 @@ rules: pattern-sources: - pattern: "MapValueToReceiver.Source(...)" pattern-sinks: - - pattern: "($C : *MapValueToReceiver.Controller).Serve()" + - patterns: + - pattern: "$*C.Serve()" + - focus-metavariable: $C diff --git a/core/opentaint-go-querylang/samples-go/ShellExecArgConstraint/rule.yaml b/core/opentaint-go-querylang/samples-go/ShellExecArgConstraint/rule.yaml index 9e1cc41bc..708725a89 100644 --- a/core/opentaint-go-querylang/samples-go/ShellExecArgConstraint/rule.yaml +++ b/core/opentaint-go-querylang/samples-go/ShellExecArgConstraint/rule.yaml @@ -8,7 +8,8 @@ rules: - pattern: "ShellExecArgConstraint.Source(...)" pattern-sinks: - patterns: - - pattern: 'exec.Command("$NAME", ...)' + - pattern: 'exec.Command("$NAME", ..., $*UNTRUSTED, ...)' + - focus-metavariable: $UNTRUSTED - metavariable-regex: metavariable: $NAME regex: ^(.*/)?(sh|bash|zsh|dash)$ diff --git a/core/opentaint-go-querylang/samples-go/TypedArgSink/rule.yaml b/core/opentaint-go-querylang/samples-go/TypedArgSink/rule.yaml index 87e06709c..405793bd0 100644 --- a/core/opentaint-go-querylang/samples-go/TypedArgSink/rule.yaml +++ b/core/opentaint-go-querylang/samples-go/TypedArgSink/rule.yaml @@ -8,5 +8,5 @@ rules: - pattern: "TypedArgSink.Source(...)" pattern-sinks: - patterns: - - pattern: "fmt.Fprint(($W : io.Writer), $A)" + - pattern: "fmt.Fprint(($W : io.Writer), $*A)" - focus-metavariable: $A diff --git a/core/opentaint-go-querylang/src/main/kotlin/org/opentaint/semgrep/go/pattern/SemgrepGoPattern.kt b/core/opentaint-go-querylang/src/main/kotlin/org/opentaint/semgrep/go/pattern/SemgrepGoPattern.kt index d3e63c5fb..095a96718 100644 --- a/core/opentaint-go-querylang/src/main/kotlin/org/opentaint/semgrep/go/pattern/SemgrepGoPattern.kt +++ b/core/opentaint-go-querylang/src/main/kotlin/org/opentaint/semgrep/go/pattern/SemgrepGoPattern.kt @@ -28,7 +28,7 @@ data class MetavarName(val name: String) : Name // 5.1 Pattern atoms // ---------------------------------------------------------------------------- -data class Metavar(val name: String) : SemgrepGoPattern { +data class Metavar(val name: String, val star: Boolean = false) : SemgrepGoPattern { override val children: List get() = emptyList() } @@ -44,7 +44,7 @@ data class DeepExpr(val nested: SemgrepGoPattern) : SemgrepGoPattern { override val children: List get() = listOf(nested) } -data class TypedMetavar(val name: String, val type: TypeName) : SemgrepGoPattern { +data class TypedMetavar(val name: String, val type: TypeName, val star: Boolean = false) : SemgrepGoPattern { override val children: List get() = emptyList() } diff --git a/core/opentaint-go-querylang/src/main/kotlin/org/opentaint/semgrep/go/pattern/SemgrepGoPatternParser.kt b/core/opentaint-go-querylang/src/main/kotlin/org/opentaint/semgrep/go/pattern/SemgrepGoPatternParser.kt index f454a19b3..91bc47399 100644 --- a/core/opentaint-go-querylang/src/main/kotlin/org/opentaint/semgrep/go/pattern/SemgrepGoPatternParser.kt +++ b/core/opentaint-go-querylang/src/main/kotlin/org/opentaint/semgrep/go/pattern/SemgrepGoPatternParser.kt @@ -101,6 +101,7 @@ class SemgrepGoPatternParser : SemgrepPatternParser { private fun GoParser.IdentifierContext.parseName(): Name { METAVAR_IDENT()?.let { return MetavarName(it.text) } + METAVAR_STAR_IDENT()?.let { throw SemgrepGoParsingFailedException(this, "Star is not expected here") } METAVAR_ELLIPSIS()?.let { return MetavarName(it.text.removePrefix("$...")) } ANONYMOUS_METAVAR()?.let { return MetavarName("_") } return ConcreteName(text) @@ -238,6 +239,7 @@ private class SemgrepGoPatternParserVisitor : GoParserBaseVisitor `$X`: drop the `*` that follows the leading `$`, yielding the plain metavar name. + private fun String.stripStar(): String = "$" + substring(2) + private fun parseOperandName(ctx: GoParser.OperandNameContext): SemgrepGoPattern { ctx.qualifiedIdent()?.let { val (pkg, sel) = parseQualifiedIdentParts(it) return SelectorExpr(Identifier(pkg), sel) } ctx.METAVAR_IDENT()?.let { return Metavar(it.text) } + ctx.METAVAR_STAR_IDENT()?.let { return Metavar(it.text.stripStar(), star = true) } ctx.ANONYMOUS_METAVAR()?.let { return Metavar("_") } ctx.METAVAR_ELLIPSIS()?.let { return EllipsisMetavar(it.text.removePrefix("$...")) } ctx.IDENTIFIER()?.let { return Identifier(ConcreteName(it.text)) } diff --git a/core/opentaint-go-querylang/src/main/kotlin/org/opentaint/semgrep/go/pattern/conversion/GoPatternToActionListConverter.kt b/core/opentaint-go-querylang/src/main/kotlin/org/opentaint/semgrep/go/pattern/conversion/GoPatternToActionListConverter.kt index 3a586d92f..10736069f 100644 --- a/core/opentaint-go-querylang/src/main/kotlin/org/opentaint/semgrep/go/pattern/conversion/GoPatternToActionListConverter.kt +++ b/core/opentaint-go-querylang/src/main/kotlin/org/opentaint/semgrep/go/pattern/conversion/GoPatternToActionListConverter.kt @@ -353,12 +353,17 @@ class GoPatternToActionListConverter : ActionListBuilder { is MetavarName -> Triple(emptyList(), IsMetavar(MetavarAtom.create(n.name)), null) } - is Metavar -> Triple(emptyList(), IsMetavar(MetavarAtom.create(recv.name)), null) + is Metavar -> Triple(emptyList(), IsMetavar(MetavarAtom.create(recv.name), star = recv.star), null) is TypedMetavar -> { val t = transformType(recv.type) Triple( emptyList(), - ParamCondition.And(listOf(IsMetavar(MetavarAtom.create(recv.name)), ParamCondition.TypeIs(t))), + ParamCondition.And( + listOf( + IsMetavar(MetavarAtom.create(recv.name), star = recv.star), + ParamCondition.TypeIs(t), + ), + ), null, ) } @@ -458,10 +463,10 @@ class GoPatternToActionListConverter : ActionListBuilder { is MetavarName -> ParamCondition.StringValueMetaVar(MetavarAtom.create(c.name)) } is StringEllipsis -> ParamCondition.AnyStringLiteral - is Metavar -> IsMetavar(MetavarAtom.create(pattern.name)) + is Metavar -> IsMetavar(MetavarAtom.create(pattern.name), star = pattern.star) is TypedMetavar -> ParamCondition.And( listOf( - IsMetavar(MetavarAtom.create(pattern.name)), + IsMetavar(MetavarAtom.create(pattern.name), star = pattern.star), ParamCondition.TypeIs(transformType(pattern.type)), ), ) @@ -482,7 +487,7 @@ class GoPatternToActionListConverter : ActionListBuilder { if (names.size == 1) { val name = names.first() if (name != null) { - conditions += IsMetavar(MetavarAtom.create(name)) + conditions += IsMetavar(MetavarAtom.create(name.name), star = name.star) } return transformAssignmentValue(conditions, value) @@ -498,22 +503,27 @@ class GoPatternToActionListConverter : ActionListBuilder { } val assignedName = names[assignedNameIdx]!! - conditions += IsMetavar(MetavarAtom.create(assignedName)) + conditions += IsMetavar(MetavarAtom.create(assignedName.name), star = assignedName.star) conditions += createFieldModifier(prevModifier = null, "tuple$$assignedNameIdx") return transformAssignmentValue(conditions, value) } + // Name + star of an assignment target. A bare `Metavar` (`$*X`) or a typed metavar (`($*X : T)`) + // can be starred; other target shapes carry star = false. Threading star lets `$*X = src()` (or + // its typed form) taint every nested field. + private data class AssignmentTarget(val name: String, val star: Boolean) + private fun SemgrepGoPattern.assignmentTargetName( conditions: MutableList - ): String? = when { - this is Metavar -> name + ): AssignmentTarget? = when { + this is Metavar -> AssignmentTarget(name, star) this is TypedMetavar -> { conditions += ParamCondition.TypeIs(transformType(type)) - name + AssignmentTarget(name, star = star) } - this is Identifier && name is MetavarName -> name.name + this is Identifier && name is MetavarName -> AssignmentTarget(name.name, star = false) this is Identifier && name is ConcreteName && name.name == "_" -> null else -> transformationFailed("Assignment_target_not_metavar") diff --git a/core/opentaint-go-querylang/src/main/kotlin/org/opentaint/semgrep/go/pattern/conversion/GoTaintStrategy.kt b/core/opentaint-go-querylang/src/main/kotlin/org/opentaint/semgrep/go/pattern/conversion/GoTaintStrategy.kt index 7fa451789..1fb3d9aca 100644 --- a/core/opentaint-go-querylang/src/main/kotlin/org/opentaint/semgrep/go/pattern/conversion/GoTaintStrategy.kt +++ b/core/opentaint-go-querylang/src/main/kotlin/org/opentaint/semgrep/go/pattern/conversion/GoTaintStrategy.kt @@ -11,7 +11,6 @@ import org.opentaint.semgrep.go.pattern.conversion.go.matchAnything import org.opentaint.semgrep.go.pattern.conversion.go.mkGoAssignMark import org.opentaint.semgrep.go.pattern.conversion.go.mkGoCleanMark import org.opentaint.semgrep.go.pattern.conversion.go.mkGoContainsMark -import org.opentaint.semgrep.go.pattern.conversion.go.mkGoContainsMarkOnAnyAccessor import org.opentaint.semgrep.pattern.Mark import org.opentaint.semgrep.pattern.TaintRuleMatchAnything import org.opentaint.semgrep.pattern.conversion.LanguageStrategy.SinkDiscardMode @@ -51,7 +50,7 @@ data object GoTaintStrategy : data object GoMarkConditionBuilder : MarkConditionBuilder { override fun checkTaintMark(mark: Mark.GeneratedMark, pos: PositionBaseWithModifiers): GoSerializedCondition = - mark.mkGoContainsMarkOnAnyAccessor(pos) + mark.mkGoContainsMark(pos) override fun negate(cond: GoSerializedCondition) = GoSerializedCondition.not(cond) override fun and(args: List) = GoSerializedCondition.and(args) @@ -82,8 +81,6 @@ data object GoTaintStrategy : pos: PositionBaseWithModifiers ): GoSerializedAssignAction = mark.mkGoAssignMark(pos) - // todo: cleaners are not ready for any accessor -// mark.mkGoAssignMarkOnAnyAccessor(pos) override fun assignedMark(assign: GoSerializedAssignAction): Mark.GeneratedMark = Mark.parseMark(assign.kind) diff --git a/core/opentaint-go-querylang/src/main/kotlin/org/opentaint/semgrep/go/pattern/conversion/go/GoSerializedRuleUtils.kt b/core/opentaint-go-querylang/src/main/kotlin/org/opentaint/semgrep/go/pattern/conversion/go/GoSerializedRuleUtils.kt index 0ac33f98e..4b18b47e5 100644 --- a/core/opentaint-go-querylang/src/main/kotlin/org/opentaint/semgrep/go/pattern/conversion/go/GoSerializedRuleUtils.kt +++ b/core/opentaint-go-querylang/src/main/kotlin/org/opentaint/semgrep/go/pattern/conversion/go/GoSerializedRuleUtils.kt @@ -6,6 +6,7 @@ import org.opentaint.dataflow.configuration.go.serialized.GoSerializedCleanActio import org.opentaint.dataflow.configuration.go.serialized.GoSerializedCondition import org.opentaint.dataflow.configuration.jvm.serialized.PositionBase import org.opentaint.dataflow.configuration.jvm.serialized.PositionBaseWithModifiers +import org.opentaint.dataflow.configuration.jvm.serialized.PositionModifier import org.opentaint.semgrep.pattern.Mark.GeneratedMark internal fun PositionBase.baseGo(): PositionBaseWithModifiers.BaseOnly = PositionBaseWithModifiers.BaseOnly(this) @@ -18,14 +19,13 @@ internal fun GoNameMatcher.matchAnything(): Boolean = internal fun GeneratedMark.mkGoContainsMark(pos: PositionBaseWithModifiers): GoSerializedCondition.ContainsMark = GoSerializedCondition.ContainsMark(taintMarkStr(), pos) -internal fun GeneratedMark.mkGoContainsMarkOnAnyAccessor(pos: PositionBaseWithModifiers): GoSerializedCondition = - GoSerializedCondition.ContainsMarkOnAnyAccessor(taintMarkStr(), pos) - internal fun GeneratedMark.mkGoAssignMark(pos: PositionBaseWithModifiers): GoSerializedAssignAction = - GoSerializedAssignAction.Direct(taintMarkStr(), pos) + GoSerializedAssignAction(taintMarkStr(), pos) -internal fun GeneratedMark.mkGoAssignMarkOnAnyAccessor(pos: PositionBaseWithModifiers): GoSerializedAssignAction = - GoSerializedAssignAction.AnyAccessor(taintMarkStr(), pos) +internal fun PositionBaseWithModifiers.withAnyField(): PositionBaseWithModifiers = when (this) { + is PositionBaseWithModifiers.BaseOnly -> PositionBaseWithModifiers.WithModifiers(base, listOf(PositionModifier.AnyField)) + is PositionBaseWithModifiers.WithModifiers -> copy(modifiers = modifiers + PositionModifier.AnyField) +} internal fun GeneratedMark.mkGoCleanMark(pos: PositionBaseWithModifiers): GoSerializedCleanAction = GoSerializedCleanAction(taintMarkStr(), pos) diff --git a/core/opentaint-go-querylang/src/main/kotlin/org/opentaint/semgrep/go/pattern/conversion/go/GoTaintRuleGeneration.kt b/core/opentaint-go-querylang/src/main/kotlin/org/opentaint/semgrep/go/pattern/conversion/go/GoTaintRuleGeneration.kt index 3ecbbc358..81e4ee1a4 100644 --- a/core/opentaint-go-querylang/src/main/kotlin/org/opentaint/semgrep/go/pattern/conversion/go/GoTaintRuleGeneration.kt +++ b/core/opentaint-go-querylang/src/main/kotlin/org/opentaint/semgrep/go/pattern/conversion/go/GoTaintRuleGeneration.kt @@ -249,8 +249,17 @@ private fun GoTaintRuleGenerationCtx.buildGoStateAssignActions( val requiredVariables = stateAfter.register.assignedVars.keys val result = requiredVariables.flatMapTo(mutableListOf()) { varName -> val varPosition = edgeCondition.accessedVarPosition[varName] ?: return@flatMapTo emptyList() - varPosition.positions.flatMap { stateAssignMark(varPosition.varName, stateAfter, it) } + varPosition.positions.flatMap { sp -> + sp.bases().flatMap { + stateAssignMark(varPosition.varName, stateAfter, it) + } + } + } + + if (stateAfter in globalStateAssignStates) { + result += globalStateMarkName(stateAfter).mkGoAssignMark(goStateVarPosition) } + return result } @@ -260,12 +269,24 @@ private fun GoTaintRuleGenerationCtx.buildGoStateCleanActions( edgeCondition: GoEvaluatedEdgeCondition ): List { val result = edgeCondition.accessedVarPosition.values.flatMapTo(mutableListOf()) { varPosition -> - varPosition.positions.flatMap { stateCleanMark(varPosition.varName, stateAfter, stateBefore, it) } + varPosition.positions.flatMap { sp -> + sp.bases().flatMap { + stateCleanMark(varPosition.varName, stateAfter, stateBefore, it) + } + } } result += stateCleanMark(varName = null, stateAfter, stateBefore, position = null) + + if (stateBefore in globalStateAssignStates) { + result += globalStateMarkName(stateBefore).mkGoCleanMark(goStateVarPosition) + } + return result } +private val GoTaintRuleGenerationCtx.goStateVarPosition: PositionBaseWithModifiers + get() = PositionBase.ClassStatic(prefix.artificialState("pos").taintMarkStr()).baseGo() + private fun GoEvaluatedEdgeCondition.addGoStateCheck( ctx: GoTaintRuleGenerationCtx, checkGlobalState: Boolean, @@ -273,13 +294,13 @@ private fun GoEvaluatedEdgeCondition.addGoStateCheck( ): GoEvaluatedEdgeCondition { val stateChecks = mutableListOf() if (checkGlobalState) { - stateChecks += ctx.globalStateMarkName(stateOfEdge).mkGoContainsMark( - PositionBase.ClassStatic(ctx.prefix.artificialState("pos").taintMarkStr()).baseGo() - ) + stateChecks += ctx.globalStateMarkName(stateOfEdge).mkGoContainsMark(ctx.goStateVarPosition) } else { for (metaVar in stateOfEdge.register.assignedVars.keys) { - for (pos in accessedVarPosition[metaVar]?.positions.orEmpty()) { - stateChecks += ctx.containsStateMark(metaVar, stateOfEdge, pos) + for (sp in accessedVarPosition[metaVar]?.positions.orEmpty()) { + sp.bases().forEach { + stateChecks += ctx.containsStateMark(metaVar, stateOfEdge, it) + } } } } @@ -609,7 +630,7 @@ private fun findGoMetaVarPositionUtil( val varPosition = varPositions.getOrPut(condition.metavar) { GoRegisterVarPosition(condition.metavar, hashSetOf()) } - varPosition.positions.add(position) + varPosition.positions.add(GoStarredPosition(position, condition.star)) } private fun evaluateGoParamCondition( @@ -627,7 +648,11 @@ private fun evaluateGoParamCondition( // todo: semantic metavar constraint semgrepRuleTrace.error(IgnoredMetavarConstraint(condition.metavar)) } - return ctx.containsMarkWithAnyStateBefore(edgeState, condition.metavar, position) + val contains = ctx.containsMarkWithAnyStateBefore(edgeState, condition.metavar, position) + if (!condition.star) return contains + + val containsAnyField = ctx.containsMarkWithAnyStateBefore(edgeState, condition.metavar, position.withAnyField()) + return GoSerializedCondition.or(listOf(contains, containsAnyField)) } is ParamCondition.TypeIs -> { return ctx.goTypeMatcher(condition.typeName, semgrepRuleTrace) @@ -744,3 +769,6 @@ private fun List.toSerializedPosModifiers(): List = ma else -> PositionModifier.Field("", it, "") } } + +private fun GoStarredPosition.bases(): List = + if (star) listOf(position, position.withAnyField()) else listOf(position) diff --git a/core/opentaint-go-querylang/src/main/kotlin/org/opentaint/semgrep/go/pattern/conversion/go/GoTaintRuleGenerationCtxExt.kt b/core/opentaint-go-querylang/src/main/kotlin/org/opentaint/semgrep/go/pattern/conversion/go/GoTaintRuleGenerationCtxExt.kt index 6aa96e0d2..edacf24da 100644 --- a/core/opentaint-go-querylang/src/main/kotlin/org/opentaint/semgrep/go/pattern/conversion/go/GoTaintRuleGenerationCtxExt.kt +++ b/core/opentaint-go-querylang/src/main/kotlin/org/opentaint/semgrep/go/pattern/conversion/go/GoTaintRuleGenerationCtxExt.kt @@ -43,7 +43,12 @@ internal data class GoEvaluatedEdgeCondition( internal data class GoRegisterVarPosition( val varName: MetavarAtom, - val positions: MutableSet, + val positions: MutableSet, +) + +internal data class GoStarredPosition( + val position: PositionBaseWithModifiers, + val star: Boolean, ) data class FieldModifierCtx( diff --git a/core/opentaint-go-querylang/src/test/kotlin/org/opentaint/semgrep/GoMassiveSampleTest.kt b/core/opentaint-go-querylang/src/test/kotlin/org/opentaint/semgrep/GoMassiveSampleTest.kt index 5f52ea4af..e915ed949 100644 --- a/core/opentaint-go-querylang/src/test/kotlin/org/opentaint/semgrep/GoMassiveSampleTest.kt +++ b/core/opentaint-go-querylang/src/test/kotlin/org/opentaint/semgrep/GoMassiveSampleTest.kt @@ -626,6 +626,58 @@ class GoMassiveSampleTest : GoSampleBasedTestBase("GO_MASSIVE_SAMPLES_DIR") { @Test fun xss20VariadicSprintf() = runSampleDefault("xss_20_variadic_sprintf") - + + // ─── Star-operator ($*VAR) field-taint e2e samples (parity with Java StarSource/StarSink/StarSanitizer) ─── + + @Test + fun star01SinkField() = runSampleDefault("star_01_sink_field") + + @Test + fun star02SourceField() = runSampleDefault("star_02_source_field") + + @Test + fun star03SanitizerField() = runSampleDefault("star_03_sanitizer_field") + + // ─── Deep-nesting matrix: taint hidden 5+ fields deep and/or 5+ calls deep ─── + + @Test + fun star04DeepSinkField() = runSampleDefault("star_04_deep_sink_field") + + @Test + fun star05DeepSourceField() = runSampleDefault("star_05_deep_source_field") + + @Test + fun star06DeepSanitizerField() = runSampleDefault("star_06_deep_sanitizer_field") + + @Test + fun star07InterprocChain() = runSampleDefault("star_07_interproc_chain") + + @Test + fun star08SourceAndSink() = runSampleDefault("star_08_source_and_sink") + + // ─── Star matrix: 5x interproc depth combined with 5x field depth, one sample per star feature + // (parity with Java StarMatrix*) ─── + + @Test + fun star09MatrixSource() = runSampleDefault("star_09_matrix_source") + + @Test + fun star10MatrixSink() = runSampleDefault("star_10_matrix_sink") + + @Test + fun star11MatrixPropagator() = runSampleDefault("star_11_matrix_propagator") + + @Test + fun star12MatrixSanitizer() = runSampleDefault("star_12_matrix_sanitizer") + + @Test + fun star13MatrixPatternNot() = runSampleDefault("star_13_matrix_pattern_not") + + @Test + fun star14MatrixPatternInside() = runSampleDefault("star_14_matrix_pattern_inside") + + @Test + fun star15MatrixPatternNotInside() = runSampleDefault("star_15_matrix_pattern_not_inside") + private fun runSampleDefault(name: String) = runSample(name, useDefaultConfig = true) } diff --git a/core/opentaint-go-querylang/src/test/kotlin/org/opentaint/semgrep/pattern/GoStarOperatorEmitTest.kt b/core/opentaint-go-querylang/src/test/kotlin/org/opentaint/semgrep/pattern/GoStarOperatorEmitTest.kt new file mode 100644 index 000000000..c6d2ad2f7 --- /dev/null +++ b/core/opentaint-go-querylang/src/test/kotlin/org/opentaint/semgrep/pattern/GoStarOperatorEmitTest.kt @@ -0,0 +1,236 @@ +package org.opentaint.semgrep.pattern + +import org.opentaint.dataflow.configuration.go.serialized.GoSerializedAssignAction +import org.opentaint.dataflow.configuration.go.serialized.GoSerializedCleanAction +import org.opentaint.dataflow.configuration.go.serialized.GoSerializedCondition +import org.opentaint.dataflow.configuration.go.serialized.GoSerializedItem +import org.opentaint.dataflow.configuration.go.serialized.GoSerializedRule +import org.opentaint.dataflow.configuration.jvm.serialized.PositionBaseWithModifiers +import org.opentaint.dataflow.configuration.jvm.serialized.PositionModifier +import org.opentaint.semgrep.go.pattern.conversion.GoLanguageStrategy +import kotlin.io.path.Path +import kotlin.test.Test +import kotlin.test.assertTrue + +/** + * Unit-level checks that the Go taint-rule emitter threads the `$*X` star operator, giving Go the + * same semantics as Java: `$X` = base-only taint; `$*X` = base + all nested fields (any-accessor). + * + * Mirrors [org.opentaint.semgrep.StarOperatorRuleGenTest] on the Java side, but the Go engine models + * "any field" on serialized actions as an AnyField position modifier. Conditions retain their + * specialized ContainsMarkOnAnyAccessor representation. + */ +class GoStarOperatorEmitTest { + + private fun PositionBaseWithModifiers.hasAnyField(): Boolean = + this is PositionBaseWithModifiers.WithModifiers && PositionModifier.AnyField in modifiers + + private fun PositionBaseWithModifiers.withoutAnyField(): PositionBaseWithModifiers = when (this) { + is PositionBaseWithModifiers.BaseOnly -> this + is PositionBaseWithModifiers.WithModifiers -> { + val prefix = modifiers.takeWhile { it != PositionModifier.AnyField } + if (prefix.isEmpty()) PositionBaseWithModifiers.BaseOnly(base) else copy(modifiers = prefix) + } + } + + private fun emitItems(ruleText: String): List { + val loader = SemgrepRuleLoader(listOf(GoLanguageStrategy())) + loader.registerRuleSet(ruleText, Path("star.yaml"), Path("."), SemgrepLoadTrace()) + val (rule, _) = loader.loadRules().rulesWithMeta.single() + + @Suppress("UNCHECKED_CAST") + val taintRule = rule as TaintRuleFromSemgrep + return taintRule.taintRules.flatMap { it.rules } + } + + private fun flatten(c: GoSerializedCondition): List = when (c) { + is GoSerializedCondition.Or -> listOf(c) + c.anyOf.flatMap { flatten(it) } + is GoSerializedCondition.And -> listOf(c) + c.allOf.flatMap { flatten(it) } + is GoSerializedCondition.Not -> listOf(c) + flatten(c.not) + else -> listOf(c) + } + + private fun sinkConditions(items: List): List = + items.filterIsInstance() + .mapNotNull { it.condition } + .flatMap { flatten(it) } + + private fun sourceTaint(items: List): List = + items.filterIsInstance().flatMap { it.taint } + + private fun cleanerCleans(items: List): List = + items.filterIsInstance().flatMap { it.cleans } + + @Test + fun `starred sink checks base and any-accessor on the same arg`() { + val items = emitItems( + """ + rules: + - id: go-star-sink + languages: [go] + mode: taint + message: x + severity: ERROR + pattern-sources: + - pattern: "util.Source(...)" + pattern-sinks: + - patterns: + - pattern: "util.Sink(${'$'}*Y)" + - focus-metavariable: ${'$'}Y + """.trimIndent() + ) + val conditions = sinkConditions(items) + val base = conditions.filterIsInstance() + val anyAccessor = conditions.filterIsInstance() + + assertTrue(anyAccessor.isNotEmpty(), "expected a ContainsMarkOnAnyAccessor in the starred sink; got $conditions") + assertTrue(base.isNotEmpty(), "expected a plain ContainsMark in the starred sink; got $conditions") + + // Base coherence: each any-accessor check must be paired with a plain ContainsMark on the + // SAME mark and SAME position -- the two arms of an Or over the metavar's resolved position. + anyAccessor.forEach { af -> + assertTrue( + base.any { it.tainted == af.tainted && it.pos == af.pos }, + "any-accessor check $af has no paired plain ContainsMark on same mark/pos; base=$base" + ) + } + } + + @Test + fun `starred assignment-LHS source taints base and any-accessor`() { + val items = emitItems( + """ + rules: + - id: go-star-source + languages: [go] + mode: taint + message: x + severity: ERROR + pattern-sources: + - pattern: "${'$'}*X = util.Source()" + pattern-sinks: + - pattern: "util.Sink(${'$'}Y)" + """.trimIndent() + ) + val taint = sourceTaint(items) + val direct = taint.filterIsInstance().filter { !it.pos.hasAnyField() } + val anyAccessor = taint.filterIsInstance().filter { it.pos.hasAnyField() } + + assertTrue(direct.isNotEmpty(), "expected a Direct assign in the starred source; got $taint") + assertTrue(anyAccessor.isNotEmpty(), "expected an AnyAccessor assign in the starred source; got $taint") + + // Coherence: every any-accessor assign mirrors a direct one on the same mark and position. + anyAccessor.forEach { any -> + assertTrue( + direct.any { it.kind == any.kind && it.pos == any.pos.withoutAnyField() }, + "any-accessor assign $any has no paired Direct on same mark/pos; direct=$direct" + ) + } + } + + @Test + fun `starred sanitizer cleans base and any-accessor`() { + val items = emitItems( + """ + rules: + - id: go-star-sanitizer + languages: [go] + mode: taint + message: x + severity: ERROR + pattern-sources: + - pattern: "${'$'}X = util.Source()" + pattern-sanitizers: + - patterns: + - pattern: "util.Clean(${'$'}*X)" + - focus-metavariable: ${'$'}X + pattern-sinks: + - pattern: "util.Sink(${'$'}X)" + """.trimIndent() + ) + val cleans = cleanerCleans(items) + val direct = cleans.filterIsInstance().filter { !it.pos.hasAnyField() } + val anyAccessor = cleans.filterIsInstance().filter { it.pos.hasAnyField() } + + assertTrue(direct.isNotEmpty(), "expected a Direct clean in the starred sanitizer; got $cleans") + assertTrue(anyAccessor.isNotEmpty(), "expected an AnyAccessor clean in the starred sanitizer; got $cleans") + + // Coherence: every any-accessor clean mirrors a direct one on the same mark and position. + // The direct clean is REQUIRED: the any-accessor clean removes marks stored under an ANY + // accessor but does NOT reach a concrete base mark, so both must be emitted. + anyAccessor.forEach { any -> + assertTrue( + direct.any { it.taintKind == any.taintKind && it.pos == any.pos.withoutAnyField() }, + "any-accessor clean $any has no paired Direct on same mark/pos; direct=$direct" + ) + } + } + + @Test + fun `starred TYPED sink checks base and any-accessor with the type constraint retained`() { + // `($*Y : string)` is a starred TYPED metavar. It lowers to And(IsMetavar(star), TypeIs), + // which the automata flattens into two per-position atoms; the starred IsMetavar atom must + // still thread the any-accessor arm, and the TypeIs atom must still emit an IsType check. + val items = emitItems( + """ + rules: + - id: go-star-typed-sink + languages: [go] + mode: taint + message: x + severity: ERROR + pattern-sources: + - pattern: "util.Source(...)" + pattern-sinks: + - patterns: + - pattern: "util.Sink((${'$'}*Y : string))" + - focus-metavariable: ${'$'}Y + """.trimIndent() + ) + val conditions = sinkConditions(items) + val base = conditions.filterIsInstance() + val anyAccessor = conditions.filterIsInstance() + + assertTrue(anyAccessor.isNotEmpty(), "expected a ContainsMarkOnAnyAccessor for the starred typed sink; got $conditions") + assertTrue(base.isNotEmpty(), "expected a plain ContainsMark for the starred typed sink; got $conditions") + assertTrue( + conditions.any { it is GoSerializedCondition.IsType }, + "expected the type constraint (IsType) to survive alongside the star; got $conditions" + ) + anyAccessor.forEach { af -> + assertTrue( + base.any { it.tainted == af.tainted && it.pos == af.pos }, + "any-accessor check $af has no paired plain ContainsMark on same mark/pos; base=$base" + ) + } + } + + @Test + fun `non-star sink checks base only`() { + val items = emitItems( + """ + rules: + - id: go-plain-sink + languages: [go] + mode: taint + message: x + severity: ERROR + pattern-sources: + - pattern: "util.Source(...)" + pattern-sinks: + - patterns: + - pattern: "util.Sink(${'$'}Y)" + - focus-metavariable: ${'$'}Y + """.trimIndent() + ) + val conditions = sinkConditions(items) + assertTrue( + conditions.filterIsInstance().none { it.pos.hasAnyField() }, + "a non-star \$Y sink must be base-only (no AnyField modifier); got $conditions" + ) + assertTrue( + conditions.any { it is GoSerializedCondition.ContainsMark }, + "expected a base ContainsMark in the plain sink; got $conditions" + ) + } +} diff --git a/core/opentaint-go-querylang/src/test/kotlin/org/opentaint/semgrep/pattern/SemgrepGoPatternParserTest.kt b/core/opentaint-go-querylang/src/test/kotlin/org/opentaint/semgrep/pattern/SemgrepGoPatternParserTest.kt index 396f65176..fa70c7969 100644 --- a/core/opentaint-go-querylang/src/test/kotlin/org/opentaint/semgrep/pattern/SemgrepGoPatternParserTest.kt +++ b/core/opentaint-go-querylang/src/test/kotlin/org/opentaint/semgrep/pattern/SemgrepGoPatternParserTest.kt @@ -108,6 +108,90 @@ class SemgrepGoPatternParserTest { assertNotNull(find(ast) { it is FuncDecl }) } + /** Collects every pattern node in the AST (self + descendants). */ + private fun collect(p: SemgrepGoPattern): List = + listOf(p) + p.children.flatMap { collect(it) } + + private fun metavars(pattern: String): List = + collect(parse(pattern)).filterIsInstance() + + @Test fun starredMetavarInCallArgument() { + val y = metavars("Sink(\$*Y)").single { it.name == "\$Y" } + assertTrue(y.star, "expected \$*Y to be starred") + } + + /** Star count tolerating a parse failure (a retired/invalid form yields no starred metavar). */ + private fun starCount(pattern: String): Int { + val r = parser.parseSemgrepGoPattern(pattern) + return if (r is SemgrepGoPatternParsingResult.Ok) + collect(r.pattern).filterIsInstance().count { it.star } + else 0 + } + + @Test fun prefixStarNotSuffixMarksTheMetavar() { + // The star is a `$*` prefix bound into the metavar token. `$Y * z` stays multiplication, + // and the retired suffix form `$Y*` is no longer a starred metavar. + assertEquals(1, starCount("Sink(\$*Y)"), "\$*Y must be a star") + assertEquals(0, starCount("Sink(\$Y * z)"), "\$Y * z must not be a star") + assertEquals(0, starCount("Sink(\$Y*)"), "retired suffix \$Y* must not be a star") + } + + @Test fun plainMetavarIsNotStarred() { + val y = metavars("Sink(\$Y)").single { it.name == "\$Y" } + assertTrue(!y.star, "plain \$Y must not be starred") + } + + @Test fun starredMetavarOnAssignmentLhs() { + val x = metavars("\$*X = Source()").single { it.name == "\$X" } + assertTrue(x.star, "expected LHS \$*X to be starred") + } + + private fun typedMetavars(pattern: String): List = + collect(parse(pattern)).filterIsInstance() + + @Test fun starredTypedMetavar() { + // `($*Y : SomeType)` parses to a starred typed metavar carrying its type constraint. + val tm = typedMetavars("Sink((\$*Y : SomeType))").single { it.name == "\$Y" } + assertTrue(tm.star, "expected (\$*Y : SomeType) to be a starred typed metavar") + } + + @Test fun plainTypedMetavarIsNotStarred() { + // `($Y : SomeType)` stays an unstarred typed metavar (byte-identical to before). + val tm = typedMetavars("Sink((\$Y : SomeType))").single { it.name == "\$Y" } + assertTrue(!tm.star, "plain (\$Y : SomeType) must not be starred") + } + + @Test fun retiredSuffixTypedMetavarIsNotStarred() { + // The retired suffix forms `($Y* : T)` and the spaced `($Y * : T)` no longer denote a + // starred typed metavar: the star is now a `$*` prefix, so neither parses as one. + for (p in listOf("Sink((\$Y* : SomeType))", "Sink((\$Y * : SomeType))")) { + val r = parser.parseSemgrepGoPattern(p) + val starred = r is SemgrepGoPatternParsingResult.Ok && + typedMetavars(p).any { it.star } + assertTrue(!starred, "`$p` must not parse as a starred typed metavar; got $r") + } + } + + @Test fun starredTypedReceiverParses() { + // Typed receiver form `($*C : *exec.Cmd).Run()` parses with both star and the type restored. + // The `*` in `*exec.Cmd` is a pointer type, distinct from the metavar's `$*` star prefix. + val tm = typedMetavars("(\$*C : *exec.Cmd).Run()").single { it.name == "\$C" } + assertTrue(tm.star, "expected (\$*C : *exec.Cmd) receiver to be a starred typed metavar") + } + + @Test fun prefixDerefStillParses() { + // `*p` is a prefix deref (STAR precedes the operand), not a starred metavar. + val ast = parse("*p") + assertEquals(0, collect(ast).filterIsInstance().count { it.star }) + } + + @Test fun binaryMulStillParses() { + // `a*b` is multiplication; no starred metavars and still a valid parse. + val ast = parse("a*b") + assertTrue(ast !is SemgrepGoPattern.Raw) + assertEquals(0, collect(ast).filterIsInstance().count { it.star }) + } + @Test fun structuralSmokeTest() { // 5 representative patterns -> AST non-Raw val patterns = listOf( diff --git a/core/opentaint-go-querylang/src/test/kotlin/org/opentaint/semgrep/pattern/conversion/go/GoTaintRuleEmitterTest.kt b/core/opentaint-go-querylang/src/test/kotlin/org/opentaint/semgrep/pattern/conversion/go/GoTaintRuleEmitterTest.kt index 803d83984..e679adeeb 100644 --- a/core/opentaint-go-querylang/src/test/kotlin/org/opentaint/semgrep/pattern/conversion/go/GoTaintRuleEmitterTest.kt +++ b/core/opentaint-go-querylang/src/test/kotlin/org/opentaint/semgrep/pattern/conversion/go/GoTaintRuleEmitterTest.kt @@ -9,14 +9,18 @@ import org.opentaint.dataflow.configuration.go.serialized.GoSerializedPassAction import org.opentaint.dataflow.configuration.go.serialized.GoSerializedRule import org.opentaint.dataflow.configuration.jvm.serialized.PositionBase import org.opentaint.dataflow.configuration.jvm.serialized.PositionBaseWithModifiers +import org.opentaint.dataflow.configuration.jvm.serialized.PositionModifier +import org.opentaint.dataflow.go.rules.ActionPosition import org.opentaint.dataflow.go.GoFunctionSignature import org.opentaint.dataflow.go.rules.GoTaintConfiguration import org.opentaint.dataflow.go.rules.Position +import org.opentaint.dataflow.go.rules.RemoveMark import org.opentaint.ir.go.type.GoIRUnsafePointerType import org.opentaint.semgrep.go.pattern.conversion.loadGoTaintConfiguration import org.opentaint.semgrep.pattern.TaintRuleFromSemgrep import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFalse import kotlin.test.assertTrue class GoTaintRuleEmitterTest { @@ -46,7 +50,7 @@ class GoTaintRuleEmitterTest { val src = cfg.sourceForFunction("util.Source".signature(0), allRelevant = false).single() assertEquals("util.Source", src.function) assertEquals("taint", src.actionsAfter.single().mark) - assertEquals(Position.Result, src.actionsAfter.single().rawPosition()) + assertEquals(ActionPosition.Exact(Position.Result), src.actionsAfter.single().pos) } @Test @@ -113,7 +117,7 @@ class GoTaintRuleEmitterTest { pkg = GoNameMatcher.Simple("util"), function = GoNameMatcher.Pattern(".*"), condition = null, - taint = listOf(GoSerializedAssignAction.Direct("taint", baseOnly(PositionBase.Result))), + taint = listOf(GoSerializedAssignAction("taint", baseOnly(PositionBase.Result))), info = null ), ) @@ -143,6 +147,44 @@ class GoTaintRuleEmitterTest { assertEquals("util.Clean", cfg.cleanerForFunction("util.Clean".signature(1), allRelevant = false).single().function) } + @Test + fun `any-accessor cleaner lowers to RemoveMark with specialized position while direct stays exact`() { + val pos = baseOnly(PositionBase.Argument(0)) + val anyPos = PositionBaseWithModifiers.WithModifiers( + PositionBase.Argument(0), + listOf(PositionModifier.AnyField), + ) + + val anyRule = rule( + GoSerializedRule.Cleaner( + pkg = GoNameMatcher.Simple("util"), + function = GoNameMatcher.Simple("Clean"), + cleans = listOf(GoSerializedCleanAction("taint", anyPos)), + info = null, + ), + ) + val anyCfg = GoTaintConfiguration().loadGoTaintConfiguration(anyRule) + val anyAction = anyCfg.cleanerForFunction("util.Clean".signature(1), allRelevant = false) + .single().actionsAfter.filterIsInstance().single() + assertEquals("taint", anyAction.mark) + assertEquals(ActionPosition.AnyAccessorAfter(Position.Argument(0)), anyAction.pos) + + // Direct position must remain exact. + val directRule = rule( + GoSerializedRule.Cleaner( + pkg = GoNameMatcher.Simple("util"), + function = GoNameMatcher.Simple("Clean"), + cleans = listOf(GoSerializedCleanAction("taint", pos)), + info = null, + ), + ) + val directCfg = GoTaintConfiguration().loadGoTaintConfiguration(directRule) + val directAction = directCfg.cleanerForFunction("util.Clean".signature(1), allRelevant = false) + .single().actionsAfter.filterIsInstance().single() + assertEquals("taint", directAction.mark) + assertEquals(ActionPosition.Exact(Position.Argument(0)), directAction.pos) + } + private val anyType = GoIRUnsafePointerType fun String.signature(args: Int): GoFunctionSignature = diff --git a/core/opentaint-java-querylang/samples/src/main/java/taint/StarDeepSanitizer.java b/core/opentaint-java-querylang/samples/src/main/java/taint/StarDeepSanitizer.java new file mode 100644 index 000000000..43cf9a78b --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/java/taint/StarDeepSanitizer.java @@ -0,0 +1,50 @@ +package taint; + +import base.RuleSample; +import base.RuleSet; + +/** + * Starred SANITIZER, taint hidden 5 fields deep. `clean($*C)` must clear the taint on the + * whole object INCLUDING nested fields at every depth, so a subsequent depth-5 field read is + * clean. Default AnyAccessorDisabled (matches StarSanitizer). + */ +@RuleSet("taint/StarDeepSanitizer.yaml") +public abstract class StarDeepSanitizer implements RuleSample { + String src() { return "tainted"; } + L0 clean(L0 b) { return b; } // $*C sanitizer: clears object + all fields at all depths + void sink(String data) {} + + static final class L0 { public L1 f; } + static final class L1 { public L2 f; } + static final class L2 { public L3 f; } + static final class L3 { public L4 f; } + static final class L4 { public String v; } + + private static L0 build() { + L0 o = new L0(); + o.f = new L1(); + o.f.f = new L2(); + o.f.f.f = new L3(); + o.f.f.f.f = new L4(); + return o; + } + + // Positive: tainted depth-5 field reaches the sink with NO sanitizer between. + final static class PositiveTaintedDeep extends StarDeepSanitizer { + @Override public void entrypoint() { + L0 o = build(); + o.f.f.f.f.v = src(); + sink(o.f.f.f.f.v); + } + } + + // Negative: the $*C sanitizer must clean the depth-5 field taint on the returned object. + final static class NegativeSanitizedDeep extends StarDeepSanitizer { + @Override public void entrypoint() { + L0 o = build(); + o.f.f.f.f.v = src(); + L0 cleaned = clean(o); + sink(cleaned.f.f.f.f.v); // depth-5 field taint must be gone after $*C + } + } +} diff --git a/core/opentaint-java-querylang/samples/src/main/java/taint/StarDeepSink.java b/core/opentaint-java-querylang/samples/src/main/java/taint/StarDeepSink.java new file mode 100644 index 000000000..39fe8bb2d --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/java/taint/StarDeepSink.java @@ -0,0 +1,82 @@ +package taint; + +import base.RuleSample; +import base.RuleSet; + +/** + * Starred SINK, taint hidden at graduated field depths. A plain source taints a field N levels + * down; the starred whole-object sink `sink($*Y)` must observe the any-field taint. + * + * The deep cases were parked as KnownFn* while deep concrete field-store FACT PRODUCTION was + * broken (the IR lowers `o.f.v1 = src()` through a temp, so the fact was rooted at the temp + * and never at `o.f.v1` — see DeepFieldStoreFn). The upstream fix (49c8792b9, #304) makes the + * interprocedural precise READ work (DeepFieldStoreFn is green) and the DEPTH-5 starred-sink + * observation work (PositiveDepth5 live below). RESIDUAL GAP: the DEPTH-2 starred-sink + * observation still misses (stable repro, independent of the any-accessor unroll strategy) — + * KnownFnDepth2 stays parked, see its comment for the root cause. + */ +@RuleSet("taint/StarDeepSink.yaml") +public abstract class StarDeepSink implements RuleSample { + String src() { return "tainted"; } + void sink(L0 b) {} + + static final class L0 { public String v0; public L1 f; } + static final class L1 { public String v1; public L2 f; } + static final class L2 { public String v2; public L3 f; } + static final class L3 { public String v3; public L4 f; } + static final class L4 { public String v; } + + private static L0 build() { + L0 o = new L0(); + o.f = new L1(); + o.f.f = new L2(); + o.f.f.f = new L3(); + o.f.f.f.f = new L4(); + return o; + } + + // Positive (WORKS): taint at field depth 1 — the starred sink observes it. + final static class PositiveDepth1 extends StarDeepSink { + @Override public void entrypoint() { + L0 o = build(); + o.v0 = src(); // depth-1 field + sink(o); + } + } + + // KNOWN FALSE NEGATIVE: the depth-2 concrete field mark is not observed by the starred sink, + // while the DEEPER depth-5 case works and the same store on a LOCALLY ALLOCATED base works + // (DeepFieldStoreFn). Root cause: the base comes from the opaque `build()` call, so the store + // is lowered to `%tmp = o.f; %tmp.v1 = src()` with `%tmp` live across the opaque `src()` call; + // DSUAliasAnalysis.invalidateOuterHeapAliases must break the live `%tmp ~ o.f` link there, and + // since the DSU cannot hold a singleton set the whole pair is dropped, so the tainted store + // through the temp is never rebased onto `o.f.v1`. Depth >= 3 escapes only by accident: those + // temps are dead at the call and dead-local cleanup has already orphaned the chain. + // See DSUAliasAnalysisInvalidateOuterHeapAliasesTest.invalidateDropsLiveHeapAliasLosingPathRelation, + // which pins the same loss at the alias-analysis level. Unpark both together. + final static class KnownFnDepth2 extends StarDeepSink { + @Override public void entrypoint() { + L0 o = build(); + o.f.v1 = src(); + sink(o); + } + } + + // Positive: depth-5 concrete field mark observed by the starred sink. + final static class PositiveDepth5 extends StarDeepSink { + @Override public void entrypoint() { + L0 o = build(); + o.f.f.f.f.v = src(); + sink(o); + } + } + + // Negative: no field ever tainted. + final static class NegativeCleanObject extends StarDeepSink { + @Override public void entrypoint() { + L0 o = build(); + o.f.f.f.f.v = "safe"; + sink(o); + } + } +} diff --git a/core/opentaint-java-querylang/samples/src/main/java/taint/StarDeepSource.java b/core/opentaint-java-querylang/samples/src/main/java/taint/StarDeepSource.java new file mode 100644 index 000000000..018b2575a --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/java/taint/StarDeepSource.java @@ -0,0 +1,56 @@ +package taint; + +import base.RuleSample; +import base.RuleSet; + +/** + * Starred SOURCE, taint hidden 5 fields deep. `$*X = src()` taints the whole L0 object AND + * every nested field at every depth; a 5-level field read must still observe the taint once + * the any-accessor is unrolled to concrete field reads (AnyAccessorEnabled). + * + * Depth axis: field nesting L0.f.f.f.f.v (5 hops). Removing the source `*` makes every + * Positive a false negative, proving the star is load-bearing. + */ +@RuleSet("taint/StarDeepSource.yaml") +public abstract class StarDeepSource implements RuleSample { + L0 src() { return new L0(); } + void sink(String s) {} + + static final class L0 { public L1 f; } + static final class L1 { public L2 f; } + static final class L2 { public L3 f; } + static final class L3 { public L4 f; } + static final class L4 { public String v; } + + // Positive: starred source's any-field taint reaches a depth-5 field read. + final static class PositiveDeepFieldRead extends StarDeepSource { + @Override public void entrypoint() { + L0 o = src(); // $*X = src(): whole object + any-field taint + String v = o.f.f.f.f.v; // depth-5 read, any-accessor unrolled + sink(v); + } + } + + // Positive: read a shallower (depth-3) field — still tainted by the whole-object star. + final static class PositiveShallowFieldRead extends StarDeepSource { + @Override public void entrypoint() { + L0 o = src(); + L3 mid = o.f.f.f; // depth-3 read: a sub-object is still tainted + String v = mid.f.v; + sink(v); + } + } + + // Negative: object built locally, no source flows in, so no field is tainted. + final static class NegativeCleanDeep extends StarDeepSource { + @Override public void entrypoint() { + L0 o = new L0(); + o.f = new L1(); + o.f.f = new L2(); + o.f.f.f = new L3(); + o.f.f.f.f = new L4(); + o.f.f.f.f.v = "safe"; + sink(o.f.f.f.f.v); + } + } +} diff --git a/core/opentaint-java-querylang/samples/src/main/java/taint/StarInterproc.java b/core/opentaint-java-querylang/samples/src/main/java/taint/StarInterproc.java new file mode 100644 index 000000000..fa63b8f77 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/java/taint/StarInterproc.java @@ -0,0 +1,68 @@ +package taint; + +import base.RuleSample; +import base.RuleSet; + +/** + * Starred SOURCE threaded through a 5+ deep interprocedural call chain that ALTERNATELY hides + * taint inside an object and exposes it again. `$*X = src()` taints the whole Box + any-field; + * the chain unwraps to a scalar, re-wraps into a fresh Box field, unwraps again, and finally + * reaches a plain sink. Needs AnyAccessorEnabled so the source star reaches the first concrete + * field read. + */ +@RuleSet("taint/StarInterproc.yaml") +public abstract class StarInterproc implements RuleSample { + Box src() { return new Box(); } + void sink(String s) {} + + static final class Box { public String v; } + + // step1..step5: 5 interprocedural hops. Alternation: + // step1 pass object -> step2 EXPOSE field to scalar -> step3 HIDE scalar in new Box + // -> step4 pass object -> step5 EXPOSE field to scalar reaching the sink. + protected Box step1(Box b) { return b; } + protected String step2(Box b) { return b.v; } + protected Box step3(String s) { Box n = new Box(); n.v = s; return n; } + protected Box step4(Box b) { return b; } + protected String step5(Box b) { return b.v; } + + // Positive: taint survives 5 hops of hide/expose alternation from a starred source. + final static class PositiveAlternatingChain extends StarInterproc { + @Override public void entrypoint() { + Box b = src(); // $*X = src(): whole-object + any-field taint + Box b1 = step1(b); // hop 1: object passes through + String s2 = step2(b1); // hop 2: EXPOSE (any-field unrolls to b1.v) + Box b3 = step3(s2); // hop 3: HIDE the scalar back into a field + Box b4 = step4(b3); // hop 4: object passes through + String s5 = step5(b4); // hop 5: EXPOSE the field again + sink(s5); + } + } + + // Positive: simplest 5-hop pass-through, field exposed only at the end. + final static class PositivePassThroughChain extends StarInterproc { + @Override public void entrypoint() { + Box b = src(); + Box b1 = step1(b); + Box b2 = step1(b1); + Box b3 = step1(b2); + Box b4 = step4(b3); + String s = step5(b4); + sink(s); + } + } + + // Negative: fresh untainted Box threaded through the same chain. + final static class NegativeCleanChain extends StarInterproc { + @Override public void entrypoint() { + Box b = new Box(); + b.v = "safe"; + Box b1 = step1(b); + String s2 = step2(b1); + Box b3 = step3(s2); + Box b4 = step4(b3); + String s5 = step5(b4); + sink(s5); + } + } +} diff --git a/core/opentaint-java-querylang/samples/src/main/java/taint/StarMatrixPatternInside.java b/core/opentaint-java-querylang/samples/src/main/java/taint/StarMatrixPatternInside.java new file mode 100644 index 000000000..1e1562fa0 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/java/taint/StarMatrixPatternInside.java @@ -0,0 +1,76 @@ +package taint; + +import base.RuleSample; +import base.RuleSet; + +/** + * Starred sink gated by PATTERN-INSIDE, 5+ interprocedural depth x 5+ field depth combined. + * The sink `$R.consume($*Y)` only counts when the receiver comes from `openSink()` in the same + * method (pattern-inside). A starred source five calls deep taints a whole L0; the object + * travels five hops; the consume call sits five calls deep. The gated method uses openSink() + * (flagged); the ungated one obtains its receiver elsewhere (not a sink at all). + */ +@RuleSet("taint/StarMatrixPatternInside.yaml") +public abstract class StarMatrixPatternInside implements RuleSample { + L0 src() { return new L0(); } + Out openSink() { return new Out(); } + Out plainOut() { return new Out(); } + + static final class Out { void consume(L0 o) {} } + + static final class L0 { public L1 f; } + static final class L1 { public L2 f; } + static final class L2 { public L3 f; } + static final class L3 { public L4 f; } + static final class L4 { public String v; } + + // Source five calls deep. + protected L0 src5() { return src4(); } + protected L0 src4() { return src3(); } + protected L0 src3() { return src2(); } + protected L0 src2() { return src1(); } + protected L0 src1() { L0 o = src(); return o; } // $*X = src() matches HERE, depth 5 + + // Five object pass-hops. + protected L0 p1(L0 o) { return o; } + protected L0 p2(L0 o) { return o; } + protected L0 p3(L0 o) { return o; } + protected L0 p4(L0 o) { return o; } + protected L0 p5(L0 o) { return o; } + + // Sink chain five calls deep, ending in the pattern-inside-gated consume. + protected void k1(L0 o) { k2(o); } + protected void k2(L0 o) { k3(o); } + protected void k3(L0 o) { k4(o); } + protected void k4(L0 o) { k5(o); } + protected void k5(L0 o) { + Out r = openSink(); // pattern-inside context + r.consume(o); // starred sink matches HERE, depth 5 + } + + // Same-depth chain whose consume receiver does NOT come from openSink(). + protected void j1(L0 o) { j2(o); } + protected void j2(L0 o) { j3(o); } + protected void j3(L0 o) { j4(o); } + protected void j4(L0 o) { j5(o); } + protected void j5(L0 o) { + Out r = plainOut(); // no pattern-inside context + r.consume(o); + } + + // Positive: tainted object consumed inside the gated context. + final static class PositiveGatedConsume extends StarMatrixPatternInside { + @Override public void entrypoint() { + L0 o = src5(); + k1(p5(p4(p3(p2(p1(o)))))); + } + } + + // Negative: same tainted object, but the consume call lacks the pattern-inside context. + final static class NegativeUngatedConsume extends StarMatrixPatternInside { + @Override public void entrypoint() { + L0 o = src5(); + j1(p5(p4(p3(p2(p1(o)))))); + } + } +} diff --git a/core/opentaint-java-querylang/samples/src/main/java/taint/StarMatrixPatternNot.java b/core/opentaint-java-querylang/samples/src/main/java/taint/StarMatrixPatternNot.java new file mode 100644 index 000000000..a317d93d4 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/java/taint/StarMatrixPatternNot.java @@ -0,0 +1,67 @@ +package taint; + +import base.RuleSample; +import base.RuleSet; + +/** + * Starred PATTERN-NOT sink, 5+ interprocedural depth x 5+ field depth combined. The sink is + * `emit($*Y, $MODE)` with `pattern-not: emit($*Y, "safe")` — the starred metavar occurrence + * appears in BOTH the pattern and the pattern-not (the constraint solver keeps $Y and $*Y + * distinct, so the forms must agree). A starred source five calls deep taints a whole L0; the + * object travels five hops and is emitted five calls deep — flagged in "html" mode, excluded + * by the pattern-not in "safe" mode. + */ +@RuleSet("taint/StarMatrixPatternNot.yaml") +public abstract class StarMatrixPatternNot implements RuleSample { + L0 src() { return new L0(); } + void emit(L0 o, String mode) {} + + static final class L0 { public L1 f; } + static final class L1 { public L2 f; } + static final class L2 { public L3 f; } + static final class L3 { public L4 f; } + static final class L4 { public String v; } + + // Source five calls deep. + protected L0 src5() { return src4(); } + protected L0 src4() { return src3(); } + protected L0 src3() { return src2(); } + protected L0 src2() { return src1(); } + protected L0 src1() { L0 o = src(); return o; } // $*X = src() matches HERE, depth 5 + + // Five object pass-hops. + protected L0 p1(L0 o) { return o; } + protected L0 p2(L0 o) { return o; } + protected L0 p3(L0 o) { return o; } + protected L0 p4(L0 o) { return o; } + protected L0 p5(L0 o) { return o; } + + // Two sink chains five calls deep: one emits in a flagged mode, one in the excluded mode. + protected void k1(L0 o) { k2(o); } + protected void k2(L0 o) { k3(o); } + protected void k3(L0 o) { k4(o); } + protected void k4(L0 o) { k5(o); } + protected void k5(L0 o) { emit(o, "html"); } // matches the sink, depth 5 + + protected void j1(L0 o) { j2(o); } + protected void j2(L0 o) { j3(o); } + protected void j3(L0 o) { j4(o); } + protected void j4(L0 o) { j5(o); } + protected void j5(L0 o) { emit(o, "safe"); } // excluded by pattern-not, depth 5 + + // Positive: tainted object emitted in a non-excluded mode. + final static class PositiveEmitHtml extends StarMatrixPatternNot { + @Override public void entrypoint() { + L0 o = src5(); + k1(p5(p4(p3(p2(p1(o)))))); + } + } + + // Negative: same tainted object, but the emit call matches the pattern-not. + final static class NegativeEmitSafe extends StarMatrixPatternNot { + @Override public void entrypoint() { + L0 o = src5(); + j1(p5(p4(p3(p2(p1(o)))))); + } + } +} diff --git a/core/opentaint-java-querylang/samples/src/main/java/taint/StarMatrixPatternNotInside.java b/core/opentaint-java-querylang/samples/src/main/java/taint/StarMatrixPatternNotInside.java new file mode 100644 index 000000000..f94d9d295 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/java/taint/StarMatrixPatternNotInside.java @@ -0,0 +1,80 @@ +package taint; + +import base.RuleSample; +import base.RuleSet; + +/** + * Starred sink guarded by PATTERN-NOT-INSIDE, 5+ interprocedural depth x 5+ field depth + * combined. The sink `use($*Y)` sits in a `pattern-inside` context that INTRODUCES the guard + * receiver (`$G = checker(); ...`), and `pattern-not-inside: $G.check($*Y); ...` suppresses it + * — every not-inside metavar must be introduced and wired by the pattern-inside/sink patterns + * (the shipped setContentType suppression idiom; a not-inside with unbound metavars is dropped + * during automata-to-taint-rule conversion). A starred source five + * calls deep taints a whole L0; the object travels five hops; the use call sits five calls + * deep — flagged in the unguarded method, suppressed in the guarded one. + */ +@RuleSet("taint/StarMatrixPatternNotInside.yaml") +public abstract class StarMatrixPatternNotInside implements RuleSample { + L0 src() { return new L0(); } + void use(L0 o) {} + Checker checker() { return new Checker(); } + + static final class Checker { void check(L0 o) {} } + + static final class L0 { public L1 f; } + static final class L1 { public L2 f; } + static final class L2 { public L3 f; } + static final class L3 { public L4 f; } + static final class L4 { public String v; } + + // Source five calls deep. + protected L0 src5() { return src4(); } + protected L0 src4() { return src3(); } + protected L0 src3() { return src2(); } + protected L0 src2() { return src1(); } + protected L0 src1() { L0 o = src(); return o; } // $*X = src() matches HERE, depth 5 + + // Five object pass-hops. + protected L0 p1(L0 o) { return o; } + protected L0 p2(L0 o) { return o; } + protected L0 p3(L0 o) { return o; } + protected L0 p4(L0 o) { return o; } + protected L0 p5(L0 o) { return o; } + + // Unguarded sink chain five calls deep. + protected void k1(L0 o) { k2(o); } + protected void k2(L0 o) { k3(o); } + protected void k3(L0 o) { k4(o); } + protected void k4(L0 o) { k5(o); } + protected void k5(L0 o) { + Checker g = checker(); // pattern-inside context (binds $G), no check() -> flagged + use(o); // starred sink matches HERE, depth 5 + } + + // Guarded sink chain five calls deep: guard() precedes the use in the same method. + protected void j1(L0 o) { j2(o); } + protected void j2(L0 o) { j3(o); } + protected void j3(L0 o) { j4(o); } + protected void j4(L0 o) { j5(o); } + protected void j5(L0 o) { + Checker g = checker(); // pattern-inside context (binds $G) + g.check(o); // pattern-not-inside: $G.check($*Y) precedes -> suppressed + use(o); + } + + // Positive: tainted object used without the guard. + final static class PositiveUnguardedUse extends StarMatrixPatternNotInside { + @Override public void entrypoint() { + L0 o = src5(); + k1(p5(p4(p3(p2(p1(o)))))); + } + } + + // Negative: same tainted object, but the use is preceded by guard(). + final static class NegativeGuardedUse extends StarMatrixPatternNotInside { + @Override public void entrypoint() { + L0 o = src5(); + j1(p5(p4(p3(p2(p1(o)))))); + } + } +} diff --git a/core/opentaint-java-querylang/samples/src/main/java/taint/StarMatrixPropagator.java b/core/opentaint-java-querylang/samples/src/main/java/taint/StarMatrixPropagator.java new file mode 100644 index 000000000..7f6459938 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/java/taint/StarMatrixPropagator.java @@ -0,0 +1,83 @@ +package taint; + +import base.RuleSample; +import base.RuleSet; + +/** + * Starred PROPAGATOR — BOTH occurrences starred (`$*T = pass($*F)`) — at 5+ interprocedural + * depth x 5+ field depth. A starred source five calls deep taints a whole L0; the object + * travels five pass-hops to the propagator call, whose starred FROM observes the any-field + * taint of the whole argument and whose starred TO assigns whole-object taint to the fresh + * M0 result. The M0 is then unwrapped ONE field level per hop across five calls + * (M0->..->String) — only possible if the TO really carries any-field taint — and the scalar + * travels five calls down a sink chain to a plain sink. + */ +@RuleSet("taint/StarMatrixPropagator.yaml") +public abstract class StarMatrixPropagator implements RuleSample { + L0 src() { return new L0(); } + M0 pass(L0 o) { return new M0(); } + void sink(String s) {} + + static final class L0 { public L1 f; } + static final class L1 { public L2 f; } + static final class L2 { public L3 f; } + static final class L3 { public L4 f; } + static final class L4 { public String v; } + + static final class M0 { public M1 f; } + static final class M1 { public M2 f; } + static final class M2 { public M3 f; } + static final class M3 { public M4 f; } + static final class M4 { public String v; } + + // Source five calls deep. + protected L0 src5() { return src4(); } + protected L0 src4() { return src3(); } + protected L0 src3() { return src2(); } + protected L0 src2() { return src1(); } + protected L0 src1() { L0 o = src(); return o; } // $*X = src() matches HERE, depth 5 + + // Five object pass-hops before the propagator. + protected L0 p1(L0 o) { return o; } + protected L0 p2(L0 o) { return o; } + protected L0 p3(L0 o) { return o; } + protected L0 p4(L0 o) { return o; } + protected L0 p5(L0 o) { return o; } + + // Five hops, each unwrapping one field level of the PROPAGATED object: taint reaches the + // scalar only if the starred TO assigned any-field taint to the M0. + protected M1 u1(M0 o) { return o.f; } + protected M2 u2(M1 o) { return o.f; } + protected M3 u3(M2 o) { return o.f; } + protected M4 u4(M3 o) { return o.f; } + protected String u5(M4 o) { return o.v; } + + // Sink five calls deep. + protected void k1(String s) { k2(s); } + protected void k2(String s) { k3(s); } + protected void k3(String s) { k4(s); } + protected void k4(String s) { k5(s); } + protected void k5(String s) { sink(s); } // sink() called HERE, depth 5 + + // Positive: deep source -> 5 hops -> starred propagator -> per-hop unwrap -> deep sink. + final static class PositivePropagatedDeep extends StarMatrixPropagator { + @Override public void entrypoint() { + L0 o = src5(); + L0 o5 = p5(p4(p3(p2(p1(o))))); + M0 t = pass(o5); // $*T = pass($*F): whole object in, whole object out + String s = u5(u4(u3(u2(u1(t))))); + k1(s); + } + } + + // Negative: an untainted object through the identical propagator and chains. + final static class NegativeCleanPropagated extends StarMatrixPropagator { + @Override public void entrypoint() { + L0 o = new L0(); + L0 o5 = p5(p4(p3(p2(p1(o))))); + M0 t = pass(o5); + String s = u5(u4(u3(u2(u1(t))))); + k1(s); + } + } +} diff --git a/core/opentaint-java-querylang/samples/src/main/java/taint/StarMatrixSanitizer.java b/core/opentaint-java-querylang/samples/src/main/java/taint/StarMatrixSanitizer.java new file mode 100644 index 000000000..14359b9e9 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/java/taint/StarMatrixSanitizer.java @@ -0,0 +1,69 @@ +package taint; + +import base.RuleSample; +import base.RuleSet; + +/** + * Starred SANITIZER, 5+ interprocedural depth x 5+ field depth combined. A starred source five + * calls deep taints a whole L0. On the sanitized path the object goes through `sanitize()` — a + * HELPER whose body calls the starred-clean `clean()` (the wrapper shape behind the OWASP + * escapeHtml FPs, i.e. the deep-mark-exclusion fix's sample-level regression test). Afterwards + * five hops unwrap one field level each and the scalar travels five calls down to the sink; + * the whole-object clean must have removed the any-field taint at every depth. + */ +@RuleSet("taint/StarMatrixSanitizer.yaml") +public abstract class StarMatrixSanitizer implements RuleSample { + L0 src() { return new L0(); } + L0 clean(L0 o) { return o; } + void sink(String s) {} + + static final class L0 { public L1 f; } + static final class L1 { public L2 f; } + static final class L2 { public L3 f; } + static final class L3 { public L4 f; } + static final class L4 { public String v; } + + // Source five calls deep. + protected L0 src5() { return src4(); } + protected L0 src4() { return src3(); } + protected L0 src3() { return src2(); } + protected L0 src2() { return src1(); } + protected L0 src1() { L0 o = src(); return o; } // $*X = src() matches HERE, depth 5 + + // The starred clean sits INSIDE a wrapper: its whole-object effect must survive the + // wrapper's interprocedural summary (deep mark exclusions). + protected L0 sanitize(L0 o) { return clean(o); } + + // Five hops, each unwrapping exactly one field level. + protected L1 u1(L0 o) { return o.f; } + protected L2 u2(L1 o) { return o.f; } + protected L3 u3(L2 o) { return o.f; } + protected L4 u4(L3 o) { return o.f; } + protected String u5(L4 o) { return o.v; } + + // Sink five calls deep. + protected void k1(String s) { k2(s); } + protected void k2(String s) { k3(s); } + protected void k3(String s) { k4(s); } + protected void k4(String s) { k5(s); } + protected void k5(String s) { sink(s); } // sink() called HERE, depth 5 + + // Positive: the unsanitized path flags. + final static class PositiveUnsanitizedDeep extends StarMatrixSanitizer { + @Override public void entrypoint() { + L0 o = src5(); + String s = u5(u4(u3(u2(u1(o))))); + k1(s); + } + } + + // Negative: the wrapped whole-object clean clears the taint at every field depth. + final static class NegativeSanitizedDeep extends StarMatrixSanitizer { + @Override public void entrypoint() { + L0 o = src5(); + L0 c = sanitize(o); + String s = u5(u4(u3(u2(u1(c))))); + k1(s); + } + } +} diff --git a/core/opentaint-java-querylang/samples/src/main/java/taint/StarMatrixSink.java b/core/opentaint-java-querylang/samples/src/main/java/taint/StarMatrixSink.java new file mode 100644 index 000000000..93a0033ac --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/java/taint/StarMatrixSink.java @@ -0,0 +1,63 @@ +package taint; + +import base.RuleSample; +import base.RuleSet; + +/** + * Starred SINK, 5+ interprocedural depth x 5+ field depth combined. A starred source taints the + * INNERMOST object (L4) five calls deep; five hops then each WRAP it one level deeper + * (L4->L3->..->L0), and the outermost object travels five calls down a sink chain to + * `sink($*Y)` — the starred sink must observe the whole-object taint buried five field levels + * down the wrapped object. + */ +@RuleSet("taint/StarMatrixSink.yaml") +public abstract class StarMatrixSink implements RuleSample { + L4 src() { return new L4(); } + void sink(L0 o) {} + + static final class L0 { public L1 f; } + static final class L1 { public L2 f; } + static final class L2 { public L3 f; } + static final class L3 { public L4 f; } + static final class L4 { public String v; } + + // Source five calls deep: the starred source statement is inside src1. + protected L4 src5() { return src4(); } + protected L4 src4() { return src3(); } + protected L4 src3() { return src2(); } + protected L4 src2() { return src1(); } + protected L4 src1() { L4 o = src(); return o; } // $*X = src() matches HERE, depth 5 + + // Five hops, each WRAPPING one field level (hide direction). + protected L3 w1(L4 o) { L3 n = new L3(); n.f = o; return n; } + protected L2 w2(L3 o) { L2 n = new L2(); n.f = o; return n; } + protected L1 w3(L2 o) { L1 n = new L1(); n.f = o; return n; } + protected L0 w4(L1 o) { L0 n = new L0(); n.f = o; return n; } + protected L0 w5(L0 o) { return o; } + + // Sink five calls deep. + protected void k1(L0 o) { k2(o); } + protected void k2(L0 o) { k3(o); } + protected void k3(L0 o) { k4(o); } + protected void k4(L0 o) { k5(o); } + protected void k5(L0 o) { sink(o); } // sink($*Y) matches HERE, depth 5 + + // Positive: the tainted L4 is wrapped five levels deep; the starred sink observes it. + final static class PositiveWrappedDeep extends StarMatrixSink { + @Override public void entrypoint() { + L4 t = src5(); + L0 o = w5(w4(w3(w2(w1(t))))); + k1(o); + } + } + + // Negative: an untainted L4 wrapped and threaded through the identical chains. + final static class NegativeCleanWrapped extends StarMatrixSink { + @Override public void entrypoint() { + L4 t = new L4(); + t.v = "safe"; + L0 o = w5(w4(w3(w2(w1(t))))); + k1(o); + } + } +} diff --git a/core/opentaint-java-querylang/samples/src/main/java/taint/StarMatrixSource.java b/core/opentaint-java-querylang/samples/src/main/java/taint/StarMatrixSource.java new file mode 100644 index 000000000..cdc6d89bf --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/java/taint/StarMatrixSource.java @@ -0,0 +1,67 @@ +package taint; + +import base.RuleSample; +import base.RuleSet; + +/** + * Starred SOURCE, 5+ interprocedural depth x 5+ field depth combined. The source statement + * `$*X = src()` sits FIVE calls deep (src1..src5); the tainted whole object then climbs back + * up and is unwrapped ONE field level per hop across five more calls (u1..u5, L0->..->String), + * and the scalar finally travels five calls down a sink chain (k1..k5) to a plain sink. + * The 5-level field taint is carried by the $* source's abstract any-field mark. + */ +@RuleSet("taint/StarMatrixSource.yaml") +public abstract class StarMatrixSource implements RuleSample { + L0 src() { return new L0(); } + void sink(String s) {} + + static final class L0 { public L1 f; } + static final class L1 { public L2 f; } + static final class L2 { public L3 f; } + static final class L3 { public L4 f; } + static final class L4 { public String v; } + + // Source five calls deep: the starred source statement is inside src1. + protected L0 src5() { return src4(); } + protected L0 src4() { return src3(); } + protected L0 src3() { return src2(); } + protected L0 src2() { return src1(); } + protected L0 src1() { L0 o = src(); return o; } // $*X = src() matches HERE, depth 5 + + // Five hops, each unwrapping exactly one field level: interproc depth x field depth. + protected L1 u1(L0 o) { return o.f; } + protected L2 u2(L1 o) { return o.f; } + protected L3 u3(L2 o) { return o.f; } + protected L4 u4(L3 o) { return o.f; } + protected String u5(L4 o) { return o.v; } + + // Sink five calls deep. + protected void k1(String s) { k2(s); } + protected void k2(String s) { k3(s); } + protected void k3(String s) { k4(s); } + protected void k4(String s) { k5(s); } + protected void k5(String s) { sink(s); } // sink() called HERE, depth 5 + + // Positive: deep source -> 5x1-field unwrap hops -> deep sink. + final static class PositiveDeepChain extends StarMatrixSource { + @Override public void entrypoint() { + L0 o = src5(); + String s = u5(u4(u3(u2(u1(o))))); + k1(s); + } + } + + // Negative: an untainted object through the identical chains. + final static class NegativeCleanChain extends StarMatrixSource { + @Override public void entrypoint() { + L0 o = new L0(); + o.f = new L1(); + o.f.f = new L2(); + o.f.f.f = new L3(); + o.f.f.f.f = new L4(); + o.f.f.f.f.v = "safe"; + String s = u5(u4(u3(u2(u1(o))))); + k1(s); + } + } +} diff --git a/core/opentaint-java-querylang/samples/src/main/java/taint/StarMixedExclusionSanitizer.java b/core/opentaint-java-querylang/samples/src/main/java/taint/StarMixedExclusionSanitizer.java new file mode 100644 index 000000000..0fbc22d54 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/java/taint/StarMixedExclusionSanitizer.java @@ -0,0 +1,108 @@ +package taint; + +import base.RuleSample; +import base.RuleSet; + +/** + * Mixed exclusion kinds on ONE flow: a starred whole-object clean (deep exclusions on the + * wrapper summary's initial fact) combined with a plain value clean (depth-1 exclusion via an + * ordinary sanitizer inside another summarized helper). The same caller initial fact is + * refined by BOTH summary applications; with lossy replace semantics the later plain + * refinement dropped the accumulated deep entry and could resurrect the cleaned whole-object + * mark (the always-propagate-deep-marks regression). + */ +@RuleSet("taint/StarMixedExclusionSanitizer.yaml") +public abstract class StarMixedExclusionSanitizer implements RuleSample { + L0 src() { return new L0(); } + L0 cleanAll(L0 o) { return o; } + String cleanValue(String s) { return s; } + void sink(String s) {} + + static final class L0 { public L1 f; } + static final class L1 { public String v; } + + protected L0 srcWrapped() { L0 o = src(); return o; } + + // Starred clean behind a wrapper summary: the summary's initial fact acquires the DEEP + // exclusion. + protected L0 sanitizeAll(L0 o) { return cleanAll(o); } + + // Plain clean behind a wrapper summary: the refinement carries a PLAIN (depth-1) part. + protected String sanitizeValue(String s) { return cleanValue(s); } + + protected String unwrap(L0 o) { return o.f.v; } + + // Starred clean + constant store into the cleaned region inside ONE summarized helper: + // the deep exclusion and the safe store must compose — the store must not resurrect the + // cleaned whole-object mark on the returned object. Two store depths: the String leaf + // and the field itself (killing the whole subtree under f). + protected L0 sanitizeAllAndAssign(L0 o) { + cleanAll(o); + o.f.v = "safe"; + return o; + } + + protected L0 sanitizeAllAndAssignField(L0 o) { + cleanAll(o); + o.f = new L1(); + return o; + } + + // Positive: no sanitizer on the path. + final static class PositiveUnsanitized extends StarMixedExclusionSanitizer { + @Override public void entrypoint() { + L0 o = srcWrapped(); + sink(unwrap(o)); + } + } + + // Negative: starred clean, then the SAME flow continues through the plain-sanitizer + // summary as well — the later mixed refinement must keep the deep entry. + final static class NegativeStarThenPlain extends StarMixedExclusionSanitizer { + @Override public void entrypoint() { + L0 o = srcWrapped(); + L0 c = sanitizeAll(o); + String s = unwrap(c); + String t = sanitizeValue(s); + sink(t); + } + } + + // Negative: starred clean alone through the wrapper — deep exclusion baseline. + final static class NegativeStarOnly extends StarMixedExclusionSanitizer { + @Override public void entrypoint() { + L0 o = srcWrapped(); + L0 c = sanitizeAll(o); + sink(unwrap(c)); + } + } + + // Negative: starred clean followed by a constant store into the cleaned region, both + // behind one helper summary. + final static class NegativeStarThenAssign extends StarMixedExclusionSanitizer { + @Override public void entrypoint() { + L0 o = srcWrapped(); + L0 c = sanitizeAllAndAssign(o); + sink(unwrap(c)); + } + } + + // Negative: starred clean followed by a field-level overwrite (fresh subtree), both + // behind one helper summary. + final static class NegativeStarThenAssignField extends StarMixedExclusionSanitizer { + @Override public void entrypoint() { + L0 o = srcWrapped(); + L0 c = sanitizeAllAndAssignField(o); + sink(unwrap(c)); + } + } + + // Negative: plain clean alone — depth-1 exclusion baseline. + final static class NegativePlainOnly extends StarMixedExclusionSanitizer { + @Override public void entrypoint() { + L0 o = srcWrapped(); + String s = unwrap(o); + sink(sanitizeValue(s)); + } + } +} diff --git a/core/opentaint-java-querylang/samples/src/main/java/taint/StarNestedWrapperSanitizer.java b/core/opentaint-java-querylang/samples/src/main/java/taint/StarNestedWrapperSanitizer.java new file mode 100644 index 000000000..c3d89cffe --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/java/taint/StarNestedWrapperSanitizer.java @@ -0,0 +1,108 @@ +package taint; + +import base.RuleSample; +import base.RuleSet; + +/** + * Summary COMPOSITION regressions for whole-object sanitizer cleans (deep mark exclusions): + * every way a starred clean can hide behind summary levels must stay effective. + * + * - NegativeSanitizedTwoWrappers: the clean under TWO nested wrapper summaries + * (sanitize2 -> sanitize1 -> clean), unwrap flow in the caller — the deep exclusion on each + * summary's initial fact prunes the caller's whole-object mark at delta application. + * - NegativeSanitizedInHelper / NegativeSanitizedNested: the clean AND the sinkward unwrap + * flow inside a summarized helper — the exclusion must survive being carried through the + * helper's own summary. This requires monotone (union, not replace) initial-fact exclusion + * refinement and the deep-entry carry on the delta-application path + * (MethodCallSummaryHandler); with lossy replace semantics a later application through an + * exclusion-free passthrough edge downgraded the refined initial and resurrected the + * cleaned mark — the historic false positive here. + */ +@RuleSet("taint/StarNestedWrapperSanitizer.yaml") +public abstract class StarNestedWrapperSanitizer implements RuleSample { + L0 src() { return new L0(); } + L0 clean(L0 o) { return o; } + void sink(String s) {} + + static final class L0 { public L1 f; } + static final class L1 { public L2 f; } + static final class L2 { public L3 f; } + static final class L3 { public L4 f; } + static final class L4 { public String v; } + + // Source five calls deep. + protected L0 src5() { return src4(); } + protected L0 src4() { return src3(); } + protected L0 src3() { return src2(); } + protected L0 src2() { return src1(); } + protected L0 src1() { L0 o = src(); return o; } // $*X = src() matches HERE, depth 5 + + // The starred clean under TWO wrapper summaries. + protected L0 sanitize1(L0 o) { return clean(o); } + protected L0 sanitize2(L0 o) { return sanitize1(o); } + + // Five hops, each unwrapping exactly one field level. + protected L1 u1(L0 o) { return o.f; } + protected L2 u2(L1 o) { return o.f; } + protected L3 u3(L2 o) { return o.f; } + protected L4 u4(L3 o) { return o.f; } + protected String u5(L4 o) { return o.v; } + + // The whole sanitized flow inside one more summarized helper. + protected String helper(L0 o) { + L0 c = sanitize2(o); + return u5(u4(u3(u2(u1(c))))); + } + + // Sink five calls deep. + protected void k1(String s) { k2(s); } + protected void k2(String s) { k3(s); } + protected void k3(String s) { k4(s); } + protected void k4(String s) { k5(s); } + protected void k5(String s) { sink(s); } // sink() called HERE, depth 5 + + // Positive: the unsanitized path flags. + final static class PositiveUnsanitizedNested extends StarNestedWrapperSanitizer { + @Override public void entrypoint() { + L0 o = src5(); + String s = u5(u4(u3(u2(u1(o))))); + k1(s); + } + } + + // Negative: clean + unwrap flow inside a summarized helper, two wrapper levels above the + // clean. + final static class NegativeSanitizedNested extends StarNestedWrapperSanitizer { + @Override public void entrypoint() { + L0 o = src5(); + String s = helper(o); + k1(s); + } + } + + // Negative: two wrapper levels, flow at the entrypoint — the deep exclusion composes + // across nested wrapper summaries. + final static class NegativeSanitizedTwoWrappers extends StarNestedWrapperSanitizer { + @Override public void entrypoint() { + L0 o = src5(); + L0 c = sanitize2(o); + String s = u5(u4(u3(u2(u1(c))))); + k1(s); + } + } + + // Negative: ONE wrapper level (like StarMatrixSanitizer), with the sanitized flow itself + // inside a summarized helper. + protected String helperOneWrapper(L0 o) { + L0 c = sanitize1(o); + return u5(u4(u3(u2(u1(c))))); + } + + final static class NegativeSanitizedInHelper extends StarNestedWrapperSanitizer { + @Override public void entrypoint() { + L0 o = src5(); + String s = helperOneWrapper(o); + k1(s); + } + } +} diff --git a/core/opentaint-java-querylang/samples/src/main/java/taint/StarSanitizer.java b/core/opentaint-java-querylang/samples/src/main/java/taint/StarSanitizer.java new file mode 100644 index 000000000..aa645e810 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/java/taint/StarSanitizer.java @@ -0,0 +1,31 @@ +package taint; + +import base.RuleSample; +import base.RuleSet; + +@RuleSet("taint/StarSanitizer.yaml") +public abstract class StarSanitizer implements RuleSample { + String src() { return "tainted"; } + static final class Box { String value; String getValue() { return value; } } + Box clean(Box b) { return b; } // $*C sanitizer: cleans the object + all its fields + void sink(String data) {} + + // Positive: tainted field reaches sink with NO sanitizer between + final static class PositiveTaintedField extends StarSanitizer { + @Override public void entrypoint() { + Box b = new Box(); + b.value = src(); + sink(b.getValue()); + } + } + + // Negative: the $*C sanitizer must clean the field taint on the value flowing onward + final static class NegativeSanitizedField extends StarSanitizer { + @Override public void entrypoint() { + Box b = new Box(); + b.value = src(); + Box cleaned = clean(b); + sink(cleaned.getValue()); // field taint must be gone after $*C sanitizer + } + } +} diff --git a/core/opentaint-java-querylang/samples/src/main/java/taint/StarSink.java b/core/opentaint-java-querylang/samples/src/main/java/taint/StarSink.java new file mode 100644 index 000000000..5261f0291 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/java/taint/StarSink.java @@ -0,0 +1,28 @@ +package taint; + +import base.RuleSample; +import base.RuleSet; + +@RuleSet("taint/StarSink.yaml") +public abstract class StarSink implements RuleSample { + String src() { return "tainted"; } + static final class Box { String value; } + void sink(Box b) {} + + final static class PositiveTaintedField extends StarSink { + @Override public void entrypoint() { + String data = src(); + Box b = new Box(); + b.value = data; // taints a field + sink(b); // $*Y sink fires on tainted field + } + } + + final static class NegativeCleanObject extends StarSink { + @Override public void entrypoint() { + Box b = new Box(); + b.value = "safe"; + sink(b); + } + } +} diff --git a/core/opentaint-java-querylang/samples/src/main/java/taint/StarSource.java b/core/opentaint-java-querylang/samples/src/main/java/taint/StarSource.java new file mode 100644 index 000000000..177a10b3e --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/java/taint/StarSource.java @@ -0,0 +1,38 @@ +package taint; + +import base.RuleSample; +import base.RuleSet; + +@RuleSet("taint/StarSource.yaml") +public abstract class StarSource implements RuleSample { + Box src() { return new Box(); } + void sink(String s) {} + + static final class Box { + private String value; + String getValue() { return value; } + void setValue(String value) { this.value = value; } + } + + // Positive: the STARRED source ($*X = src()) taints the whole Box AND every field. + // The concrete field read b.getValue() therefore inherits the taint (the source-star's + // any-field taint is unrolled to the field read) and reaches the plain sink. + final static class PositiveStarredSourceField extends StarSource { + @Override public void entrypoint() { + Box b = src(); // $*X = src(): whole-object + any-field taint + String v = b.getValue(); // any-accessor taint unrolls to the concrete field + sink(v); // plain sink observes the tainted field + } + } + + // Negative: the Box is built locally (not from the starred source), so no field is + // tainted and the extracted value stays clean. + final static class NegativeCleanField extends StarSource { + @Override public void entrypoint() { + Box b = new Box(); + b.setValue("safe"); + String v = b.getValue(); + sink(v); + } + } +} diff --git a/core/opentaint-java-querylang/samples/src/main/java/taint/StarSourceAndSanitizer.java b/core/opentaint-java-querylang/samples/src/main/java/taint/StarSourceAndSanitizer.java new file mode 100644 index 000000000..06bd36e25 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/java/taint/StarSourceAndSanitizer.java @@ -0,0 +1,40 @@ +package taint; + +import base.RuleSample; +import base.RuleSet; + +/** + * Starred SOURCE + starred SANITIZER: `$*X = src()` taints the whole object and every nested + * field; `clean($*C)` must clear the whole object including nested fields. A depth-4 field read + * follows. Uses AnyAccessorEnabled so the source star reaches the concrete field read. + */ +@RuleSet("taint/StarSourceAndSanitizer.yaml") +public abstract class StarSourceAndSanitizer implements RuleSample { + L0 src() { return new L0(); } + L0 clean(L0 b) { return b; } + void sink(String data) {} + + static final class L0 { public L1 f; } + static final class L1 { public L2 f; } + static final class L2 { public L3 f; } + static final class L3 { public String v; } + + // Positive: starred-source field taint reaches the sink with NO sanitizer between. + final static class PositiveDeepUnsanitized extends StarSourceAndSanitizer { + @Override public void entrypoint() { + L0 o = src(); // $*X whole-object taint + String v = o.f.f.f.v; // depth-4 read + sink(v); + } + } + + // Negative: the starred sanitizer clears the whole-object taint before the field read. + final static class NegativeDeepSanitized extends StarSourceAndSanitizer { + @Override public void entrypoint() { + L0 o = src(); + L0 cleaned = clean(o); // $*C clears object + all nested fields + String v = cleaned.f.f.f.v; + sink(v); + } + } +} diff --git a/core/opentaint-java-querylang/samples/src/main/java/taint/StarSourceAndSink.java b/core/opentaint-java-querylang/samples/src/main/java/taint/StarSourceAndSink.java new file mode 100644 index 000000000..3590d49d9 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/java/taint/StarSourceAndSink.java @@ -0,0 +1,40 @@ +package taint; + +import base.RuleSample; +import base.RuleSet; + +/** + * BOTH ends starred: `$*X = src()` (whole-object source) and `sink($*Y)` (whole-object sink), + * with a nested object extracted in between. The source star taints every field of the outer + * object; a nested sub-object is pulled out and handed to the starred sink, which must observe + * it as tainted. Uses AnyAccessorEnabled so the source star reaches the extracted sub-object. + */ +@RuleSet("taint/StarSourceAndSink.yaml") +public abstract class StarSourceAndSink implements RuleSample { + Outer src() { return new Outer(); } + void sink(Inner i) {} + + static final class Outer { public Mid f; } + static final class Mid { public Inner f; } + static final class Inner { public String v; } + + // Positive: whole-object source taint reaches a nested sub-object handed to the starred sink. + final static class PositiveNestedObjectToStarSink extends StarSourceAndSink { + @Override public void entrypoint() { + Outer o = src(); // $*X: whole object + any-field taint + Inner inner = o.f.f; // extract a depth-2 nested object + sink(inner); // $*Y: starred sink observes the tainted sub-object + } + } + + // Negative: locally-built object, nothing tainted. + final static class NegativeCleanNested extends StarSourceAndSink { + @Override public void entrypoint() { + Outer o = new Outer(); + o.f = new Mid(); + o.f.f = new Inner(); + o.f.f.v = "safe"; + sink(o.f.f); + } + } +} diff --git a/core/opentaint-java-querylang/samples/src/main/resources/example/ArrayExample.yaml b/core/opentaint-java-querylang/samples/src/main/resources/example/ArrayExample.yaml index ca100e50a..a9eb34e87 100644 --- a/core/opentaint-java-querylang/samples/src/main/resources/example/ArrayExample.yaml +++ b/core/opentaint-java-querylang/samples/src/main/resources/example/ArrayExample.yaml @@ -21,8 +21,8 @@ rules: - pattern: |- @EntryPoint - $RETURNTYPE $METHOD(String[] $PARAM) { - otherElementSink($PARAM); + $RETURNTYPE $METHOD(String[] $*PARAM) { + otherElementSink($*PARAM); } - pattern: |- @@ -35,5 +35,5 @@ rules: # elementSink($PARAM[...]); - pattern: |- - $PARAM = src(); - otherElementSink($PARAM); + $*PARAM = src(); + otherElementSink($*PARAM); diff --git a/core/opentaint-java-querylang/samples/src/main/resources/taint/StarDeepSanitizer.yaml b/core/opentaint-java-querylang/samples/src/main/resources/taint/StarDeepSanitizer.yaml new file mode 100644 index 000000000..2eebd38e8 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/resources/taint/StarDeepSanitizer.yaml @@ -0,0 +1,19 @@ +rules: + - id: taint-StarDeepSanitizer + languages: + - java + severity: ERROR + message: match taint/StarDeepSanitizer + mode: taint + pattern-sources: + - patterns: + - pattern: $X = src(); + - focus-metavariable: $X + pattern-sanitizers: + - patterns: + - pattern: clean($*C); + - focus-metavariable: $C + pattern-sinks: + - patterns: + - pattern: sink($Y); + - focus-metavariable: $Y diff --git a/core/opentaint-java-querylang/samples/src/main/resources/taint/StarDeepSink.yaml b/core/opentaint-java-querylang/samples/src/main/resources/taint/StarDeepSink.yaml new file mode 100644 index 000000000..e5e127795 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/resources/taint/StarDeepSink.yaml @@ -0,0 +1,15 @@ +rules: + - id: taint-StarDeepSink + languages: + - java + severity: ERROR + message: match taint/StarDeepSink + mode: taint + pattern-sources: + - patterns: + - pattern: $X = src(); + - focus-metavariable: $X + pattern-sinks: + - patterns: + - pattern: sink($*Y); + - focus-metavariable: $Y diff --git a/core/opentaint-java-querylang/samples/src/main/resources/taint/StarDeepSource.yaml b/core/opentaint-java-querylang/samples/src/main/resources/taint/StarDeepSource.yaml new file mode 100644 index 000000000..916c66ff7 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/resources/taint/StarDeepSource.yaml @@ -0,0 +1,15 @@ +rules: + - id: taint-StarDeepSource + languages: + - java + severity: ERROR + message: match taint/StarDeepSource + mode: taint + pattern-sources: + - patterns: + - pattern: $*X = src(); + - focus-metavariable: $X + pattern-sinks: + - patterns: + - pattern: sink($Y); + - focus-metavariable: $Y diff --git a/core/opentaint-java-querylang/samples/src/main/resources/taint/StarInterproc.yaml b/core/opentaint-java-querylang/samples/src/main/resources/taint/StarInterproc.yaml new file mode 100644 index 000000000..5b449e7e1 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/resources/taint/StarInterproc.yaml @@ -0,0 +1,15 @@ +rules: + - id: taint-StarInterproc + languages: + - java + severity: ERROR + message: match taint/StarInterproc + mode: taint + pattern-sources: + - patterns: + - pattern: $*X = src(); + - focus-metavariable: $X + pattern-sinks: + - patterns: + - pattern: sink($Y); + - focus-metavariable: $Y diff --git a/core/opentaint-java-querylang/samples/src/main/resources/taint/StarMatrixPatternInside.yaml b/core/opentaint-java-querylang/samples/src/main/resources/taint/StarMatrixPatternInside.yaml new file mode 100644 index 000000000..0459d0a99 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/resources/taint/StarMatrixPatternInside.yaml @@ -0,0 +1,18 @@ +rules: + - id: taint-StarMatrixPatternInside + languages: + - java + severity: ERROR + message: match taint/StarMatrixPatternInside + mode: taint + pattern-sources: + - patterns: + - pattern: $*X = src(); + - focus-metavariable: $X + pattern-sinks: + - patterns: + - pattern-inside: | + $R = openSink(); + ... + - pattern: $R.consume($*Y); + - focus-metavariable: $Y diff --git a/core/opentaint-java-querylang/samples/src/main/resources/taint/StarMatrixPatternNot.yaml b/core/opentaint-java-querylang/samples/src/main/resources/taint/StarMatrixPatternNot.yaml new file mode 100644 index 000000000..fb54256dc --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/resources/taint/StarMatrixPatternNot.yaml @@ -0,0 +1,16 @@ +rules: + - id: taint-StarMatrixPatternNot + languages: + - java + severity: ERROR + message: match taint/StarMatrixPatternNot + mode: taint + pattern-sources: + - patterns: + - pattern: $*X = src(); + - focus-metavariable: $X + pattern-sinks: + - patterns: + - pattern: emit($*Y, $MODE); + - pattern-not: emit($*Y, "safe"); + - focus-metavariable: $Y diff --git a/core/opentaint-java-querylang/samples/src/main/resources/taint/StarMatrixPatternNotInside.yaml b/core/opentaint-java-querylang/samples/src/main/resources/taint/StarMatrixPatternNotInside.yaml new file mode 100644 index 000000000..7493244e5 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/resources/taint/StarMatrixPatternNotInside.yaml @@ -0,0 +1,21 @@ +rules: + - id: taint-StarMatrixPatternNotInside + languages: + - java + severity: ERROR + message: match taint/StarMatrixPatternNotInside + mode: taint + pattern-sources: + - patterns: + - pattern: $*X = src(); + - focus-metavariable: $X + pattern-sinks: + - patterns: + - pattern-inside: | + $G = checker(); + ... + - pattern: use($*Y); + - pattern-not-inside: | + $G.check($*Y); + ... + - focus-metavariable: $Y diff --git a/core/opentaint-java-querylang/samples/src/main/resources/taint/StarMatrixPropagator.yaml b/core/opentaint-java-querylang/samples/src/main/resources/taint/StarMatrixPropagator.yaml new file mode 100644 index 000000000..fc138f507 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/resources/taint/StarMatrixPropagator.yaml @@ -0,0 +1,20 @@ +rules: + - id: taint-StarMatrixPropagator + languages: + - java + severity: ERROR + message: match taint/StarMatrixPropagator + mode: taint + pattern-sources: + - patterns: + - pattern: $*X = src(); + - focus-metavariable: $X + pattern-propagators: + - patterns: + - pattern: $*T = pass($*F); + from: $F + to: $T + pattern-sinks: + - patterns: + - pattern: sink($Y); + - focus-metavariable: $Y diff --git a/core/opentaint-java-querylang/samples/src/main/resources/taint/StarMatrixSanitizer.yaml b/core/opentaint-java-querylang/samples/src/main/resources/taint/StarMatrixSanitizer.yaml new file mode 100644 index 000000000..ddf493144 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/resources/taint/StarMatrixSanitizer.yaml @@ -0,0 +1,19 @@ +rules: + - id: taint-StarMatrixSanitizer + languages: + - java + severity: ERROR + message: match taint/StarMatrixSanitizer + mode: taint + pattern-sources: + - patterns: + - pattern: $*X = src(); + - focus-metavariable: $X + pattern-sanitizers: + - patterns: + - pattern: clean($*C); + - focus-metavariable: $C + pattern-sinks: + - patterns: + - pattern: sink($Y); + - focus-metavariable: $Y diff --git a/core/opentaint-java-querylang/samples/src/main/resources/taint/StarMatrixSink.yaml b/core/opentaint-java-querylang/samples/src/main/resources/taint/StarMatrixSink.yaml new file mode 100644 index 000000000..42811f6c5 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/resources/taint/StarMatrixSink.yaml @@ -0,0 +1,15 @@ +rules: + - id: taint-StarMatrixSink + languages: + - java + severity: ERROR + message: match taint/StarMatrixSink + mode: taint + pattern-sources: + - patterns: + - pattern: $*X = src(); + - focus-metavariable: $X + pattern-sinks: + - patterns: + - pattern: sink($*Y); + - focus-metavariable: $Y diff --git a/core/opentaint-java-querylang/samples/src/main/resources/taint/StarMatrixSource.yaml b/core/opentaint-java-querylang/samples/src/main/resources/taint/StarMatrixSource.yaml new file mode 100644 index 000000000..0a1cabc16 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/resources/taint/StarMatrixSource.yaml @@ -0,0 +1,15 @@ +rules: + - id: taint-StarMatrixSource + languages: + - java + severity: ERROR + message: match taint/StarMatrixSource + mode: taint + pattern-sources: + - patterns: + - pattern: $*X = src(); + - focus-metavariable: $X + pattern-sinks: + - patterns: + - pattern: sink($Y); + - focus-metavariable: $Y diff --git a/core/opentaint-java-querylang/samples/src/main/resources/taint/StarMixedExclusionSanitizer.yaml b/core/opentaint-java-querylang/samples/src/main/resources/taint/StarMixedExclusionSanitizer.yaml new file mode 100644 index 000000000..01b9f68a5 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/resources/taint/StarMixedExclusionSanitizer.yaml @@ -0,0 +1,22 @@ +rules: + - id: taint-StarMixedExclusionSanitizer + languages: + - java + severity: ERROR + message: match taint/StarMixedExclusionSanitizer + mode: taint + pattern-sources: + - patterns: + - pattern: $*X = src(); + - focus-metavariable: $X + pattern-sanitizers: + - patterns: + - pattern: cleanAll($*C); + - focus-metavariable: $C + - patterns: + - pattern: cleanValue($V); + - focus-metavariable: $V + pattern-sinks: + - patterns: + - pattern: sink($Y); + - focus-metavariable: $Y diff --git a/core/opentaint-java-querylang/samples/src/main/resources/taint/StarNestedWrapperSanitizer.yaml b/core/opentaint-java-querylang/samples/src/main/resources/taint/StarNestedWrapperSanitizer.yaml new file mode 100644 index 000000000..38bd2ed02 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/resources/taint/StarNestedWrapperSanitizer.yaml @@ -0,0 +1,19 @@ +rules: + - id: taint-StarNestedWrapperSanitizer + languages: + - java + severity: ERROR + message: match taint/StarNestedWrapperSanitizer + mode: taint + pattern-sources: + - patterns: + - pattern: $*X = src(); + - focus-metavariable: $X + pattern-sanitizers: + - patterns: + - pattern: clean($*C); + - focus-metavariable: $C + pattern-sinks: + - patterns: + - pattern: sink($Y); + - focus-metavariable: $Y diff --git a/core/opentaint-java-querylang/samples/src/main/resources/taint/StarSanitizer.yaml b/core/opentaint-java-querylang/samples/src/main/resources/taint/StarSanitizer.yaml new file mode 100644 index 000000000..b9a08969f --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/resources/taint/StarSanitizer.yaml @@ -0,0 +1,19 @@ +rules: + - id: taint-StarSanitizer + languages: + - java + severity: ERROR + message: match taint/StarSanitizer + mode: taint + pattern-sources: + - patterns: + - pattern: $X = src(); + - focus-metavariable: $X + pattern-sanitizers: + - patterns: + - pattern: clean($*C); + - focus-metavariable: $C + pattern-sinks: + - patterns: + - pattern: sink($Y); + - focus-metavariable: $Y diff --git a/core/opentaint-java-querylang/samples/src/main/resources/taint/StarSink.yaml b/core/opentaint-java-querylang/samples/src/main/resources/taint/StarSink.yaml new file mode 100644 index 000000000..f36937634 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/resources/taint/StarSink.yaml @@ -0,0 +1,15 @@ +rules: + - id: taint-StarSink + languages: + - java + severity: ERROR + message: match taint/StarSink + mode: taint + pattern-sources: + - patterns: + - pattern: $X = src(); + - focus-metavariable: $X + pattern-sinks: + - patterns: + - pattern: sink($*Y); + - focus-metavariable: $Y diff --git a/core/opentaint-java-querylang/samples/src/main/resources/taint/StarSource.yaml b/core/opentaint-java-querylang/samples/src/main/resources/taint/StarSource.yaml new file mode 100644 index 000000000..202688c19 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/resources/taint/StarSource.yaml @@ -0,0 +1,15 @@ +rules: + - id: taint-StarSource + languages: + - java + severity: ERROR + message: match taint/StarSource + mode: taint + pattern-sources: + - patterns: + - pattern: $*X = src(); + - focus-metavariable: $X + pattern-sinks: + - patterns: + - pattern: sink($Y); + - focus-metavariable: $Y diff --git a/core/opentaint-java-querylang/samples/src/main/resources/taint/StarSourceAndSanitizer.yaml b/core/opentaint-java-querylang/samples/src/main/resources/taint/StarSourceAndSanitizer.yaml new file mode 100644 index 000000000..a2ffbb951 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/resources/taint/StarSourceAndSanitizer.yaml @@ -0,0 +1,19 @@ +rules: + - id: taint-StarSourceAndSanitizer + languages: + - java + severity: ERROR + message: match taint/StarSourceAndSanitizer + mode: taint + pattern-sources: + - patterns: + - pattern: $*X = src(); + - focus-metavariable: $X + pattern-sanitizers: + - patterns: + - pattern: clean($*C); + - focus-metavariable: $C + pattern-sinks: + - patterns: + - pattern: sink($Y); + - focus-metavariable: $Y diff --git a/core/opentaint-java-querylang/samples/src/main/resources/taint/StarSourceAndSink.yaml b/core/opentaint-java-querylang/samples/src/main/resources/taint/StarSourceAndSink.yaml new file mode 100644 index 000000000..0647f9760 --- /dev/null +++ b/core/opentaint-java-querylang/samples/src/main/resources/taint/StarSourceAndSink.yaml @@ -0,0 +1,15 @@ +rules: + - id: taint-StarSourceAndSink + languages: + - java + severity: ERROR + message: match taint/StarSourceAndSink + mode: taint + pattern-sources: + - patterns: + - pattern: $*X = src(); + - focus-metavariable: $X + pattern-sinks: + - patterns: + - pattern: sink($*Y); + - focus-metavariable: $Y diff --git a/core/opentaint-java-querylang/src/main/antlr/JavaLexer.g4 b/core/opentaint-java-querylang/src/main/antlr/JavaLexer.g4 index fea4795ea..1a633dc4a 100644 --- a/core/opentaint-java-querylang/src/main/antlr/JavaLexer.g4 +++ b/core/opentaint-java-querylang/src/main/antlr/JavaLexer.g4 @@ -212,6 +212,8 @@ LINE_COMMENT : '//' ~[\r\n]* -> channel(HIDDEN); IDENTIFIER: LetterNoDollar LetterOrDigit*; +STARRED_METAVAR: [$] '*' MetavarFirstLetter MetavarLetter*; + METAVAR: [$] MetavarFirstLetter MetavarLetter*; ANONYMOUS_METAVAR: [$] '_'; diff --git a/core/opentaint-java-querylang/src/main/antlr/JavaParser.g4 b/core/opentaint-java-querylang/src/main/antlr/JavaParser.g4 index 1d6a1bbb7..3bdb9a706 100644 --- a/core/opentaint-java-querylang/src/main/antlr/JavaParser.g4 +++ b/core/opentaint-java-querylang/src/main/antlr/JavaParser.g4 @@ -316,6 +316,7 @@ formalParameter formalParameterMetavar : METAVAR | ANONYMOUS_METAVAR + | STARRED_METAVAR ; lastFormalParameter @@ -492,6 +493,7 @@ localVariableDeclaration identifier : METAVAR | ANONYMOUS_METAVAR + | STARRED_METAVAR | IDENTIFIER | MODULE | OPEN @@ -513,6 +515,7 @@ identifier typeIdentifier // Identifiers that are not restricted for type declarations : METAVAR | ANONYMOUS_METAVAR + | STARRED_METAVAR | IDENTIFIER | MODULE | OPEN 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 6491ad5f9..1093fb2ad 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 @@ -8,7 +8,7 @@ data object AnonymousMetavar : SemgrepJavaPattern { override val children: List = emptyList() } -data class Metavar(val name: String) : SemgrepJavaPattern { +data class Metavar(val name: String, val star: Boolean = false) : SemgrepJavaPattern { override val children: List = emptyList() } @@ -16,7 +16,7 @@ data class EllipsisMetavar(val name: String) : SemgrepJavaPattern { override val children: List = emptyList() } -data class TypedMetavar(val name: String, val type: TypeName) : SemgrepJavaPattern { +data class TypedMetavar(val name: String, val type: TypeName, val star: Boolean = false) : SemgrepJavaPattern { override val children: List = emptyList() } @@ -190,6 +190,7 @@ sealed interface Name data class ConcreteName(val name: String) : Name data class MetavarName(val metavarName: String) : Name +data class StarMetavarName(val metavarName: String) : Name data object AnonymousName: Name sealed interface TypeName { diff --git a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/SemgrepJavaPatternMatcher.kt b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/SemgrepJavaPatternMatcher.kt index 0dc9d267d..18a2478a2 100644 --- a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/SemgrepJavaPatternMatcher.kt +++ b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/SemgrepJavaPatternMatcher.kt @@ -420,6 +420,7 @@ class SemgrepJavaPatternMatcher( } pattern } + is StarMetavarName, is AnonymousName -> TODO() } if (acc.isEmpty()) cur else "$acc\\.$cur" @@ -803,6 +804,7 @@ class SemgrepJavaPatternMatcher( ) } - AnonymousName -> TODO() + is StarMetavarName, + is AnonymousName -> TODO() } } 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 24df8f9a6..792a8bd1d 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 @@ -121,14 +121,18 @@ class SemgrepJavaPatternParser { } } +private fun String.stripStar(): String = "$" + substring(2) + private fun IdentifierContext.parseName(): Name = withRule { tryRule(IdentifierContext::METAVAR) { return MetavarName(it.text) } + tryRule(IdentifierContext::STARRED_METAVAR) { return StarMetavarName(it.text.stripStar()) } tryRule(IdentifierContext::ANONYMOUS_METAVAR) { return AnonymousName } return ConcreteName(text) } private fun TypeIdentifierContext.parseTypeIdentifierName(): Name = withRule { tryRule(TypeIdentifierContext::METAVAR) { return MetavarName(it.text) } + tryRule(TypeIdentifierContext::STARRED_METAVAR) { return StarMetavarName(it.text.stripStar()) } tryRule(TypeIdentifierContext::ANONYMOUS_METAVAR) { this@parseTypeIdentifierName.todo() } return ConcreteName(text) } @@ -278,10 +282,14 @@ private class SemgrepJavaPatternParserVisitor : JavaParserBaseVisitor TypedMetavar(name.metavarName, type, star = false) + is StarMetavarName -> TypedMetavar(name.metavarName, type, star = true) + is AnonymousName, + is ConcreteName -> ctx.parsingFailed("Expected variable name to be a metavar name") + } } override fun visitVariableDeclarator(ctx: VariableDeclaratorContext): VariableAssignment = ctx.withRule { @@ -344,6 +352,7 @@ private class SemgrepJavaPatternParserVisitor : JavaParserBaseVisitor Identifier(name.name) - is MetavarName -> Metavar(name.metavarName) + is MetavarName -> Metavar(name.metavarName, star = false) + is StarMetavarName -> Metavar(name.metavarName, star = true) is AnonymousName -> AnonymousMetavar } } diff --git a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/AddExprRewriter.kt b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/AddExprRewriter.kt index e3fd42849..f3853b273 100644 --- a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/AddExprRewriter.kt +++ b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/AddExprRewriter.kt @@ -6,6 +6,7 @@ import org.opentaint.semgrep.pattern.MetavarName import org.opentaint.semgrep.pattern.MethodInvocation import org.opentaint.semgrep.pattern.NormalizedSemgrepRule import org.opentaint.semgrep.pattern.SemgrepJavaPattern +import org.opentaint.semgrep.pattern.StarMetavarName // todo: rewrite all AddExpr as string concat for now // we can consider split on string/non-string @@ -40,6 +41,7 @@ private fun flatStringConcat(pattern: SemgrepJavaPattern): List {} } diff --git a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/CatchStatementRewriter.kt b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/CatchStatementRewriter.kt index 41a4c9e89..a9ea87fa9 100644 --- a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/CatchStatementRewriter.kt +++ b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/CatchStatementRewriter.kt @@ -9,6 +9,7 @@ import org.opentaint.semgrep.pattern.Name import org.opentaint.semgrep.pattern.NormalizedSemgrepRule import org.opentaint.semgrep.pattern.PatternSequence import org.opentaint.semgrep.pattern.SemgrepJavaPattern +import org.opentaint.semgrep.pattern.StarMetavarName import org.opentaint.semgrep.pattern.TypeName // todo: for now we rewrite all catch statements as typed assign @@ -19,13 +20,13 @@ fun rewriteCatchStatement(rule: NormalizedSemgrepRule): List exceptionVariable: Name, handlerBlock: SemgrepJavaPattern ): List { - val exceptionMetaVarName = when (exceptionVariable) { + val exceptionMetaVar = when (exceptionVariable) { is ConcreteName, is AnonymousName -> return super.createCatchStatement(exceptionTypes, exceptionVariable, handlerBlock) - is MetavarName -> exceptionVariable.metavarName - } - val exceptionMetaVar = Metavar(exceptionMetaVarName) + is MetavarName -> Metavar(exceptionVariable.metavarName, star = false) + is StarMetavarName -> Metavar(exceptionVariable.metavarName, star = true) + } return exceptionTypes.flatMap { type -> super.createVariableAssignment(type, exceptionMetaVar, value = Ellipsis).map { assign -> 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 0028786b0..7d682c138 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 @@ -49,7 +49,7 @@ data class SpecificStringValue(val value: String) : SpecificConstantValue data object SpecificNullValue : SpecificConstantValue @Serializable -data class IsMetavar(val metavar: MetavarAtom) : ParamCondition.Atom +data class IsMetavar(val metavar: MetavarAtom, val star: Boolean = false) : ParamCondition.Atom @Serializable sealed interface MetavarAtom { 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 76e83a085..1be4e2eac 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 @@ -236,7 +236,7 @@ interface PatternRewriter { createStringLiteral(content.rewriteName()) fun TypedMetavar.rewriteTypedMetavar(): List = - createTypedMetavar(name, type.rewriteTypeName()) + createTypedMetavar(name, type.rewriteTypeName(), star) fun VariableAssignment.rewriteVariableAssignment(): List { val newType = type?.rewriteTypeName() @@ -329,7 +329,8 @@ interface PatternRewriter { fun createReturnStmt(value: SemgrepJavaPattern?): List = listOf(ReturnStmt(value)) fun createStringLiteral(content: Name): List = listOf(StringLiteral(content)) - fun createTypedMetavar(name: String, type: TypeName): List = listOf(TypedMetavar(name, type)) + fun createTypedMetavar(name: String, type: TypeName, star: Boolean = false): List = + listOf(TypedMetavar(name, type, star)) fun createVariableAssignment( type: TypeName?, 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 2630c0e70..6bf08b6e2 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 @@ -34,6 +34,7 @@ import org.opentaint.semgrep.pattern.PatternSequence import org.opentaint.semgrep.pattern.ReturnStmt import org.opentaint.semgrep.pattern.SemgrepJavaPattern import org.opentaint.semgrep.pattern.SemgrepRuleLoadStepTrace +import org.opentaint.semgrep.pattern.StarMetavarName import org.opentaint.semgrep.pattern.StaticFieldAccess import org.opentaint.semgrep.pattern.StringEllipsis import org.opentaint.semgrep.pattern.StringLiteral @@ -134,6 +135,7 @@ class PatternToActionListConverter: ActionListBuilder { is StringLiteral -> when (val value = pattern.content) { is ConcreteName -> SpecificStringValue(value.name) is MetavarName -> StringValueMetaVar(MetavarAtom.create(value.metavarName)) + is StarMetavarName -> transformationFailed("String literal is star metavar") is AnonymousName -> ParamCondition.AnyStringLiteral } @@ -142,7 +144,7 @@ class PatternToActionListConverter: ActionListBuilder { } is Metavar -> { - IsMetavar(MetavarAtom.create(pattern.name)) + IsMetavar(MetavarAtom.create(pattern.name), star = pattern.star) } is AnonymousMetavar -> { @@ -157,7 +159,7 @@ class PatternToActionListConverter: ActionListBuilder { val typeName = transformTypeName(pattern.type) ParamCondition.And( listOf( - IsMetavar(MetavarAtom.create(pattern.name)), + IsMetavar(MetavarAtom.create(pattern.name), star = pattern.star), ParamCondition.TypeIs(typeName) ) ) @@ -171,7 +173,7 @@ class PatternToActionListConverter: ActionListBuilder { ParamCondition.SpecificStaticFieldValue(fn.name, type) } - is MetavarName, is AnonymousName -> { + is MetavarName, is StarMetavarName, is AnonymousName -> { transformationFailed("Static field name is metavar") } } @@ -182,12 +184,14 @@ class PatternToActionListConverter: ActionListBuilder { transformationFailed("Array access index is not ellipsis") } - when (pattern.obj) { - is Metavar, - is TypedMetavar -> { - // todo: dirty hack. We can ignore array access here due to the `hackResultArray` in taint configuration - return transformPatternIntoParamCondition(pattern.obj) - } + when (val obj = pattern.obj) { + // `$X[...]` array-element access: we don't model the concrete index. Force the + // metavar starred so it compiles to whole-object / any-field taint, which + // subsumes array-element (`[*]`) taint via the any-accessor machinery — the same + // mechanism that now backs array/vararg sinks in the taint config (the old + // runtime array-element condition reader has been replaced by an any-field check). + is Metavar -> return transformPatternIntoParamCondition(obj.copy(star = true)) + is TypedMetavar -> return transformPatternIntoParamCondition(obj.copy(star = true)) else -> transformationFailed("Array access object is not metavar") } } @@ -325,6 +329,7 @@ class PatternToActionListConverter: ActionListBuilder { val methodName = when (val name = pattern.methodName) { is ConcreteName -> SignatureName.Concrete(name.name) is MetavarName -> SignatureName.MetaVar(name.metavarName) + is StarMetavarName -> transformationFailed("Method name is star") is AnonymousName -> transformationFailed("Method name is anonymous") } @@ -441,11 +446,11 @@ class PatternToActionListConverter: ActionListBuilder { when (val v = pattern.variable) { is Metavar -> { - conditions += IsMetavar(MetavarAtom.create(v.name)) + conditions += IsMetavar(MetavarAtom.create(v.name), star = v.star) } is TypedMetavar -> { - conditions += IsMetavar(MetavarAtom.create(v.name)) + conditions += IsMetavar(MetavarAtom.create(v.name), star = v.star) val typeName = transformTypeName(v.type) conditions += ParamCondition.TypeIs(typeName) @@ -566,6 +571,7 @@ class PatternToActionListConverter: ActionListBuilder { val methodName = when (val name = pattern.name) { is ConcreteName -> SignatureName.Concrete(name.name) is MetavarName -> SignatureName.MetaVar(name.metavarName) + is StarMetavarName -> transformationFailed("Method name is star") is AnonymousName -> transformationFailed("Method name is anonymous") } @@ -583,6 +589,7 @@ class PatternToActionListConverter: ActionListBuilder { val positionName = when (val name = param.name) { is ConcreteName -> name.name is MetavarName -> name.metavarName + is StarMetavarName -> name.metavarName is AnonymousName -> "*" } ParamPosition.Any(paramClassifier = positionName) @@ -597,9 +604,17 @@ class PatternToActionListConverter: ActionListBuilder { is MetavarName -> { paramConditions += ParamPattern( position, - IsMetavar(MetavarAtom.create(name.metavarName)) + IsMetavar(MetavarAtom.create(name.metavarName), star = false) ) } + + is StarMetavarName -> { + paramConditions += ParamPattern( + position, + IsMetavar(MetavarAtom.create(name.metavarName), star = true) + ) + } + is AnonymousName -> {} is ConcreteName -> transformationFailed("MethodDeclaration_param_name_not_metavar") } @@ -674,7 +689,7 @@ class PatternToActionListConverter: ActionListBuilder { ): SignatureModifierValue = when (pattern) { is StringLiteral -> { when (val value = pattern.content) { - is MetavarName -> { + is MetavarName, is StarMetavarName -> { transformationFailed("Annotation_argument_is_string_with_meta_var") } diff --git a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/PatternUtils.kt b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/PatternUtils.kt index a4987a211..a9313a115 100644 --- a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/PatternUtils.kt +++ b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/PatternUtils.kt @@ -8,6 +8,7 @@ import org.opentaint.semgrep.pattern.Metavar import org.opentaint.semgrep.pattern.MetavarName import org.opentaint.semgrep.pattern.Name import org.opentaint.semgrep.pattern.SemgrepJavaPattern +import org.opentaint.semgrep.pattern.StarMetavarName fun tryExtractPatternDotSeparatedParts(pattern: SemgrepJavaPattern): List? { // note: don't match single metavar as dot separated @@ -36,6 +37,7 @@ fun tryExtractConcreteNames(names: List): List? { when (name) { is ConcreteName -> result.add(name.name) is MetavarName, + is StarMetavarName, is AnonymousName -> return null } } diff --git a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/TypeNameWithMetaVarRewriter.kt b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/TypeNameWithMetaVarRewriter.kt index da3be1c1f..18fda82f1 100644 --- a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/TypeNameWithMetaVarRewriter.kt +++ b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/TypeNameWithMetaVarRewriter.kt @@ -10,6 +10,7 @@ import org.opentaint.semgrep.pattern.Name import org.opentaint.semgrep.pattern.NormalizedSemgrepRule import org.opentaint.semgrep.pattern.ResolvedMetaVarInfo import org.opentaint.semgrep.pattern.SemgrepJavaPattern +import org.opentaint.semgrep.pattern.StarMetavarName import org.opentaint.semgrep.pattern.TypeName import org.opentaint.semgrep.pattern.flatMap import org.opentaint.semgrep.pattern.transform @@ -100,6 +101,7 @@ private fun MetaVarConstraintFormula>.transformNext( } } + is StarMetavarName -> TODO("TypeName metavar is star") is AnonymousName -> TODO("TypeName metavar is anonymous") } } diff --git a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/automata/operations/UnifyMetavars.kt b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/automata/operations/UnifyMetavars.kt index c710244fa..bfc108306 100644 --- a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/automata/operations/UnifyMetavars.kt +++ b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/automata/operations/UnifyMetavars.kt @@ -23,7 +23,7 @@ import java.util.BitSet import java.util.LinkedList import java.util.Queue -private data class MetavarUnificationContext private constructor( +private data class MetavarAtomUnificationContext private constructor( private val metavarMappings: Map ) { fun transform(metavar: MetavarAtom): MetavarAtom { @@ -38,7 +38,7 @@ private data class MetavarUnificationContext private constructor( ?: error("Ambiguous transform for metavar $metavar") } - fun unifyMetavars(metavars: Collection): MetavarUnificationContext { + fun unifyMetavars(metavars: Collection): MetavarAtomUnificationContext { // Note: size == 1 can be interesting because this can be already unified metavar if (metavars.isEmpty()) { return this @@ -63,12 +63,12 @@ private data class MetavarUnificationContext private constructor( val newMetavarMappings = metavarMappings.toMutableMap().apply { basicsToUnify.forEach { put(it, unifiedMetavar) } } - return MetavarUnificationContext(newMetavarMappings) + return MetavarAtomUnificationContext(newMetavarMappings) } - fun addMetavar(metavar: MetavarAtom): MetavarUnificationContext = unifyMetavars(listOf(metavar)) + fun addMetavar(metavar: MetavarAtom): MetavarAtomUnificationContext = unifyMetavars(listOf(metavar)) - fun intersect(other: MetavarUnificationContext): MetavarUnificationContext { + fun intersect(other: MetavarAtomUnificationContext): MetavarAtomUnificationContext { val resultMetavarMappings = buildMap { metavarMappings.forEach { (metavar, thisMapping) -> val otherMapping = other.metavarMappings[metavar] ?: return@forEach @@ -79,18 +79,49 @@ private data class MetavarUnificationContext private constructor( } } - return MetavarUnificationContext(resultMetavarMappings) + return MetavarAtomUnificationContext(resultMetavarMappings) } companion object { - val EMPTY: MetavarUnificationContext - get() = MetavarUnificationContext(emptyMap()) + val EMPTY: MetavarAtomUnificationContext + get() = MetavarAtomUnificationContext(emptyMap()) + } +} + +private data class StarredMetaVar(val mv: MetavarAtom, val star: Boolean) + +private data class MetavarUnificationContext( + private val nonStarred: MetavarAtomUnificationContext, + private val starred: MetavarAtomUnificationContext, +) { + fun transform(metavar: StarredMetaVar): MetavarAtom = + if (metavar.star) starred.transform(metavar.mv) else nonStarred.transform(metavar.mv) + + fun addMetavar(smv: StarredMetaVar): MetavarUnificationContext = + if (smv.star) { + copy(starred = starred.addMetavar(smv.mv)) + } else { + copy(nonStarred = nonStarred.addMetavar(smv.mv)) + } + + fun intersect(other: MetavarUnificationContext): MetavarUnificationContext = + MetavarUnificationContext( + nonStarred = nonStarred.intersect(other.nonStarred), + starred = starred.intersect(other.starred) + ) + + fun unifyMetavars(metavars: Collection): MetavarUnificationContext { + val (starMv, nonStarMv) = metavars.partition { it.star } + return MetavarUnificationContext( + nonStarred = nonStarred.unifyMetavars(nonStarMv.map { it.mv }), + starred = starred.unifyMetavars(starMv.map { it.mv }) + ) } } fun AutomataBuilderCtx.unifyMetavars(automata: SemgrepRuleAutomata): SemgrepRuleAutomata { val newInitialNode = AutomataNode() - val initialContext = MetavarUnificationContext.EMPTY + val initialContext = MetavarUnificationContext(MetavarAtomUnificationContext.EMPTY, MetavarAtomUnificationContext.EMPTY) val nodeMapping: MutableMap, AutomataNode> = hashMapOf() val nodeQueue: Queue> = LinkedList() @@ -226,8 +257,8 @@ private fun Predicate.transform(context: MetavarUnificationContext): Predicate { val condition = constraint.condition val newCondition = when (condition) { - is IsMetavar -> IsMetavar(context.transform(condition.metavar)) - is StringValueMetaVar -> StringValueMetaVar(context.transform(condition.metaVar)) + is IsMetavar -> IsMetavar(context.transform(StarredMetaVar(condition.metavar, condition.star)), condition.star) + is StringValueMetaVar -> StringValueMetaVar(context.transform(StarredMetaVar(condition.metaVar, star = false))) else -> return this } @@ -295,7 +326,7 @@ private fun MetavarUnificationContext.extendByPositivePredicates( .fold(initial = this, MetavarUnificationContext::unifyMetavars) } -private fun Predicate.metavarWithPosition(): Pair? { +private fun Predicate.metavarWithPosition(): Pair? { if (constraint !is ParamConstraint) { return null } @@ -304,9 +335,9 @@ private fun Predicate.metavarWithPosition(): Pair? { val condition = constraint.condition if (condition is IsMetavar) { - return condition.metavar to position + return StarredMetaVar(condition.metavar, condition.star) to position } else if (condition is StringValueMetaVar) { - return condition.metaVar to position + return StarredMetaVar(condition.metaVar, star = false) to position } return null } 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 3a99ff867..b611208eb 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 @@ -123,7 +123,8 @@ private fun RuleConversionCtx.convertAutomataToTaint return ctx.createRuleGroup(rules) } -private data class RegisterVarPosition(val varName: MetavarAtom, val positions: MutableSet) +private data class StarredPosition(val position: PositionBase, val star: Boolean) +private data class RegisterVarPosition(val varName: MetavarAtom, val positions: MutableSet) private data class RuleCondition( val enclosingClassPackage: SerializedSimpleNameMatcher, @@ -371,8 +372,10 @@ private fun JavaTaintRuleGenerationCtx.buildStateAssignAction( val requiredVariables = state.register.assignedVars.keys val result = requiredVariables.flatMapTo(mutableListOf()) { varName -> val varPosition = edgeCondition.accessedVarPosition[varName] ?: return@flatMapTo emptyList() - varPosition.positions.flatMap { - stateAssignMark(varPosition.varName, state, it.base()) + varPosition.positions.flatMap { sp -> + sp.bases().flatMap { + stateAssignMark(varPosition.varName, state, it) + } } } @@ -389,8 +392,10 @@ private fun JavaTaintRuleGenerationCtx.buildStateCleanAction( edgeCondition: EvaluatedEdgeCondition ): List { val result = edgeCondition.accessedVarPosition.values.flatMapTo(mutableListOf()) { varPosition -> - varPosition.positions.flatMap { - stateCleanMark(varPosition.varName, state, stateBefore, it.base()) + varPosition.positions.flatMap { sp -> + sp.bases().flatMap { + stateCleanMark(varPosition.varName, state, stateBefore, it) + } } } @@ -413,8 +418,10 @@ private fun EvaluatedEdgeCondition.addStateCheck( stateChecks += ctx.globalStateMarkName(state).mkContainsMark(ctx.stateVarPosition) } else { for (metaVar in state.register.assignedVars.keys) { - for (pos in accessedVarPosition[metaVar]?.positions.orEmpty()) { - stateChecks += ctx.containsStateMark(metaVar, state, pos.base()) + for (sp in accessedVarPosition[metaVar]?.positions.orEmpty()) { + sp.bases().forEach { + stateChecks += ctx.containsStateMark(metaVar, state, it) + } } } } @@ -1103,7 +1110,7 @@ private fun findMetaVarPosition( val varPosition = varPositions.getOrPut(condition.metavar) { RegisterVarPosition(condition.metavar, hashSetOf()) } - varPosition.positions.add(position) + varPosition.positions.add(StarredPosition(position, condition.star)) } private fun JavaTaintRuleGenerationCtx.evaluateParamCondition( @@ -1122,7 +1129,11 @@ private fun JavaTaintRuleGenerationCtx.evaluateParamCondition( semgrepRuleTrace.error(IgnoredMetavarConstraint(condition.metavar)) } - return containsMarkWithAnyStateBefore(state, condition.metavar, position.base()) + val contains = containsMarkWithAnyStateBefore(state, condition.metavar, position.base()) + if (!condition.star) return contains + + val containsAnyField = containsMarkWithAnyStateBefore(state, condition.metavar, position.base().withAnyField()) + return serializedConditionOr(listOf(contains, containsAnyField)) } is ParamCondition.TypeIs -> { @@ -1281,6 +1292,11 @@ private fun MetaVarConstraintFormula.toSerializedConditionCubes( } } +private fun StarredPosition.bases(): List { + val pos = position.base() + return if (!star) listOf(pos) else listOf(pos, pos.withAnyField()) +} + private fun List.toSerializedOr(transformer: (T) -> SerializedCondition): SerializedCondition = serializedConditionOr(map(transformer)) diff --git a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/GeneratedEdgeElimination.kt b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/GeneratedEdgeElimination.kt index 776ca7e30..6320c5ee4 100644 --- a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/GeneratedEdgeElimination.kt +++ b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/GeneratedEdgeElimination.kt @@ -343,7 +343,7 @@ data class StringConcatCtx( return when (condition) { is IsMetavar -> { val newMetavars = metavarMapping[condition.metavar] ?: return listOf(condition) - val modified = newMetavars.map(::IsMetavar) + val modified = newMetavars.map { IsMetavar(it, condition.star) } if (condition.metavar !in newMetavars || newMetavars.size > 1) { return modified + ParamCondition.TypeIs(stringType) @@ -393,7 +393,7 @@ fun eliminateStringConcat( val predCondition = it.asConditionOnStringConcat() ?: return@any false - check(predCondition == IsMetavar(metavar)) { "Unexpected condition" } + check(predCondition is IsMetavar && predCondition.metavar == metavar) { "Unexpected condition" } !it.negated } @@ -405,7 +405,7 @@ fun eliminateStringConcat( val predCondition = it.asConditionOnStringConcat() ?: return@any false - check(predCondition == IsMetavar(metavar)) { "Unexpected condition" } + check(predCondition is IsMetavar && predCondition.metavar == metavar) { "Unexpected condition" } !it.negated } 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 38da9b511..c581407e2 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 @@ -11,7 +11,6 @@ import org.opentaint.semgrep.pattern.MetaVarConstraintFormulaCube import org.opentaint.semgrep.pattern.MetaVarConstraints import org.opentaint.semgrep.pattern.ResolvedMetaVarInfo import org.opentaint.semgrep.pattern.conversion.IsMetavar -import org.opentaint.semgrep.pattern.conversion.MetavarAtom import org.opentaint.semgrep.pattern.conversion.ParamCondition.Atom import org.opentaint.semgrep.pattern.conversion.LanguageTypeOps import org.opentaint.semgrep.pattern.conversion.SemgrepPatternAction.SignatureName @@ -313,7 +312,6 @@ fun MethodFormulaManager.simplifyMethodFormulaCube( } private class MethodConstraintsSolver { - private val positiveMetaVars = hashMapOf>() private val positiveParams = hashMapOf>() private var positiveNumberOfArgs: NumberOfArgsConstraint? = null private val positiveMethodModifiers = hashSetOf() @@ -333,12 +331,26 @@ private class MethodConstraintsSolver { fun addPositive(constraint: MethodConstraint): Unit? { when (constraint) { is ParamConstraint -> { - positiveParams.getOrPut(constraint.position, ::hashSetOf).add(constraint.condition) - - if (constraint.condition is IsMetavar) { - positiveMetaVars.getOrPut(constraint.position, ::hashSetOf) - .addAll(constraint.condition.metavar.basics) + val posSet = positiveParams.getOrPut(constraint.position, ::hashSetOf) + val cond = constraint.condition + if (cond is IsMetavar) { + val condBasics = cond.metavar.basics + val relatedMetaVars = posSet + .filterIsInstance() + .filter { it.metavar.basics.any { b -> b in condBasics } } + + // A (base, star=false) implies A* (base OR any-field, star=true), so A subsumes + // A* at the same position: `A ^ A*` == A. Keep only the base literal. + if (!cond.star) { + posSet.removeAll(relatedMetaVars.filterTo(hashSetOf()) { it.star }) + } else { + if (relatedMetaVars.any { !it.star }) { + // A* is redundant when a coinciding base A is already required. + return Unit + } + } } + posSet.add(cond) } is NumberOfArgsConstraint -> { @@ -366,9 +378,24 @@ private class MethodConstraintsSolver { val currentPositive = positiveParams[constraint.position].orEmpty() if (constraint.condition in currentPositive) return null - if (constraint.condition is IsMetavar) { - val posMetaVars = positiveMetaVars[constraint.position].orEmpty() - if (constraint.condition.metavar.basics.any { it in posMetaVars }) return null + val negCond = constraint.condition + if (negCond is IsMetavar) { + val positiveMetaVars = currentPositive + .filterIsInstance() + .filter { pos -> + pos.metavar.basics.any { it in negCond.metavar.basics } + } + + // Contradiction (whole-match exclusion) per the A => A* truth table: + // !A* is UNSAT if A or A* is positive (A => A*); + // !A is UNSAT only if A (star=false) is positive -- if ONLY A* (star=true) + // is positive, `!A ^ A*` is SAT (field-only) and must be kept below. + val contradiction = if (negCond.star) { + positiveMetaVars.isNotEmpty() + } else { + positiveMetaVars.any { !it.star } + } + if (contradiction) return null } } diff --git a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/SerializedRuleUtils.kt b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/SerializedRuleUtils.kt index c7089462a..a90e91077 100644 --- a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/SerializedRuleUtils.kt +++ b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/SerializedRuleUtils.kt @@ -1,8 +1,9 @@ package org.opentaint.semgrep.pattern.conversion.taint -import org.opentaint.dataflow.configuration.TaintCleanReach + import org.opentaint.dataflow.configuration.jvm.serialized.PositionBase import org.opentaint.dataflow.configuration.jvm.serialized.PositionBaseWithModifiers +import org.opentaint.dataflow.configuration.jvm.serialized.PositionModifier import org.opentaint.dataflow.configuration.jvm.serialized.SerializedCondition import org.opentaint.dataflow.configuration.jvm.serialized.SerializedCondition.Companion.mkFalse import org.opentaint.dataflow.configuration.jvm.serialized.SerializedFunctionNameMatcher @@ -14,6 +15,13 @@ import org.opentaint.semgrep.pattern.Mark.GeneratedMark fun PositionBase.base(): PositionBaseWithModifiers = PositionBaseWithModifiers.BaseOnly(this) +fun PositionBaseWithModifiers.withAnyField(): PositionBaseWithModifiers = when (this) { + is PositionBaseWithModifiers.BaseOnly -> + PositionBaseWithModifiers.WithModifiers(base, listOf(PositionModifier.AnyField)) + is PositionBaseWithModifiers.WithModifiers -> + PositionBaseWithModifiers.WithModifiers(base, modifiers + PositionModifier.AnyField) +} + fun anyName() = SerializedSimpleNameMatcher.Pattern(".*") fun anyFunction() = SerializedFunctionNameMatcher.Complex(anyName(), anyName(), anyName()) @@ -50,8 +58,4 @@ fun GeneratedMark.mkAssignMark(pos: PositionBaseWithModifiers) = SerializedTaintAssignAction(taintMarkStr(), pos = pos) fun GeneratedMark.mkCleanMark(pos: PositionBaseWithModifiers) = - SerializedTaintCleanAction( - taintMarkStr(), - pos = pos, - reach = TaintCleanReach.Exact, - ) + SerializedTaintCleanAction(taintMarkStr(), pos = pos) diff --git a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/TaintAutomataGeneration.kt b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/TaintAutomataGeneration.kt index f1a250015..9181fc4a2 100644 --- a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/TaintAutomataGeneration.kt +++ b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/TaintAutomataGeneration.kt @@ -473,7 +473,7 @@ private fun MethodConstraint.replaceMetavar(replace: (MetavarAtom) -> MetavarAto } val newCondition = when (condition) { - is IsMetavar -> IsMetavar(replace(condition.metavar) ?: return null) + is IsMetavar -> IsMetavar(replace(condition.metavar) ?: return null, condition.star) is ParamCondition.StringValueMetaVar -> ParamCondition.StringValueMetaVar( replace(condition.metaVar) ?: return null ) diff --git a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/TaintRuleProcessing.kt b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/TaintRuleProcessing.kt index 00bab88b8..98c1b956d 100644 --- a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/TaintRuleProcessing.kt +++ b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/TaintRuleProcessing.kt @@ -506,6 +506,15 @@ private fun forkState( return newState } +// True when this edge assigns a whole-object (`$*X`) metavar. Used to propagate the star onto the +// synthesized `generated_source` mark for focus-free source patterns (e.g. `$*X = src()`), so the +// generated source taints the value AND all of its nested fields, matching the starred intent. +private fun EdgeEffect.assignsStarredMetaVar(): Boolean = + assignMetaVar.values.asSequence().flatten().any { + val constraint = it.predicate.constraint + constraint is ParamConstraint && (constraint.condition as? IsMetavar)?.star == true + } + private fun ensureSourceStateVars( automata: TaintRegisterStateAutomata, focusMetaVars: Set @@ -529,7 +538,10 @@ private fun ensureSourceStateVars( val effectVars = edge.effect.assignMetaVar.toMutableMap() // todo: currently we taint only result, but semgrep taint all subexpr by default - val condition = ParamConstraint(Position.Result, IsMetavar(freshVar)) + val condition = ParamConstraint( + Position.Result, + IsMetavar(freshVar, star = edge.effect.assignsStarredMetaVar()) + ) val predicate = Predicate(positivePredicate.signature, condition) effectVars[freshVar] = listOf(MethodPredicate(predicate, negated = false)) val effect = EdgeEffect(effectVars) @@ -544,7 +556,7 @@ private fun ensureSourceStateVars( val condition = ParamConstraint( Position.Argument(Position.ArgumentIndex.Any("tainted")), - IsMetavar(freshVar) + IsMetavar(freshVar, star = edge.effect.assignsStarredMetaVar()) ) val predicate = Predicate(positivePredicate.signature, condition) effectVars[freshVar] = listOf(MethodPredicate(predicate, negated = false)) @@ -560,7 +572,7 @@ private fun ensureSourceStateVars( val condition = ParamConstraint( Position.Argument(Position.ArgumentIndex.Concrete(idx = 0)), - IsMetavar(freshVar) + IsMetavar(freshVar, star = edge.effect.assignsStarredMetaVar()) ) val predicate = Predicate(positivePredicate.signature, condition) effectVars[freshVar] = listOf(MethodPredicate(predicate, negated = false)) diff --git a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/composition/TaintCleanCompositionStrategy.kt b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/composition/TaintCleanCompositionStrategy.kt index 48bdfbdc2..93a3c49e0 100644 --- a/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/composition/TaintCleanCompositionStrategy.kt +++ b/core/opentaint-java-querylang/src/main/kotlin/org/opentaint/semgrep/pattern/conversion/taint/composition/TaintCleanCompositionStrategy.kt @@ -2,6 +2,7 @@ package org.opentaint.semgrep.pattern.conversion.taint.composition import org.opentaint.dataflow.configuration.jvm.serialized.PositionBase import org.opentaint.dataflow.configuration.jvm.serialized.PositionBaseWithModifiers +import org.opentaint.dataflow.configuration.jvm.serialized.PositionModifier import org.opentaint.semgrep.pattern.Mark import org.opentaint.semgrep.pattern.conversion.MetavarAtom import org.opentaint.semgrep.pattern.conversion.TaintRuleStrategy @@ -9,6 +10,7 @@ import org.opentaint.semgrep.pattern.conversion.taint.TaintAutomataEdges import org.opentaint.semgrep.pattern.conversion.taint.TaintRegisterStateAutomata import org.opentaint.semgrep.pattern.conversion.taint.TaintRuleGenerationCtx import org.opentaint.semgrep.pattern.conversion.taint.base +import org.opentaint.semgrep.pattern.conversion.taint.withAnyField class TaintCleanCompositionStrategy( private val rule: TaintAutomataEdges, @@ -24,13 +26,34 @@ class TaintCleanCompositionStrategy( ): List? { if (state !in rule.automata.finalAcceptStates) return null + val cleanerPos = cleanerPositions(pos) + + return cleans.flatMap { c -> cleanerPos.map { strategy.createCleanAction(c, it) } } + } + + private fun cleanerPositions(pos: PositionBaseWithModifiers?): List { val cleanerPos = mutableListOf(PositionBase.Result.base()) if (bySideEffect) { cleanerPos += PositionBase.AnyArgument(classifier = "tainted").base() cleanerPos += PositionBase.This.base() } - return cleans.flatMap { c -> cleanerPos.map { strategy.createCleanAction(c, it) } } + val isStar = pos is PositionBaseWithModifiers.WithModifiers && + pos.modifiers.contains(PositionModifier.AnyField) + + // star ($*X): clean the any-field of each cleaner position (Result.*, etc.), + // on the SAME base as the plain value clean — not the raw metavar position. + val cleanerEmitPositions = if (isStar) cleanerPos.map { it.withAnyField() } else cleanerPos + + // Also clean the focus metavar's own position (`pos`, e.g. the sanitized argument). A + // pass-through sanitizer (`clean($C) { return $C }`) carries the argument's taint into its + // result, but the clean runs on the argument-keyed fact at call-to-start, where the Result + // position does not yet exist — cleaning only Result misses it. Cleaning the focus position + // removes the taint on the flow entering the call; it is flow-specific, so a separate use of + // the same variable outside this call stays tainted. For a star clean `pos` already carries + // the AnyField modifier, so this stays coherent with the plain-value arm's base. + val emitPositions = (cleanerEmitPositions + listOfNotNull(pos)).distinct() + return emitPositions } override fun stateAccessedMarks( diff --git a/core/opentaint-java-querylang/src/test/kotlin/org/opentaint/semgrep/ExampleTest.kt b/core/opentaint-java-querylang/src/test/kotlin/org/opentaint/semgrep/ExampleTest.kt index 1f427deee..71870e43c 100644 --- a/core/opentaint-java-querylang/src/test/kotlin/org/opentaint/semgrep/ExampleTest.kt +++ b/core/opentaint-java-querylang/src/test/kotlin/org/opentaint/semgrep/ExampleTest.kt @@ -13,6 +13,7 @@ import org.opentaint.dataflow.configuration.jvm.serialized.SerializedTaintPassAc import org.opentaint.semgrep.pattern.conversion.taint.anyFunction import org.opentaint.semgrep.pattern.conversion.taint.base import org.opentaint.semgrep.util.SampleBasedTest +import org.opentaint.semgrep.util.TestAnalysisRunner import kotlin.test.Test @TestInstance(PER_CLASS) @@ -193,7 +194,9 @@ class ExampleTest : SampleBasedTest() { fun `test tricky pattern not`() = runTest(EXPECT_STATE_VAR) @Test - fun `test array example`() = runTest() + fun `test array example`() = runTest( + unrollStrategy = TestAnalysisRunner.AnyAccessorEnabled + ) @Test fun `test join with taint and matching left`() = runTest() diff --git a/core/opentaint-java-querylang/src/test/kotlin/org/opentaint/semgrep/SerializedConditionRoundTripTest.kt b/core/opentaint-java-querylang/src/test/kotlin/org/opentaint/semgrep/SerializedConditionRoundTripTest.kt new file mode 100644 index 000000000..83a194b11 --- /dev/null +++ b/core/opentaint-java-querylang/src/test/kotlin/org/opentaint/semgrep/SerializedConditionRoundTripTest.kt @@ -0,0 +1,59 @@ +package org.opentaint.semgrep + +import org.opentaint.dataflow.configuration.ConfigurationLoader +import org.opentaint.dataflow.configuration.jvm.serialized.PositionBase +import org.opentaint.dataflow.configuration.jvm.serialized.PositionBaseWithModifiers +import org.opentaint.dataflow.configuration.jvm.serialized.PositionModifier +import org.opentaint.dataflow.configuration.jvm.serialized.SerializedCondition +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +/** + * Guards the $*VAR star-operator serialization footgun: ContainsMarkOnAnyField is + * field-shape-identical to ContainsMark, so before it got a distinct discriminating + * key a YAML round-trip silently demoted it to plain ContainsMark, losing the + * any-field/field-sensitive semantics. + */ +class SerializedConditionRoundTripTest { + private val yaml = ConfigurationLoader.yaml + + private val pos = PositionBaseWithModifiers.WithModifiers( + base = PositionBase.Argument(0), + modifiers = listOf(PositionModifier.AnyField), + ) + + @Test + fun `ContainsMarkOnAnyField survives a YAML round-trip`() { + val original: SerializedCondition = SerializedCondition.ContainsMark( + tainted = "untrusted", + pos = pos, + ) + + val encoded = yaml.encodeToString(SerializedCondition.serializer(), original) + val decoded = yaml.decodeFromString(SerializedCondition.serializer(), encoded) + + assertTrue( + decoded is SerializedCondition.ContainsMark, + "round-trip must preserve ContainsMarkOnAnyField, got ${decoded::class.simpleName}; yaml=\n$encoded", + ) + assertEquals(original, decoded) + } + + @Test + fun `ContainsMark still round-trips as ContainsMark`() { + val original: SerializedCondition = SerializedCondition.ContainsMark( + tainted = "untrusted", + pos = pos, + ) + + val encoded = yaml.encodeToString(SerializedCondition.serializer(), original) + val decoded = yaml.decodeFromString(SerializedCondition.serializer(), encoded) + + assertTrue( + decoded is SerializedCondition.ContainsMark, + "round-trip must preserve ContainsMark, got ${decoded::class.simpleName}; yaml=\n$encoded", + ) + assertEquals(original, decoded) + } +} diff --git a/core/opentaint-java-querylang/src/test/kotlin/org/opentaint/semgrep/StarOperatorParseTest.kt b/core/opentaint-java-querylang/src/test/kotlin/org/opentaint/semgrep/StarOperatorParseTest.kt new file mode 100644 index 000000000..61d582e9d --- /dev/null +++ b/core/opentaint-java-querylang/src/test/kotlin/org/opentaint/semgrep/StarOperatorParseTest.kt @@ -0,0 +1,109 @@ +package org.opentaint.semgrep + +import org.opentaint.semgrep.pattern.Metavar +import org.opentaint.semgrep.pattern.SemgrepJavaPattern +import org.opentaint.semgrep.pattern.SemgrepJavaPatternParser +import org.opentaint.semgrep.pattern.SemgrepJavaPatternParsingResult +import org.opentaint.semgrep.pattern.SemgrepLoadTrace +import org.opentaint.semgrep.pattern.SemgrepRuleLoader +import org.opentaint.semgrep.pattern.TypedMetavar +import org.opentaint.semgrep.pattern.conversion.JavaLanguageStrategy +import org.opentaint.semgrep.pattern.errorEntries +import kotlin.io.path.Path +import kotlin.test.Test +import kotlin.test.assertTrue + +class StarOperatorParseTest { + private fun collect(p: SemgrepJavaPattern): List = + listOf(p) + p.children.flatMap { collect(it) } + + private fun metavars(pattern: String): List = + collect(parseJavaSemgrepPattern(pattern)).filterIsInstance() + + private fun blockingErrors(ruleText: String): List { + val trace = SemgrepLoadTrace() + val loader = SemgrepRuleLoader(listOf(JavaLanguageStrategy())) + loader.registerRuleSet(ruleText, Path("repro.yaml"), Path("."), trace) + loader.loadRules() + return trace.errorEntries().map { "${it.severity}/${it.step}: ${it.message}" } + } + + @Test + fun `starred metavar in call argument`() { + val mvs = metavars("sink(\$*Y);") + val y = mvs.single { it.name == "\$Y" } + assertTrue(y.star, "expected \$*Y to be starred") + } + + @Test + fun `starred typed variable declaration`() { + // F5: the starred `variableDeclaratorId` alternative in a TYPED declaration must load + // (no parse exception) and the declared LHS metavar must carry star=true. + val mvs = metavars("String \$*UNTRUSTED = \$REQ.getParameter(\"q\");") + val u = mvs.single { it.name == "\$UNTRUSTED" } + assertTrue(u.star, "expected typed-declaration \$*UNTRUSTED to be starred") + } + + @Test + fun `starred typed metavar in receiver position`() { + // `(Type $*VAR).m()` is how a sink observes taint buried in a field of its RECEIVER — + // e.g. a java.io.File whose path is tainted but whose base carries no mark. Before the + // typedVariableExpression grammar accepted STARRED_METAVAR this failed to parse, and the + // whole pattern was dropped SILENTLY (the rule count shrank, no error was reported). + val typed = collect(parseJavaSemgrepPattern("(java.io.File \$*FILE).exists();")) + .filterIsInstance() + val f = typed.single { it.name == "\$FILE" } + assertTrue(f.star, "expected receiver \$*FILE to be starred") + } + + @Test + fun `starred typed metavar in argument position`() { + val typed = collect(parseJavaSemgrepPattern("sink((java.nio.file.Path \$*P));")) + .filterIsInstance() + val p = typed.single { it.name == "\$P" } + assertTrue(p.star, "expected parenthesised typed \$*P to be starred") + } + + @Test + fun `starred bare return value`() { + val mvs = metavars("return \$*UNTRUSTED;") + val u = mvs.single { it.name == "\$UNTRUSTED" } + assertTrue(u.star) + } + + @Test + fun `star pattern loads without blocking errors and binds same name`() { + // \$UNTRUSTED appears starred in the source and unstarred in the sink; + // they must refer to the same metavariable and the rule must load cleanly. + val rule = """ + rules: + - id: star-bind-repro + options: { lib: true } + severity: NOTE + message: x + languages: [java] + mode: taint + pattern-sources: + - pattern: ${'$'}*UNTRUSTED = src(); + pattern-sinks: + - pattern: sink(${'$'}UNTRUSTED); + """.trimIndent() + val errors = blockingErrors(rule) + assertTrue(errors.isEmpty(), "star rule failed to load:\n" + errors.joinToString("\n")) + } + + /** + * Parses a single Java semgrep pattern string into a [SemgrepJavaPattern], throwing on failure. + * + * Thin convenience wrapper over [SemgrepJavaPatternParser.parseSemgrepJavaPattern] using the exact + * same parser path the rule loader uses (see conversion/SemgrepPatternParser.kt). + */ + private fun parseJavaSemgrepPattern(pattern: String): SemgrepJavaPattern = + when (val result = SemgrepJavaPatternParser().parseSemgrepJavaPattern(pattern)) { + is SemgrepJavaPatternParsingResult.Ok -> result.pattern + is SemgrepJavaPatternParsingResult.ParserFailure -> throw result.exception + is SemgrepJavaPatternParsingResult.OtherFailure -> throw result.exception + is SemgrepJavaPatternParsingResult.FailedASTParsing -> + error("Failed to parse pattern '$pattern': ${result.errorMessages}") + } +} diff --git a/core/opentaint-java-querylang/src/test/kotlin/org/opentaint/semgrep/StarOperatorRuleGenTest.kt b/core/opentaint-java-querylang/src/test/kotlin/org/opentaint/semgrep/StarOperatorRuleGenTest.kt new file mode 100644 index 000000000..9e94e6c12 --- /dev/null +++ b/core/opentaint-java-querylang/src/test/kotlin/org/opentaint/semgrep/StarOperatorRuleGenTest.kt @@ -0,0 +1,352 @@ +package org.opentaint.semgrep + +import org.opentaint.dataflow.configuration.jvm.serialized.SerializedCondition +import org.opentaint.dataflow.configuration.jvm.serialized.SinkRule +import org.opentaint.semgrep.pattern.SemgrepLoadTrace +import org.opentaint.semgrep.pattern.SemgrepRuleLoader +import org.opentaint.semgrep.pattern.TaintRuleFromSemgrep +import org.opentaint.semgrep.pattern.conversion.JavaLanguageStrategy +import org.opentaint.semgrep.pattern.createTaintConfig +import org.opentaint.dataflow.configuration.jvm.serialized.SerializedItem +import org.opentaint.dataflow.configuration.jvm.serialized.SerializedTaintConfig +import org.opentaint.dataflow.configuration.jvm.serialized.PositionBase +import org.opentaint.dataflow.configuration.jvm.serialized.PositionBaseWithModifiers +import org.opentaint.dataflow.configuration.jvm.serialized.PositionModifier +import org.opentaint.dataflow.configuration.jvm.serialized.SourceRule +import kotlin.io.path.Path +import kotlin.test.Test +import kotlin.test.assertTrue + +class StarOperatorRuleGenTest { + protected fun config(ruleText: String): SerializedTaintConfig { + val trace = SemgrepLoadTrace() + val loader = SemgrepRuleLoader(listOf(JavaLanguageStrategy())) + loader.registerRuleSet(ruleText, Path("star.yaml"), Path("."), trace) + val (rule, _) = loader.loadRules().rulesWithMeta.single() + @Suppress("UNCHECKED_CAST") + return (rule as TaintRuleFromSemgrep).createTaintConfig() + } + + private fun allConditions(cfg: SerializedTaintConfig): List { + val sinkRules: List = buildList { + addAll(cfg.sink.orEmpty()) + addAll(cfg.methodExitSink.orEmpty()) + addAll(cfg.methodEntrySink.orEmpty()) + } + val sourceRules: List = buildList { + addAll(cfg.source.orEmpty()) + addAll(cfg.methodExitSource.orEmpty()) + addAll(cfg.entryPoint.orEmpty()) + } + val conditions = sinkRules.mapNotNull { it.condition } + + sourceRules.mapNotNull { it.condition } + + cfg.passThrough.orEmpty().mapNotNull { it.condition } + return conditions.flatMap { flatten(it) } + } + + private fun flatten(c: SerializedCondition): List = when (c) { + is SerializedCondition.Or -> listOf(c) + c.anyOf.flatMap { flatten(it) } + is SerializedCondition.And -> listOf(c) + c.allOf.flatMap { flatten(it) } + else -> listOf(c) + } + + private fun sourceAssignPositions(cfg: SerializedTaintConfig): List = + (cfg.source.orEmpty() + cfg.entryPoint.orEmpty()).filterIsInstance() + .flatMap { it.taint } + .map { it.pos } + + private fun cleanPositions(cfg: SerializedTaintConfig): List = + cfg.cleaner.orEmpty() + .flatMap { it.cleans } + .map { it.pos } + + @Test + fun `starred source assigns value and any-field`() { + val cfg = config( + """ + rules: + - id: star-source + severity: NOTE + message: x + languages: [java] + mode: taint + pattern-sources: + - patterns: + - pattern: sink(${'$'}*X); + - focus-metavariable: ${'$'}X + pattern-sinks: + - pattern: other(${'$'}Y); + """.trimIndent() + ) + val positions = sourceAssignPositions(cfg) + assertTrue( + positions.any { it is PositionBaseWithModifiers.BaseOnly }, + "expected a plain-value assign; got $positions" + ) + assertTrue( + positions.any { + it is PositionBaseWithModifiers.WithModifiers && + it.modifiers.contains(PositionModifier.AnyField) + }, + "expected an any-field assign; got $positions" + ) + } + + @Test + fun `starred assignment-LHS source assigns value and any-field`() { + // F1: the star sits on the assignment LHS metavar (`$*X = src()`), not a call argument. + // The whole-object taint must still emit BOTH a plain-value assign and an any-field assign. + val cfg = config( + """ + rules: + - id: star-assign-source + severity: NOTE + message: x + languages: [java] + mode: taint + pattern-sources: + - pattern: ${'$'}*X = src(); + pattern-sinks: + - pattern: sink(${'$'}Y); + """.trimIndent() + ) + val positions = sourceAssignPositions(cfg) + assertTrue( + positions.any { it is PositionBaseWithModifiers.BaseOnly }, + "expected a plain-value assign; got $positions" + ) + assertTrue( + positions.any { + it is PositionBaseWithModifiers.WithModifiers && + it.modifiers.contains(PositionModifier.AnyField) + }, + "expected an any-field assign; got $positions" + ) + } + + @Test + fun `starred typed-declaration source assigns value and any-field`() { + // F5: a starred TYPED declaration (`String $*X = src()`) must load and thread the star + // through the assignment path, emitting both a plain-value and an any-field assign. + val cfg = config( + """ + rules: + - id: star-typed-decl-source + severity: NOTE + message: x + languages: [java] + mode: taint + pattern-sources: + - pattern: String ${'$'}*X = src(); + pattern-sinks: + - pattern: sink(${'$'}Y); + """.trimIndent() + ) + val positions = sourceAssignPositions(cfg) + assertTrue( + positions.any { it is PositionBaseWithModifiers.BaseOnly }, + "expected a plain-value assign; got $positions" + ) + assertTrue( + positions.any { + it is PositionBaseWithModifiers.WithModifiers && + it.modifiers.contains(PositionModifier.AnyField) + }, + "expected an any-field assign; got $positions" + ) + } + + @Test + fun `starred sanitizer cleans value and any-field`() { + val cfg = config( + """ + rules: + - id: star-sanitizer + severity: NOTE + message: x + languages: [java] + mode: taint + pattern-sources: + - pattern: ${'$'}X = src(); + pattern-sanitizers: + - patterns: + - pattern: clean(${'$'}*X); + - focus-metavariable: ${'$'}X + pattern-sinks: + - pattern: sink(${'$'}X); + """.trimIndent() + ) + val positions = cleanPositions(cfg) + val plain = positions.filterIsInstance() + val anyField = positions.filterIsInstance() + .filter { it.modifiers.contains(PositionModifier.AnyField) } + assertTrue(plain.isNotEmpty(), "expected a plain-value clean; got $positions") + assertTrue(anyField.isNotEmpty(), "expected an any-field clean; got $positions") + // Base coherence: the any-field clean must sit on the SAME base as the + // plain value clean (both PositionBase.Result), not the raw metavar + // argument position — otherwise field taint survives the sanitizer. + assertTrue( + plain.any { it.base == PositionBase.Result }, + "expected plain clean on Result; got $positions" + ) + assertTrue( + anyField.any { it.base == PositionBase.Result }, + "expected any-field clean on Result (same base as plain value clean); got $positions" + ) + } + + @Test + fun `starred sanitizer assignment cleans returned value and any-field`() { + val cfg = config( + """ + rules: + - id: starred-receiver-sanitizer + severity: NOTE + message: x + languages: [java] + mode: taint + pattern-sources: + - pattern: ${'$'}X = src(); + pattern-sanitizers: + - patterns: + - pattern: ${'$'}*CLEAN = ${'$'}REQ.clean(); + - focus-metavariable: ${'$'}CLEAN + pattern-sinks: + - pattern: sink(${'$'}X); + """.trimIndent() + ) + val positions = cleanPositions(cfg) + + assertTrue( + positions.any { + it is PositionBaseWithModifiers.WithModifiers && + it.base == PositionBase.Result && + it.modifiers.contains(PositionModifier.AnyField) + }, + "expected an any-field clean on Result; got $positions" + ) + } + + @Test + fun `starred sink produces ContainsMarkOnAnyField`() { + val cfg = config( + """ + rules: + - id: star-sink + severity: NOTE + message: x + languages: [java] + mode: taint + pattern-sources: + - pattern: ${'$'}X = src(); + pattern-sinks: + - patterns: + - pattern: sink(${'$'}*Y); + - focus-metavariable: ${'$'}Y + """.trimIndent() + ) + val conditions = allConditions(cfg) + val anyField = conditions.filterIsInstance() + assertTrue(anyField.isNotEmpty(), "expected a ContainsMarkOnAnyField in the starred sink config") + + // Base coherence: every any-field check must be paired with a plain + // ContainsMark on the SAME mark and SAME position base — a starred sink + // matches the value OR any of its nested fields, both anchored to the + // metavar's resolved position. A base/mark mismatch would be a silent bug. + val plain = conditions.filterIsInstance() + anyField.forEach { af -> + assertTrue( + plain.any { it.tainted == af.tainted && it.pos.base == af.pos.base }, + "any-field check $af has no paired plain ContainsMark on the same mark/base; plain=$plain" + ) + } + } + + @Test + fun `starred propagator copies over any-field`() { + val cfg = config( + """ + rules: + - id: star-prop + severity: NOTE + message: x + languages: [java] + mode: taint + pattern-sources: + - pattern: ${'$'}X = src(); + pattern-propagators: + - patterns: + - pattern: ${'$'}TO = wrap(${'$'}*X); + from: ${'$'}X + to: ${'$'}TO + pattern-sinks: + - pattern: sink(${'$'}TO); + """.trimIndent() + ) + // A starred propagator source occurrence must reference an any-field position + val anyField = allConditions(cfg).any { it is SerializedCondition.ContainsMark } || + sourceAssignPositions(cfg).any { + it is PositionBaseWithModifiers.WithModifiers && it.modifiers.contains(PositionModifier.AnyField) + } + assertTrue(anyField, "expected any-field involvement in starred propagator") + } + + @Test + fun `starred pattern-not sink still generates any-field check`() { + val cfg = config( + """ + rules: + - id: star-not + severity: NOTE + message: x + languages: [java] + mode: taint + pattern-sources: + - pattern: ${'$'}X = src(); + pattern-sinks: + - patterns: + - pattern: sink(${'$'}*Y); + - pattern-not: sink(safe()); + - focus-metavariable: ${'$'}Y + """.trimIndent() + ) + assertTrue( + allConditions(cfg).any { it is SerializedCondition.ContainsMark }, + "pattern-not sink must still carry the any-field check" + ) + } + + // Mirrors the shipped xss sanitizer shape: the starred metavar sits inside a `pattern-either` + // at a NON-first arg position with leading/trailing varargs `...`, focused separately. This is + // the shape the OWASP escapeHtml sanitizer uses; if the any-field clean is lost here (but not + // in the simple `clean($*X)` case), that explains why starring the shipped sanitizer was a no-op. + @Test + fun `starred sanitizer in pattern-either with varargs still cleans any-field on Result`() { + val cfg = config( + """ + rules: + - id: star-san-either + severity: NOTE + message: x + languages: [java] + mode: taint + pattern-sources: + - pattern: ${'$'}X = src(); + pattern-sanitizers: + - patterns: + - pattern-either: + - pattern: esc(..., ${'$'}*X, ...); + - focus-metavariable: ${'$'}X + pattern-sinks: + - pattern: sink(${'$'}X); + """.trimIndent() + ) + val positions = cleanPositions(cfg) + val anyField = positions.filterIsInstance() + .filter { it.modifiers.contains(PositionModifier.AnyField) } + assertTrue( + anyField.any { it.base == PositionBase.Result }, + "expected any-field clean on Result for the pattern-either/varargs sanitizer; got $positions" + ) + } +} diff --git a/core/opentaint-java-querylang/src/test/kotlin/org/opentaint/semgrep/StarOperatorTest.kt b/core/opentaint-java-querylang/src/test/kotlin/org/opentaint/semgrep/StarOperatorTest.kt new file mode 100644 index 000000000..3af1d4319 --- /dev/null +++ b/core/opentaint-java-querylang/src/test/kotlin/org/opentaint/semgrep/StarOperatorTest.kt @@ -0,0 +1,118 @@ +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 org.opentaint.semgrep.util.TestAnalysisRunner +import kotlin.test.Test + +@TestInstance(PER_CLASS) +class StarOperatorTest : SampleBasedTest() { + // The starred SOURCE ($*X = src()) taints the whole object and every field; a concrete + // field read only inherits that taint once the any-accessor is unrolled to a field read. + // Mirror the Go harness and enable unrolling for THIS sample only (StarSink/StarSanitizer + // keep the default AnyAccessorDisabled). Removing the source `*` makes the Positive a false + // negative, proving the star is load-bearing here. + @Test + fun `star source field flow`() = + runTest(unrollStrategy = TestAnalysisRunner.AnyAccessorEnabled) + + @Test + fun `star sink any field`() = runTest() + + @Test + fun `star sanitizer clears field taint`() = runTest() + + // ---- Deep-nesting matrix: taint hidden 5+ fields deep and/or 5+ calls deep ---- + + // Starred source, taint 5 fields deep, unhidden by a nested field read. Needs the + // any-accessor unroll (like `star source field flow`) so the source star reaches a + // concrete deep field read. + @Test + fun `star deep source field flow`() = + runTest(unrollStrategy = TestAnalysisRunner.AnyAccessorEnabled) + + // Starred sink observes taint written 5 fields deep (default unroll). + @Test + fun `star deep sink any field`() = runTest() + + // Starred sanitizer must clear taint 5 fields deep (default unroll). + @Test + fun `star deep sanitizer clears field taint`() = runTest() + + // Starred source threaded through a 5+ hop interprocedural chain that alternately hides + // taint inside an object and exposes it. Needs the any-accessor unroll. + @Test + fun `star interprocedural chain`() = + runTest(unrollStrategy = TestAnalysisRunner.AnyAccessorEnabled) + + // Both ends starred: whole-object source + whole-object sink, nested object in between. + @Test + fun `star source and sink`() = + runTest(unrollStrategy = TestAnalysisRunner.AnyAccessorEnabled) + + // Starred source + starred sanitizer over a deep field chain. + @Test + fun `star source and sanitizer`() = + runTest(unrollStrategy = TestAnalysisRunner.AnyAccessorEnabled) + + // ---- Combined matrix: 5+ interprocedural depth x 5+ field depth, sources/sinks deep ---- + // + // Every StarMatrix* sample places the source statement 5 calls deep, the sink call 5 calls + // deep, and moves the taint one field level per hop (or threads a 5-level object), so the + // interprocedural and field dimensions are exercised TOGETHER, not separately. + + @Test + fun `star matrix source - deep source, per-hop unwrap, deep sink`() = + runTest(unrollStrategy = TestAnalysisRunner.AnyAccessorEnabled) + + @Test + fun `star matrix sink - deep source, per-hop wrap, deep starred sink`() = + runTest(unrollStrategy = TestAnalysisRunner.AnyAccessorEnabled) + + // Both propagator occurrences starred ($*T = pass($*F)): the FROM observes any-field taint + // of the whole argument, the TO assigns whole-object taint verified by a per-hop unwrap. + @Test + fun `star matrix propagator - starred from and to move whole-object taint`() = + runTest(unrollStrategy = TestAnalysisRunner.AnyAccessorEnabled) + + // The starred clean sits inside a wrapper helper — the deep-mark-exclusion regression shape. + @Test + fun `star matrix sanitizer - wrapped whole-object clean across summaries`() = + runTest(unrollStrategy = TestAnalysisRunner.AnyAccessorEnabled) + + // Composition stress for the deep mark exclusions: the starred clean under TWO wrapper + // summaries, with the sanitized flow itself inside a further summarized helper. + @Test + fun `star nested wrapper sanitizer - deep exclusion composes across summary levels`() = + runTest(unrollStrategy = TestAnalysisRunner.AnyAccessorEnabled) + + // Mixed exclusion kinds on one flow: deep (starred clean) + plain (value clean) refinements + // of the same initial fact — the always-propagate-deep-marks regression net. + @Test + fun `star mixed exclusion sanitizer - deep and plain exclusions compose`() = + runTest(unrollStrategy = TestAnalysisRunner.AnyAccessorEnabled) + + @Test + fun `star matrix pattern-not - starred sink with excluded emit mode`() = + runTest(unrollStrategy = TestAnalysisRunner.AnyAccessorEnabled) + + @Test + fun `star matrix pattern-inside - starred sink gated by receiver origin`() = + runTest(unrollStrategy = TestAnalysisRunner.AnyAccessorEnabled) + + // The pattern-inside context that wires the guard receiver ($G = checker(); ...) converts + // via the state-var mechanism, like the shipped setContentType suppression. + @Test + fun `star matrix pattern-not-inside - starred sink suppressed by guard`() = + runTest( + expectStateVar = true, + unrollStrategy = TestAnalysisRunner.AnyAccessorEnabled, + ) + + @AfterAll + fun close() { + closeRunner() + } +} diff --git a/core/opentaint-java-querylang/src/test/kotlin/org/opentaint/semgrep/StarPatternNotCoincidenceTest.kt b/core/opentaint-java-querylang/src/test/kotlin/org/opentaint/semgrep/StarPatternNotCoincidenceTest.kt new file mode 100644 index 000000000..aad16b2da --- /dev/null +++ b/core/opentaint-java-querylang/src/test/kotlin/org/opentaint/semgrep/StarPatternNotCoincidenceTest.kt @@ -0,0 +1,98 @@ +package org.opentaint.semgrep + +import org.opentaint.dataflow.configuration.jvm.serialized.PositionBaseWithModifiers +import org.opentaint.dataflow.configuration.jvm.serialized.PositionModifier +import org.opentaint.dataflow.configuration.jvm.serialized.SerializedTaintConfig +import org.opentaint.dataflow.configuration.jvm.serialized.SerializedItem +import org.opentaint.semgrep.pattern.SemgrepLoadTrace +import org.opentaint.semgrep.pattern.SemgrepRuleLoader +import org.opentaint.semgrep.pattern.TaintRuleFromSemgrep +import org.opentaint.semgrep.pattern.conversion.JavaLanguageStrategy +import org.opentaint.semgrep.pattern.createTaintConfig +import kotlin.io.path.Path +import kotlin.test.Test +import kotlin.test.assertTrue + +/** + * The star / pattern-not coincidence matrix on a METHOD-DECLARATION source (a `pattern-not` that + * negates the same parameter position). This is a distinct code path from the call-argument shape in + * [StarPatternNotFieldOnlyTest]: a method-declaration `pattern-not` reaches + * `MethodConstraintsSolver.addNegative`, whereas a call-argument `pattern-not` collapses at an + * earlier automata-transform phase. + * + * With `$X` (base, A) and `$*X` (whole-object, A*) kept DISTINCT under the implication `A => A*`: + * - T/F (`$*X` positive ^ `pattern-not $X`) => `!A ^ A*` = field-only (keep field, drop base). + * - T/T (`$*X` positive ^ `pattern-not $*X`) => `!A* ^ A*` = contradiction = exclude-all. + * The two must produce DIFFERENT configs. + */ +class StarPatternNotCoincidenceTest { + private fun loadConfig(ruleText: String): SerializedTaintConfig? { + val trace = SemgrepLoadTrace() + val loader = SemgrepRuleLoader(listOf(JavaLanguageStrategy())) + loader.registerRuleSet(ruleText, Path("star.yaml"), Path("."), trace) + val ruleWithMeta = loader.loadRules().rulesWithMeta.singleOrNull() + @Suppress("UNCHECKED_CAST") + return (ruleWithMeta?.first as? TaintRuleFromSemgrep)?.createTaintConfig() + } + + /** + * A method-declaration source whose `pattern-not` negates the same `$UNTRUSTED` parameter. + * @param positiveStar star on the positive `$UNTRUSTED` occurrence + * @param notMetavar the metavar spelled in the `pattern-not` param slot (with or without `*`) + */ + private fun methodRule(id: String, positiveStar: Boolean, notMetavar: String): String { + val pos = if (positiveStar) "${'$'}*UNTRUSTED" else "${'$'}UNTRUSTED" + return """ + rules: + - id: $id + severity: NOTE + message: x + languages: [java] + mode: taint + pattern-sources: + - patterns: + - pattern: | + @${'$'}ANNOTATION(...) + ${'$'}RETURNTYPE ${'$'}METHODNAME(..., ${'$'}TYPE $pos,...) { + ... + } + - pattern-not: | + @${'$'}ANNOTATION(...) + ${'$'}RETURNTYPE ${'$'}METHODNAME(..., @PathVariable ${'$'}TYPE $notMetavar,...) { + ... + } + pattern-sinks: + - pattern: sink(${'$'}S); + """.trimIndent() + } + + @Test + fun `T-F is a supported field-only exclusion and loads`() { + // `$*UNTRUSTED` positive (A*) + `pattern-not $UNTRUSTED` (base A) => `!A ^ A*` = field-only. + val config = loadConfig(methodRule("cmp", positiveStar = true, notMetavar = "${'$'}UNTRUSTED")) + assertTrue(config != null, "field-only T/F rule must load") + } + + @Test + fun `T-F is field-only, distinct from the full-exclusion T-T case`() { + // Same id so the two configs are comparable (marks embed the rule id). + val tf = loadConfig(methodRule("cmp", positiveStar = true, notMetavar = "${'$'}UNTRUSTED")) + val tt = loadConfig(methodRule("cmp", positiveStar = true, notMetavar = "${'$'}*UNTRUSTED")) + assertTrue(tf != null, "T/F (field-only) rule must load") + // T/T (`$*UNTRUSTED` ^ `pattern-not $*UNTRUSTED`) is a genuine contradiction => exclude-all, + // which drops the source, so its config differs from the field-only T/F config. + assertTrue(tf != tt, "T/F (field-only) must differ from T/T (exclude-all); tt=$tt") + } + + @Test + fun `structural non-coinciding pattern-not loads`() { + // The pattern-not negates the same position with a DIFFERENT metavar (`$OTHER`) — a genuine + // structural exclusion, not a coincidence with the positive `$*UNTRUSTED`. + val config = loadConfig(methodRule("structural", positiveStar = true, notMetavar = "${'$'}OTHER")) + assertTrue(config != null, "a non-coinciding structural pattern-not rule must load") + } + private fun PositionBaseWithModifiers.hasAnyField(): Boolean = + this is PositionBaseWithModifiers.WithModifiers && + PositionModifier.AnyField in modifiers + +} diff --git a/core/opentaint-java-querylang/src/test/kotlin/org/opentaint/semgrep/StarPatternNotFieldOnlyTest.kt b/core/opentaint-java-querylang/src/test/kotlin/org/opentaint/semgrep/StarPatternNotFieldOnlyTest.kt new file mode 100644 index 000000000..9c3ce4594 --- /dev/null +++ b/core/opentaint-java-querylang/src/test/kotlin/org/opentaint/semgrep/StarPatternNotFieldOnlyTest.kt @@ -0,0 +1,155 @@ +package org.opentaint.semgrep + +import org.opentaint.dataflow.configuration.jvm.serialized.PositionBaseWithModifiers +import org.opentaint.dataflow.configuration.jvm.serialized.PositionModifier +import org.opentaint.dataflow.configuration.jvm.serialized.SerializedCondition +import org.opentaint.dataflow.configuration.jvm.serialized.SerializedItem +import org.opentaint.dataflow.configuration.jvm.serialized.SerializedTaintConfig +import org.opentaint.dataflow.configuration.jvm.serialized.SinkRule +import org.opentaint.semgrep.pattern.SemgrepLoadTrace +import org.opentaint.semgrep.pattern.SemgrepRuleLoader +import org.opentaint.semgrep.pattern.TaintRuleFromSemgrep +import org.opentaint.semgrep.pattern.conversion.JavaLanguageStrategy +import org.opentaint.semgrep.pattern.createTaintConfig +import org.opentaint.semgrep.pattern.errorEntries +import kotlin.io.path.Path +import kotlin.test.Test +import kotlin.test.assertTrue + +/** + * `pattern: sink($*X)` (whole-object / any-field) with a coinciding `pattern-not: sink($X)` + * (base/value) at the SAME position is a SCOPED exclusion: exclude the base value, keep the + * fields. With $X and $*X kept distinct and the implication A => A* encoded, that cell + * (`!A ^ A*`) is SAT and must compile to `ContainsMarkOnAnyField(pos) ^ !ContainsMark(pos)`. + */ +class StarPatternNotFieldOnlyTest { + private fun config(ruleText: String): SerializedTaintConfig { + val trace = SemgrepLoadTrace() + val loader = SemgrepRuleLoader(listOf(JavaLanguageStrategy())) + loader.registerRuleSet(ruleText, Path("star.yaml"), Path("."), trace) + val rules = loader.loadRules().rulesWithMeta + val rule = rules.singleOrNull()?.first + ?: error("expected exactly 1 rule, got ${rules.size}; trace errors=${trace.errorEntries().map { it.message }}") + @Suppress("UNCHECKED_CAST") + return (rule as TaintRuleFromSemgrep).createTaintConfig() + } + + private fun flatten(c: SerializedCondition): List = when (c) { + is SerializedCondition.Or -> listOf(c) + c.anyOf.flatMap { flatten(it) } + is SerializedCondition.And -> listOf(c) + c.allOf.flatMap { flatten(it) } + is SerializedCondition.Not -> listOf(c) + flatten(c.not) + else -> listOf(c) + } + + private fun sinkConditions(cfg: SerializedTaintConfig): List = + (cfg.sink.orEmpty() + cfg.methodExitSink.orEmpty() + cfg.methodEntrySink.orEmpty()) + .filterIsInstance() + .mapNotNull { it.condition } + .flatMap { flatten(it) } + + private val rule = """ + rules: + - id: fo + severity: NOTE + message: x + languages: [java] + mode: taint + pattern-sources: + - pattern: ${'$'}X = src(); + pattern-sinks: + - patterns: + - pattern: sink(${'$'}*X); + - pattern-not: sink(${'$'}X); + - focus-metavariable: ${'$'}X + """.trimIndent() + + + private fun ruleCount(sinkPatterns: String): Int { + val trace = SemgrepLoadTrace() + val loader = SemgrepRuleLoader(listOf(JavaLanguageStrategy())) + val text = """ + rules: + - id: c + severity: NOTE + message: x + languages: [java] + mode: taint + pattern-sources: + - pattern: ${'$'}X = src(); + pattern-sinks: + - patterns: +$sinkPatterns + - focus-metavariable: ${'$'}X + """.trimIndent() + loader.registerRuleSet(text, Path("c.yaml"), Path("."), trace) + return loader.loadRules().rulesWithMeta.size + } + + @Test + fun `starred sink with coinciding unstarred pattern-not is field-only`() { + // !A ^ A* + val conds = sinkConditions(config(rule)) + assertTrue( + conds.anyContainsMark(anyField = true), + "field-only sink must REQUIRE any-field taint (ContainsMarkOnAnyField); got $conds" + ) + assertTrue( + conds.anyNotContainsMark(anyField = false), + "field-only sink must EXCLUDE base/value taint (Not(ContainsMark)); got $conds" + ) + } + + @Test + fun `A and A star both positive minimizes to A (base only, no any-field)`() { + // A ^ A* == A: base subsumes base-or-any-field. + val conds = sinkConditions(config(""" + rules: + - id: aa + severity: NOTE + message: x + languages: [java] + mode: taint + pattern-sources: + - pattern: ${'$'}X = src(); + pattern-sinks: + - patterns: + - pattern: sink(${'$'}X); + - pattern: sink(${'$'}*X); + - focus-metavariable: ${'$'}X + """.trimIndent())) + assertTrue( + conds.anyContainsMark(anyField = false), + "A ^ A* must keep the base ContainsMark; got $conds" + ) + assertTrue( + conds.noneContainsMark(anyField = true), + "A ^ A* must drop the redundant any-field (solution is A); got $conds" + ) + } + + @Test + fun `base positive with starred pattern-not is unsatisfiable`() { + // A ^ !A* : A => A*, so excluding A* contradicts A -> UNSAT -> the sink variant is dropped. + val n = ruleCount( + " - pattern: sink(${'$'}X);\n" + + " - pattern-not: sink(${'$'}*X);" + ) + assertTrue(n == 0, "A ^ !A* must be unsatisfiable (rule dropped); got rules=$n") + } + private fun List.anyContainsMark(anyField: Boolean): Boolean = + any { it is SerializedCondition.ContainsMark && it.pos.hasAnyField() == anyField } + + private fun List.noneContainsMark(anyField: Boolean): Boolean = + none { it is SerializedCondition.ContainsMark && it.pos.hasAnyField() == anyField } + + private fun List.anyNotContainsMark(anyField: Boolean): Boolean = + any { condition -> + val negated = (condition as? SerializedCondition.Not)?.not as? SerializedCondition.ContainsMark + negated?.pos?.hasAnyField() == anyField + } + + private fun PositionBaseWithModifiers.hasAnyField(): Boolean = + this is PositionBaseWithModifiers.WithModifiers && + PositionModifier.AnyField in modifiers + +} diff --git a/core/opentaint-java-querylang/src/test/kotlin/org/opentaint/semgrep/pattern/CreateTaintConfig.kt b/core/opentaint-java-querylang/src/test/kotlin/org/opentaint/semgrep/pattern/CreateTaintConfig.kt new file mode 100644 index 000000000..1d9aab42b --- /dev/null +++ b/core/opentaint-java-querylang/src/test/kotlin/org/opentaint/semgrep/pattern/CreateTaintConfig.kt @@ -0,0 +1,28 @@ +package org.opentaint.semgrep.pattern + +import org.opentaint.dataflow.configuration.jvm.serialized.SerializedFieldRule +import org.opentaint.dataflow.configuration.jvm.serialized.SerializedItem +import org.opentaint.dataflow.configuration.jvm.serialized.SerializedRule +import org.opentaint.dataflow.configuration.jvm.serialized.SerializedTaintConfig + +/** + * Test helper: flattens a generated rule into a [SerializedTaintConfig] so the star-operator + * rule-generation tests can inspect the emitted source/sink/passThrough/cleaner items directly. + * + * The production analyzer no longer needs this (rules flow through the rule provider), so it lives + * in the test scope. + */ +fun TaintRuleFromSemgrep.createTaintConfig(): SerializedTaintConfig { + val rules = taintRules.flatMap { it.rules } + return SerializedTaintConfig( + entryPoint = rules.filterIsInstance(), + source = rules.filterIsInstance(), + methodExitSource = rules.filterIsInstance(), + sink = rules.filterIsInstance(), + passThrough = rules.filterIsInstance(), + cleaner = rules.filterIsInstance(), + methodExitSink = rules.filterIsInstance(), + methodEntrySink = rules.filterIsInstance(), + staticFieldSource = rules.filterIsInstance(), + ) +} diff --git a/core/opentaint-java-querylang/src/test/kotlin/org/opentaint/semgrep/util/SampleBasedTest.kt b/core/opentaint-java-querylang/src/test/kotlin/org/opentaint/semgrep/util/SampleBasedTest.kt index 257088163..46eabecbf 100644 --- a/core/opentaint-java-querylang/src/test/kotlin/org/opentaint/semgrep/util/SampleBasedTest.kt +++ b/core/opentaint-java-querylang/src/test/kotlin/org/opentaint/semgrep/util/SampleBasedTest.kt @@ -1,6 +1,7 @@ package org.opentaint.semgrep.util import base.RuleSample +import org.opentaint.dataflow.ap.ifds.access.AnyAccessorUnrollStrategy import org.opentaint.dataflow.configuration.jvm.serialized.SerializedItem import org.opentaint.dataflow.configuration.jvm.serialized.SerializedTaintAssignAction import org.opentaint.dataflow.configuration.jvm.serialized.SerializedTaintConfig @@ -21,12 +22,14 @@ abstract class SampleBasedTest( ) { inline fun runTest( expectStateVar: Boolean = false, + unrollStrategy: AnyAccessorUnrollStrategy = AnyAccessorUnrollStrategy.AnyAccessorDisabled, noinline provideAdditionalRules: (SerializedTaintConfig) -> SerializedTaintConfig = { it } - ) = runClassTest(getFullyQualifiedClassName(), expectStateVar, provideAdditionalRules) + ) = runClassTest(getFullyQualifiedClassName(), expectStateVar, unrollStrategy, provideAdditionalRules) fun runClassTest( sampleClassName: String, expectStateVar: Boolean, + unrollStrategy: AnyAccessorUnrollStrategy, provideAdditionalRules: (SerializedTaintConfig) -> SerializedTaintConfig ) { val data = sampleData[sampleClassName] ?: error("No sample data for $sampleClassName") @@ -57,7 +60,7 @@ abstract class SampleBasedTest( val configWithExtraRules = provideAdditionalRules(SerializedTaintConfig()) - val results = runner.run(javaRule, configWithExtraRules, configurationRequired, allSamples) + val results = runner.run(javaRule, configWithExtraRules, configurationRequired, allSamples, unrollStrategy) val missedPositive = hashSetOf() for (sample in data.positiveClasses) { diff --git a/core/opentaint-java-querylang/src/test/kotlin/org/opentaint/semgrep/util/TestAnalysisRunner.kt b/core/opentaint-java-querylang/src/test/kotlin/org/opentaint/semgrep/util/TestAnalysisRunner.kt index d49df73e5..b07b7f2e6 100644 --- a/core/opentaint-java-querylang/src/test/kotlin/org/opentaint/semgrep/util/TestAnalysisRunner.kt +++ b/core/opentaint-java-querylang/src/test/kotlin/org/opentaint/semgrep/util/TestAnalysisRunner.kt @@ -4,6 +4,9 @@ import kotlinx.coroutines.runBlocking import org.opentaint.common.sast.dataflow.TaintAnalyzer import org.opentaint.common.sast.dataflow.TaintAnalyzerOptions import org.opentaint.config.JavaDefaultConfigLoader +import org.opentaint.dataflow.ap.ifds.Accessor +import org.opentaint.dataflow.ap.ifds.ElementAccessor +import org.opentaint.dataflow.ap.ifds.FieldAccessor import org.opentaint.dataflow.ap.ifds.access.AnyAccessorUnrollStrategy import org.opentaint.dataflow.ap.ifds.access.ApMode import org.opentaint.dataflow.ap.ifds.trace.VulnerabilityWithTrace @@ -68,15 +71,19 @@ class TestAnalysisRunner( } @Suppress("UNCHECKED_CAST") - private fun setupEngine(configProvider: TaintRulesProvider): TaintAnalyzer { + private fun setupEngine( + configProvider: TaintRulesProvider, + unrollStrategy: AnyAccessorUnrollStrategy, + ): TaintAnalyzer { val options = TaintAnalyzerOptions( ifdsTimeout = 1.minutes, ifdsApMode = ApMode.Tree ) + val strategy = unrollStrategy val analyzer = object : TaintAnalyzer(options) { override val unrollStrategy: AnyAccessorUnrollStrategy - get() = AnyAccessorUnrollStrategy.AnyAccessorDisabled + get() = strategy override fun analysisGraph() = ifdsAnalysisGraph override fun analysisManager() = JIRAnalysisManager(cp, refManager, configProvider) @@ -97,7 +104,8 @@ class TestAnalysisRunner( rule: TaintRuleFromSemgrep, config: SerializedTaintConfig, useDefaultConfig: Boolean, - samples: Set + samples: Set, + unrollStrategy: AnyAccessorUnrollStrategy = AnyAccessorUnrollStrategy.AnyAccessorDisabled, ): Map> = samples.associate { sample -> val cls = cp.findClassOrNull(sample) ?: error("No sample in CP") @@ -105,7 +113,7 @@ class TestAnalysisRunner( ?: error("No entrypoint in $sample") val rulesProvider = rulesProvider(rule, config, useDefaultConfig) - setupEngine(rulesProvider).use { engine -> + setupEngine(rulesProvider, unrollStrategy).use { engine -> val traces = engine.analyzeWithIfds(listOf(ep)).first sample to traces } @@ -133,4 +141,17 @@ class TestAnalysisRunner( cfg = JIRMethodExitRuleProvider(cfg) return cfg } + + companion object { + /** + * Mirrors the Go sample harness ([GoSampleBasedTestBase]): unrolls the whole-object + * any-accessor taint of a starred source/sink down to concrete field and element reads. + * Opt in per-sample (e.g. the starred-SOURCE sample) so a source-star's any-field taint + * reaches a concrete field read; the default stays [AnyAccessorUnrollStrategy.AnyAccessorDisabled]. + */ + val AnyAccessorEnabled: AnyAccessorUnrollStrategy = object : AnyAccessorUnrollStrategy { + override fun unrollAccessor(accessor: Accessor): Boolean = + accessor is FieldAccessor || accessor is ElementAccessor + } + } } diff --git a/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/jvm/sast/dataflow/StringConcatRuleProvider.kt b/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/jvm/sast/dataflow/StringConcatRuleProvider.kt index 3e15cce49..e6ab0d161 100644 --- a/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/jvm/sast/dataflow/StringConcatRuleProvider.kt +++ b/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/jvm/sast/dataflow/StringConcatRuleProvider.kt @@ -1,6 +1,7 @@ package org.opentaint.jvm.sast.dataflow import org.opentaint.dataflow.ap.ifds.access.FactAp +import org.opentaint.dataflow.configuration.jvm.ActionPosition import org.opentaint.dataflow.configuration.jvm.Argument import org.opentaint.dataflow.configuration.jvm.CopyAllMarks import org.opentaint.dataflow.configuration.jvm.Result @@ -24,7 +25,7 @@ class StringConcatRuleProvider(private val base: TaintRulesProvider) : TaintRule return TaintPassThrough( method = method, condition = mkTrue(), - actionsAfter = possibleArgs.map { CopyAllMarks(from = it, to = Result) }, + actionsAfter = possibleArgs.map { CopyAllMarks(from = ActionPosition.Exact(it), to = ActionPosition.Exact(Result)) }, info = null ) } diff --git a/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/jvm/sast/dataflow/rules/MethodTaintConfigurationResolver.kt b/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/jvm/sast/dataflow/rules/MethodTaintConfigurationResolver.kt index 4a46a7796..577f89dcc 100644 --- a/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/jvm/sast/dataflow/rules/MethodTaintConfigurationResolver.kt +++ b/core/opentaint-jvm-sast-dataflow/src/main/kotlin/org/opentaint/jvm/sast/dataflow/rules/MethodTaintConfigurationResolver.kt @@ -6,6 +6,9 @@ import org.opentaint.dataflow.configuration.CommonCondition import org.opentaint.dataflow.configuration.CommonTaintConfigurationSinkMeta import org.opentaint.dataflow.configuration.isFalse import org.opentaint.dataflow.configuration.jvm.Action +import org.opentaint.dataflow.configuration.jvm.ActionPosition +import org.opentaint.dataflow.configuration.jvm.ActionPosition.AnyAccessorAfter +import org.opentaint.dataflow.configuration.jvm.ActionPosition.Exact import org.opentaint.dataflow.configuration.jvm.Argument import org.opentaint.dataflow.configuration.jvm.AssignMark import org.opentaint.dataflow.configuration.jvm.ClassStatic @@ -20,7 +23,6 @@ import org.opentaint.dataflow.configuration.jvm.ConstantMatches import org.opentaint.dataflow.configuration.jvm.ConstantStringValue import org.opentaint.dataflow.configuration.jvm.ContainsMark import org.opentaint.dataflow.configuration.jvm.CopyAllMarks -import org.opentaint.dataflow.jvm.ap.ifds.taint.ContainsMarkOnAnyField import org.opentaint.dataflow.configuration.jvm.CopyMark import org.opentaint.dataflow.configuration.jvm.IsConstant import org.opentaint.dataflow.configuration.jvm.IsNull @@ -62,11 +64,13 @@ import org.opentaint.dataflow.configuration.jvm.serialized.SerializedTypeNameMat import org.opentaint.dataflow.configuration.jvm.serialized.SinkMetaData import org.opentaint.dataflow.configuration.jvm.serialized.SinkRule import org.opentaint.dataflow.configuration.jvm.serialized.SourceRule +import org.opentaint.dataflow.configuration.jvm.serialized.beforeFirstAnyField import org.opentaint.dataflow.configuration.mkAnd import org.opentaint.dataflow.configuration.mkFalse import org.opentaint.dataflow.configuration.mkOr import org.opentaint.dataflow.configuration.mkTrue import org.opentaint.dataflow.configuration.simplify +import org.opentaint.dataflow.jvm.ap.ifds.taint.ContainsMarkOnAnyField import org.opentaint.ir.api.jvm.JIRAnnotated import org.opentaint.ir.api.jvm.JIRAnnotation import org.opentaint.ir.api.jvm.JIRClassType @@ -200,7 +204,7 @@ class MethodTaintConfigurationResolver( is SerializedRule.Sink -> { TaintMethodSink( method, condition, - trackFactsReachAnalysisEnd?.flatMap { it.resolveNoArray(ctx) }.orEmpty(), + trackFactsReachAnalysisEnd?.flatMap { it.resolve(ctx) }.orEmpty(), ruleId(), meta(), info, serializedId ) } @@ -208,7 +212,7 @@ class MethodTaintConfigurationResolver( is SerializedRule.MethodExitSink -> { TaintMethodExitSink( method, condition, - trackFactsReachAnalysisEnd?.flatMap { it.resolveNoArray(ctx) }.orEmpty(), + trackFactsReachAnalysisEnd?.flatMap { it.resolve(ctx) }.orEmpty(), ruleId(), meta(), info, serializedId ) } @@ -216,7 +220,7 @@ class MethodTaintConfigurationResolver( is SerializedRule.MethodEntrySink -> { TaintMethodEntrySink( method, condition, - trackFactsReachAnalysisEnd?.flatMap { it.resolveNoArray(ctx) }.orEmpty(), + trackFactsReachAnalysisEnd?.flatMap { it.resolve(ctx) }.orEmpty(), ruleId(), meta(), info, serializedId ) } @@ -444,18 +448,20 @@ class MethodTaintConfigurationResolver( } } - is SerializedCondition.ContainsMark -> mkOr( - pos.resolvePosition(ctx) - .flatMap { it.resolveArrayPosition() } - .map { position -> - val mark = taintMarkManager.taintMark(tainted) - if (position is PositionWithAccess && position.access == PositionAccessor.AnyFieldAccessor) { - ContainsMarkOnAnyField(position.base, mark).atom() + is SerializedCondition.ContainsMark -> { + val (position, hasAnyField) = pos.beforeFirstAnyField() + val mark = taintMarkManager.taintMark(tainted) + mkOr( + position.resolvePosition(ctx).map { + if (hasAnyField) { + ContainsMarkOnAnyField(it, mark).atom() } else { - ContainsMark(position, mark).atom() + ContainsMark(it, mark).atom() } } - ) + ) + } + is SerializedCondition.IsType -> resolveIsType(ctx) @@ -542,16 +548,23 @@ class MethodTaintConfigurationResolver( return classType.declaredMethods.find { it.method == method } } - private fun SerializedTaintAssignAction.resolveWithArray(ctx: AnyArgSpecializationCtx): List = - pos.resolvePositionWithAnnotationConstraint(ctx, annotatedWith?.asAnnotationConstraint()) - .flatMap { it.resolveArrayPosition() } + private fun SerializedTaintAssignAction.resolve(ctx: AnyArgSpecializationCtx): List = + pos.resolveActionPosition(ctx, annotatedWith?.asAnnotationConstraint()) .map { AssignMark(taintMarkManager.taintMark(kind), it) } - private fun SerializedTaintAssignAction.resolveNoArray(ctx: AnyArgSpecializationCtx): List = - pos.resolvePositionWithAnnotationConstraint(ctx, annotatedWith?.asAnnotationConstraint()) - .flatMap { it.resolveArrayPosition() } + // Source actions on an array- or Object-typed position taint the element as well as the + // position itself. The starred rules that express this explicitly land in 3-rules; until + // then the duplication has to stay here or array sources lose their element taint. + private fun SerializedTaintAssignAction.resolveWithArray(ctx: AnyArgSpecializationCtx): List = + pos.resolveActionPosition(ctx, annotatedWith?.asAnnotationConstraint()) + .flatMap { it.resolveArrayActionPosition() } .map { AssignMark(taintMarkManager.taintMark(kind), it) } + private fun ActionPosition.resolveArrayActionPosition(): List = when (this) { + is Exact -> position.resolveArrayPosition().map { Exact(it) } + is AnyAccessorAfter -> listOf(this) + } + private fun Position.resolveArrayPosition(): List = when (this) { is ClassStatic -> listOf(this) is PositionWithAccess -> base.resolveArrayPosition().map { PositionWithAccess(it, access) } @@ -571,8 +584,8 @@ class MethodTaintConfigurationResolver( } private fun SerializedTaintPassAction.resolve(ctx: AnyArgSpecializationCtx): List = - from.resolvePosition(ctx).flatMap { fromPos -> - to.resolvePosition(ctx).map { toPos -> + from.resolveActionPosition(ctx).flatMap { fromPos -> + to.resolveActionPosition(ctx).map { toPos -> val taintKind = taintKind if (taintKind == null) { CopyAllMarks(fromPos, toPos) @@ -583,15 +596,19 @@ class MethodTaintConfigurationResolver( } private fun SerializedTaintCleanAction.resolve(ctx: AnyArgSpecializationCtx): List = - pos.resolvePosition(ctx) - .map { pos -> - val taintKind = taintKind - if (taintKind == null) { - RemoveAllMarks(pos) - } else { - RemoveMark(taintMarkManager.taintMark(taintKind), pos, reach) - } - } + pos.resolveActionPosition(ctx).map { pos -> + taintKind?.let { RemoveMark(taintMarkManager.taintMark(it), pos) } ?: RemoveAllMarks(pos) + } + + private fun PositionBaseWithModifiers.resolveActionPosition( + ctx: AnyArgSpecializationCtx, + annotation: AnnotationConstraint? = null, + ): List { + val (position, hasAnyField) = beforeFirstAnyField() + return position.resolvePositionWithAnnotationConstraint(ctx, annotation).map { + if (hasAnyField) AnyAccessorAfter(it) else Exact(it) + } + } private fun PositionBaseWithModifiers.resolvePosition( ctx: AnyArgSpecializationCtx, @@ -617,7 +634,7 @@ class MethodTaintConfigurationResolver( resolvedBase.map { b -> modifiers.fold(b) { basePos, modifier -> val accessor = when (modifier) { - PositionModifier.AnyField -> PositionAccessor.AnyFieldAccessor + PositionModifier.AnyField -> error("AnyField must be resolved before ordinary positions") PositionModifier.ArrayElement -> PositionAccessor.ElementAccessor is PositionModifier.Field -> { PositionAccessor.FieldAccessor( 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 a2d7c5bf2..8253b1753 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 @@ -1,5 +1,6 @@ package org.opentaint.jvm.sast.dataflow.rules +import org.opentaint.dataflow.configuration.jvm.ActionPosition import org.opentaint.dataflow.configuration.jvm.AssignMark import org.opentaint.dataflow.configuration.jvm.Result import org.opentaint.dataflow.configuration.jvm.TaintCleaner @@ -209,7 +210,7 @@ class TaintConfiguration(private val cp: JIRClasspath) { if (action.pos !is PositionBaseWithModifiers.BaseOnly || action.pos.base !is PositionBase.Result) { TODO("Complex field action position") } - actions += AssignMark(taintMarkManager.taintMark(action.kind), Result) + actions += AssignMark(taintMarkManager.taintMark(action.kind), ActionPosition.Exact(Result)) } return listOf(TaintStaticFieldSource(field, mkTrue(), actions, info, serializedId)) } diff --git a/core/samples/src/main/java/test/samples/CleanerDslSample.java b/core/samples/src/main/java/test/samples/CleanerDslSample.java index 6003d6ab8..5ce3e9d58 100644 --- a/core/samples/src/main/java/test/samples/CleanerDslSample.java +++ b/core/samples/src/main/java/test/samples/CleanerDslSample.java @@ -1,29 +1,31 @@ package test.samples; public class CleanerDslSample { - public static class Node { + public interface MatrixValue { } + + public static class Node implements MatrixValue { public Level2 k; public Node child; } - public static class Level2 { + public static class Level2 implements MatrixValue { public Level3 k; public Node p; } - public static class Level3 { + public static class Level3 implements MatrixValue { public Level4 k; } - public static class Level4 { + public static class Level4 implements MatrixValue { public Level5 k; } - public static class Level5 { + public static class Level5 implements MatrixValue { public Level6 k; } - public static class Level6 { + public static class Level6 implements MatrixValue { } public Node sourcePlain() { @@ -427,95 +429,95 @@ public void recursiveAnyOnlyDepth2Sink(Node value) { } // Every matrix endpoint has a distinct method so its rule id identifies one exact coordinate. - public void sinkPlainPlainPlainDepth0(Object value) { } - public void sinkPlainPlainPlainDepth1(Object value) { } - public void sinkPlainPlainPlainDepth2(Object value) { } - public void sinkPlainPlainPlainDepth3(Object value) { } - public void sinkPlainPlainPlainDepth4(Object value) { } - public void sinkPlainPlainPlainDepth5(Object value) { } - public void sinkPlainPlainAnyDepth0(Object value) { } - public void sinkPlainPlainAnyDepth1(Object value) { } - public void sinkPlainPlainAnyDepth2(Object value) { } - public void sinkPlainPlainAnyDepth3(Object value) { } - public void sinkPlainPlainAnyDepth4(Object value) { } - public void sinkPlainPlainAnyDepth5(Object value) { } - public void sinkPlainAnyPlainDepth0(Object value) { } - public void sinkPlainAnyPlainDepth1(Object value) { } - public void sinkPlainAnyPlainDepth2(Object value) { } - public void sinkPlainAnyPlainDepth3(Object value) { } - public void sinkPlainAnyPlainDepth4(Object value) { } - public void sinkPlainAnyPlainDepth5(Object value) { } - public void sinkPlainAnyAnyDepth0(Object value) { } - public void sinkPlainAnyAnyDepth1(Object value) { } - public void sinkPlainAnyAnyDepth2(Object value) { } - public void sinkPlainAnyAnyDepth3(Object value) { } - public void sinkPlainAnyAnyDepth4(Object value) { } - public void sinkPlainAnyAnyDepth5(Object value) { } - public void sinkAnyPlainPlainDepth0(Object value) { } - public void sinkAnyPlainPlainDepth1(Object value) { } - public void sinkAnyPlainPlainDepth2(Object value) { } - public void sinkAnyPlainPlainDepth3(Object value) { } - public void sinkAnyPlainPlainDepth4(Object value) { } - public void sinkAnyPlainPlainDepth5(Object value) { } - public void sinkAnyPlainAnyDepth0(Object value) { } - public void sinkAnyPlainAnyDepth1(Object value) { } - public void sinkAnyPlainAnyDepth2(Object value) { } - public void sinkAnyPlainAnyDepth3(Object value) { } - public void sinkAnyPlainAnyDepth4(Object value) { } - public void sinkAnyPlainAnyDepth5(Object value) { } - public void sinkAnyAnyPlainDepth0(Object value) { } - public void sinkAnyAnyPlainDepth1(Object value) { } - public void sinkAnyAnyPlainDepth2(Object value) { } - public void sinkAnyAnyPlainDepth3(Object value) { } - public void sinkAnyAnyPlainDepth4(Object value) { } - public void sinkAnyAnyPlainDepth5(Object value) { } - public void sinkAnyAnyAnyDepth0(Object value) { } - public void sinkAnyAnyAnyDepth1(Object value) { } - public void sinkAnyAnyAnyDepth2(Object value) { } - public void sinkAnyAnyAnyDepth3(Object value) { } - public void sinkAnyAnyAnyDepth4(Object value) { } - public void sinkAnyAnyAnyDepth5(Object value) { } - - public void sinkPlainPlainPlainStackDepth1(Object value) { } - public void sinkPlainPlainPlainStackDepth2(Object value) { } - public void sinkPlainPlainPlainStackDepth3(Object value) { } - public void sinkPlainPlainPlainStackDepth4(Object value) { } - public void sinkPlainPlainPlainStackDepth5(Object value) { } - public void sinkPlainPlainAnyStackDepth1(Object value) { } - public void sinkPlainPlainAnyStackDepth2(Object value) { } - public void sinkPlainPlainAnyStackDepth3(Object value) { } - public void sinkPlainPlainAnyStackDepth4(Object value) { } - public void sinkPlainPlainAnyStackDepth5(Object value) { } - public void sinkPlainAnyPlainStackDepth1(Object value) { } - public void sinkPlainAnyPlainStackDepth2(Object value) { } - public void sinkPlainAnyPlainStackDepth3(Object value) { } - public void sinkPlainAnyPlainStackDepth4(Object value) { } - public void sinkPlainAnyPlainStackDepth5(Object value) { } - public void sinkPlainAnyAnyStackDepth1(Object value) { } - public void sinkPlainAnyAnyStackDepth2(Object value) { } - public void sinkPlainAnyAnyStackDepth3(Object value) { } - public void sinkPlainAnyAnyStackDepth4(Object value) { } - public void sinkPlainAnyAnyStackDepth5(Object value) { } - public void sinkAnyPlainPlainStackDepth1(Object value) { } - public void sinkAnyPlainPlainStackDepth2(Object value) { } - public void sinkAnyPlainPlainStackDepth3(Object value) { } - public void sinkAnyPlainPlainStackDepth4(Object value) { } - public void sinkAnyPlainPlainStackDepth5(Object value) { } - public void sinkAnyPlainAnyStackDepth1(Object value) { } - public void sinkAnyPlainAnyStackDepth2(Object value) { } - public void sinkAnyPlainAnyStackDepth3(Object value) { } - public void sinkAnyPlainAnyStackDepth4(Object value) { } - public void sinkAnyPlainAnyStackDepth5(Object value) { } - public void sinkAnyAnyPlainStackDepth1(Object value) { } - public void sinkAnyAnyPlainStackDepth2(Object value) { } - public void sinkAnyAnyPlainStackDepth3(Object value) { } - public void sinkAnyAnyPlainStackDepth4(Object value) { } - public void sinkAnyAnyPlainStackDepth5(Object value) { } - public void sinkAnyAnyAnyStackDepth1(Object value) { } - public void sinkAnyAnyAnyStackDepth2(Object value) { } - public void sinkAnyAnyAnyStackDepth3(Object value) { } - public void sinkAnyAnyAnyStackDepth4(Object value) { } - public void sinkAnyAnyAnyStackDepth5(Object value) { } + public void sinkPlainPlainPlainDepth0(MatrixValue value) { } + public void sinkPlainPlainPlainDepth1(MatrixValue value) { } + public void sinkPlainPlainPlainDepth2(MatrixValue value) { } + public void sinkPlainPlainPlainDepth3(MatrixValue value) { } + public void sinkPlainPlainPlainDepth4(MatrixValue value) { } + public void sinkPlainPlainPlainDepth5(MatrixValue value) { } + public void sinkPlainPlainAnyDepth0(MatrixValue value) { } + public void sinkPlainPlainAnyDepth1(MatrixValue value) { } + public void sinkPlainPlainAnyDepth2(MatrixValue value) { } + public void sinkPlainPlainAnyDepth3(MatrixValue value) { } + public void sinkPlainPlainAnyDepth4(MatrixValue value) { } + public void sinkPlainPlainAnyDepth5(MatrixValue value) { } + public void sinkPlainAnyPlainDepth0(MatrixValue value) { } + public void sinkPlainAnyPlainDepth1(MatrixValue value) { } + public void sinkPlainAnyPlainDepth2(MatrixValue value) { } + public void sinkPlainAnyPlainDepth3(MatrixValue value) { } + public void sinkPlainAnyPlainDepth4(MatrixValue value) { } + public void sinkPlainAnyPlainDepth5(MatrixValue value) { } + public void sinkPlainAnyAnyDepth0(MatrixValue value) { } + public void sinkPlainAnyAnyDepth1(MatrixValue value) { } + public void sinkPlainAnyAnyDepth2(MatrixValue value) { } + public void sinkPlainAnyAnyDepth3(MatrixValue value) { } + public void sinkPlainAnyAnyDepth4(MatrixValue value) { } + public void sinkPlainAnyAnyDepth5(MatrixValue value) { } + public void sinkAnyPlainPlainDepth0(MatrixValue value) { } + public void sinkAnyPlainPlainDepth1(MatrixValue value) { } + public void sinkAnyPlainPlainDepth2(MatrixValue value) { } + public void sinkAnyPlainPlainDepth3(MatrixValue value) { } + public void sinkAnyPlainPlainDepth4(MatrixValue value) { } + public void sinkAnyPlainPlainDepth5(MatrixValue value) { } + public void sinkAnyPlainAnyDepth0(MatrixValue value) { } + public void sinkAnyPlainAnyDepth1(MatrixValue value) { } + public void sinkAnyPlainAnyDepth2(MatrixValue value) { } + public void sinkAnyPlainAnyDepth3(MatrixValue value) { } + public void sinkAnyPlainAnyDepth4(MatrixValue value) { } + public void sinkAnyPlainAnyDepth5(MatrixValue value) { } + public void sinkAnyAnyPlainDepth0(MatrixValue value) { } + public void sinkAnyAnyPlainDepth1(MatrixValue value) { } + public void sinkAnyAnyPlainDepth2(MatrixValue value) { } + public void sinkAnyAnyPlainDepth3(MatrixValue value) { } + public void sinkAnyAnyPlainDepth4(MatrixValue value) { } + public void sinkAnyAnyPlainDepth5(MatrixValue value) { } + public void sinkAnyAnyAnyDepth0(MatrixValue value) { } + public void sinkAnyAnyAnyDepth1(MatrixValue value) { } + public void sinkAnyAnyAnyDepth2(MatrixValue value) { } + public void sinkAnyAnyAnyDepth3(MatrixValue value) { } + public void sinkAnyAnyAnyDepth4(MatrixValue value) { } + public void sinkAnyAnyAnyDepth5(MatrixValue value) { } + + public void sinkPlainPlainPlainStackDepth1(MatrixValue value) { } + public void sinkPlainPlainPlainStackDepth2(MatrixValue value) { } + public void sinkPlainPlainPlainStackDepth3(MatrixValue value) { } + public void sinkPlainPlainPlainStackDepth4(MatrixValue value) { } + public void sinkPlainPlainPlainStackDepth5(MatrixValue value) { } + public void sinkPlainPlainAnyStackDepth1(MatrixValue value) { } + public void sinkPlainPlainAnyStackDepth2(MatrixValue value) { } + public void sinkPlainPlainAnyStackDepth3(MatrixValue value) { } + public void sinkPlainPlainAnyStackDepth4(MatrixValue value) { } + public void sinkPlainPlainAnyStackDepth5(MatrixValue value) { } + public void sinkPlainAnyPlainStackDepth1(MatrixValue value) { } + public void sinkPlainAnyPlainStackDepth2(MatrixValue value) { } + public void sinkPlainAnyPlainStackDepth3(MatrixValue value) { } + public void sinkPlainAnyPlainStackDepth4(MatrixValue value) { } + public void sinkPlainAnyPlainStackDepth5(MatrixValue value) { } + public void sinkPlainAnyAnyStackDepth1(MatrixValue value) { } + public void sinkPlainAnyAnyStackDepth2(MatrixValue value) { } + public void sinkPlainAnyAnyStackDepth3(MatrixValue value) { } + public void sinkPlainAnyAnyStackDepth4(MatrixValue value) { } + public void sinkPlainAnyAnyStackDepth5(MatrixValue value) { } + public void sinkAnyPlainPlainStackDepth1(MatrixValue value) { } + public void sinkAnyPlainPlainStackDepth2(MatrixValue value) { } + public void sinkAnyPlainPlainStackDepth3(MatrixValue value) { } + public void sinkAnyPlainPlainStackDepth4(MatrixValue value) { } + public void sinkAnyPlainPlainStackDepth5(MatrixValue value) { } + public void sinkAnyPlainAnyStackDepth1(MatrixValue value) { } + public void sinkAnyPlainAnyStackDepth2(MatrixValue value) { } + public void sinkAnyPlainAnyStackDepth3(MatrixValue value) { } + public void sinkAnyPlainAnyStackDepth4(MatrixValue value) { } + public void sinkAnyPlainAnyStackDepth5(MatrixValue value) { } + public void sinkAnyAnyPlainStackDepth1(MatrixValue value) { } + public void sinkAnyAnyPlainStackDepth2(MatrixValue value) { } + public void sinkAnyAnyPlainStackDepth3(MatrixValue value) { } + public void sinkAnyAnyPlainStackDepth4(MatrixValue value) { } + public void sinkAnyAnyPlainStackDepth5(MatrixValue value) { } + public void sinkAnyAnyAnyStackDepth1(MatrixValue value) { } + public void sinkAnyAnyAnyStackDepth2(MatrixValue value) { } + public void sinkAnyAnyAnyStackDepth3(MatrixValue value) { } + public void sinkAnyAnyAnyStackDepth4(MatrixValue value) { } + public void sinkAnyAnyAnyStackDepth5(MatrixValue value) { } public void fieldStorePlainSink(Node value) { } public void fieldStoreAnySink(Node value) { } diff --git a/core/src/main/kotlin/org/opentaint/jvm/sast/project/spring/SpringRuleProvider.kt b/core/src/main/kotlin/org/opentaint/jvm/sast/project/spring/SpringRuleProvider.kt index 82d42d283..7f3778015 100644 --- a/core/src/main/kotlin/org/opentaint/jvm/sast/project/spring/SpringRuleProvider.kt +++ b/core/src/main/kotlin/org/opentaint/jvm/sast/project/spring/SpringRuleProvider.kt @@ -5,6 +5,7 @@ import org.opentaint.dataflow.ap.ifds.ClassStaticAccessor import org.opentaint.dataflow.ap.ifds.access.FactAp import org.opentaint.dataflow.ap.ifds.access.InitialFactAp import org.opentaint.dataflow.configuration.CommonConditionRewriter +import org.opentaint.dataflow.configuration.jvm.ActionPosition import org.opentaint.dataflow.configuration.jvm.Argument import org.opentaint.dataflow.configuration.jvm.AssignMark import org.opentaint.dataflow.configuration.jvm.ClassStatic @@ -70,10 +71,14 @@ class SpringRuleProvider( // todo: better handling of suspend functions if (paramTypeName.isKotlinContinuation()) return emptyList() - val allFieldsPosition = PositionWithAccess(assign.position, PositionAccessor.AnyFieldAccessor) - val allFieldsAssign = AssignMark(assign.mark, allFieldsPosition) + return when (val p = assign.position) { + is ActionPosition.AnyAccessorAfter -> listOf(assign) + is ActionPosition.Exact -> { + val allFieldsAssign = AssignMark(assign.mark, ActionPosition.AnyAccessorAfter(p.position)) - return listOf(assign, allFieldsAssign) + listOf(assign, allFieldsAssign) + } + } } override fun sourceRulesForMethod(method: CommonMethod, statement: CommonInst, fact: FactAp?, allRelevant: Boolean): Iterable { @@ -117,7 +122,7 @@ class SpringRuleProvider( val cleaner = TaintCleaner( method, mkTrue(), - cleanupPositions.map { RemoveAllMarks(it) }, + cleanupPositions.map { RemoveAllMarks(ActionPosition.Exact(it)) }, info = null ) @@ -169,7 +174,7 @@ class SpringRuleProvider( private fun RepositoryMethodInfo.actions(): List? { val actions = mutableListOf() val repoPos = PositionWithAccess(This, repositoryContent) - actions += CopyAllMarks(This, This) + actions += CopyAllMarks(ActionPosition.Exact(This), ActionPosition.Exact(This)) when (kind) { SpringRepoQueryKind.SAVE -> { @@ -181,18 +186,18 @@ class SpringRuleProvider( is SpringRepoQueryReturn.Unknown -> return null is SpringRepoQueryReturn.Primitive, is SpringRepoQueryReturn.Single -> { - actions += CopyAllMarks(from = entityPos, to = repoPos) + actions += CopyAllMarks(from = ActionPosition.Exact(entityPos), to = ActionPosition.Exact(repoPos)) } is SpringRepoQueryReturn.Iterable -> { actions += CopyAllMarks( - from = PositionWithAccess(entityPos, iterableElement), - to = repoPos + from = ActionPosition.Exact(PositionWithAccess(entityPos, iterableElement)), + to = ActionPosition.Exact(repoPos) ) } } - actions += CopyAllMarks(from = entityPos, to = Result) + actions += CopyAllMarks(from = ActionPosition.Exact(entityPos), to = ActionPosition.Exact(Result)) } SpringRepoQueryKind.FIND -> @@ -205,20 +210,20 @@ class SpringRuleProvider( is SpringRepoQueryReturn.Primitive -> {} is SpringRepoQueryReturn.Entity -> { - actions += CopyAllMarks(from = repoPos, to = Result) + actions += CopyAllMarks(from = ActionPosition.Exact(repoPos), to = ActionPosition.Exact(Result)) } is SpringRepoQueryReturn.Iterable -> { actions += CopyAllMarks( - from = repoPos, - to = PositionWithAccess(Result, iterableElement) + from = ActionPosition.Exact(repoPos), + to = ActionPosition.Exact(PositionWithAccess(Result, iterableElement)) ) } is SpringRepoQueryReturn.Optional -> { actions += CopyAllMarks( - from = repoPos, - to = PositionWithAccess(Result, optionalElement) + from = ActionPosition.Exact(repoPos), + to = ActionPosition.Exact(PositionWithAccess(Result, optionalElement)) ) } } diff --git a/core/src/test/kotlin/org/opentaint/go/sast/dataflow/CmdInjEnvSinkDiagTest.kt b/core/src/test/kotlin/org/opentaint/go/sast/dataflow/CmdInjEnvSinkDiagTest.kt index 05397cae1..355bc1443 100644 --- a/core/src/test/kotlin/org/opentaint/go/sast/dataflow/CmdInjEnvSinkDiagTest.kt +++ b/core/src/test/kotlin/org/opentaint/go/sast/dataflow/CmdInjEnvSinkDiagTest.kt @@ -11,6 +11,7 @@ import org.opentaint.dataflow.configuration.go.serialized.GoSinkMetaData import org.opentaint.dataflow.configuration.jvm.serialized.PositionBase.Result import org.opentaint.dataflow.configuration.jvm.serialized.PositionBase.This import org.opentaint.dataflow.configuration.jvm.serialized.PositionBaseWithModifiers +import org.opentaint.dataflow.configuration.jvm.serialized.PositionModifier import org.opentaint.go.config.GoDefaultConfigLoader import kotlin.test.Test @@ -31,7 +32,7 @@ class CmdInjEnvSinkDiagTest : AnalysisTest() { private val combinedOutputSink = Sink( pkg = GoNameMatcher.Pattern("(.*/)?exec\\.Cmd\\)"), function = GoNameMatcher.Simple("CombinedOutput"), - condition = GoSerializedCondition.ContainsMarkOnAnyAccessor("taint", PositionBaseWithModifiers.BaseOnly(This)), + condition = GoSerializedCondition.ContainsMark("taint", PositionBaseWithModifiers.WithModifiers(This, listOf(PositionModifier.AnyField))), trackFactsReachAnalysisEnd = emptyList(), id = "cmdinj-test", meta = GoSinkMetaData("Taint sink: CombinedOutput"), diff --git a/core/src/test/kotlin/org/opentaint/jvm/sast/project/tester/ProjectAnalyzerTester.kt b/core/src/test/kotlin/org/opentaint/jvm/sast/project/tester/ProjectAnalyzerTester.kt index 8bdf63551..234372928 100644 --- a/core/src/test/kotlin/org/opentaint/jvm/sast/project/tester/ProjectAnalyzerTester.kt +++ b/core/src/test/kotlin/org/opentaint/jvm/sast/project/tester/ProjectAnalyzerTester.kt @@ -11,6 +11,7 @@ import org.opentaint.dataflow.ap.ifds.access.InitialFactAp import org.opentaint.dataflow.ap.ifds.trace.VulnerabilityWithTrace import org.opentaint.dataflow.configuration.CommonCondition import org.opentaint.dataflow.configuration.CommonTaintConfigurationSinkMeta +import org.opentaint.dataflow.configuration.jvm.ActionPosition import org.opentaint.dataflow.configuration.jvm.AssignMark import org.opentaint.dataflow.configuration.jvm.ContainsMark import org.opentaint.dataflow.configuration.jvm.TaintCleaner @@ -142,7 +143,7 @@ private fun createTestConfig( TaintMethodSource( method = method, condition = mkTrue(), - actionsAfter = listOf(AssignMark(mark, specializePosition(it, source.position).single())), + actionsAfter = listOf(AssignMark(mark, ActionPosition.Exact(specializePosition(it, source.position).single()))), info = null ) }