mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 19:59:26 +00:00
fix(taint): the field join links methods only through the object itself
profile.php:424 survived the hook-argument fix through the field join: in another function of the same file, `$user = new stdClass; $user->ID = $_POST['user_id']`, and the join keys a field by its TEXT across the whole file, so the WP_User parameter's `$user->ID` became a field source. The join exists to carry object state between methods ($this->attachments written in the constructor, read in _delete_files). That reading of a field's text holds only when the base IS the object ($this, this, self). Any other base is a variable, matched within its own method only, where the engine's dataflow already sees it. A real-Joern control covers both (a local variable's field in another function stays quiet; $this state across methods is traced) and fails on the old query. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
53d20691c3
commit
d57966a6b6
@@ -409,13 +409,29 @@
|
||||
}
|
||||
)
|
||||
|
||||
def fieldIsTainted(fileName: String, fieldCode: String): Boolean =
|
||||
// The join carries OBJECT STATE between methods: `$this->attachments` written in the constructor, read in
|
||||
// `_delete_files()`. That is what a field keyed by its text means only when the base IS the object. Any other
|
||||
// base is a variable, and the same text in two functions names two variables: PMPro's
|
||||
// `$user = new stdClass; $user->ID = $_POST['user_id']` in one function made the `WP_User` parameter's
|
||||
// `$user->ID` in another an SQL injection source. So a non-object base is joined within its own method only,
|
||||
// where it is one variable -- and where the engine's own dataflow already sees it.
|
||||
val objectBases = Set("$this", "this", "self")
|
||||
|
||||
def fieldScope(fieldCode: String, reader: io.shiftleft.codepropertygraph.generated.nodes.Method): String = {
|
||||
val base = fieldCode.split("->|\\.").headOption.map(_.trim).getOrElse("")
|
||||
if (objectBases.contains(base)) "" else reader.fullName
|
||||
}
|
||||
|
||||
def fieldIsTainted(fileName: String, fieldCode: String, scope: String = ""): Boolean =
|
||||
fieldTaintMemo.getOrElseUpdate(
|
||||
(sourcesS, fieldParamSrc, sanitizersS, fileName, fieldCode), {
|
||||
(sourcesS, fieldParamSrc, sanitizersS, fileName, scope + "|" + fieldCode), {
|
||||
val seeds = seedsIn(fileName)
|
||||
if (seeds.isEmpty) false
|
||||
else
|
||||
methodsIn(fileName).flatMap(_.ast.isCall.nameExact(ASSIGNMENT).l).exists { assignment =>
|
||||
methodsIn(fileName)
|
||||
.filter(m => scope.isEmpty || m.fullName == scope)
|
||||
.flatMap(_.ast.isCall.nameExact(ASSIGNMENT).l)
|
||||
.exists { assignment =>
|
||||
val args = assignment.argument.l
|
||||
args.size >= 2 && (args.head match {
|
||||
case target: io.shiftleft.codepropertygraph.generated.nodes.Call
|
||||
@@ -437,10 +453,14 @@
|
||||
// `$this->rest_hits` in one method and reads `$this->rest_hits->current_page_id` in another, so an exact
|
||||
// code match sees two unrelated strings where the source text says one is inside the other. Every prefix
|
||||
// that ends on a `->` boundary is checked, left to right.
|
||||
def taintedPrefixes(code: String, fileName: String): Boolean = {
|
||||
def taintedPrefixes(code: String, fileName: String, reader: io.shiftleft.codepropertygraph.generated.nodes.Method): Boolean = {
|
||||
val parts = code.split("->").map(_.trim).filter(_.nonEmpty)
|
||||
if (parts.length < 2) false
|
||||
else (2 to parts.length).exists(n => fieldIsTainted(fileName, parts.take(n).mkString("->")))
|
||||
else
|
||||
(2 to parts.length).exists { n =>
|
||||
val prefix = parts.take(n).mkString("->")
|
||||
fieldIsTainted(fileName, prefix, fieldScope(prefix, reader))
|
||||
}
|
||||
}
|
||||
|
||||
// Off by request only, and defaulting to ON when the field is absent, so nothing about the shipped
|
||||
@@ -450,7 +470,7 @@
|
||||
if (fieldSrc != "true") Iterator.empty
|
||||
else {
|
||||
val reads = cpg.call.nameExact(FIELD_ACCESS).filter(c => inScope(c.method)).l
|
||||
if (reads.isEmpty) Iterator.empty else reads.filter(r => taintedPrefixes(r.code.trim, fileS)).iterator
|
||||
if (reads.isEmpty) Iterator.empty else reads.filter(r => taintedPrefixes(r.code.trim, fileS, r.method)).iterator
|
||||
}
|
||||
|
||||
// ---- the hook half of the two-stage join (task 5.11) ----------------------------------------------
|
||||
@@ -508,7 +528,7 @@
|
||||
val framework: List[io.shiftleft.codepropertygraph.generated.nodes.CfgNode] =
|
||||
callback.ast.isCall.filter(isSourceCall).l
|
||||
val fields: List[io.shiftleft.codepropertygraph.generated.nodes.CfgNode] =
|
||||
callback.ast.isCall.nameExact(FIELD_ACCESS).l.filter(r => taintedPrefixes(r.code.trim, callback.filename))
|
||||
callback.ast.isCall.nameExact(FIELD_ACCESS).l.filter(r => taintedPrefixes(r.code.trim, callback.filename, r.method))
|
||||
framework ++ fields
|
||||
}
|
||||
|
||||
@@ -811,7 +831,7 @@
|
||||
.nameExact(FIELD_ACCESS)
|
||||
.filter(c => inScope(c.method))
|
||||
.l
|
||||
.exists(r => prefixKind(r.code.trim, fedFields(r.method.filename)) == "parameter" && taintedPrefixes(r.code.trim, r.method.filename))
|
||||
.exists(r => prefixKind(r.code.trim, fedFields(r.method.filename)) == "parameter" && taintedPrefixes(r.code.trim, r.method.filename, r.method))
|
||||
if (confirmed) "source" else "parameter"
|
||||
}
|
||||
val carried = carriedKind.nonEmpty
|
||||
|
||||
Reference in New Issue
Block a user