fix(taint): a field is attacker input only when a caller feeds it request data

The field join (task 5.11) seeded every parameter of every method in the
file, so every setter made its field a source. Adjudicated on PMPro, six of
eight false SQL injections in a 1-in-5 sample were object fields holding
database or internal values ($this->membership_id, $morder->user_id).

A parameter now seeds the join only where some call site passes it request
data without a sanitizer on the way (memoised per method and index); a
request source read in the file still seeds as before. A real-Joern control
covers both directions -- a setter only given a constant stays quiet, a
constructor handed $_GET (the CVE-2023-6559 shape) is still found -- and
fails on the old query with exactly the PMPro false positive.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
This commit is contained in:
Marius Ciepluch
2026-09-23 21:47:41 +02:00
co-authored by Claude Opus 5.5
parent faf4f643df
commit 6ebbc89d64
2 changed files with 98 additions and 3 deletions
+37 -3
View File
@@ -81,7 +81,11 @@
// field join's flow queries. Keyed by the parameters the answer depends on, so one family's seeds never
// answer another's.
val methodsByFile = scala.collection.mutable.Map.empty[String, List[io.shiftleft.codepropertygraph.generated.nodes.Method]]
val seedsByFile = scala.collection.mutable.Map.empty[(String, String, String), List[io.shiftleft.codepropertygraph.generated.nodes.CfgNode]]
val seedsByFile = scala.collection.mutable.Map.empty[(String, String, String, String), List[io.shiftleft.codepropertygraph.generated.nodes.CfgNode]]
// Whether any caller hands a method's parameter request data, per (sources, sanitizers, method, index); and
// the request-source calls themselves, which every such question starts from.
val callerFedMemo = scala.collection.mutable.Map.empty[(String, String, String, Int), Boolean]
val sourceCallMemo = scala.collection.mutable.Map.empty[String, List[io.shiftleft.codepropertygraph.generated.nodes.Call]]
val fieldTaintMemo = scala.collection.mutable.Map.empty[(String, String, String, String, String), Boolean]
val hookKeysMemo = scala.collection.mutable.Map.empty[(String, String, String), Set[String]]
@@ -268,14 +272,44 @@
// Framework sources are untrusted wherever they appear. Parameters are untrusted only where the caller has
// said this region is an entry point -- the same contract `parameterNodes` carries, and for the same
// reason: an arbitrary helper's parameters carry whatever its caller happened to have.
//
// A parameter seeds the FIELD join only where some CALLER hands it request data. Counting every parameter of
// every method in the file made each setter a source: `$morder->user_id = $user_id` in a gateway's hook
// callback, `$this->membership_id = ...` loaded from the order's own row. Adjudicated on PMPro 2026-09-23,
// six of eight false SQL injections in a 1-in-5 sample were exactly that -- object fields holding database
// or internal values, read as attacker input. MW WP Form's CVE-2023-6559, which the join exists for, is the
// case the rule keeps: its constructor's `$attachments` is passed the uploaded files by the controller.
def requestSourceCalls: List[io.shiftleft.codepropertygraph.generated.nodes.Call] =
sourceCallMemo.getOrElseUpdate(sourcesS, cpg.call.filter(c => sourcePatterns.exists(p => c.code.contains(p))).l)
def fedByCaller(
m: io.shiftleft.codepropertygraph.generated.nodes.Method,
param: io.shiftleft.codepropertygraph.generated.nodes.MethodParameterIn
): Boolean =
callerFedMemo.getOrElseUpdate(
(sourcesS, sanitizersS, m.fullName, param.index), {
val sources = requestSourceCalls
sources.nonEmpty && m.callIn.l.exists { call =>
call.argument.l.filter(_.argumentIndex == param.index).exists { arg =>
arg.ast.isCall.exists(c => sourcePatterns.exists(p => c.code.contains(p))) ||
arg.start.reachableByFlows(sources.iterator).l.exists(f =>
!f.elements.map(_.code).l.exists(code => mentionsToken(code, sanitizerNames))
)
}
}
}
)
def seedsIn(fileName: String) =
seedsByFile.getOrElseUpdate(
(sourcesS, paramSrc, fileName), {
(sourcesS, sanitizersS, paramSrc, fileName), {
val methods = methodsIn(fileName)
val framework: List[io.shiftleft.codepropertygraph.generated.nodes.CfgNode] =
methods.flatMap(_.ast.isCall.filter(c => sourcePatterns.exists(p => c.code.contains(p))).l)
val params: List[io.shiftleft.codepropertygraph.generated.nodes.CfgNode] =
if (parameterSources == "true") methods.flatMap(_.parameter.l) else Nil
if (parameterSources == "true")
methods.flatMap(m => m.parameter.l.filter(p => p.name != "this" && fedByCaller(m, p)))
else Nil
framework ++ params
}
)