From 309492bd0534bb8dcb897d80fe2aad811ab9ae15 Mon Sep 17 00:00:00 2001 From: Aleksandr Misonizhnik Date: Fri, 22 May 2026 16:40:16 +0200 Subject: [PATCH 1/6] feat(analyzer): Add third-party + stdlib taint passThrough propagators Extract the passThrough (propagator) rules from misonijnik/codeql-rules that the rule-test suite depends on but the main-built analyzer jar does not yet bundle. Adds 13 per-jar propagator files under config/jar-split and extends stdlib.yaml, so a future analyzer release ships them by default and the rules CI no longer needs to overlay them. --- .../config/config/jar-split/ant-1.10.14.yaml | 13 +++ .../jar-split/commons-codec-1.16.0.yaml | 16 +++ .../config/jar-split/commons-io-2.15.1.yaml | 8 ++ .../config/jar-split/groovy-3.0.21.yaml | 9 ++ .../config/jar-split/httpcore5-5.2.4.yaml | 7 ++ .../jar-split/jenkins-core-2.426.3.yaml | 7 ++ .../config/jar-split/mvel2-2.5.2.Final.yaml | 26 +++++ .../config/jar-split/okhttp-4.12.0.yaml | 16 +++ .../config/jar-split/spring-jdbc-5.3.39.yaml | 9 ++ .../jar-split/spring-ldap-core-2.4.1.yaml | 109 ++++++++++++++++++ .../config/jar-split/spring-web-5.3.39.yaml | 16 +++ .../jar-split/unboundid-ldapsdk-6.0.11.yaml | 5 + .../jar-split/velocity-engine-core-2.3.yaml | 13 +++ .../config/config/stdlib.yaml | 87 ++++++++++++++ 14 files changed, 341 insertions(+) create mode 100644 core/opentaint-config/config/config/jar-split/ant-1.10.14.yaml create mode 100644 core/opentaint-config/config/config/jar-split/commons-codec-1.16.0.yaml create mode 100644 core/opentaint-config/config/config/jar-split/commons-io-2.15.1.yaml create mode 100644 core/opentaint-config/config/config/jar-split/groovy-3.0.21.yaml create mode 100644 core/opentaint-config/config/config/jar-split/httpcore5-5.2.4.yaml create mode 100644 core/opentaint-config/config/config/jar-split/jenkins-core-2.426.3.yaml create mode 100644 core/opentaint-config/config/config/jar-split/mvel2-2.5.2.Final.yaml create mode 100644 core/opentaint-config/config/config/jar-split/okhttp-4.12.0.yaml create mode 100644 core/opentaint-config/config/config/jar-split/spring-jdbc-5.3.39.yaml create mode 100644 core/opentaint-config/config/config/jar-split/spring-ldap-core-2.4.1.yaml create mode 100644 core/opentaint-config/config/config/jar-split/spring-web-5.3.39.yaml create mode 100644 core/opentaint-config/config/config/jar-split/unboundid-ldapsdk-6.0.11.yaml create mode 100644 core/opentaint-config/config/config/jar-split/velocity-engine-core-2.3.yaml diff --git a/core/opentaint-config/config/config/jar-split/ant-1.10.14.yaml b/core/opentaint-config/config/config/jar-split/ant-1.10.14.yaml new file mode 100644 index 000000000..603e04614 --- /dev/null +++ b/core/opentaint-config/config/config/jar-split/ant-1.10.14.yaml @@ -0,0 +1,13 @@ +passThrough: +# Apache Ant: FileSet.setDir(File) / setFile(File) — the file argument +# is stored on the FileSet instance, so a subsequent +# `copy.addFileset(fs)` sink that requires a tainted $FILE detects +# the flow. +- function: org.apache.tools.ant.types.FileSet#setDir + copy: + - from: arg(0) + to: this +- function: org.apache.tools.ant.types.FileSet#setFile + copy: + - from: arg(0) + to: this diff --git a/core/opentaint-config/config/config/jar-split/commons-codec-1.16.0.yaml b/core/opentaint-config/config/config/jar-split/commons-codec-1.16.0.yaml new file mode 100644 index 000000000..10459d5ee --- /dev/null +++ b/core/opentaint-config/config/config/jar-split/commons-codec-1.16.0.yaml @@ -0,0 +1,16 @@ +passThrough: +# Apache Commons Codec — Base64 encode/decode just re-codes bytes +# without disturbing the underlying tainted data, so taint should +# flow from input to output. +- function: org.apache.commons.codec.binary.Base64#encodeBase64String + copy: + - from: arg(0) + to: result +- function: org.apache.commons.codec.binary.Base64#encodeBase64 + copy: + - from: arg(0) + to: result +- function: org.apache.commons.codec.binary.Base64#decodeBase64 + copy: + - from: arg(0) + to: result diff --git a/core/opentaint-config/config/config/jar-split/commons-io-2.15.1.yaml b/core/opentaint-config/config/config/jar-split/commons-io-2.15.1.yaml new file mode 100644 index 000000000..ee9f1aa58 --- /dev/null +++ b/core/opentaint-config/config/config/jar-split/commons-io-2.15.1.yaml @@ -0,0 +1,8 @@ +passThrough: +# Apache Commons IO: IOUtils.toString(InputStream|Reader|URL, ...) just +# reads bytes/chars from its input and produces a String — taint flows +# from the input source argument to the resulting String. +- function: org.apache.commons.io.IOUtils#toString + copy: + - from: arg(0) + to: result diff --git a/core/opentaint-config/config/config/jar-split/groovy-3.0.21.yaml b/core/opentaint-config/config/config/jar-split/groovy-3.0.21.yaml new file mode 100644 index 000000000..07bb9afcb --- /dev/null +++ b/core/opentaint-config/config/config/jar-split/groovy-3.0.21.yaml @@ -0,0 +1,9 @@ +passThrough: +# Groovy compiler: CompilationUnit.addSource(name, source) — the +# source text becomes part of the CompilationUnit instance that's +# later compiled by .compile(), so the source-text argument taints +# the unit. +- function: org.codehaus.groovy.control.CompilationUnit#addSource + copy: + - from: arg(1) + to: this diff --git a/core/opentaint-config/config/config/jar-split/httpcore5-5.2.4.yaml b/core/opentaint-config/config/config/jar-split/httpcore5-5.2.4.yaml new file mode 100644 index 000000000..92c8a81fd --- /dev/null +++ b/core/opentaint-config/config/config/jar-split/httpcore5-5.2.4.yaml @@ -0,0 +1,7 @@ +passThrough: +# Apache HttpComponents 5 — String-arg wrapper constructor that the +# SSRF sink rules use as an inline taint carrier. +- function: org.apache.hc.core5.http.io.entity.StringEntity# + copy: + - from: arg(*) + to: this diff --git a/core/opentaint-config/config/config/jar-split/jenkins-core-2.426.3.yaml b/core/opentaint-config/config/config/jar-split/jenkins-core-2.426.3.yaml new file mode 100644 index 000000000..565b6f2f5 --- /dev/null +++ b/core/opentaint-config/config/config/jar-split/jenkins-core-2.426.3.yaml @@ -0,0 +1,7 @@ +passThrough: +# hudson.FilePath wrapper constructor — taint flows from any +# String/File/URL argument into the constructed FilePath instance. +- function: hudson.FilePath# + copy: + - from: arg(*) + to: this diff --git a/core/opentaint-config/config/config/jar-split/mvel2-2.5.2.Final.yaml b/core/opentaint-config/config/config/jar-split/mvel2-2.5.2.Final.yaml new file mode 100644 index 000000000..5dc56a586 --- /dev/null +++ b/core/opentaint-config/config/config/jar-split/mvel2-2.5.2.Final.yaml @@ -0,0 +1,26 @@ +passThrough: +# MVEL compile / executeExpression chain — compileExpression(expr) +# returns a Serializable that's later passed to executeExpression / +# MVELRuntime.execute as a tainted compiled program. The compile +# methods just pass the input expression text through to the result. +- function: org.mvel2.MVEL#compileExpression + copy: + - from: arg(0) + to: result +- function: org.mvel2.MVEL#compileSetExpression + copy: + - from: arg(0) + to: result +- function: org.mvel2.MVEL#compileGetExpression + copy: + - from: arg(0) + to: result +# JSR-223 ScriptEngine compile / compiledScript +- function: org.mvel2.jsr223.MvelScriptEngine#compile + copy: + - from: arg(0) + to: result +- function: org.mvel2.jsr223.MvelScriptEngine#compiledScript + copy: + - from: arg(0) + to: result diff --git a/core/opentaint-config/config/config/jar-split/okhttp-4.12.0.yaml b/core/opentaint-config/config/config/jar-split/okhttp-4.12.0.yaml new file mode 100644 index 000000000..8a22331b3 --- /dev/null +++ b/core/opentaint-config/config/config/jar-split/okhttp-4.12.0.yaml @@ -0,0 +1,16 @@ +passThrough: +# OkHttp Request.Builder — `new Request.Builder().url($X).build()` chain. +# `.url()` mutates the builder and returns it (taint flows arg→this and +# arg→result and this→result so the chain propagates through `.build()`). +- function: okhttp3.Request$Builder#url + copy: + - from: arg(0) + to: result + - from: arg(0) + to: this + - from: this + to: result +- function: okhttp3.Request$Builder#build + copy: + - from: this + to: result diff --git a/core/opentaint-config/config/config/jar-split/spring-jdbc-5.3.39.yaml b/core/opentaint-config/config/config/jar-split/spring-jdbc-5.3.39.yaml new file mode 100644 index 000000000..cfc9b8e02 --- /dev/null +++ b/core/opentaint-config/config/config/jar-split/spring-jdbc-5.3.39.yaml @@ -0,0 +1,9 @@ +passThrough: +# Spring JDBC: NamedParameterUtils.parseSqlStatement(sql) returns a +# ParsedSql wrapping the original SQL, which is then passed to +# (Named)JdbcTemplate query/update sinks. The parse step itself just +# preserves taint into the result. +- function: org.springframework.jdbc.core.namedparam.NamedParameterUtils#parseSqlStatement + copy: + - from: arg(0) + to: result diff --git a/core/opentaint-config/config/config/jar-split/spring-ldap-core-2.4.1.yaml b/core/opentaint-config/config/config/jar-split/spring-ldap-core-2.4.1.yaml new file mode 100644 index 000000000..8b44a1e5b --- /dev/null +++ b/core/opentaint-config/config/config/jar-split/spring-ldap-core-2.4.1.yaml @@ -0,0 +1,109 @@ +passThrough: +# Spring LDAP query builder chain: +# LdapQueryBuilder.query().base(dn).where(attr).is(val) ... -> LdapQuery +# +# The chain mixes the public LdapQueryBuilder/ConditionCriteria/ +# ContainerCriteria interfaces with the package-private +# DefaultConditionCriteria / DefaultContainerCriteria impls. The +# analyzer's chain-split sees the impl-class call sites, so both +# interface and impl entries are needed. +# +# The direct `arg(0) → result` form is what actually propagates taint +# through the chain; the two-step `arg(0)→this` + `this→result` form +# alone wasn't enough (the chain has too many implicit intermediates +# for two-step propagation to reach end-to-end without the direct +# shortcut). +- function: org.springframework.ldap.query.LdapQueryBuilder#base + copy: + - from: arg(0) + to: result + - from: arg(0) + to: this + - from: this + to: result +- function: org.springframework.ldap.query.LdapQueryBuilder#where + copy: + - from: arg(0) + to: result + - from: arg(0) + to: this + - from: this + to: result +- function: org.springframework.ldap.query.LdapQueryBuilder#filter + copy: + - from: arg(0) + to: result + - from: arg(0) + to: this + - from: this + to: result +- function: org.springframework.ldap.query.ConditionCriteria#is + copy: + - from: arg(0) + to: result + - from: arg(0) + to: this + - from: this + to: result +- function: org.springframework.ldap.query.ConditionCriteria#like + copy: + - from: arg(0) + to: result + - from: arg(0) + to: this + - from: this + to: result +- function: org.springframework.ldap.query.ConditionCriteria#whitespaceWildcardsLike + copy: + - from: arg(0) + to: result + - from: arg(0) + to: this + - from: this + to: result +- function: org.springframework.ldap.query.ContainerCriteria#and + copy: + - from: this + to: result +- function: org.springframework.ldap.query.ContainerCriteria#or + copy: + - from: this + to: result +- function: org.springframework.ldap.query.DefaultConditionCriteria#is + copy: + - from: arg(0) + to: this + - from: arg(0) + to: result + - from: this + to: result +- function: org.springframework.ldap.query.DefaultConditionCriteria#like + copy: + - from: arg(0) + to: this + - from: arg(0) + to: result + - from: this + to: result +- function: org.springframework.ldap.query.DefaultConditionCriteria#whitespaceWildcardsLike + copy: + - from: arg(0) + to: this + - from: arg(0) + to: result + - from: this + to: result +- function: org.springframework.ldap.query.DefaultContainerCriteria#and + copy: + - from: this + to: result +- function: org.springframework.ldap.query.DefaultContainerCriteria#or + copy: + - from: this + to: result +- function: org.springframework.ldap.query.DefaultContainerCriteria#append + copy: + - from: arg(0) + to: this + - from: this + to: result diff --git a/core/opentaint-config/config/config/jar-split/spring-web-5.3.39.yaml b/core/opentaint-config/config/config/jar-split/spring-web-5.3.39.yaml new file mode 100644 index 000000000..0d3ac94bd --- /dev/null +++ b/core/opentaint-config/config/config/jar-split/spring-web-5.3.39.yaml @@ -0,0 +1,16 @@ +passThrough: +# Spring RequestEntity static factories + builder .build() — used by +# the SSRF rule's chained-builder pattern: +# RequestEntity.get(URI.create($X)).build() +- function: org.springframework.http.RequestEntity#get + copy: + - from: arg(0) + to: result +- function: org.springframework.http.RequestEntity$BodyBuilder#build + copy: + - from: this + to: result +- function: org.springframework.http.RequestEntity$HeadersBuilder#build + copy: + - from: this + to: result diff --git a/core/opentaint-config/config/config/jar-split/unboundid-ldapsdk-6.0.11.yaml b/core/opentaint-config/config/config/jar-split/unboundid-ldapsdk-6.0.11.yaml new file mode 100644 index 000000000..466f26e62 --- /dev/null +++ b/core/opentaint-config/config/config/jar-split/unboundid-ldapsdk-6.0.11.yaml @@ -0,0 +1,5 @@ +passThrough: +- function: com.unboundid.ldap.sdk.SearchRequest# + copy: + - from: arg(*) + to: this diff --git a/core/opentaint-config/config/config/jar-split/velocity-engine-core-2.3.yaml b/core/opentaint-config/config/config/jar-split/velocity-engine-core-2.3.yaml new file mode 100644 index 000000000..309cc5d3e --- /dev/null +++ b/core/opentaint-config/config/config/jar-split/velocity-engine-core-2.3.yaml @@ -0,0 +1,13 @@ +passThrough: +# Apache Velocity: VelocityContext.put($k, $v) and the AbstractContext +# super-class — taint flows from the value argument into the context +# instance so a tainted value carried into the context reaches a +# subsequent VelocityEngine.evaluate / Template.merge sink. +- function: org.apache.velocity.VelocityContext#put + copy: + - from: arg(1) + to: this +- function: org.apache.velocity.context.AbstractContext#put + copy: + - from: arg(1) + to: this diff --git a/core/opentaint-config/config/config/stdlib.yaml b/core/opentaint-config/config/config/stdlib.yaml index 6d809f14f..1d9e8bfbd 100644 --- a/core/opentaint-config/config/config/stdlib.yaml +++ b/core/opentaint-config/config/config/stdlib.yaml @@ -21358,3 +21358,90 @@ passThrough: to: - this - .java.io.InputStream##java.lang.Object + +# ── Collection / Iterator / Iterable / Enumeration ───────────────────── +- function: java.util.Collection#iterator + copy: + - from: this + to: result +- function: java.lang.Iterable#iterator + copy: + - from: this + to: result +- function: java.util.Iterator#next + copy: + - from: this + to: result +- function: java.util.Enumeration#nextElement + copy: + - from: this + to: result + +# ── java.lang.String#getBytes (String → byte[]) ──────────────────────── +- function: java.lang.String#getBytes + copy: + - from: this + to: result + +# ── java.util.Base64$Encoder ────────────────────────────────────────── +- function: java.util.Base64$Encoder#encodeToString + copy: + - from: arg(0) + to: result +- function: java.util.Base64$Encoder#encode + copy: + - from: arg(0) + to: result + +# ── java.net.URL (String) constructor (direct arg→this; the existing +# URL#(String) entry uses arg(*) which doesn't apply consistently +# enough for tests like UnsafeStaplerServeFileServlet) ──────────────── +- function: java.net.URL# + signature: (java.lang.String) void + copy: + - from: arg(0) + to: this + +# ── java.net.URI ────────────────────────────────────────────────────── +- function: java.net.URI#create + copy: + - from: arg(0) + to: result + +# ── javax.management JMX (stdlib management API) ─────────────────────── +- function: javax.management.remote.JMXServiceURL# + copy: + - from: arg(*) + to: this +- function: javax.management.remote.JMXConnectorFactory#newJMXConnector + copy: + - from: arg(0) + to: result + +# ── javax.xml.transform.stream.StreamSource ──────────────────────────── +- function: javax.xml.transform.stream.StreamSource# + copy: + - from: arg(*) + to: this + +# ── java.net.http.HttpRequest$Builder (Java 11+ HttpClient) ─────────── +- function: java.net.http.HttpRequest#newBuilder + copy: + - from: arg(0) + to: result +- function: java.net.http.HttpRequest$Builder#uri + copy: + - from: arg(0) + to: result + - from: arg(0) + to: this + - from: this + to: result +- function: java.net.http.HttpRequest$Builder#build + copy: + - from: this + to: result +- function: java.net.http.HttpRequest$Builder#GET + copy: + - from: this + to: result From 050c869800769240b248796c17999951bf3d98dc Mon Sep 17 00:00:00 2001 From: Aleksandr Misonizhnik Date: Sun, 24 May 2026 10:08:56 +0200 Subject: [PATCH 2/6] refactor(analyzer): Complete passThrough builder chains and package coverage Refine the propagators added in 309492bd0 using the engine's actual propagation semantics (no implicit this->result for unmodeled library calls; whole-object taint subsumes field reads but virtual-field taint does not satisfy a whole-object sink). Fix broken fluent chains by modeling every intermediate link, not just the endpoints: - okhttp Request.Builder: cover all builder methods, not only url/build. - spring-web RequestEntity: cover all static factories and the HeadersBuilder/BodyBuilder chain, not only get/build. - java.net.http HttpRequest.Builder: cover header/POST/PUT/method/etc., not only uri/GET/build. Broaden per-package coverage: - commons-io IOUtils: toByteArray/toCharArray/readLines. - commons-codec Base64: URL-safe and chunked encode variants. - java.util.Base64.Decoder: decode/wrap (decode direction). - spring-jdbc: substituteNamedParameters/parseSqlStatementIntoString. - spring-ldap: LdapQueryBuilder config carriers + gte/lte/not (iface+impl). - velocity: Context interface put. - groovy CompilationUnit.addSource: arg(*) to cover File/URL/SourceUnit overloads where the source is the first argument. Wrapper constructors keep whole-object arg(*)->this on purpose: their sinks consume the whole object, so a virtual-field-only taint would be a false negative. --- .../jar-split/commons-codec-1.16.0.yaml | 12 +++ .../config/jar-split/commons-io-2.15.1.yaml | 19 ++++- .../config/jar-split/groovy-3.0.21.yaml | 12 +-- .../config/jar-split/okhttp-4.12.0.yaml | 78 +++++++++++++++++- .../config/jar-split/spring-jdbc-5.3.39.yaml | 15 +++- .../jar-split/spring-ldap-core-2.4.1.yaml | 62 ++++++++++++++ .../config/jar-split/spring-web-5.3.39.yaml | 82 ++++++++++++++++++- .../jar-split/velocity-engine-core-2.3.yaml | 14 +++- .../config/config/stdlib.yaml | 74 ++++++++++++++++- 9 files changed, 344 insertions(+), 24 deletions(-) diff --git a/core/opentaint-config/config/config/jar-split/commons-codec-1.16.0.yaml b/core/opentaint-config/config/config/jar-split/commons-codec-1.16.0.yaml index 10459d5ee..b7fde8002 100644 --- a/core/opentaint-config/config/config/jar-split/commons-codec-1.16.0.yaml +++ b/core/opentaint-config/config/config/jar-split/commons-codec-1.16.0.yaml @@ -10,6 +10,18 @@ passThrough: copy: - from: arg(0) to: result +- function: org.apache.commons.codec.binary.Base64#encodeBase64URLSafeString + copy: + - from: arg(0) + to: result +- function: org.apache.commons.codec.binary.Base64#encodeBase64URLSafe + copy: + - from: arg(0) + to: result +- function: org.apache.commons.codec.binary.Base64#encodeBase64Chunked + copy: + - from: arg(0) + to: result - function: org.apache.commons.codec.binary.Base64#decodeBase64 copy: - from: arg(0) diff --git a/core/opentaint-config/config/config/jar-split/commons-io-2.15.1.yaml b/core/opentaint-config/config/config/jar-split/commons-io-2.15.1.yaml index ee9f1aa58..f5a37647d 100644 --- a/core/opentaint-config/config/config/jar-split/commons-io-2.15.1.yaml +++ b/core/opentaint-config/config/config/jar-split/commons-io-2.15.1.yaml @@ -1,8 +1,21 @@ passThrough: -# Apache Commons IO: IOUtils.toString(InputStream|Reader|URL, ...) just -# reads bytes/chars from its input and produces a String — taint flows -# from the input source argument to the resulting String. +# Apache Commons IO: IOUtils read helpers consume an input source +# (InputStream | Reader | URL | byte[] | char[]) given as the first +# argument and materialize its content — taint flows from that input +# source to the returned String / byte[] / char[] / List. - function: org.apache.commons.io.IOUtils#toString copy: - from: arg(0) to: result +- function: org.apache.commons.io.IOUtils#toByteArray + copy: + - from: arg(0) + to: result +- function: org.apache.commons.io.IOUtils#toCharArray + copy: + - from: arg(0) + to: result +- function: org.apache.commons.io.IOUtils#readLines + copy: + - from: arg(0) + to: result diff --git a/core/opentaint-config/config/config/jar-split/groovy-3.0.21.yaml b/core/opentaint-config/config/config/jar-split/groovy-3.0.21.yaml index 07bb9afcb..5bbb797a5 100644 --- a/core/opentaint-config/config/config/jar-split/groovy-3.0.21.yaml +++ b/core/opentaint-config/config/config/jar-split/groovy-3.0.21.yaml @@ -1,9 +1,11 @@ passThrough: -# Groovy compiler: CompilationUnit.addSource(name, source) — the -# source text becomes part of the CompilationUnit instance that's -# later compiled by .compile(), so the source-text argument taints -# the unit. +# Groovy compiler: CompilationUnit.addSource(...) — the source becomes +# part of the CompilationUnit instance that is later run by .compile(), +# so the source argument taints the unit. The source is the 2nd arg of +# addSource(name, source|InputStream) but the 1st arg of the +# addSource(File) / addSource(URL) / addSource(SourceUnit) overloads, so +# copy from any argument into the unit. - function: org.codehaus.groovy.control.CompilationUnit#addSource copy: - - from: arg(1) + - from: arg(*) to: this diff --git a/core/opentaint-config/config/config/jar-split/okhttp-4.12.0.yaml b/core/opentaint-config/config/config/jar-split/okhttp-4.12.0.yaml index 8a22331b3..c142e5ee0 100644 --- a/core/opentaint-config/config/config/jar-split/okhttp-4.12.0.yaml +++ b/core/opentaint-config/config/config/jar-split/okhttp-4.12.0.yaml @@ -1,15 +1,87 @@ passThrough: -# OkHttp Request.Builder — `new Request.Builder().url($X).build()` chain. -# `.url()` mutates the builder and returns it (taint flows arg→this and -# arg→result and this→result so the chain propagates through `.build()`). +# OkHttp Request.Builder fluent chain, e.g. +# new Request.Builder().url($X).addHeader(..).post($BODY).build() +# Library calls have no implicit this->result propagation, so every +# builder method that may appear mid-chain must carry the builder taint +# to its return value, or the chain breaks at the first unmodeled link. +# `.url()` additionally seeds the builder from its (tainted) argument and +# `.build()` carries the accumulated builder taint into the Request. - function: okhttp3.Request$Builder#url copy: - from: arg(0) + to: this + - from: arg(0) + to: result + - from: this + to: result +- function: okhttp3.Request$Builder#header + copy: + - from: arg(*) + to: this + - from: this + to: result +- function: okhttp3.Request$Builder#addHeader + copy: + - from: arg(*) + to: this + - from: this + to: result +- function: okhttp3.Request$Builder#headers + copy: + - from: arg(0) + to: this + - from: this + to: result +- function: okhttp3.Request$Builder#post + copy: + - from: arg(0) + to: this + - from: this + to: result +- function: okhttp3.Request$Builder#put + copy: + - from: arg(0) + to: this + - from: this to: result +- function: okhttp3.Request$Builder#patch + copy: + - from: arg(0) + to: this + - from: this + to: result +- function: okhttp3.Request$Builder#delete + copy: - from: arg(0) to: this - from: this to: result +- function: okhttp3.Request$Builder#method + copy: + - from: arg(*) + to: this + - from: this + to: result +- function: okhttp3.Request$Builder#get + copy: + - from: this + to: result +- function: okhttp3.Request$Builder#head + copy: + - from: this + to: result +- function: okhttp3.Request$Builder#removeHeader + copy: + - from: this + to: result +- function: okhttp3.Request$Builder#cacheControl + copy: + - from: this + to: result +- function: okhttp3.Request$Builder#tag + copy: + - from: this + to: result - function: okhttp3.Request$Builder#build copy: - from: this diff --git a/core/opentaint-config/config/config/jar-split/spring-jdbc-5.3.39.yaml b/core/opentaint-config/config/config/jar-split/spring-jdbc-5.3.39.yaml index cfc9b8e02..157a69417 100644 --- a/core/opentaint-config/config/config/jar-split/spring-jdbc-5.3.39.yaml +++ b/core/opentaint-config/config/config/jar-split/spring-jdbc-5.3.39.yaml @@ -1,9 +1,18 @@ passThrough: # Spring JDBC: NamedParameterUtils.parseSqlStatement(sql) returns a -# ParsedSql wrapping the original SQL, which is then passed to -# (Named)JdbcTemplate query/update sinks. The parse step itself just -# preserves taint into the result. +# ParsedSql wrapping the original SQL; substituteNamedParameters(parsedSql|sql, +# ..) then expands it back into the SQL String passed to the +# (Named)JdbcTemplate query/update sinks. Each step preserves taint from +# its SQL/ParsedSql input (arg(0)) into the result. - function: org.springframework.jdbc.core.namedparam.NamedParameterUtils#parseSqlStatement copy: - from: arg(0) to: result +- function: org.springframework.jdbc.core.namedparam.NamedParameterUtils#substituteNamedParameters + copy: + - from: arg(0) + to: result +- function: org.springframework.jdbc.core.namedparam.NamedParameterUtils#parseSqlStatementIntoString + copy: + - from: arg(0) + to: result diff --git a/core/opentaint-config/config/config/jar-split/spring-ldap-core-2.4.1.yaml b/core/opentaint-config/config/config/jar-split/spring-ldap-core-2.4.1.yaml index 8b44a1e5b..26ec5b8ce 100644 --- a/core/opentaint-config/config/config/jar-split/spring-ldap-core-2.4.1.yaml +++ b/core/opentaint-config/config/config/jar-split/spring-ldap-core-2.4.1.yaml @@ -61,6 +61,48 @@ passThrough: to: this - from: this to: result +- function: org.springframework.ldap.query.ConditionCriteria#gte + copy: + - from: arg(0) + to: result + - from: arg(0) + to: this + - from: this + to: result +- function: org.springframework.ldap.query.ConditionCriteria#lte + copy: + - from: arg(0) + to: result + - from: arg(0) + to: this + - from: this + to: result +# `.not()` is a value-less modifier that returns the same criteria — pure +# chain carrier. +- function: org.springframework.ldap.query.ConditionCriteria#not + copy: + - from: this + to: result +# LdapQueryBuilder configuration methods (searchScope/countLimit/timeLimit/ +# attributes) return the builder unchanged; they are pure chain carriers +# that may appear between base() and where(), so the builder taint must +# survive across them. +- function: org.springframework.ldap.query.LdapQueryBuilder#searchScope + copy: + - from: this + to: result +- function: org.springframework.ldap.query.LdapQueryBuilder#countLimit + copy: + - from: this + to: result +- function: org.springframework.ldap.query.LdapQueryBuilder#timeLimit + copy: + - from: this + to: result +- function: org.springframework.ldap.query.LdapQueryBuilder#attributes + copy: + - from: this + to: result - function: org.springframework.ldap.query.ContainerCriteria#and copy: - from: this @@ -93,6 +135,26 @@ passThrough: to: result - from: this to: result +- function: org.springframework.ldap.query.DefaultConditionCriteria#gte + copy: + - from: arg(0) + to: this + - from: arg(0) + to: result + - from: this + to: result +- function: org.springframework.ldap.query.DefaultConditionCriteria#lte + copy: + - from: arg(0) + to: this + - from: arg(0) + to: result + - from: this + to: result +- function: org.springframework.ldap.query.DefaultConditionCriteria#not + copy: + - from: this + to: result - function: org.springframework.ldap.query.DefaultContainerCriteria#and copy: - from: this diff --git a/core/opentaint-config/config/config/jar-split/spring-web-5.3.39.yaml b/core/opentaint-config/config/config/jar-split/spring-web-5.3.39.yaml index 0d3ac94bd..a1f382a3c 100644 --- a/core/opentaint-config/config/config/jar-split/spring-web-5.3.39.yaml +++ b/core/opentaint-config/config/config/jar-split/spring-web-5.3.39.yaml @@ -1,12 +1,71 @@ passThrough: -# Spring RequestEntity static factories + builder .build() — used by -# the SSRF rule's chained-builder pattern: -# RequestEntity.get(URI.create($X)).build() +# Spring RequestEntity fluent chain, e.g. the SSRF rule's pattern +# RequestEntity.get(URI.create($X)).accept(..).header(..).build() +# The static factories seed the builder from the (tainted) URI argument; +# every intermediate HeadersBuilder/BodyBuilder method must carry the +# builder taint to its return value (no implicit this->result exists), +# and .build() carries it into the constructed RequestEntity which the +# RestTemplate sink consumes whole. +# +# ── Static factories (URI is the first arg, except method(HttpMethod, URI)) ── - function: org.springframework.http.RequestEntity#get copy: - from: arg(0) to: result -- function: org.springframework.http.RequestEntity$BodyBuilder#build +- function: org.springframework.http.RequestEntity#head + copy: + - from: arg(0) + to: result +- function: org.springframework.http.RequestEntity#post + copy: + - from: arg(0) + to: result +- function: org.springframework.http.RequestEntity#put + copy: + - from: arg(0) + to: result +- function: org.springframework.http.RequestEntity#patch + copy: + - from: arg(0) + to: result +- function: org.springframework.http.RequestEntity#delete + copy: + - from: arg(0) + to: result +- function: org.springframework.http.RequestEntity#options + copy: + - from: arg(0) + to: result +- function: org.springframework.http.RequestEntity#method + copy: + - from: arg(*) + to: result +# ── HeadersBuilder chain (get/head/delete/options return this) ── +- function: org.springframework.http.RequestEntity$HeadersBuilder#header + copy: + - from: arg(*) + to: result + - from: this + to: result +- function: org.springframework.http.RequestEntity$HeadersBuilder#headers + copy: + - from: arg(*) + to: result + - from: this + to: result +- function: org.springframework.http.RequestEntity$HeadersBuilder#accept + copy: + - from: this + to: result +- function: org.springframework.http.RequestEntity$HeadersBuilder#acceptCharset + copy: + - from: this + to: result +- function: org.springframework.http.RequestEntity$HeadersBuilder#ifModifiedSince + copy: + - from: this + to: result +- function: org.springframework.http.RequestEntity$HeadersBuilder#ifNoneMatch copy: - from: this to: result @@ -14,3 +73,18 @@ passThrough: copy: - from: this to: result +# ── BodyBuilder chain (post/put/patch/method return this) ── +- function: org.springframework.http.RequestEntity$BodyBuilder#contentLength + copy: + - from: this + to: result +- function: org.springframework.http.RequestEntity$BodyBuilder#contentType + copy: + - from: this + to: result +- function: org.springframework.http.RequestEntity$BodyBuilder#body + copy: + - from: arg(0) + to: result + - from: this + to: result diff --git a/core/opentaint-config/config/config/jar-split/velocity-engine-core-2.3.yaml b/core/opentaint-config/config/config/jar-split/velocity-engine-core-2.3.yaml index 309cc5d3e..a92cf0e63 100644 --- a/core/opentaint-config/config/config/jar-split/velocity-engine-core-2.3.yaml +++ b/core/opentaint-config/config/config/jar-split/velocity-engine-core-2.3.yaml @@ -1,8 +1,14 @@ passThrough: -# Apache Velocity: VelocityContext.put($k, $v) and the AbstractContext -# super-class — taint flows from the value argument into the context -# instance so a tainted value carried into the context reaches a -# subsequent VelocityEngine.evaluate / Template.merge sink. +# Apache Velocity: Context.put($k, $v) — the value argument is stored on +# the context instance, so a tainted value carried into the context +# reaches a subsequent VelocityEngine.evaluate / Template.merge sink that +# consumes the whole context. Entries cover the Context interface, the +# AbstractContext super-class and the concrete VelocityContext, since the +# call site may be typed to any of them. +- function: org.apache.velocity.context.Context#put + copy: + - from: arg(1) + to: this - function: org.apache.velocity.VelocityContext#put copy: - from: arg(1) diff --git a/core/opentaint-config/config/config/stdlib.yaml b/core/opentaint-config/config/config/stdlib.yaml index 1d9e8bfbd..bbb4a244a 100644 --- a/core/opentaint-config/config/config/stdlib.yaml +++ b/core/opentaint-config/config/config/stdlib.yaml @@ -21383,7 +21383,7 @@ passThrough: - from: this to: result -# ── java.util.Base64$Encoder ────────────────────────────────────────── +# ── java.util.Base64$Encoder / $Decoder ─────────────────────────────── - function: java.util.Base64$Encoder#encodeToString copy: - from: arg(0) @@ -21392,6 +21392,14 @@ passThrough: copy: - from: arg(0) to: result +- function: java.util.Base64$Decoder#decode + copy: + - from: arg(0) + to: result +- function: java.util.Base64$Decoder#wrap + copy: + - from: arg(0) + to: result # ── java.net.URL (String) constructor (direct arg→this; the existing # URL#(String) entry uses arg(*) which doesn't apply consistently @@ -21425,6 +21433,12 @@ passThrough: to: this # ── java.net.http.HttpRequest$Builder (Java 11+ HttpClient) ─────────── +# Fluent chain, e.g. +# HttpRequest.newBuilder(URI.create($X)).header(..).GET().build() +# newBuilder seeds the builder from the (tainted) URI; every intermediate +# builder method must carry the builder taint to its return value (no +# implicit this->result exists) and .build() carries it into the +# HttpRequest that the HttpClient send/sendAsync sink consumes whole. - function: java.net.http.HttpRequest#newBuilder copy: - from: arg(0) @@ -21437,7 +21451,43 @@ passThrough: to: this - from: this to: result -- function: java.net.http.HttpRequest$Builder#build +- function: java.net.http.HttpRequest$Builder#header + copy: + - from: arg(*) + to: this + - from: this + to: result +- function: java.net.http.HttpRequest$Builder#headers + copy: + - from: arg(*) + to: this + - from: this + to: result +- function: java.net.http.HttpRequest$Builder#setHeader + copy: + - from: arg(*) + to: this + - from: this + to: result +- function: java.net.http.HttpRequest$Builder#POST + copy: + - from: arg(0) + to: this + - from: this + to: result +- function: java.net.http.HttpRequest$Builder#PUT + copy: + - from: arg(0) + to: this + - from: this + to: result +- function: java.net.http.HttpRequest$Builder#method + copy: + - from: arg(*) + to: this + - from: this + to: result +- function: java.net.http.HttpRequest$Builder#DELETE copy: - from: this to: result @@ -21445,3 +21495,23 @@ passThrough: copy: - from: this to: result +- function: java.net.http.HttpRequest$Builder#timeout + copy: + - from: this + to: result +- function: java.net.http.HttpRequest$Builder#version + copy: + - from: this + to: result +- function: java.net.http.HttpRequest$Builder#expectContinue + copy: + - from: this + to: result +- function: java.net.http.HttpRequest$Builder#copy + copy: + - from: this + to: result +- function: java.net.http.HttpRequest$Builder#build + copy: + - from: this + to: result From 0b85ac2a2cdd5c8bebabadce0453833692571271 Mon Sep 17 00:00:00 2001 From: Aleksandr Misonizhnik Date: Mon, 25 May 2026 16:35:37 +0200 Subject: [PATCH 3/6] refactor(analyzer): Route wrapper/builder taint through typed virtual fields MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make the new propagators precise and type-correct. A bare `arg -> this` (or `arg -> result`) makes the whole object abstract-tainted, so every later field read of it matches via abstract refinement — over-tainting — and copies a value of one type onto an object of another. Instead, store the differently-typed argument into a `` virtual field of the target: from: arg(i) to: - this # or result - .java.lang.Object##java.lang.Object This is precise (reading unrelated real fields no longer matches, since the node is no longer abstract) and type-safe (a field accessor owned by java.lang.Object is always type-compatible, so the slot survives every chain step and the Builder -> Product type change of build()/body() under a plain this->result carry). A sink that consumes the whole object still matches the slot by prefix. Applied to: StringEntity, hudson.FilePath, unboundid SearchRequest, JMXServiceURL/JMXConnectorFactory, StreamSource, java.net.URL(String), VelocityContext.put, CompilationUnit.addSource, ant FileSet set*, and the okhttp Request.Builder / java.net.http HttpRequest.Builder / spring-web RequestEntity / spring-ldap query-builder chains. Builder data methods store into both result (chained `a().b()`) and this (non-chained `x.a(); x.b()`) and carry this->result; every chain link keeps this->result so the chain survives. Pure transforms that *produce* the tainted value (toString/decode/getBytes/URI.create/MVEL.compile/ substituteNamedParameters/iterator.next) stay bare, since the result is itself the tainted value with no surrounding state to over-taint. --- .../config/config/jar-split/ant-1.10.14.yaml | 14 +- .../config/jar-split/groovy-3.0.21.yaml | 9 +- .../config/jar-split/httpcore5-5.2.4.yaml | 9 +- .../jar-split/jenkins-core-2.426.3.yaml | 7 +- .../config/jar-split/okhttp-4.12.0.yaml | 92 ++++++++++--- .../jar-split/spring-ldap-core-2.4.1.yaml | 126 +++++++++++++----- .../config/jar-split/spring-web-5.3.39.yaml | 71 +++++++--- .../jar-split/unboundid-ldapsdk-6.0.11.yaml | 8 +- .../jar-split/velocity-engine-core-2.3.yaml | 22 ++- .../config/config/stdlib.yaml | 108 ++++++++++++--- 10 files changed, 365 insertions(+), 101 deletions(-) diff --git a/core/opentaint-config/config/config/jar-split/ant-1.10.14.yaml b/core/opentaint-config/config/config/jar-split/ant-1.10.14.yaml index 603e04614..1ec6fb375 100644 --- a/core/opentaint-config/config/config/jar-split/ant-1.10.14.yaml +++ b/core/opentaint-config/config/config/jar-split/ant-1.10.14.yaml @@ -1,13 +1,19 @@ passThrough: # Apache Ant: FileSet.setDir(File) / setFile(File) — the file argument # is stored on the FileSet instance, so a subsequent -# `copy.addFileset(fs)` sink that requires a tainted $FILE detects -# the flow. +# `copy.addFileset(fs)` sink that requires a tainted $FILE detects the +# flow. The File argument has a different type than FileSet, so it is +# stored in a virtual field instead of tainting the whole instance; the +# sink that consumes the whole FileSet still matches it by prefix. - function: org.apache.tools.ant.types.FileSet#setDir copy: - from: arg(0) - to: this + to: + - this + - .java.lang.Object##java.lang.Object - function: org.apache.tools.ant.types.FileSet#setFile copy: - from: arg(0) - to: this + to: + - this + - .java.lang.Object##java.lang.Object diff --git a/core/opentaint-config/config/config/jar-split/groovy-3.0.21.yaml b/core/opentaint-config/config/config/jar-split/groovy-3.0.21.yaml index 5bbb797a5..c17a91074 100644 --- a/core/opentaint-config/config/config/jar-split/groovy-3.0.21.yaml +++ b/core/opentaint-config/config/config/jar-split/groovy-3.0.21.yaml @@ -4,8 +4,13 @@ passThrough: # so the source argument taints the unit. The source is the 2nd arg of # addSource(name, source|InputStream) but the 1st arg of the # addSource(File) / addSource(URL) / addSource(SourceUnit) overloads, so -# copy from any argument into the unit. +# copy from any argument. The argument type differs from CompilationUnit, +# so it is stored in a virtual field rather than tainting the whole +# instance; a sink that consumes the whole unit still matches it by +# prefix. - function: org.codehaus.groovy.control.CompilationUnit#addSource copy: - from: arg(*) - to: this + to: + - this + - .java.lang.Object##java.lang.Object diff --git a/core/opentaint-config/config/config/jar-split/httpcore5-5.2.4.yaml b/core/opentaint-config/config/config/jar-split/httpcore5-5.2.4.yaml index 92c8a81fd..fced88b16 100644 --- a/core/opentaint-config/config/config/jar-split/httpcore5-5.2.4.yaml +++ b/core/opentaint-config/config/config/jar-split/httpcore5-5.2.4.yaml @@ -1,7 +1,12 @@ passThrough: # Apache HttpComponents 5 — String-arg wrapper constructor that the -# SSRF sink rules use as an inline taint carrier. +# SSRF sink rules use as an inline taint carrier. The argument has a +# different type than the StringEntity, so it is stored in a virtual +# field rather than tainting the whole instance; a sink that consumes +# the whole entity still matches it by prefix. - function: org.apache.hc.core5.http.io.entity.StringEntity# copy: - from: arg(*) - to: this + to: + - this + - .java.lang.Object##java.lang.Object diff --git a/core/opentaint-config/config/config/jar-split/jenkins-core-2.426.3.yaml b/core/opentaint-config/config/config/jar-split/jenkins-core-2.426.3.yaml index 565b6f2f5..b2fb57a12 100644 --- a/core/opentaint-config/config/config/jar-split/jenkins-core-2.426.3.yaml +++ b/core/opentaint-config/config/config/jar-split/jenkins-core-2.426.3.yaml @@ -1,7 +1,12 @@ passThrough: # hudson.FilePath wrapper constructor — taint flows from any # String/File/URL argument into the constructed FilePath instance. +# The argument type differs from FilePath, so it is stored in a virtual +# field instead of tainting the whole object; a path-traversal sink that +# consumes the whole FilePath still matches it by prefix. - function: hudson.FilePath# copy: - from: arg(*) - to: this + to: + - this + - .java.lang.Object##java.lang.Object diff --git a/core/opentaint-config/config/config/jar-split/okhttp-4.12.0.yaml b/core/opentaint-config/config/config/jar-split/okhttp-4.12.0.yaml index c142e5ee0..ddb0841e8 100644 --- a/core/opentaint-config/config/config/jar-split/okhttp-4.12.0.yaml +++ b/core/opentaint-config/config/config/jar-split/okhttp-4.12.0.yaml @@ -1,65 +1,127 @@ passThrough: # OkHttp Request.Builder fluent chain, e.g. # new Request.Builder().url($X).addHeader(..).post($BODY).build() -# Library calls have no implicit this->result propagation, so every -# builder method that may appear mid-chain must carry the builder taint -# to its return value, or the chain breaks at the first unmodeled link. -# `.url()` additionally seeds the builder from its (tainted) argument and -# `.build()` carries the accumulated builder taint into the Request. +# +# Data-bearing methods store their (differently-typed) argument into a +# virtual field instead of tainting the whole builder — this keeps +# precision (unrelated builder/request state stays untainted) while a +# sink that consumes the whole Request still matches the slot by prefix. +# The slot is owned by java.lang.Object so it survives the +# Builder -> Request type change of .build() under a plain this->result +# carry. +# +# Each data method stores into both `result` (for chained use, +# `.url($X).build()`) and `this` (for non-chained use, `b.url($X); b.build()`), +# and carries this->result to forward taint accumulated by earlier links. +# Pure chain links carry this->result only. Library calls have no implicit +# this->result, so every link must forward the builder taint or the chain +# breaks at the first unmodeled method. - function: okhttp3.Request$Builder#url copy: - from: arg(0) - to: this + to: + - result + - .java.lang.Object##java.lang.Object - from: arg(0) - to: result + to: + - this + - .java.lang.Object##java.lang.Object - from: this to: result - function: okhttp3.Request$Builder#header copy: - from: arg(*) - to: this + to: + - result + - .java.lang.Object##java.lang.Object + - from: arg(*) + to: + - this + - .java.lang.Object##java.lang.Object - from: this to: result - function: okhttp3.Request$Builder#addHeader copy: - from: arg(*) - to: this + to: + - result + - .java.lang.Object##java.lang.Object + - from: arg(*) + to: + - this + - .java.lang.Object##java.lang.Object - from: this to: result - function: okhttp3.Request$Builder#headers copy: - from: arg(0) - to: this + to: + - result + - .java.lang.Object##java.lang.Object + - from: arg(0) + to: + - this + - .java.lang.Object##java.lang.Object - from: this to: result - function: okhttp3.Request$Builder#post copy: - from: arg(0) - to: this + to: + - result + - .java.lang.Object##java.lang.Object + - from: arg(0) + to: + - this + - .java.lang.Object##java.lang.Object - from: this to: result - function: okhttp3.Request$Builder#put copy: - from: arg(0) - to: this + to: + - result + - .java.lang.Object##java.lang.Object + - from: arg(0) + to: + - this + - .java.lang.Object##java.lang.Object - from: this to: result - function: okhttp3.Request$Builder#patch copy: - from: arg(0) - to: this + to: + - result + - .java.lang.Object##java.lang.Object + - from: arg(0) + to: + - this + - .java.lang.Object##java.lang.Object - from: this to: result - function: okhttp3.Request$Builder#delete copy: - from: arg(0) - to: this + to: + - result + - .java.lang.Object##java.lang.Object + - from: arg(0) + to: + - this + - .java.lang.Object##java.lang.Object - from: this to: result - function: okhttp3.Request$Builder#method copy: - from: arg(*) - to: this + to: + - result + - .java.lang.Object##java.lang.Object + - from: arg(*) + to: + - this + - .java.lang.Object##java.lang.Object - from: this to: result - function: okhttp3.Request$Builder#get diff --git a/core/opentaint-config/config/config/jar-split/spring-ldap-core-2.4.1.yaml b/core/opentaint-config/config/config/jar-split/spring-ldap-core-2.4.1.yaml index 26ec5b8ce..4d05cd01c 100644 --- a/core/opentaint-config/config/config/jar-split/spring-ldap-core-2.4.1.yaml +++ b/core/opentaint-config/config/config/jar-split/spring-ldap-core-2.4.1.yaml @@ -8,73 +8,109 @@ passThrough: # analyzer's chain-split sees the impl-class call sites, so both # interface and impl entries are needed. # -# The direct `arg(0) → result` form is what actually propagates taint -# through the chain; the two-step `arg(0)→this` + `this→result` form -# alone wasn't enough (the chain has too many implicit intermediates -# for two-step propagation to reach end-to-end without the direct -# shortcut). +# Data-bearing methods store their (differently-typed) attribute/value +# argument into a virtual field instead of tainting the whole builder / +# criteria object — precise, while an LdapTemplate sink consuming the +# whole LdapQuery matches the slot by prefix. The slot is stored into +# both `result` (chained use, the dominant style here) and `this` +# (non-chained use), and each link carries this->result. The +# slot is owned by java.lang.Object so it survives the +# LdapQueryBuilder -> ConditionCriteria -> ContainerCriteria -> LdapQuery +# type changes along the chain under plain this->result carries. - function: org.springframework.ldap.query.LdapQueryBuilder#base copy: - from: arg(0) - to: result + to: + - result + - .java.lang.Object##java.lang.Object - from: arg(0) - to: this + to: + - this + - .java.lang.Object##java.lang.Object - from: this to: result - function: org.springframework.ldap.query.LdapQueryBuilder#where copy: - from: arg(0) - to: result + to: + - result + - .java.lang.Object##java.lang.Object - from: arg(0) - to: this + to: + - this + - .java.lang.Object##java.lang.Object - from: this to: result - function: org.springframework.ldap.query.LdapQueryBuilder#filter copy: - from: arg(0) - to: result + to: + - result + - .java.lang.Object##java.lang.Object - from: arg(0) - to: this + to: + - this + - .java.lang.Object##java.lang.Object - from: this to: result - function: org.springframework.ldap.query.ConditionCriteria#is copy: - from: arg(0) - to: result + to: + - result + - .java.lang.Object##java.lang.Object - from: arg(0) - to: this + to: + - this + - .java.lang.Object##java.lang.Object - from: this to: result - function: org.springframework.ldap.query.ConditionCriteria#like copy: - from: arg(0) - to: result + to: + - result + - .java.lang.Object##java.lang.Object - from: arg(0) - to: this + to: + - this + - .java.lang.Object##java.lang.Object - from: this to: result - function: org.springframework.ldap.query.ConditionCriteria#whitespaceWildcardsLike copy: - from: arg(0) - to: result + to: + - result + - .java.lang.Object##java.lang.Object - from: arg(0) - to: this + to: + - this + - .java.lang.Object##java.lang.Object - from: this to: result - function: org.springframework.ldap.query.ConditionCriteria#gte copy: - from: arg(0) - to: result + to: + - result + - .java.lang.Object##java.lang.Object - from: arg(0) - to: this + to: + - this + - .java.lang.Object##java.lang.Object - from: this to: result - function: org.springframework.ldap.query.ConditionCriteria#lte copy: - from: arg(0) - to: result + to: + - result + - .java.lang.Object##java.lang.Object - from: arg(0) - to: this + to: + - this + - .java.lang.Object##java.lang.Object - from: this to: result # `.not()` is a value-less modifier that returns the same criteria — pure @@ -114,41 +150,61 @@ passThrough: - function: org.springframework.ldap.query.DefaultConditionCriteria#is copy: - from: arg(0) - to: this + to: + - result + - .java.lang.Object##java.lang.Object - from: arg(0) - to: result + to: + - this + - .java.lang.Object##java.lang.Object - from: this to: result - function: org.springframework.ldap.query.DefaultConditionCriteria#like copy: - from: arg(0) - to: this + to: + - result + - .java.lang.Object##java.lang.Object - from: arg(0) - to: result + to: + - this + - .java.lang.Object##java.lang.Object - from: this to: result - function: org.springframework.ldap.query.DefaultConditionCriteria#whitespaceWildcardsLike copy: - from: arg(0) - to: this + to: + - result + - .java.lang.Object##java.lang.Object - from: arg(0) - to: result + to: + - this + - .java.lang.Object##java.lang.Object - from: this to: result - function: org.springframework.ldap.query.DefaultConditionCriteria#gte copy: - from: arg(0) - to: this + to: + - result + - .java.lang.Object##java.lang.Object - from: arg(0) - to: result + to: + - this + - .java.lang.Object##java.lang.Object - from: this to: result - function: org.springframework.ldap.query.DefaultConditionCriteria#lte copy: - from: arg(0) - to: this + to: + - result + - .java.lang.Object##java.lang.Object - from: arg(0) - to: result + to: + - this + - .java.lang.Object##java.lang.Object - from: this to: result - function: org.springframework.ldap.query.DefaultConditionCriteria#not @@ -166,6 +222,12 @@ passThrough: - function: org.springframework.ldap.query.DefaultContainerCriteria#append copy: - from: arg(0) - to: this + to: + - result + - .java.lang.Object##java.lang.Object + - from: arg(0) + to: + - this + - .java.lang.Object##java.lang.Object - from: this to: result diff --git a/core/opentaint-config/config/config/jar-split/spring-web-5.3.39.yaml b/core/opentaint-config/config/config/jar-split/spring-web-5.3.39.yaml index a1f382a3c..14eb52e54 100644 --- a/core/opentaint-config/config/config/jar-split/spring-web-5.3.39.yaml +++ b/core/opentaint-config/config/config/jar-split/spring-web-5.3.39.yaml @@ -1,56 +1,89 @@ passThrough: # Spring RequestEntity fluent chain, e.g. the SSRF rule's pattern # RequestEntity.get(URI.create($X)).accept(..).header(..).build() -# The static factories seed the builder from the (tainted) URI argument; -# every intermediate HeadersBuilder/BodyBuilder method must carry the -# builder taint to its return value (no implicit this->result exists), -# and .build() carries it into the constructed RequestEntity which the -# RestTemplate sink consumes whole. +# +# The static factories store their (differently-typed) URI argument into +# a virtual field of the created builder instead of tainting it whole; +# data-bearing builder methods do the same. Every builder method also +# carries this->result so the chain survives (no implicit this->result +# exists), and .build() carries the slot into the RequestEntity — the +# slot is owned by java.lang.Object so it survives the +# Builder -> RequestEntity type change under a plain this->result carry. +# The RestTemplate sink that consumes the whole RequestEntity matches the +# slot by prefix. # # ── Static factories (URI is the first arg, except method(HttpMethod, URI)) ── - function: org.springframework.http.RequestEntity#get copy: - from: arg(0) - to: result + to: + - result + - .java.lang.Object##java.lang.Object - function: org.springframework.http.RequestEntity#head copy: - from: arg(0) - to: result + to: + - result + - .java.lang.Object##java.lang.Object - function: org.springframework.http.RequestEntity#post copy: - from: arg(0) - to: result + to: + - result + - .java.lang.Object##java.lang.Object - function: org.springframework.http.RequestEntity#put copy: - from: arg(0) - to: result + to: + - result + - .java.lang.Object##java.lang.Object - function: org.springframework.http.RequestEntity#patch copy: - from: arg(0) - to: result + to: + - result + - .java.lang.Object##java.lang.Object - function: org.springframework.http.RequestEntity#delete copy: - from: arg(0) - to: result + to: + - result + - .java.lang.Object##java.lang.Object - function: org.springframework.http.RequestEntity#options copy: - from: arg(0) - to: result + to: + - result + - .java.lang.Object##java.lang.Object - function: org.springframework.http.RequestEntity#method copy: - from: arg(*) - to: result + to: + - result + - .java.lang.Object##java.lang.Object # ── HeadersBuilder chain (get/head/delete/options return this) ── - function: org.springframework.http.RequestEntity$HeadersBuilder#header copy: - from: arg(*) - to: result + to: + - result + - .java.lang.Object##java.lang.Object + - from: arg(*) + to: + - this + - .java.lang.Object##java.lang.Object - from: this to: result - function: org.springframework.http.RequestEntity$HeadersBuilder#headers copy: - from: arg(*) - to: result + to: + - result + - .java.lang.Object##java.lang.Object + - from: arg(*) + to: + - this + - .java.lang.Object##java.lang.Object - from: this to: result - function: org.springframework.http.RequestEntity$HeadersBuilder#accept @@ -85,6 +118,12 @@ passThrough: - function: org.springframework.http.RequestEntity$BodyBuilder#body copy: - from: arg(0) - to: result + to: + - result + - .java.lang.Object##java.lang.Object + - from: arg(0) + to: + - this + - .java.lang.Object##java.lang.Object - from: this to: result diff --git a/core/opentaint-config/config/config/jar-split/unboundid-ldapsdk-6.0.11.yaml b/core/opentaint-config/config/config/jar-split/unboundid-ldapsdk-6.0.11.yaml index 466f26e62..dd663a056 100644 --- a/core/opentaint-config/config/config/jar-split/unboundid-ldapsdk-6.0.11.yaml +++ b/core/opentaint-config/config/config/jar-split/unboundid-ldapsdk-6.0.11.yaml @@ -1,5 +1,11 @@ passThrough: +# com.unboundid.ldap.sdk.SearchRequest constructor — the base DN / filter +# arguments have a different type than SearchRequest, so they are stored +# in a virtual field rather than tainting the whole instance; an LDAP +# sink that consumes the whole SearchRequest still matches it by prefix. - function: com.unboundid.ldap.sdk.SearchRequest# copy: - from: arg(*) - to: this + to: + - this + - .java.lang.Object##java.lang.Object diff --git a/core/opentaint-config/config/config/jar-split/velocity-engine-core-2.3.yaml b/core/opentaint-config/config/config/jar-split/velocity-engine-core-2.3.yaml index a92cf0e63..80f9cfc36 100644 --- a/core/opentaint-config/config/config/jar-split/velocity-engine-core-2.3.yaml +++ b/core/opentaint-config/config/config/jar-split/velocity-engine-core-2.3.yaml @@ -2,18 +2,28 @@ passThrough: # Apache Velocity: Context.put($k, $v) — the value argument is stored on # the context instance, so a tainted value carried into the context # reaches a subsequent VelocityEngine.evaluate / Template.merge sink that -# consumes the whole context. Entries cover the Context interface, the -# AbstractContext super-class and the concrete VelocityContext, since the -# call site may be typed to any of them. +# consumes the whole context. The value type differs from the context, +# so it is stored in a virtual field rather than tainting the whole +# instance (which would over-taint every other entry read back out of +# the context); the sink that consumes the whole context still matches it +# by prefix. Entries cover the Context interface, the AbstractContext +# super-class and the concrete VelocityContext, since the call site may +# be typed to any of them. - function: org.apache.velocity.context.Context#put copy: - from: arg(1) - to: this + to: + - this + - .java.lang.Object##java.lang.Object - function: org.apache.velocity.VelocityContext#put copy: - from: arg(1) - to: this + to: + - this + - .java.lang.Object##java.lang.Object - function: org.apache.velocity.context.AbstractContext#put copy: - from: arg(1) - to: this + to: + - this + - .java.lang.Object##java.lang.Object diff --git a/core/opentaint-config/config/config/stdlib.yaml b/core/opentaint-config/config/config/stdlib.yaml index bbb4a244a..7aa8463a7 100644 --- a/core/opentaint-config/config/config/stdlib.yaml +++ b/core/opentaint-config/config/config/stdlib.yaml @@ -21401,14 +21401,19 @@ passThrough: - from: arg(0) to: result -# ── java.net.URL (String) constructor (direct arg→this; the existing -# URL#(String) entry uses arg(*) which doesn't apply consistently -# enough for tests like UnsafeStaplerServeFileServlet) ──────────────── +# ── java.net.URL (String) constructor — the String argument has a +# different type than URL, so it is stored in a virtual field rather +# than tainting the whole instance; an SSRF/file sink that consumes +# the whole URL still matches it by prefix. (The existing arg(*) +# URL# entry didn't apply consistently enough for tests like +# UnsafeStaplerServeFileServlet.) ───────────────────────────────────── - function: java.net.URL# signature: (java.lang.String) void copy: - from: arg(0) - to: this + to: + - this + - .java.lang.Object##java.lang.Object # ── java.net.URI ────────────────────────────────────────────────────── - function: java.net.URI#create @@ -21416,75 +21421,134 @@ passThrough: - from: arg(0) to: result -# ── javax.management JMX (stdlib management API) ─────────────────────── +# ── javax.management JMX (stdlib management API) — the URL/service args +# have different types than the JMXServiceURL / JMXConnector they end +# up in, so they are stored in virtual fields rather than tainting the +# whole instance; a sink consuming the whole object matches by prefix. - function: javax.management.remote.JMXServiceURL# copy: - from: arg(*) - to: this + to: + - this + - .java.lang.Object##java.lang.Object - function: javax.management.remote.JMXConnectorFactory#newJMXConnector copy: - from: arg(0) - to: result + to: + - result + - .java.lang.Object##java.lang.Object -# ── javax.xml.transform.stream.StreamSource ──────────────────────────── +# ── javax.xml.transform.stream.StreamSource — the InputStream/Reader/ +# systemId arg differs in type from StreamSource, so it goes into a +# virtual field; the XXE sink consuming the whole source matches by +# prefix. ────────────────────────────────────────────────────────── - function: javax.xml.transform.stream.StreamSource# copy: - from: arg(*) - to: this + to: + - this + - .java.lang.Object##java.lang.Object # ── java.net.http.HttpRequest$Builder (Java 11+ HttpClient) ─────────── # Fluent chain, e.g. # HttpRequest.newBuilder(URI.create($X)).header(..).GET().build() -# newBuilder seeds the builder from the (tainted) URI; every intermediate -# builder method must carry the builder taint to its return value (no -# implicit this->result exists) and .build() carries it into the -# HttpRequest that the HttpClient send/sendAsync sink consumes whole. +# Data-bearing methods store their (differently-typed) argument into a +# virtual field instead of tainting the whole builder — precise, while a +# sink consuming the whole HttpRequest matches the slot by prefix. The +# slot is owned by java.lang.Object so it survives the +# Builder -> HttpRequest type change of .build() under a plain +# this->result carry. Each data method stores into both `result` (chained +# use) and `this` (non-chained use) and carries this->result; pure links +# carry this->result only (no implicit this->result exists for library +# calls). - function: java.net.http.HttpRequest#newBuilder copy: - from: arg(0) - to: result + to: + - result + - .java.lang.Object##java.lang.Object - function: java.net.http.HttpRequest$Builder#uri copy: - from: arg(0) - to: result + to: + - result + - .java.lang.Object##java.lang.Object - from: arg(0) - to: this + to: + - this + - .java.lang.Object##java.lang.Object - from: this to: result - function: java.net.http.HttpRequest$Builder#header copy: - from: arg(*) - to: this + to: + - result + - .java.lang.Object##java.lang.Object + - from: arg(*) + to: + - this + - .java.lang.Object##java.lang.Object - from: this to: result - function: java.net.http.HttpRequest$Builder#headers copy: - from: arg(*) - to: this + to: + - result + - .java.lang.Object##java.lang.Object + - from: arg(*) + to: + - this + - .java.lang.Object##java.lang.Object - from: this to: result - function: java.net.http.HttpRequest$Builder#setHeader copy: - from: arg(*) - to: this + to: + - result + - .java.lang.Object##java.lang.Object + - from: arg(*) + to: + - this + - .java.lang.Object##java.lang.Object - from: this to: result - function: java.net.http.HttpRequest$Builder#POST copy: - from: arg(0) - to: this + to: + - result + - .java.lang.Object##java.lang.Object + - from: arg(0) + to: + - this + - .java.lang.Object##java.lang.Object - from: this to: result - function: java.net.http.HttpRequest$Builder#PUT copy: - from: arg(0) - to: this + to: + - result + - .java.lang.Object##java.lang.Object + - from: arg(0) + to: + - this + - .java.lang.Object##java.lang.Object - from: this to: result - function: java.net.http.HttpRequest$Builder#method copy: - from: arg(*) - to: this + to: + - result + - .java.lang.Object##java.lang.Object + - from: arg(*) + to: + - this + - .java.lang.Object##java.lang.Object - from: this to: result - function: java.net.http.HttpRequest$Builder#DELETE From c3cc00bece9b3924a5d8eb01d2a8cc67563874bd Mon Sep 17 00:00:00 2001 From: Aleksandr Misonizhnik Date: Mon, 25 May 2026 17:47:17 +0200 Subject: [PATCH 4/6] refactor(analyzer): Drop passThrough approximations duplicated elsewhere MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Several new propagators repeated rules already present in other bundled config files (all .yaml under /config are concatenated at load time, so a second copy of the same function is a real runtime duplicate). Removed: - commons-codec: Base64#encodeBase64 / #decodeBase64 — already in config.yaml (identical arg(0)->result). Kept the String / URL-safe / chunked variants, which are not. - stdlib: dropped the catch-all (no-signature) entries that existing signature-specific rules already cover better: String#getBytes, URI#create, Enumeration#nextElement (existing () entry already has this->result), URL#(String) (existing same-signature entry), Base64$Decoder#decode (existing typed overloads), and StreamSource# (jmod.yaml has all typed overloads). Also dropped Collection#iterator (covered by the kept Iterable#iterator via override). Kept Iterable#iterator / Iterator#next generics — the existing entries only model element-level (.Element) flow, so these add the whole-collection this->result carry. - spring-web-5.3.39: dropped the RequestEntity HeadersBuilder/BodyBuilder chain methods — spring-web-7.0.2.yaml already provides them. Kept only the static factories (get/post/method/...), because v7 models those as this->result, a no-op for static methods; the arg-based form here is what actually carries the URI. Note: spring-web-7.0.2.yaml's RequestEntity static factories are dead (this-based on static methods); they should be fixed or removed there separately. --- .../jar-split/commons-codec-1.16.0.yaml | 12 +-- .../config/jar-split/spring-web-5.3.39.yaml | 91 +++---------------- .../config/config/stdlib.yaml | 55 +---------- 3 files changed, 20 insertions(+), 138 deletions(-) diff --git a/core/opentaint-config/config/config/jar-split/commons-codec-1.16.0.yaml b/core/opentaint-config/config/config/jar-split/commons-codec-1.16.0.yaml index b7fde8002..5c87db7ed 100644 --- a/core/opentaint-config/config/config/jar-split/commons-codec-1.16.0.yaml +++ b/core/opentaint-config/config/config/jar-split/commons-codec-1.16.0.yaml @@ -1,15 +1,13 @@ passThrough: # Apache Commons Codec — Base64 encode/decode just re-codes bytes # without disturbing the underlying tainted data, so taint should -# flow from input to output. +# flow from input to output. The plain encodeBase64 / decodeBase64 +# overloads are already covered by config.yaml; only the String / URL-safe +# / chunked variants are added here. - function: org.apache.commons.codec.binary.Base64#encodeBase64String copy: - from: arg(0) to: result -- function: org.apache.commons.codec.binary.Base64#encodeBase64 - copy: - - from: arg(0) - to: result - function: org.apache.commons.codec.binary.Base64#encodeBase64URLSafeString copy: - from: arg(0) @@ -22,7 +20,3 @@ passThrough: copy: - from: arg(0) to: result -- function: org.apache.commons.codec.binary.Base64#decodeBase64 - copy: - - from: arg(0) - to: result diff --git a/core/opentaint-config/config/config/jar-split/spring-web-5.3.39.yaml b/core/opentaint-config/config/config/jar-split/spring-web-5.3.39.yaml index 14eb52e54..fa7c9fd40 100644 --- a/core/opentaint-config/config/config/jar-split/spring-web-5.3.39.yaml +++ b/core/opentaint-config/config/config/jar-split/spring-web-5.3.39.yaml @@ -1,18 +1,19 @@ passThrough: -# Spring RequestEntity fluent chain, e.g. the SSRF rule's pattern +# Spring RequestEntity static factories, e.g. the SSRF rule's pattern # RequestEntity.get(URI.create($X)).accept(..).header(..).build() # -# The static factories store their (differently-typed) URI argument into -# a virtual field of the created builder instead of tainting it whole; -# data-bearing builder methods do the same. Every builder method also -# carries this->result so the chain survives (no implicit this->result -# exists), and .build() carries the slot into the RequestEntity — the -# slot is owned by java.lang.Object so it survives the -# Builder -> RequestEntity type change under a plain this->result carry. -# The RestTemplate sink that consumes the whole RequestEntity matches the -# slot by prefix. -# -# ── Static factories (URI is the first arg, except method(HttpMethod, URI)) ── +# Only the static factories live here: the HeadersBuilder / BodyBuilder +# chain methods (header/accept/contentType/body/build/...) are already +# provided by spring-web-7.0.2.yaml (RequestEntity is the same class in +# both jars), so they are not duplicated here. The factories ARE needed +# here because spring-web-7.0.2.yaml models them as `this -> result`, +# which is a no-op for these *static* methods; the arg-based form below +# is what actually carries the (differently-typed) URI argument into the +# created builder. It is stored in a java.lang.Object-owned +# virtual field (precise, and it survives the builder -> RequestEntity +# type change of .build() under spring-web-7.0.2's this->result carry); +# the RestTemplate sink consuming the whole RequestEntity matches the +# slot by prefix. (URI is the first arg, except method(HttpMethod, URI).) - function: org.springframework.http.RequestEntity#get copy: - from: arg(0) @@ -61,69 +62,3 @@ passThrough: to: - result - .java.lang.Object##java.lang.Object -# ── HeadersBuilder chain (get/head/delete/options return this) ── -- function: org.springframework.http.RequestEntity$HeadersBuilder#header - copy: - - from: arg(*) - to: - - result - - .java.lang.Object##java.lang.Object - - from: arg(*) - to: - - this - - .java.lang.Object##java.lang.Object - - from: this - to: result -- function: org.springframework.http.RequestEntity$HeadersBuilder#headers - copy: - - from: arg(*) - to: - - result - - .java.lang.Object##java.lang.Object - - from: arg(*) - to: - - this - - .java.lang.Object##java.lang.Object - - from: this - to: result -- function: org.springframework.http.RequestEntity$HeadersBuilder#accept - copy: - - from: this - to: result -- function: org.springframework.http.RequestEntity$HeadersBuilder#acceptCharset - copy: - - from: this - to: result -- function: org.springframework.http.RequestEntity$HeadersBuilder#ifModifiedSince - copy: - - from: this - to: result -- function: org.springframework.http.RequestEntity$HeadersBuilder#ifNoneMatch - copy: - - from: this - to: result -- function: org.springframework.http.RequestEntity$HeadersBuilder#build - copy: - - from: this - to: result -# ── BodyBuilder chain (post/put/patch/method return this) ── -- function: org.springframework.http.RequestEntity$BodyBuilder#contentLength - copy: - - from: this - to: result -- function: org.springframework.http.RequestEntity$BodyBuilder#contentType - copy: - - from: this - to: result -- function: org.springframework.http.RequestEntity$BodyBuilder#body - copy: - - from: arg(0) - to: - - result - - .java.lang.Object##java.lang.Object - - from: arg(0) - to: - - this - - .java.lang.Object##java.lang.Object - - from: this - to: result diff --git a/core/opentaint-config/config/config/stdlib.yaml b/core/opentaint-config/config/config/stdlib.yaml index 7aa8463a7..7a2aed726 100644 --- a/core/opentaint-config/config/config/stdlib.yaml +++ b/core/opentaint-config/config/config/stdlib.yaml @@ -21359,11 +21359,9 @@ passThrough: - this - .java.io.InputStream##java.lang.Object -# ── Collection / Iterator / Iterable / Enumeration ───────────────────── -- function: java.util.Collection#iterator - copy: - - from: this - to: result +# ── Iterator / Iterable (whole-collection -> iterator/element; existing +# sig-specific entries only model element-level vf, so these add the +# whole-object carry) ────────────────────────────────────────────── - function: java.lang.Iterable#iterator copy: - from: this @@ -21372,18 +21370,8 @@ passThrough: copy: - from: this to: result -- function: java.util.Enumeration#nextElement - copy: - - from: this - to: result - -# ── java.lang.String#getBytes (String → byte[]) ──────────────────────── -- function: java.lang.String#getBytes - copy: - - from: this - to: result -# ── java.util.Base64$Encoder / $Decoder ─────────────────────────────── +# ── java.util.Base64$Encoder ────────────────────────────────────────── - function: java.util.Base64$Encoder#encodeToString copy: - from: arg(0) @@ -21392,35 +21380,11 @@ passThrough: copy: - from: arg(0) to: result -- function: java.util.Base64$Decoder#decode - copy: - - from: arg(0) - to: result - function: java.util.Base64$Decoder#wrap copy: - from: arg(0) to: result -# ── java.net.URL (String) constructor — the String argument has a -# different type than URL, so it is stored in a virtual field rather -# than tainting the whole instance; an SSRF/file sink that consumes -# the whole URL still matches it by prefix. (The existing arg(*) -# URL# entry didn't apply consistently enough for tests like -# UnsafeStaplerServeFileServlet.) ───────────────────────────────────── -- function: java.net.URL# - signature: (java.lang.String) void - copy: - - from: arg(0) - to: - - this - - .java.lang.Object##java.lang.Object - -# ── java.net.URI ────────────────────────────────────────────────────── -- function: java.net.URI#create - copy: - - from: arg(0) - to: result - # ── javax.management JMX (stdlib management API) — the URL/service args # have different types than the JMXServiceURL / JMXConnector they end # up in, so they are stored in virtual fields rather than tainting the @@ -21438,17 +21402,6 @@ passThrough: - result - .java.lang.Object##java.lang.Object -# ── javax.xml.transform.stream.StreamSource — the InputStream/Reader/ -# systemId arg differs in type from StreamSource, so it goes into a -# virtual field; the XXE sink consuming the whole source matches by -# prefix. ────────────────────────────────────────────────────────── -- function: javax.xml.transform.stream.StreamSource# - copy: - - from: arg(*) - to: - - this - - .java.lang.Object##java.lang.Object - # ── java.net.http.HttpRequest$Builder (Java 11+ HttpClient) ─────────── # Fluent chain, e.g. # HttpRequest.newBuilder(URI.create($X)).header(..).GET().build() From e72752e9020b6df97691fb9c53b5c85f2b8ba630 Mon Sep 17 00:00:00 2001 From: Aleksandr Misonizhnik Date: Mon, 25 May 2026 18:00:20 +0200 Subject: [PATCH 5/6] refactor(analyzer): Fix RequestEntity static factories in v7, drop 5.3.39 file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit spring-web-7.0.2.yaml modeled the RequestEntity.get/post/head/put/patch/ delete/options/method static factories as `this -> result`, which is a no-op for static methods, so they never carried the URI argument. Replace them with the arg-based form that stores the URI into a java.lang.Object- owned slot of the returned builder; the slot survives the builder -> RequestEntity type change of .build() under the existing this->result builder carries, and the RestTemplate sink consuming the whole RequestEntity matches it by prefix. With the factories fixed, spring-web-5.3.39.yaml (which only existed to provide working arg-based factories) is fully redundant — RequestEntity is the same class in both jars and all config now lives in spring-web-7.0.2.yaml — so delete it. --- .../config/jar-split/spring-web-5.3.39.yaml | 64 -------------- .../config/jar-split/spring-web-7.0.2.yaml | 85 ++++++++----------- 2 files changed, 37 insertions(+), 112 deletions(-) delete mode 100644 core/opentaint-config/config/config/jar-split/spring-web-5.3.39.yaml diff --git a/core/opentaint-config/config/config/jar-split/spring-web-5.3.39.yaml b/core/opentaint-config/config/config/jar-split/spring-web-5.3.39.yaml deleted file mode 100644 index fa7c9fd40..000000000 --- a/core/opentaint-config/config/config/jar-split/spring-web-5.3.39.yaml +++ /dev/null @@ -1,64 +0,0 @@ -passThrough: -# Spring RequestEntity static factories, e.g. the SSRF rule's pattern -# RequestEntity.get(URI.create($X)).accept(..).header(..).build() -# -# Only the static factories live here: the HeadersBuilder / BodyBuilder -# chain methods (header/accept/contentType/body/build/...) are already -# provided by spring-web-7.0.2.yaml (RequestEntity is the same class in -# both jars), so they are not duplicated here. The factories ARE needed -# here because spring-web-7.0.2.yaml models them as `this -> result`, -# which is a no-op for these *static* methods; the arg-based form below -# is what actually carries the (differently-typed) URI argument into the -# created builder. It is stored in a java.lang.Object-owned -# virtual field (precise, and it survives the builder -> RequestEntity -# type change of .build() under spring-web-7.0.2's this->result carry); -# the RestTemplate sink consuming the whole RequestEntity matches the -# slot by prefix. (URI is the first arg, except method(HttpMethod, URI).) -- function: org.springframework.http.RequestEntity#get - copy: - - from: arg(0) - to: - - result - - .java.lang.Object##java.lang.Object -- function: org.springframework.http.RequestEntity#head - copy: - - from: arg(0) - to: - - result - - .java.lang.Object##java.lang.Object -- function: org.springframework.http.RequestEntity#post - copy: - - from: arg(0) - to: - - result - - .java.lang.Object##java.lang.Object -- function: org.springframework.http.RequestEntity#put - copy: - - from: arg(0) - to: - - result - - .java.lang.Object##java.lang.Object -- function: org.springframework.http.RequestEntity#patch - copy: - - from: arg(0) - to: - - result - - .java.lang.Object##java.lang.Object -- function: org.springframework.http.RequestEntity#delete - copy: - - from: arg(0) - to: - - result - - .java.lang.Object##java.lang.Object -- function: org.springframework.http.RequestEntity#options - copy: - - from: arg(0) - to: - - result - - .java.lang.Object##java.lang.Object -- function: org.springframework.http.RequestEntity#method - copy: - - from: arg(*) - to: - - result - - .java.lang.Object##java.lang.Object diff --git a/core/opentaint-config/config/config/jar-split/spring-web-7.0.2.yaml b/core/opentaint-config/config/config/jar-split/spring-web-7.0.2.yaml index a6ce562b7..f4a6125c7 100644 --- a/core/opentaint-config/config/config/jar-split/spring-web-7.0.2.yaml +++ b/core/opentaint-config/config/config/jar-split/spring-web-7.0.2.yaml @@ -97,30 +97,29 @@ passThrough: copy: - from: this to: result +# RequestEntity.get/post/... are STATIC factories taking the URI as the +# first argument (HttpMethod, URI for method()); a this->result copy is a +# no-op for them. Carry the URI argument into the returned builder via a +# java.lang.Object-owned slot, which survives the +# builder -> RequestEntity type change of .build() under this->result. - function: org.springframework.http.RequestEntity#delete copy: - - from: - - this - - .org.springframework.http.RequestEntity##java.lang.Object - to: result - - from: this - to: result + - from: arg(0) + to: + - result + - .java.lang.Object##java.lang.Object - function: org.springframework.http.RequestEntity#get copy: - - from: - - this - - .org.springframework.http.RequestEntity##java.lang.Object - to: result - - from: this - to: result + - from: arg(0) + to: + - result + - .java.lang.Object##java.lang.Object - function: org.springframework.http.RequestEntity#head copy: - - from: - - this - - .org.springframework.http.RequestEntity##java.lang.Object - to: result - - from: this - to: result + - from: arg(0) + to: + - result + - .java.lang.Object##java.lang.Object - function: org.springframework.http.RequestEntity# copy: - from: arg(1) @@ -137,44 +136,34 @@ passThrough: - .org.springframework.http.RequestEntity##java.lang.Object - function: org.springframework.http.RequestEntity#method copy: - - from: - - this - - .org.springframework.http.RequestEntity##java.lang.Object - to: result - - from: this - to: result + - from: arg(*) + to: + - result + - .java.lang.Object##java.lang.Object - function: org.springframework.http.RequestEntity#options copy: - - from: - - this - - .org.springframework.http.RequestEntity##java.lang.Object - to: result - - from: this - to: result + - from: arg(0) + to: + - result + - .java.lang.Object##java.lang.Object - function: org.springframework.http.RequestEntity#patch copy: - - from: - - this - - .org.springframework.http.RequestEntity##java.lang.Object - to: result - - from: this - to: result + - from: arg(0) + to: + - result + - .java.lang.Object##java.lang.Object - function: org.springframework.http.RequestEntity#post copy: - - from: - - this - - .org.springframework.http.RequestEntity##java.lang.Object - to: result - - from: this - to: result + - from: arg(0) + to: + - result + - .java.lang.Object##java.lang.Object - function: org.springframework.http.RequestEntity#put copy: - - from: - - this - - .org.springframework.http.RequestEntity##java.lang.Object - to: result - - from: this - to: result + - from: arg(0) + to: + - result + - .java.lang.Object##java.lang.Object - function: org.springframework.http.ResponseEntity$BodyBuilder#body copy: - from: arg(0) From 1f41a7558cd85d9df309128d90e52fb203809f06 Mon Sep 17 00:00:00 2001 From: Aleksandr Misonizhnik Date: Mon, 25 May 2026 18:12:24 +0200 Subject: [PATCH 6/6] refactor(analyzer): Use builder-typed RequestEntity slot with re-key at terminals MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to the static-factory fix: instead of a java.lang.Object-owned slot, store the factory's URI argument into the builder's own type slot (RequestEntity$HeadersBuilder#, which also covers BodyBuilder since it extends HeadersBuilder), and re-key it onto the RequestEntity# slot at the terminals .build() and .body(). This is more type-honest and unifies with the existing / getBody modeling (all RequestEntity construction paths now land taint in the same RequestEntity# slot), and a builder-typed slot is type-pruned if taint ever reaches a non-builder object. The cost is that every terminal returning a RequestEntity must re-key (HeadersBuilder is not a subtype of RequestEntity, so a plain this->result drops the builder-owned slot) — both build() and body() do. --- .../config/jar-split/spring-web-7.0.2.yaml | 41 ++++++++++++++----- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/core/opentaint-config/config/config/jar-split/spring-web-7.0.2.yaml b/core/opentaint-config/config/config/jar-split/spring-web-7.0.2.yaml index f4a6125c7..eb427416d 100644 --- a/core/opentaint-config/config/config/jar-split/spring-web-7.0.2.yaml +++ b/core/opentaint-config/config/config/jar-split/spring-web-7.0.2.yaml @@ -53,6 +53,14 @@ passThrough: copy: - from: this to: result + # Terminal: re-key the builder slot onto the constructed RequestEntity + # (same reason as HeadersBuilder#build above). + - from: + - this + - .org.springframework.http.RequestEntity$HeadersBuilder##java.lang.Object + to: + - result + - .org.springframework.http.RequestEntity##java.lang.Object - function: org.springframework.http.RequestEntity$BodyBuilder#contentLength copy: - from: this @@ -73,6 +81,15 @@ passThrough: copy: - from: this to: result + # Terminal: re-key the builder slot onto the constructed RequestEntity + # (HeadersBuilder is not a subtype of RequestEntity, so a plain + # this->result would drop the builder-owned slot). + - from: + - this + - .org.springframework.http.RequestEntity$HeadersBuilder##java.lang.Object + to: + - result + - .org.springframework.http.RequestEntity##java.lang.Object - function: org.springframework.http.RequestEntity$HeadersBuilder#header copy: - from: arg(0) @@ -99,27 +116,29 @@ passThrough: to: result # RequestEntity.get/post/... are STATIC factories taking the URI as the # first argument (HttpMethod, URI for method()); a this->result copy is a -# no-op for them. Carry the URI argument into the returned builder via a -# java.lang.Object-owned slot, which survives the -# builder -> RequestEntity type change of .build() under this->result. +# no-op for them. Carry the URI argument into the returned builder's +# slot (owner HeadersBuilder, which also covers BodyBuilder +# since it extends HeadersBuilder). The chain methods carry this->result, +# and the terminals .build() / .body() re-key the builder slot into the +# RequestEntity# slot (see those entries). - function: org.springframework.http.RequestEntity#delete copy: - from: arg(0) to: - result - - .java.lang.Object##java.lang.Object + - .org.springframework.http.RequestEntity$HeadersBuilder##java.lang.Object - function: org.springframework.http.RequestEntity#get copy: - from: arg(0) to: - result - - .java.lang.Object##java.lang.Object + - .org.springframework.http.RequestEntity$HeadersBuilder##java.lang.Object - function: org.springframework.http.RequestEntity#head copy: - from: arg(0) to: - result - - .java.lang.Object##java.lang.Object + - .org.springframework.http.RequestEntity$HeadersBuilder##java.lang.Object - function: org.springframework.http.RequestEntity# copy: - from: arg(1) @@ -139,31 +158,31 @@ passThrough: - from: arg(*) to: - result - - .java.lang.Object##java.lang.Object + - .org.springframework.http.RequestEntity$HeadersBuilder##java.lang.Object - function: org.springframework.http.RequestEntity#options copy: - from: arg(0) to: - result - - .java.lang.Object##java.lang.Object + - .org.springframework.http.RequestEntity$HeadersBuilder##java.lang.Object - function: org.springframework.http.RequestEntity#patch copy: - from: arg(0) to: - result - - .java.lang.Object##java.lang.Object + - .org.springframework.http.RequestEntity$HeadersBuilder##java.lang.Object - function: org.springframework.http.RequestEntity#post copy: - from: arg(0) to: - result - - .java.lang.Object##java.lang.Object + - .org.springframework.http.RequestEntity$HeadersBuilder##java.lang.Object - function: org.springframework.http.RequestEntity#put copy: - from: arg(0) to: - result - - .java.lang.Object##java.lang.Object + - .org.springframework.http.RequestEntity$HeadersBuilder##java.lang.Object - function: org.springframework.http.ResponseEntity$BodyBuilder#body copy: - from: arg(0)