From d3775a2c6683f39f9c9cd147b87258523f06713d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 11 Aug 2026 08:11:11 +0000 Subject: [PATCH 1/4] Initial plan From 925d3e2c219ccd601c727c54822a2d6852fbad7a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 11 Aug 2026 08:24:07 +0000 Subject: [PATCH 2/4] Java: replace non-existent String.valueOf(CharSequence) summary with a synthetic callable for valueOf(Object) on CharSequence arguments Co-authored-by: hvitved <3667920+hvitved@users.noreply.github.com> --- .../2026-08-11-string-valueof-charsequence.md | 4 ++ java/ql/lib/ext/java.lang.model.yml | 1 - .../semmle/code/java/dataflow/FlowSummary.qll | 1 + .../semmle/code/java/frameworks/Strings.qll | 40 +++++++++++++++++++ .../dataflow/string-valueof/A.java | 26 ++++++++++++ .../dataflow/string-valueof/test.expected | 39 ++++++++++++++++++ .../dataflow/string-valueof/test.ql | 3 ++ 7 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 java/ql/lib/change-notes/2026-08-11-string-valueof-charsequence.md create mode 100644 java/ql/lib/semmle/code/java/frameworks/Strings.qll create mode 100644 java/ql/test/library-tests/dataflow/string-valueof/A.java create mode 100644 java/ql/test/library-tests/dataflow/string-valueof/test.expected create mode 100644 java/ql/test/library-tests/dataflow/string-valueof/test.ql diff --git a/java/ql/lib/change-notes/2026-08-11-string-valueof-charsequence.md b/java/ql/lib/change-notes/2026-08-11-string-valueof-charsequence.md new file mode 100644 index 000000000000..51331eb08372 --- /dev/null +++ b/java/ql/lib/change-notes/2026-08-11-string-valueof-charsequence.md @@ -0,0 +1,4 @@ +--- +category: minorAnalysis +--- +* Removed the summary model for `String.valueOf(CharSequence)`, which does not exist. Instead, taint is now propagated through calls to `String.valueOf(Object)` when the argument is a `CharSequence`, for example a `String` or a `StringBuilder`. diff --git a/java/ql/lib/ext/java.lang.model.yml b/java/ql/lib/ext/java.lang.model.yml index 9e3d9e8cee54..d613c6fe8f5b 100644 --- a/java/ql/lib/ext/java.lang.model.yml +++ b/java/ql/lib/ext/java.lang.model.yml @@ -134,7 +134,6 @@ extensions: - ["java.lang", "String", False, "valueOf", "(char)", "", "Argument[0]", "ReturnValue", "taint", "manual"] - ["java.lang", "String", False, "valueOf", "(char[])", "", "Argument[0]", "ReturnValue", "taint", "manual"] - ["java.lang", "String", False, "valueOf", "(char[],int,int)", "", "Argument[0]", "ReturnValue", "taint", "manual"] - - ["java.lang", "String", False, "valueOf", "(CharSequence)", "", "Argument[0]", "ReturnValue", "taint", "manual"] - ["java.lang", "StringBuffer", True, "StringBuffer", "(CharSequence)", "", "Argument[0]", "Argument[this]", "taint", "manual"] - ["java.lang", "StringBuffer", True, "StringBuffer", "(String)", "", "Argument[0]", "Argument[this]", "taint", "manual"] - ["java.lang", "StringBuilder", True, "StringBuilder", "", "", "Argument[0]", "Argument[this]", "taint", "manual"] diff --git a/java/ql/lib/semmle/code/java/dataflow/FlowSummary.qll b/java/ql/lib/semmle/code/java/dataflow/FlowSummary.qll index b88db0272cea..ff281b61511c 100644 --- a/java/ql/lib/semmle/code/java/dataflow/FlowSummary.qll +++ b/java/ql/lib/semmle/code/java/dataflow/FlowSummary.qll @@ -44,6 +44,7 @@ private module SyntheticCallables { private import semmle.code.java.dispatch.WrappedInvocation private import semmle.code.java.frameworks.android.Intent private import semmle.code.java.frameworks.Stream + private import semmle.code.java.frameworks.Strings } private newtype TSummarizedCallableBase = diff --git a/java/ql/lib/semmle/code/java/frameworks/Strings.qll b/java/ql/lib/semmle/code/java/frameworks/Strings.qll new file mode 100644 index 000000000000..4b8600ab3ac3 --- /dev/null +++ b/java/ql/lib/semmle/code/java/frameworks/Strings.qll @@ -0,0 +1,40 @@ +/** Definitions related to `java.lang.String`. */ +overlay[local?] +module; + +private import java +private import semmle.code.java.dataflow.FlowSummary + +/** + * A call to `String.valueOf(Object)` where the argument is a `CharSequence`, + * for example a `String` or a `StringBuilder`. + * + * Such a call is equivalent to calling `toString()` on the argument, which for + * a `CharSequence` is guaranteed to yield a string containing the characters of + * the argument, so taint is propagated. This is in contrast to `valueOf(Object)` + * calls in general, where `toString()` may not expose the state of the argument. + */ +private class StringValueOfCharSequence extends SyntheticCallable { + StringValueOfCharSequence() { this = "java.lang.String.valueOf(Object)+CharSequence" } + + override MethodCall getACall() { + exists(Method m | m = result.getMethod().getSourceDeclaration() | + m.hasQualifiedName("java.lang", "String", "valueOf") and + m.getParameterType(0) instanceof TypeObject + ) and + result + .getArgument(0) + .getType() + .(RefType) + .getAnAncestor() + .hasQualifiedName("java.lang", "CharSequence") + } + + override predicate propagatesFlow(string input, string output, boolean preservesValue) { + input = "Argument[0]" and + output = "ReturnValue" and + preservesValue = false + } + + override Type getReturnType() { result instanceof TypeString } +} diff --git a/java/ql/test/library-tests/dataflow/string-valueof/A.java b/java/ql/test/library-tests/dataflow/string-valueof/A.java new file mode 100644 index 000000000000..037372002912 --- /dev/null +++ b/java/ql/test/library-tests/dataflow/string-valueof/A.java @@ -0,0 +1,26 @@ +public class A { + String source() { return "source"; } + + void sink(Object o) {} + + void m(char[] cs, char c, Object obj) { + String s = source(); + + sink(String.valueOf(s)); // $ hasTaintFlow + + CharSequence seq = s; + sink(String.valueOf(seq)); // $ hasTaintFlow + + StringBuilder sb = new StringBuilder(s); + sink(String.valueOf(sb)); // $ hasTaintFlow + + sink(String.valueOf(s.toCharArray())); // $ hasTaintFlow + + sink(String.valueOf(s.charAt(0))); // $ hasTaintFlow + + // `toString` on an arbitrary object is not assumed to expose the state of + // the object, so no flow is expected here. + Object o = s; + sink(String.valueOf(o)); + } +} diff --git a/java/ql/test/library-tests/dataflow/string-valueof/test.expected b/java/ql/test/library-tests/dataflow/string-valueof/test.expected new file mode 100644 index 000000000000..851523f93087 --- /dev/null +++ b/java/ql/test/library-tests/dataflow/string-valueof/test.expected @@ -0,0 +1,39 @@ +models +| 1 | Summary: java.lang; CharSequence; true; charAt; ; ; Argument[this]; ReturnValue; taint; manual | +| 2 | Summary: java.lang; String; false; toCharArray; ; ; Argument[this]; ReturnValue; taint; manual | +| 3 | Summary: java.lang; String; false; valueOf; (char); ; Argument[0]; ReturnValue; taint; manual | +| 4 | Summary: java.lang; String; false; valueOf; (char[]); ; Argument[0]; ReturnValue; taint; manual | +| 5 | Summary: java.lang; StringBuilder; true; StringBuilder; ; ; Argument[0]; Argument[this]; taint; manual | +edges +| A.java:7:16:7:23 | source(...) : String | A.java:9:25:9:25 | s : String | provenance | | +| A.java:7:16:7:23 | source(...) : String | A.java:12:25:12:27 | seq : String | provenance | | +| A.java:7:16:7:23 | source(...) : String | A.java:14:42:14:42 | s : String | provenance | | +| A.java:7:16:7:23 | source(...) : String | A.java:17:25:17:25 | s : String | provenance | | +| A.java:7:16:7:23 | source(...) : String | A.java:19:25:19:25 | s : String | provenance | | +| A.java:9:25:9:25 | s : String | A.java:9:10:9:26 | valueOf(...) | provenance | java.lang.String.valueOf(Object)+CharSequence | +| A.java:12:25:12:27 | seq : String | A.java:12:10:12:28 | valueOf(...) | provenance | java.lang.String.valueOf(Object)+CharSequence | +| A.java:14:24:14:43 | new StringBuilder(...) : StringBuilder | A.java:15:25:15:26 | sb : StringBuilder | provenance | | +| A.java:14:42:14:42 | s : String | A.java:14:24:14:43 | new StringBuilder(...) : StringBuilder | provenance | MaD:5 | +| A.java:15:25:15:26 | sb : StringBuilder | A.java:15:10:15:27 | valueOf(...) | provenance | java.lang.String.valueOf(Object)+CharSequence | +| A.java:17:25:17:25 | s : String | A.java:17:25:17:39 | toCharArray(...) : char[] | provenance | MaD:2 | +| A.java:17:25:17:39 | toCharArray(...) : char[] | A.java:17:10:17:40 | valueOf(...) | provenance | MaD:4 | +| A.java:19:25:19:25 | s : String | A.java:19:25:19:35 | charAt(...) : Number | provenance | MaD:1 | +| A.java:19:25:19:35 | charAt(...) : Number | A.java:19:10:19:36 | valueOf(...) | provenance | MaD:3 | +nodes +| A.java:7:16:7:23 | source(...) : String | semmle.label | source(...) : String | +| A.java:9:10:9:26 | valueOf(...) | semmle.label | valueOf(...) | +| A.java:9:25:9:25 | s : String | semmle.label | s : String | +| A.java:12:10:12:28 | valueOf(...) | semmle.label | valueOf(...) | +| A.java:12:25:12:27 | seq : String | semmle.label | seq : String | +| A.java:14:24:14:43 | new StringBuilder(...) : StringBuilder | semmle.label | new StringBuilder(...) : StringBuilder | +| A.java:14:42:14:42 | s : String | semmle.label | s : String | +| A.java:15:10:15:27 | valueOf(...) | semmle.label | valueOf(...) | +| A.java:15:25:15:26 | sb : StringBuilder | semmle.label | sb : StringBuilder | +| A.java:17:10:17:40 | valueOf(...) | semmle.label | valueOf(...) | +| A.java:17:25:17:25 | s : String | semmle.label | s : String | +| A.java:17:25:17:39 | toCharArray(...) : char[] | semmle.label | toCharArray(...) : char[] | +| A.java:19:10:19:36 | valueOf(...) | semmle.label | valueOf(...) | +| A.java:19:25:19:25 | s : String | semmle.label | s : String | +| A.java:19:25:19:35 | charAt(...) : Number | semmle.label | charAt(...) : Number | +subpaths +testFailures diff --git a/java/ql/test/library-tests/dataflow/string-valueof/test.ql b/java/ql/test/library-tests/dataflow/string-valueof/test.ql new file mode 100644 index 000000000000..e64b8bfececb --- /dev/null +++ b/java/ql/test/library-tests/dataflow/string-valueof/test.ql @@ -0,0 +1,3 @@ +import utils.test.InlineFlowTest +import DefaultFlowTest +import TaintFlow::PathGraph From c601c6c3a28b0f6d775829e3b104db47df726d81 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 11 Aug 2026 08:25:13 +0000 Subject: [PATCH 3/4] Simplify test method signature Co-authored-by: hvitved <3667920+hvitved@users.noreply.github.com> --- java/ql/test/library-tests/dataflow/string-valueof/A.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/ql/test/library-tests/dataflow/string-valueof/A.java b/java/ql/test/library-tests/dataflow/string-valueof/A.java index 037372002912..cf9d6ea8c1d7 100644 --- a/java/ql/test/library-tests/dataflow/string-valueof/A.java +++ b/java/ql/test/library-tests/dataflow/string-valueof/A.java @@ -3,7 +3,7 @@ public class A { void sink(Object o) {} - void m(char[] cs, char c, Object obj) { + void m() { String s = source(); sink(String.valueOf(s)); // $ hasTaintFlow From 7e3c1443a92539f5c1ec3000dac2de3f7adbd44d Mon Sep 17 00:00:00 2001 From: Tom Hvitved Date: Tue, 11 Aug 2026 13:05:46 +0200 Subject: [PATCH 4/4] Java: Update test expectations --- java/ql/test/utils/modelgenerator/dataflow/p/Joiner.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/java/ql/test/utils/modelgenerator/dataflow/p/Joiner.java b/java/ql/test/utils/modelgenerator/dataflow/p/Joiner.java index ffe68e6722db..6412d1293b9d 100644 --- a/java/ql/test/utils/modelgenerator/dataflow/p/Joiner.java +++ b/java/ql/test/utils/modelgenerator/dataflow/p/Joiner.java @@ -82,6 +82,7 @@ public String toString() { } // heuristic-summary=p;Joiner;false;add;(CharSequence);;Argument[this];ReturnValue;value;df-generated + // heuristic-summary=p;Joiner;false;add;(CharSequence);;Argument[0];Argument[this];taint;df-generated // contentbased-summary=p;Joiner;false;add;(CharSequence);;Argument[this];ReturnValue;value;dfc-generated // MISSING content based summaries for "elts". This could be a synthetic field. public Joiner add(CharSequence newElement) { @@ -107,6 +108,7 @@ private int checkAddLength(int oldLen, int inc) { } // heuristic-summary=p;Joiner;false;merge;(Joiner);;Argument[this];ReturnValue;value;df-generated + // heuristic-summary=p;Joiner;false;merge;(Joiner);;Argument[0];Argument[this];taint;df-generated // contentbased-summary=p;Joiner;false;merge;(Joiner);;Argument[this];ReturnValue;value;dfc-generated // MISSING content based summaries for "elts". This could be a synthetic field. public Joiner merge(Joiner other) {