Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
434e1b0
feat(rules): express Spring whole-object source and sink taint via th…
misonijnik Jul 23, 2026
7125fd3
refactor(rules): field-sensitive java.io.File model and $*VAR syntax
misonijnik Jul 23, 2026
f81973c
test(querylang): coverage for the changed passthrough config entries
misonijnik Jul 23, 2026
f2c0ee8
test(querylang): java.nio buffer passthrough coverage before the rule…
misonijnik Jul 23, 2026
55056c0
refactor(config): split NameClassPair name/className/nameInNamespace
misonijnik Jul 23, 2026
192214e
test(e2e): behavioural coverage for the 9 rule-storage cleanup fixes
misonijnik Jul 23, 2026
33d1b1a
fix(config): close BasicControl#getID whole-object leak (star ctrlSink)
misonijnik Jul 23, 2026
e1ec9c4
test(phase3): probe DateFormatSymbols generic set./get. whole-object …
misonijnik Jul 23, 2026
fb6afdb
fix(config): close DateFormatSymbols set./get. whole-object leak
misonijnik Jul 23, 2026
eeaf821
test(config): pin taint isolation for 8 more split bean classes
misonijnik Jul 23, 2026
edaf96f
test(config): reframe ScriptContext key-insensitivity as accepted, cl…
misonijnik Jul 23, 2026
25af9c3
test(querylang): pin that a starred source reaches a field-sensitive …
misonijnik Jul 24, 2026
1d126af
fix(analyzer): Field based default get
Saloed Aug 11, 2026
b7b47f8
refactor(dataflow): drop the <rule-storage> unroll exception
misonijnik Aug 12, 2026
c127ea4
fix(dataflow): apply the default get model only when no rule matched
misonijnik Aug 12, 2026
f04463e
refactor(dataflow): delete the String bytes clean special case
misonijnik Aug 13, 2026
8afc8a9
refactor(dataflow): delete the array-element mechanism
misonijnik Aug 18, 2026
bde6829
fix(querylang): honour focus-metavariable on sanitizers
misonijnik Aug 19, 2026
1caf0c6
fix(dataflow): answer the field-unfold request on fact-to-fact edges
misonijnik Aug 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import org.opentaint.dataflow.ap.ifds.Accessor
import org.opentaint.dataflow.ap.ifds.AnalysisRunner
import org.opentaint.dataflow.ap.ifds.AnyAccessor
import org.opentaint.dataflow.ap.ifds.ExclusionSet
import org.opentaint.dataflow.ap.ifds.FieldAccessor
import org.opentaint.dataflow.ap.ifds.MethodSummaryEdgeApplicationUtils
import org.opentaint.dataflow.ap.ifds.SideEffectKind
import org.opentaint.dataflow.ap.ifds.access.FinalFactAp
import org.opentaint.dataflow.ap.ifds.access.InitialFactAp
import org.opentaint.dataflow.ap.ifds.analysis.MethodSequentFlowFunction
import org.opentaint.dataflow.ap.ifds.analysis.MethodSideEffectSummaryHandler

Expand All @@ -19,20 +21,52 @@ interface MethodSideEffectHandlerWithAnyAccessorRequestHandling : MethodSideEffe
kind: SideEffectKind
): Set<MethodSequentFlowFunction.Sequent> {
if (kind is TaintMarkFieldUnfoldRequest) {
when (summaryEffect) {
is MethodSummaryEdgeApplicationUtils.SummaryEdgeApplication.SummaryApRefinement -> {
if (!summaryEffect.delta.isEmpty) {
handleMarkAfterAnyFieldRequest(summaryEffect.delta, kind)
}
}
handleUnfoldRequest(summaryEffect, kind)
}

return super.handleZeroToFact(currentFactAp, summaryEffect, kind)
}

/**
* A callee asks for its abstract initial fact to be unfolded when a taint mark its sink needs may
* be hidden under the abstraction. The request has to be answered on fact-to-fact edges too, not
* only on zero-to-fact ones: when the caller is itself analyzed from an initial fact -- i.e. the
* tainted object was passed into the caller as well -- the callee's side effect summary arrives
* here. Dropping it loses every sink whose condition reads a *field* of a formal parameter more
* than one frame below the source.
*
* Answered only while the request is still un-refined, i.e. its fact is the bare abstraction and
* no accessor below the parameter has been materialized yet. Fact-to-fact edges vastly outnumber
* zero-to-fact ones, and refining on all of them does not terminate in any reasonable time.
*/
override fun handleFactToFact(
currentInitialFactAp: InitialFactAp,
currentFactAp: FinalFactAp,
summaryEffect: MethodSummaryEdgeApplicationUtils.SummaryEdgeApplication,
kind: SideEffectKind
): Set<MethodSequentFlowFunction.Sequent> {
if (kind is TaintMarkFieldUnfoldRequest && kind.fact.getAllAccessors().isEmpty()) {
handleUnfoldRequest(summaryEffect, kind)
}

is MethodSummaryEdgeApplicationUtils.SummaryEdgeApplication.SummaryExclusionRefinement -> {
// taint mark requested -> mark not in initial fact, delta is empty -> mark not in fact
return super.handleFactToFact(currentInitialFactAp, currentFactAp, summaryEffect, kind)
}

private fun handleUnfoldRequest(
summaryEffect: MethodSummaryEdgeApplicationUtils.SummaryEdgeApplication,
request: TaintMarkFieldUnfoldRequest
) {
when (summaryEffect) {
is MethodSummaryEdgeApplicationUtils.SummaryEdgeApplication.SummaryApRefinement -> {
if (!summaryEffect.delta.isEmpty) {
handleMarkAfterAnyFieldRequest(summaryEffect.delta, request)
}
}
}

return super.handleZeroToFact(currentFactAp, summaryEffect, kind)
is MethodSummaryEdgeApplicationUtils.SummaryEdgeApplication.SummaryExclusionRefinement -> {
// taint mark requested -> mark not in initial fact, delta is empty -> mark not in fact
}
}
}

private fun handleMarkAfterAnyFieldRequest(
Expand All @@ -41,7 +75,7 @@ interface MethodSideEffectHandlerWithAnyAccessorRequestHandling : MethodSideEffe
) {
val mark = request.mark
val allAccessors = delta.getAllAccessors()
if (mark !in allAccessors) return
val deltaHasMark = mark in allAccessors

val startAccessors = hashSetOf<Accessor>()
for (accessor in delta.getStartAccessors()) {
Expand All @@ -56,8 +90,19 @@ interface MethodSideEffectHandlerWithAnyAccessorRequestHandling : MethodSideEffe
anySuccessors.filterTo(startAccessors) { it !is AnyAccessor }
}

val relevantStartAccessors = startAccessors.filter { accessor ->
accessor == mark || delta.readAccessor(accessor)?.getAllAccessors()?.contains(mark) ?: false
// When the caller already knows where the mark sits, refine on exactly that branch. When it
// does not -- because the caller is analyzed abstractly too and only knows the *shape* the
// value takes below the callee's parameter -- refine on that shape instead, so the callee
// materializes the accessor and can answer once the mark arrives from further up. Only a
// single concrete field qualifies: that is the shape a field-sensitive library model produces
// (`file.path`, `bean.url`), and fanning out over several accessors, or over elements,
// re-analyzes far too much of the program for the chance of finding the mark.
val relevantStartAccessors = if (deltaHasMark) {
startAccessors.filter { accessor ->
accessor == mark || delta.readAccessor(accessor)?.getAllAccessors()?.contains(mark) ?: false
}
} else {
startAccessors.filter { it is FieldAccessor }.takeIf { it.size == 1 }.orEmpty()
}

if (relevantStartAccessors.isEmpty()) return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,14 @@ abstract class TaintUtil<C, Src, Sink, Trace>(val apManager: ApManager) {

abstract fun handleReachedSink(rule: Sink, factReader: FinalFactReader?, evaluatedFacts: List<InitialFactAp>)

open fun patchSinkConditionFactReader(factReaders: List<FinalFactReader>): List<FactReader> = factReaders

fun applySinkRules(
sinkRules: List<RuleWithCondition<Sink>>,
factReader: FinalFactReader?,
markAfterAnyFieldResolver: FactWithMarkAfterAnyAccessorResolver?,
) {
if (sinkRules.isEmpty()) return

val normalConditionFactReaders = factReader?.let { conditionFact(it) }.orEmpty()
val conditionFactReaders = patchSinkConditionFactReader(normalConditionFactReaders)
val conditionFactReaders = factReader?.let { conditionFact(it) }.orEmpty()

sinkRules.applyRuleWithAssumptions(
apManager,
Expand All @@ -45,7 +42,7 @@ abstract class TaintUtil<C, Src, Sink, Trace>(val apManager: ApManager) {
return@applyRuleWithAssumptions
}

factReader?.updateRefinement(normalConditionFactReaders)
factReader?.updateRefinement(conditionFactReaders)
}


Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package org.opentaint.dataflow.go.analysis

import org.opentaint.dataflow.ap.ifds.AccessPathBase
import org.opentaint.dataflow.ap.ifds.ElementAccessor
import org.opentaint.dataflow.ap.ifds.ExclusionSet
import org.opentaint.dataflow.ap.ifds.FactTypeChecker
import org.opentaint.dataflow.ap.ifds.TaintMarkAccessor
Expand All @@ -17,10 +15,7 @@ import org.opentaint.dataflow.go.GoMethodCallFactMapper.mapMethodExitToReturnFlo
import org.opentaint.dataflow.go.rules.GoAssignAction
import org.opentaint.dataflow.go.rules.GoRuleCondition
import org.opentaint.dataflow.go.rules.TaintRule
import org.opentaint.dataflow.taint.FactReader
import org.opentaint.dataflow.taint.FinalFactReader
import org.opentaint.dataflow.taint.FinalFactReaderWithPrefix
import org.opentaint.dataflow.taint.PositionAccess
import org.opentaint.dataflow.taint.TaintSourceActionEvaluator
import org.opentaint.dataflow.taint.TaintUtil
import org.opentaint.ir.go.inst.GoIRInst
Expand Down Expand Up @@ -79,16 +74,6 @@ class GoMethodCallTaintUtil(
return readers
}

override fun patchSinkConditionFactReader(factReaders: List<FinalFactReader>): List<FactReader> {
val elementWrappedReaders = factReaders.mapNotNull { reader ->
val base = reader.factAp.base as? AccessPathBase.Argument ?: return@mapNotNull null
val elementPosition = PositionAccess.Complex(PositionAccess.Simple(base), ElementAccessor)
if (!reader.containsPosition(elementPosition)) return@mapNotNull null
FinalFactReaderWithPrefix(reader, ElementAccessor)
}
return factReaders + elementWrappedReaders
}

override fun handleReachedSink(
rule: TaintRule.Sink,
factReader: FinalFactReader?,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package org.opentaint.dataflow.jvm.ap.ifds

import it.unimi.dsi.fastutil.longs.LongLongImmutablePair
import it.unimi.dsi.fastutil.longs.LongLongPair
import org.opentaint.dataflow.ap.ifds.AccessPathBase
import org.opentaint.dataflow.ap.ifds.Accessor
import org.opentaint.dataflow.ap.ifds.AnyAccessor
import org.opentaint.dataflow.ap.ifds.ClassStaticAccessor
Expand Down Expand Up @@ -32,7 +31,6 @@ import org.opentaint.ir.api.jvm.JIRRefType
import org.opentaint.ir.api.jvm.JIRType
import org.opentaint.ir.api.jvm.JIRTypeVariable
import org.opentaint.ir.api.jvm.JIRUnboundWildcard
import org.opentaint.ir.api.jvm.cfg.JIRCallExpr
import org.opentaint.ir.api.jvm.ext.ifArrayGetElementType
import org.opentaint.ir.api.jvm.ext.isAssignable
import org.opentaint.ir.api.jvm.ext.isSubClassOf
Expand Down Expand Up @@ -192,17 +190,6 @@ class JIRFactTypeChecker(private val cp: JIRClasspath) : FactTypeChecker {
return AccessorCompatibilityFilter(actualType)
}

fun callArgumentMayBeArray(call: JIRCallExpr, arg: AccessPathBase.Argument): Boolean {
val argument = call.args.getOrNull(arg.idx) ?: return false
val argType = argument.type
return argType.mayBeArray()
}

fun JIRType.mayBeArray(): Boolean {
if (this !is JIRRefType) return false
return typeMayBeArrayType(this)
}

private fun accessorActualType(accessPath: List<Accessor>): JIRType? {
val accessor = accessPath.lastOrNull() ?: return null
return when (accessor) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ class JIRMethodCallFlowFunction(
markAfterAnyAccessorResolver = null // we don't expect such marks in pass rules
)

val cleaner = JIRTaintCleanActionEvaluator(typeResolver)
val cleaner = JIRTaintCleanActionEvaluator()

val factReaderBeforeCleaner = FinalFactReader(callerFact, apManager)
val cleanRules = taintCtx.cleanRulesForCallStatement(statement, callExpr, returnValue, callerFact)
Expand Down Expand Up @@ -297,11 +297,12 @@ class JIRMethodCallFlowFunction(
}
}

analysisContext.analysisManager.params.defaultGetModel?.run {
/*todo: fix owasp, propagate default only if passThroughFacts.isNone */
val defaultRules = defaultPropagationRules(method)
val defaultPass = applyPassThrough(defaultRules, conditionEvaluator, passEvaluator)
passThroughFacts = passThroughFacts.merge(defaultPass)
if (passThroughFacts.isNone) {
analysisContext.analysisManager.params.defaultGetModel?.run {
val defaultRules = defaultPropagationRules(method)
val defaultPass = applyPassThrough(defaultRules, conditionEvaluator, passEvaluator)
passThroughFacts = passThroughFacts.merge(defaultPass)
}
}

passThroughFacts.onSome { evaluatedPass ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ class JIRMethodCallRuleBasedSummaryRewriter(
val actionsForBase = userRuleDefinedActions[fact.base].orEmpty()
if (actionsForBase.isEmpty()) return listOf(fact to startFactReader)

val cleanEvaluator = JIRTaintCleanActionEvaluator(typeResolver)
val cleanEvaluator = JIRTaintCleanActionEvaluator()
val cleanedFact = actionsForBase.entries.applyCleanerActions(
initial = EvaluatedCleanAction.initial(startFactReader)
) { (mark, actions), current ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,21 @@ class JIRMethodGetDefault(

private fun TypeName.mayBeArray(): Boolean = isArray || this == objectTypeName

private val getDefaultActions = listOf(
CopyAllMarks(from = Exact(This), to = Exact(Result))
private fun defaultField(cls: JIRClassOrInterface): PositionAccessor.FieldAccessor =
PositionAccessor.FieldAccessor(cls.name, "<get-default>", objectTypeName.typeName)

private fun defaultPosition(cls: JIRClassOrInterface) =
PositionWithAccess(This, defaultField(cls))

private fun getDefaultActions(cls: JIRClassOrInterface) = listOf(
CopyAllMarks(from = Exact(defaultPosition(cls)), to = Exact(Result))
)

private val getDefaultArrayActions = listOf(
CopyAllMarks(from = Exact(This), to = Exact(PositionWithAccess(Result, PositionAccessor.ElementAccessor)))
private fun getDefaultArrayActions(cls: JIRClassOrInterface) = listOf(
CopyAllMarks(
from = Exact(defaultPosition(cls)),
to = Exact(PositionWithAccess(Result, PositionAccessor.ElementAccessor))
)
)

fun defaultPropagationRules(method: JIRMethod): List<RuleWithCondition<TaintPassThrough>> {
Expand All @@ -42,9 +51,9 @@ class JIRMethodGetDefault(

if (!config.enableDefaultPropagationForClass(method.enclosingClass)) return emptyList()

var actions = getDefaultActions
var actions = getDefaultActions(method.enclosingClass)
if (method.returnType.mayBeArray()) {
actions = actions + getDefaultArrayActions
actions = actions + getDefaultArrayActions(method.enclosingClass)
}

val getDefaultRule = TaintPassThrough(method, mkTrue(), actions, info = null)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package org.opentaint.dataflow.jvm.ap.ifds.taint

import org.opentaint.dataflow.ap.ifds.AccessPathBase
import org.opentaint.dataflow.ap.ifds.ElementAccessor
import org.opentaint.dataflow.ap.ifds.ExclusionSet
import org.opentaint.dataflow.ap.ifds.access.ApManager
import org.opentaint.dataflow.ap.ifds.access.FinalFactAp
Expand All @@ -16,10 +14,7 @@ import org.opentaint.dataflow.jvm.ap.ifds.JIRMethodCallFactMapper
import org.opentaint.dataflow.jvm.ap.ifds.TaintConfigUtils.accept
import org.opentaint.dataflow.jvm.ap.ifds.analysis.JIRMethodAnalysisContext
import org.opentaint.dataflow.jvm.util.callee
import org.opentaint.dataflow.taint.FactReader
import org.opentaint.dataflow.taint.FinalFactReader
import org.opentaint.dataflow.taint.FinalFactReaderWithPrefix
import org.opentaint.dataflow.taint.PositionAccess
import org.opentaint.dataflow.taint.TaintSourceActionEvaluator
import org.opentaint.dataflow.taint.TaintUtil
import org.opentaint.ir.api.jvm.cfg.JIRCallExpr
Expand Down Expand Up @@ -183,25 +178,6 @@ class JIRMethodCallTaintUtil(
JIRMethodCallFactMapper.mapMethodExitToReturnFlowFact(statement, this)
.singleOrNull()

override fun patchSinkConditionFactReader(factReaders: List<FinalFactReader>): List<FactReader> {
val arrayElementFactReaders = factReaders.arrayElementConditionReaders(callExpr)
return factReaders + arrayElementFactReaders
}

private fun List<FinalFactReader>.arrayElementConditionReaders(callExpr: JIRCallExpr): List<FactReader> =
mapNotNull {
val base = it.factAp.base as? AccessPathBase.Argument ?: return@mapNotNull null

if (!analysisContext.factTypeChecker.callArgumentMayBeArray(callExpr, base)) {
return@mapNotNull null
}

val arrayElementPosition = PositionAccess.Complex(PositionAccess.Simple(base), ElementAccessor)
if (!it.containsPosition(arrayElementPosition)) return@mapNotNull null

FinalFactReaderWithPrefix(it, ElementAccessor)
}

private inline fun storeInfo(body: () -> Unit) {
if (generateTrace) return
body()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,13 @@ import org.opentaint.dataflow.configuration.jvm.Result
import org.opentaint.dataflow.configuration.jvm.This
import org.opentaint.dataflow.taint.EvaluatedCleanAction
import org.opentaint.dataflow.taint.PositionAccess
import org.opentaint.dataflow.taint.PositionTypeResolver
import org.opentaint.dataflow.taint.TaintCleanActionEvaluator

interface ConditionEvaluator<T> {
fun eval(condition: Condition): T
}

class JIRTaintCleanActionEvaluator(
private val positionTypeResolver: PositionTypeResolver,
) {
class JIRTaintCleanActionEvaluator {
private val evaluator = TaintCleanActionEvaluator()

fun evaluate(
Expand All @@ -49,28 +46,9 @@ class JIRTaintCleanActionEvaluator(
): List<EvaluatedCleanAction> {
val variable = action.position.resolveAp()
val mark = TaintMarkAccessor(action.mark.name)
val cleaned = evaluator.removeFinalFact(initialFact, variable, mark, rule, action, action.position.cleanReach())

val positionType = positionTypeResolver.resolve(variable)
if (positionType?.typeName != STRING) {
return cleaned
}

val stringBytesPosition = action.position.append(stringBytes)
val stringBytesVar = stringBytesPosition.resolveAp()
return cleaned.flatMap { f ->
evaluator.removeFinalFact(f, stringBytesVar, mark, rule, action, stringBytesPosition.cleanReach())
}
return evaluator.removeFinalFact(initialFact, variable, mark, rule, action, action.position.cleanReach())
}

companion object {
private const val STRING = "java.lang.String"

// todo: fix in config?
// string bytes virtual field fully reflects the string content.
// So, if we clean string, we should clean its byte content
private val stringBytes = PositionAccessor.FieldAccessor(STRING, "<string-bytes>", "byte[]")
}
}

fun ActionPosition.resolveBaseAp(): AccessPathBase = when (this) {
Expand All @@ -96,10 +74,6 @@ fun ActionPosition.cleanReach(): TaintCleanReach = when (this) {
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())

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
rules:
- id: builtin-slice-coverage
languages: [go]
severity: WARNING
message: "taint survives a changed builtin slice passthrough and reaches Sink"
mode: taint
pattern-sources:
- pattern: "BuiltinSliceCoverage.Source(...)"
pattern-sinks:
- pattern: "BuiltinSliceCoverage.Sink($X)"
Loading
Loading