From ce7fee4e97f4e671223df44f26b2e62d0f5d499b Mon Sep 17 00:00:00 2001 From: hugo-syn Date: Sat, 15 Aug 2026 12:25:14 +0200 Subject: [PATCH 1/2] C#: add AdditionalTaintStep extension point for taint-tracking Mirrors Java's AdditionalTaintStep (java/ql/lib/semmle/code/java/dataflow/FlowSteps.qll) and Ruby/Swift's existing ports of the same pattern: a `Unit`-based class that lets library code contribute additional taint steps applied to every taint-tracking configuration, with zero opt-in required. Wired into defaultAdditionalTaintStep in TaintTrackingPrivate.qll, tagged with model = "AdditionalTaintStep" for provenance. Co-Authored-By: Claude Sonnet 5 --- .../2026-08-15-additional-taint-step.md | 4 ++++ .../semmle/code/csharp/dataflow/FlowSteps.qll | 20 +++++++++++++++++++ .../internal/TaintTrackingPrivate.qll | 3 +++ .../AdditionalTaintStep.expected | 1 + .../AdditionalTaintStep.ql | 7 +++++++ .../dataflow/additional-taint-step/Test.cs | 18 +++++++++++++++++ .../TestAdditionalTaintStep.qll | 19 ++++++++++++++++++ 7 files changed, 72 insertions(+) create mode 100644 csharp/ql/lib/change-notes/2026-08-15-additional-taint-step.md create mode 100644 csharp/ql/lib/semmle/code/csharp/dataflow/FlowSteps.qll create mode 100644 csharp/ql/test/library-tests/dataflow/additional-taint-step/AdditionalTaintStep.expected create mode 100644 csharp/ql/test/library-tests/dataflow/additional-taint-step/AdditionalTaintStep.ql create mode 100644 csharp/ql/test/library-tests/dataflow/additional-taint-step/Test.cs create mode 100644 csharp/ql/test/library-tests/dataflow/additional-taint-step/TestAdditionalTaintStep.qll diff --git a/csharp/ql/lib/change-notes/2026-08-15-additional-taint-step.md b/csharp/ql/lib/change-notes/2026-08-15-additional-taint-step.md new file mode 100644 index 000000000000..6b87eef63b3a --- /dev/null +++ b/csharp/ql/lib/change-notes/2026-08-15-additional-taint-step.md @@ -0,0 +1,4 @@ +--- +category: feature +--- +* Added the `AdditionalTaintStep` extension point (`semmle.code.csharp.dataflow.FlowSteps`). Extend this class to add additional taint steps that apply to all taint-tracking configurations. diff --git a/csharp/ql/lib/semmle/code/csharp/dataflow/FlowSteps.qll b/csharp/ql/lib/semmle/code/csharp/dataflow/FlowSteps.qll new file mode 100644 index 000000000000..6be7cd936ff1 --- /dev/null +++ b/csharp/ql/lib/semmle/code/csharp/dataflow/FlowSteps.qll @@ -0,0 +1,20 @@ +/** + * Provides classes representing various flow steps for taint tracking. + */ + +private import codeql.util.Unit +private import semmle.code.csharp.dataflow.DataFlow + +/** + * A unit class for adding additional taint steps. + * + * Extend this class to add additional taint steps that should apply to all + * taint configurations. + */ +class AdditionalTaintStep extends Unit { + /** + * Holds if the step from `node1` to `node2` should be considered a taint + * step for all configurations. + */ + abstract predicate step(DataFlow::Node node1, DataFlow::Node node2); +} diff --git a/csharp/ql/lib/semmle/code/csharp/dataflow/internal/TaintTrackingPrivate.qll b/csharp/ql/lib/semmle/code/csharp/dataflow/internal/TaintTrackingPrivate.qll index e51eaf4bbcb4..131b2cb72e73 100644 --- a/csharp/ql/lib/semmle/code/csharp/dataflow/internal/TaintTrackingPrivate.qll +++ b/csharp/ql/lib/semmle/code/csharp/dataflow/internal/TaintTrackingPrivate.qll @@ -2,6 +2,7 @@ private import csharp private import TaintTrackingPublic private import FlowSummaryImpl as FlowSummaryImpl private import semmle.code.csharp.Caching +private import semmle.code.csharp.dataflow.FlowSteps private import semmle.code.csharp.dataflow.internal.DataFlowDispatch private import semmle.code.csharp.dataflow.internal.DataFlowPrivate private import semmle.code.csharp.dispatch.Dispatch @@ -173,6 +174,8 @@ private module Cached { or FlowSummaryImpl::Private::Steps::summaryLocalStep(nodeFrom, nodeTo.(FlowSummaryNode).getSummaryNode(), false, model) + or + any(AdditionalTaintStep a).step(nodeFrom, nodeTo) and model = "AdditionalTaintStep" } } diff --git a/csharp/ql/test/library-tests/dataflow/additional-taint-step/AdditionalTaintStep.expected b/csharp/ql/test/library-tests/dataflow/additional-taint-step/AdditionalTaintStep.expected new file mode 100644 index 000000000000..acd241d386f5 --- /dev/null +++ b/csharp/ql/test/library-tests/dataflow/additional-taint-step/AdditionalTaintStep.expected @@ -0,0 +1 @@ +| Test.cs:13:35:13:45 | access to parameter taintSource | Test.cs:13:23:13:46 | call to method Step | AdditionalTaintStep | diff --git a/csharp/ql/test/library-tests/dataflow/additional-taint-step/AdditionalTaintStep.ql b/csharp/ql/test/library-tests/dataflow/additional-taint-step/AdditionalTaintStep.ql new file mode 100644 index 000000000000..b499c2ce51d1 --- /dev/null +++ b/csharp/ql/test/library-tests/dataflow/additional-taint-step/AdditionalTaintStep.ql @@ -0,0 +1,7 @@ +import csharp +import semmle.code.csharp.dataflow.internal.TaintTrackingPrivate +import TestAdditionalTaintStep + +from DataFlow::Node src, DataFlow::Node sink, string model +where defaultAdditionalTaintStep(src, sink, model) and model = "AdditionalTaintStep" +select src, sink, model diff --git a/csharp/ql/test/library-tests/dataflow/additional-taint-step/Test.cs b/csharp/ql/test/library-tests/dataflow/additional-taint-step/Test.cs new file mode 100644 index 000000000000..051793e2c855 --- /dev/null +++ b/csharp/ql/test/library-tests/dataflow/additional-taint-step/Test.cs @@ -0,0 +1,18 @@ +class Marker +{ + // A stand-in for a framework method that isn't otherwise understood by the + // taint-tracking library, whose taint behaviour is modelled by a test-only + // `AdditionalTaintStep` subclass instead. + public static object Step(object x) => new object(); +} + +class Test +{ + void M(object taintSource) + { + var tainted = Marker.Step(taintSource); + Sink(tainted); + } + + static void Sink(object o) { } +} diff --git a/csharp/ql/test/library-tests/dataflow/additional-taint-step/TestAdditionalTaintStep.qll b/csharp/ql/test/library-tests/dataflow/additional-taint-step/TestAdditionalTaintStep.qll new file mode 100644 index 000000000000..d17109ba730c --- /dev/null +++ b/csharp/ql/test/library-tests/dataflow/additional-taint-step/TestAdditionalTaintStep.qll @@ -0,0 +1,19 @@ +import csharp +import semmle.code.csharp.dataflow.FlowSteps + +/** + * A test-only additional taint step that treats calls to `Marker.Step` as + * propagating taint from the argument to the call result, to verify that + * `AdditionalTaintStep` subclasses are picked up by `defaultAdditionalTaintStep`. + */ +private class MarkerStepTaintStep extends AdditionalTaintStep { + override predicate step(DataFlow::Node node1, DataFlow::Node node2) { + exists(MethodCall mc | + mc.getTarget().hasName("Step") and + mc.getTarget().getDeclaringType().hasName("Marker") + | + node1.asExpr() = mc.getArgument(0) and + node2.asExpr() = mc + ) + } +} From 946e116fa327062ed6c46a235e5ab9a7a3fab655 Mon Sep 17 00:00:00 2001 From: hugo-syn Date: Wed, 19 Aug 2026 12:07:14 +0200 Subject: [PATCH 2/2] C#: inline TestAdditionalTaintStep.qll into AdditionalTaintStep.ql Addresses review comment: the test-only AdditionalTaintStep subclass only has a single consumer, so fold it directly into the query file instead of keeping it in a separate .qll. Co-Authored-By: Claude Sonnet 5 --- .../AdditionalTaintStep.ql | 19 ++++++++++++++++++- .../TestAdditionalTaintStep.qll | 19 ------------------- 2 files changed, 18 insertions(+), 20 deletions(-) delete mode 100644 csharp/ql/test/library-tests/dataflow/additional-taint-step/TestAdditionalTaintStep.qll diff --git a/csharp/ql/test/library-tests/dataflow/additional-taint-step/AdditionalTaintStep.ql b/csharp/ql/test/library-tests/dataflow/additional-taint-step/AdditionalTaintStep.ql index b499c2ce51d1..39d9e154b6f7 100644 --- a/csharp/ql/test/library-tests/dataflow/additional-taint-step/AdditionalTaintStep.ql +++ b/csharp/ql/test/library-tests/dataflow/additional-taint-step/AdditionalTaintStep.ql @@ -1,6 +1,23 @@ import csharp +import semmle.code.csharp.dataflow.FlowSteps import semmle.code.csharp.dataflow.internal.TaintTrackingPrivate -import TestAdditionalTaintStep + +/** + * A test-only additional taint step that treats calls to `Marker.Step` as + * propagating taint from the argument to the call result, to verify that + * `AdditionalTaintStep` subclasses are picked up by `defaultAdditionalTaintStep`. + */ +private class MarkerStepTaintStep extends AdditionalTaintStep { + override predicate step(DataFlow::Node node1, DataFlow::Node node2) { + exists(MethodCall mc | + mc.getTarget().hasName("Step") and + mc.getTarget().getDeclaringType().hasName("Marker") + | + node1.asExpr() = mc.getArgument(0) and + node2.asExpr() = mc + ) + } +} from DataFlow::Node src, DataFlow::Node sink, string model where defaultAdditionalTaintStep(src, sink, model) and model = "AdditionalTaintStep" diff --git a/csharp/ql/test/library-tests/dataflow/additional-taint-step/TestAdditionalTaintStep.qll b/csharp/ql/test/library-tests/dataflow/additional-taint-step/TestAdditionalTaintStep.qll deleted file mode 100644 index d17109ba730c..000000000000 --- a/csharp/ql/test/library-tests/dataflow/additional-taint-step/TestAdditionalTaintStep.qll +++ /dev/null @@ -1,19 +0,0 @@ -import csharp -import semmle.code.csharp.dataflow.FlowSteps - -/** - * A test-only additional taint step that treats calls to `Marker.Step` as - * propagating taint from the argument to the call result, to verify that - * `AdditionalTaintStep` subclasses are picked up by `defaultAdditionalTaintStep`. - */ -private class MarkerStepTaintStep extends AdditionalTaintStep { - override predicate step(DataFlow::Node node1, DataFlow::Node node2) { - exists(MethodCall mc | - mc.getTarget().hasName("Step") and - mc.getTarget().getDeclaringType().hasName("Marker") - | - node1.asExpr() = mc.getArgument(0) and - node2.asExpr() = mc - ) - } -}