fix(scan): keep the field join's parameter seeds when a region's own parameters are not input

da0ee57 turned parameter sources off for the mapper's catch-all `function`
entries, and with them the field join's seeds, which read the same flag:
MW WP Form's CVE-2023-6559 ($this->Data, filled by a constructor) was lost
(10 -> 3). The field join now has its own request field,
`fieldParameterSources`, set by the old entry rule; a region's own
parameters follow the new one. Both known CVEs are found again (MW WP Form
10 -> 4, WP Statistics 39 -> 18, 91/91 and 774/774 completed). The dropped
findings sampled against their call sites all take internal values:
ExistTable('useronline'), wp_statistics_visit('today'), remove() on the
plugin's own temp directory, _render('text', ...).

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-24 02:40:00 +02:00
co-authored by Claude Opus 5.5
parent da0ee576f8
commit 7bc7acbf52
3 changed files with 13 additions and 4 deletions
+7 -4
View File
@@ -93,6 +93,7 @@
dispatchApplyS: String, dispatchApplyS: String,
functionS: String, functionS: String,
paramSrc: String, paramSrc: String,
fieldParamSrc: String,
fieldSrc: String, fieldSrc: String,
evidenceS: String, evidenceS: String,
fileS: String, fileS: String,
@@ -358,19 +359,19 @@
// reason: an arbitrary helper's parameters carry whatever its caller happened to have. // reason: an arbitrary helper's parameters carry whatever its caller happened to have.
def seedsIn(fileName: String) = def seedsIn(fileName: String) =
seedsByFile.getOrElseUpdate( seedsByFile.getOrElseUpdate(
(sourcesS, paramSrc, fileName), { (sourcesS, fieldParamSrc, fileName), {
val methods = methodsIn(fileName) val methods = methodsIn(fileName)
val framework: List[io.shiftleft.codepropertygraph.generated.nodes.CfgNode] = val framework: List[io.shiftleft.codepropertygraph.generated.nodes.CfgNode] =
methods.flatMap(_.ast.isCall.filter(c => sourcePatterns.exists(p => c.code.contains(p))).l) methods.flatMap(_.ast.isCall.filter(c => sourcePatterns.exists(p => c.code.contains(p))).l)
val params: List[io.shiftleft.codepropertygraph.generated.nodes.CfgNode] = val params: List[io.shiftleft.codepropertygraph.generated.nodes.CfgNode] =
if (parameterSources == "true") methods.flatMap(_.parameter.l) else Nil if (fieldParamSrc == "true") methods.flatMap(_.parameter.l) else Nil
framework ++ params framework ++ params
} }
) )
def fieldIsTainted(fileName: String, fieldCode: String): Boolean = def fieldIsTainted(fileName: String, fieldCode: String): Boolean =
fieldTaintMemo.getOrElseUpdate( fieldTaintMemo.getOrElseUpdate(
(sourcesS, paramSrc, sanitizersS, fileName, fieldCode), { (sourcesS, fieldParamSrc, sanitizersS, fileName, fieldCode), {
val seeds = seedsIn(fileName) val seeds = seedsIn(fileName)
if (seeds.isEmpty) false if (seeds.isEmpty) false
else else
@@ -1025,6 +1026,7 @@
val started = System.nanoTime() val started = System.nanoTime()
def field(name: String): String = req.obj.get(name).map(_.str).getOrElse("") def field(name: String): String = req.obj.get(name).map(_.str).getOrElse("")
val paramSrc = req.obj.get("parameterSources").map(_.str).getOrElse("false") val paramSrc = req.obj.get("parameterSources").map(_.str).getOrElse("false")
val fieldParamSrc = req.obj.get("fieldParameterSources").map(_.str).getOrElse(paramSrc)
val fieldSrc = req.obj.get("fieldSources").map(_.str).getOrElse("true") val fieldSrc = req.obj.get("fieldSources").map(_.str).getOrElse("true")
val evidence = req.obj.get("evidenceOnly").map(_.str).getOrElse("false") val evidence = req.obj.get("evidenceOnly").map(_.str).getOrElse("false")
val rows = ujson.Arr( val rows = ujson.Arr(
@@ -1039,6 +1041,7 @@
if (field("dispatchApply").nonEmpty) field("dispatchApply") else dispatchApply, if (field("dispatchApply").nonEmpty) field("dispatchApply") else dispatchApply,
field("function"), field("function"),
paramSrc, paramSrc,
fieldParamSrc,
fieldSrc, fieldSrc,
evidence, evidence,
field("file"), field("file"),
@@ -1060,7 +1063,7 @@
} else { } else {
println("---OUSAST-CPG-BEGIN---") println("---OUSAST-CPG-BEGIN---")
println( println(
ujson.write(ujson.Arr(rowsFor(sources, sinks, sanitizers, hookCallbacks, dispatchApply, function, parameterSources, fieldSources, evidenceOnly, file, callDepth, boundedSinks): _*)) ujson.write(ujson.Arr(rowsFor(sources, sinks, sanitizers, hookCallbacks, dispatchApply, function, parameterSources, parameterSources, fieldSources, evidenceOnly, file, callDepth, boundedSinks): _*))
) )
} }
println("---OUSAST-CPG-END---") println("---OUSAST-CPG-END---")
+1
View File
@@ -1459,6 +1459,7 @@ def _grouped(
parameter_sources=_parameters_are_input(region), parameter_sources=_parameters_are_input(region),
call_depth=ENTRY_POINT_CALL_DEPTH if entry else 0, call_depth=ENTRY_POINT_CALL_DEPTH if entry else 0,
hook_callbacks=hook_callbacks, hook_callbacks=hook_callbacks,
field_parameter_sources=entry,
), ),
) )
grouped.setdefault(kind, {})[rid] = params grouped.setdefault(kind, {})[rid] = params
+5
View File
@@ -27,6 +27,7 @@ def request_params(
parameter_sources: bool = False, parameter_sources: bool = False,
call_depth: int = 0, call_depth: int = 0,
hook_callbacks: str = "", hook_callbacks: str = "",
field_parameter_sources: bool | None = None,
) -> dict[str, object]: ) -> dict[str, object]:
"""The query parameters this arbiter sends. Shared with the batcher so there is one definition, not two. """The query parameters this arbiter sends. Shared with the batcher so there is one definition, not two.
@@ -45,6 +46,10 @@ def request_params(
"function": function, "function": function,
"file": file, "file": file,
"parameterSources": "true" if parameter_sources else "false", "parameterSources": "true" if parameter_sources else "false",
# The FIELD join's parameter seeds, apart from the region's own parameters: a constructor handed the
# uploads stores them in a field another method reads (MW WP Form, CVE-2023-6559), whatever kind of
# entry the reading method is. Defaults to the region's own rule, so a caller that sets one sets both.
"fieldParameterSources": "true" if (parameter_sources if field_parameter_sources is None else field_parameter_sources) else "false",
"callDepth": str(call_depth), "callDepth": str(call_depth),
"boundedSinks": spec.bounded_sinks, "boundedSinks": spec.bounded_sinks,
# The hook link table, `hook:callback;hook:callback`. Read out of the source text rather than the # The hook link table, `hook:callback;hook:callback`. Read out of the source text rather than the