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,
functionS: String,
paramSrc: String,
fieldParamSrc: String,
fieldSrc: String,
evidenceS: String,
fileS: String,
@@ -358,19 +359,19 @@
// reason: an arbitrary helper's parameters carry whatever its caller happened to have.
def seedsIn(fileName: String) =
seedsByFile.getOrElseUpdate(
(sourcesS, paramSrc, fileName), {
(sourcesS, fieldParamSrc, 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 (fieldParamSrc == "true") methods.flatMap(_.parameter.l) else Nil
framework ++ params
}
)
def fieldIsTainted(fileName: String, fieldCode: String): Boolean =
fieldTaintMemo.getOrElseUpdate(
(sourcesS, paramSrc, sanitizersS, fileName, fieldCode), {
(sourcesS, fieldParamSrc, sanitizersS, fileName, fieldCode), {
val seeds = seedsIn(fileName)
if (seeds.isEmpty) false
else
@@ -1025,6 +1026,7 @@
val started = System.nanoTime()
def field(name: String): String = req.obj.get(name).map(_.str).getOrElse("")
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 evidence = req.obj.get("evidenceOnly").map(_.str).getOrElse("false")
val rows = ujson.Arr(
@@ -1039,6 +1041,7 @@
if (field("dispatchApply").nonEmpty) field("dispatchApply") else dispatchApply,
field("function"),
paramSrc,
fieldParamSrc,
fieldSrc,
evidence,
field("file"),
@@ -1060,7 +1063,7 @@
} else {
println("---OUSAST-CPG-BEGIN---")
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---")
+1
View File
@@ -1459,6 +1459,7 @@ def _grouped(
parameter_sources=_parameters_are_input(region),
call_depth=ENTRY_POINT_CALL_DEPTH if entry else 0,
hook_callbacks=hook_callbacks,
field_parameter_sources=entry,
),
)
grouped.setdefault(kind, {})[rid] = params
+5
View File
@@ -27,6 +27,7 @@ def request_params(
parameter_sources: bool = False,
call_depth: int = 0,
hook_callbacks: str = "",
field_parameter_sources: bool | None = None,
) -> dict[str, object]:
"""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,
"file": file,
"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),
"boundedSinks": spec.bounded_sinks,
# The hook link table, `hook:callback;hook:callback`. Read out of the source text rather than the