Repository: joernio/joern (scala)

Issue:
[Bug][Pythonsrc] Incorrect tainted variable with tupleLiteral operator
**Describe the bug**

Clone
https://github.com/HooliCorp/flask-webgoat

```
def source = cpg.call.code("request\\..*").inAssignment.argument.isIdentifier.filterNot(_.name.startsWith("tmp"))
def sink = cpg.call.code(".*query_db.*").argument.order(1)
sink.reachableByFlows(source).map(m => (m, m.elements.location.l)).toJsonPretty
```

When the above json is read and convert into a simplified tree, we can see the variable `username` is highlighted incorrectly instead of password.

<img width="1399" alt="2023-05-07_19-28-57" src="https://user-images.githubusercontent.com/7842/236693071-1fcac5d6-92da-41f2-9367-d776519c8e6c.png">

This is because the variable username is flagged as the symbol in the flow followed by the `tupleLiteral` operator.

```
{
        "packageName":"",
        "symbol":"username",
        "filename":"/home/almalinux/work/HooliCorp/flask-webgoat/flask_webgoat/users.py",
        "className":"",
        "methodFullName":"flask_webgoat/users.py:<module>.create_user",
        "lineNumber":39,
        "classShortName":"",
        "methodShortName":"create_user",
        "node":{
          "dynamicTypeHintFullName":[
            
          ],
          "name":"username",
          "code":"username",
          "typeFullName":"ANY",
          "columnNumber":12,
          "order":1,
          "_label":"IDENTIFIER",
          "argumentIndex":1,
          "lineNumber":39,
          "id":1673
        },
        "_label":"LOCATION",
        "nodeLabel":"IDENTIFIER"
      },
      {
        "packageName":"",
        "symbol":"(username, password, int(access_level))",
        "filename":"/home/almalinux/work/HooliCorp/flask-webgoat/flask_webgoat/users.py",
        "className":"",
        "methodFullName":"flask_webgoat/users.py:<module>.create_user",
        "lineNumber":39,
        "classShortName":"",
        "methodShortName":"create_user",
        "node":{
          "name":"<operator>.tupleLiteral",
          "signature":"",
          "code":"(username, password, int(access_level))",
          "typeFullName":"ANY",
          "columnNumber":11,
          "order":2,
          "methodFullName":"<operator>.tupleLiteral",
          "_label":"CALL",
          "argumentIndex":2,
          "dynamicTypeHintFullName":[
            
          ],
          "dispatchType":"STATIC_DISPATCH",
          "lineNumber":39,
          "id":1678
        },
        "_label":"LOCATION",
        "nodeLabel":"CALL"
      },
```

The problem is masked by joern's print mode since it doesn't highlight any specific symbol.

```
joern> sink.reachableByFlows(source).p
res247: List[String] = List(
  """________________________________________________________________________________________________________________________________________________________________________________
| nodeType   | tracked                                                          | lineNumber| method      | file                                                                |
|===============================================================================================================================================================================|
| Identifier | password = tmp3 = request.form tmp3.get("password")              | 20        | create_user | /home/almalinux/work/HooliCorp/flask-webgoat/flask_webgoat/users.py |
| Identifier | password is None                                                 | 22        | create_user | /home/almalinux/work/HooliCorp/flask-webgoat/flask_webgoat/users.py |
| Identifier | len(password)                                                    | 31        | create_user | /home/almalinux/work/HooliCorp/flask-webgoat/flask_webgoat/users.py |
| Identifier | (username, password, int(access_level))                          | 39        | create_user | /home/almalinux/work/HooliCorp/flask-webgoat/flask_webgoat/users.py |
| Identifier | (username, password, int(access_level))                          | 39        | create_user | /home/almalinux/work/HooliCorp/flask-webgoat/flask_webgoat/users.py |
| Call       | (username, password, int(access_level))                          | 39        | create_user | /home/almalinux/work/HooliCorp/flask-webgoat/flask_webgoat/users.py |
| Call       | "INSERT INTO user (username, password, access_level) VALUES (... | 38        | create_user | /home/almalinux/work/HooliCorp/flask-webgoat/flask_webgoat/users.py |
| Identifier | query = "INSERT INTO user (username, password, access_level) ... | 37        | create_user | /home/almalinux/work/HooliCorp/flask-webgoat/flask_webgoat/users.py |
| Identifier | query_db(query, [], False, True)                                 | 43        | create_user | /home/almalinux/work/HooliCorp/flask-webgoat/flask_webgoat/users.py |
""",
```

**Expected behavior**
Check tupleLiteral method

**Screenshots**
If applicable, add screenshots to help explain your problem.

**Desktop (please complete the following information):**
 - OS
 - Joern Version 
 - Java version

**Additional context**
Add any other context about the problem here.

Pull request:
* Added a `PASSTHROUGH` mapping that represents an instance where parameters are not sanitized, may affect the return value, and do not cross-taint. e.g. `foo(1, 2) = 1 -> 1, 2 -> 2, 1 -> -1, 2 -> -1`. `0->0` is not included, but can be added as a mapping.
* Made it compulsory to add a parameter index if using an argument name in the semantics. This makes the summary more "method"-centric and less "caller"-centric. Argument names still have precedence, so if the argument index is not appropriate due to type unpacking used by the method, then it may be left as some arbitrary value.
* Added `PASSTHROUGH` for Python's set, dict, tuple, and list literals

Open to discussions!

Resolves #2663

Patch:
diff --git a/changelog/passthrough_semantics.md b/changelog/passthrough_semantics.md
new file mode 100644
index 000000000000..a42d38e8cbf5
--- /dev/null
+++ b/changelog/passthrough_semantics.md
@@ -0,0 +1,38 @@
+# Pass Through Semantics & Update to Named Arguments (v1.2.20; June 2023)
+
+PR: https://github.com/joernio/joern/pull/2885
+
+## Summary
+
+We have implemented the `PassThroughMapping` as a shorthand way to denote a method behaviour that:
+
+* Doesn't cross taint parameters
+* All parameters may affect the return
+* Parameter numbers may be unbounded, e.g. `varargs`-style parameters
+
+Examples of these are Python's tuple literals, where these can have a dynamic number of parameters, and they do not
+cross-taint. This aims at reducing the number of false-positives that result from cross-tainting and unbounded
+parameters.
+
+In addition to this, the initial argument name feature of the flow definitions seemed a bit cumbersome, with one
+requiring defining two flows for the same parameter. e.g. 1 -> 1, "foo" -> 1. This change now forces the user to define
+both the parameter index and an optional name, i.e., you can no longer just have an argument name. This makes the 
+summary more "method"-centric and less "caller"-centric. Argument names still have precedence, so if argument index is 
+not appropriate due to type unpacking used by the method, then it may be left as some arbitrary value.
+
+Note: `PASSTHROUGH` this does not taint 0 -> 0, but can be combined with other mappings.
+
+## New Semantic Notation
+
+```
+"method" 1 -> 2, "foo" -> 2, 1 -> "bar", "foo" -> "bar"
+"<operator>.tupleLiteral" 1 -> 1, 2 -> 2, 3 -> 3, 4 -> 4, 1 -> -1, 2 -> -1, 3 -> -1, 4 -> -1  // etc.
+```
+
+### New Semantic Notation
+
+```
+"method" 1 "foo" -> 2 "bar"
+"<operator>.tupleLiteral" PASSTHROUGH
+"setProperty" PASSTHROUGH 0 -> 0
+```
\ No newline at end of file
diff --git a/dataflowengineoss/src/main/antlr4/io/joern/dataflowengineoss/Semantics.g4 b/dataflowengineoss/src/main/antlr4/io/joern/dataflowengineoss/Semantics.g4
index fb94ab28e828..db21a02c982c 100644
--- a/dataflowengineoss/src/main/antlr4/io/joern/dataflowengineoss/Semantics.g4
+++ b/dataflowengineoss/src/main/antlr4/io/joern/dataflowengineoss/Semantics.g4
@@ -5,15 +5,19 @@ singleSemantic: methodName mapping* NEWLINE*;
 methodName : QUOTE name QUOTE;
 name : ~(NEWLINE|QUOTE)*?;
 
-mapping: src '->' dst;
+mapping: PASSTHROUGH | (src '->' dst);
 argName: QUOTE name QUOTE;
 argIdx: NUMBER;
-src: argName | argIdx;
-dst: argName | argIdx;
+src: argIdx (argName)?;
+dst: argIdx (argName)?;
+
+// Keywords
+
+PASSTHROUGH: 'PASSTHROUGH';
 
 // Lexing
 
- QUOTE : '"';
+QUOTE : '"';
 NUMBER: [-]?[0-9]+;
 NEWLINE          : '\r'? '\n';
 LINE_COMMENT : '#' .*? ('\n'|EOF)	-> channel(HIDDEN) ;
diff --git a/dataflowengineoss/src/main/scala/io/joern/dataflowengineoss/DefaultSemantics.scala b/dataflowengineoss/src/main/scala/io/joern/dataflowengineoss/DefaultSemantics.scala
index d620b41d5f7f..fd8a1df0b264 100644
--- a/dataflowengineoss/src/main/scala/io/joern/dataflowengineoss/DefaultSemantics.scala
+++ b/dataflowengineoss/src/main/scala/io/joern/dataflowengineoss/DefaultSemantics.scala
@@ -1,7 +1,6 @@
 package io.joern.dataflowengineoss
 
-import io.joern.dataflowengineoss.DefaultSemantics.F
-import io.joern.dataflowengineoss.semanticsloader.{FlowSemantic, Semantics}
+import io.joern.dataflowengineoss.semanticsloader.{FlowSemantic, PassThroughMapping, Semantics}
 import io.shiftleft.codepropertygraph.generated.Operators
 
 import scala.annotation.unused
@@ -18,6 +17,9 @@ object DefaultSemantics {
 
   private def F = (x: String, y: List[(Int, Int)]) => FlowSemantic.from(x, y)
 
+  private def PTF(x: String, ys: List[(Int, Int)] = List.empty): FlowSemantic =
+    FlowSemantic(x).copy(mappings = FlowSemantic.from(x, ys).mappings :+ PassThroughMapping)
+
   def operatorFlows: List[FlowSemantic] = List(
     F(Operators.addition, List((1, -1), (2, -1))),
     F(Operators.addressOf, List((1, -1))),
@@ -69,7 +71,12 @@ object DefaultSemantics {
     F("<operators>.assignmentAnd", List((2, 1), (1, 1))),
     F("<operators>.assignmentOr", List((2, 1), (1, 1))),
     F("<operators>.assignmentXor", List((2, 1), (1, 1))),
-    F("<operator>.tupleLiteral", List((1, 1), (2, 2), (3, 3), (4, 4), (1, -1), (2, -1), (3, -1), (4, -1)))
+
+    // Language specific operators
+    PTF("<operator>.tupleLiteral"),
+    PTF("<operator>.dictLiteral"),
+    PTF("<operator>.setLiteral"),
+    PTF("<operator>.listLiteral")
   )
 
   /** Semantic summaries for common external C/C++ calls.
@@ -95,7 +102,7 @@ object DefaultSemantics {
     F("ctime64_r", List((1, -1))),
     F("difftime", List((1, -1), (2, -1))),
     F("difftime64", List((1, -1), (2, -1))),
-    F("div", List((1, 1), (1, -1), (2, 2), (2, -1))),
+    PTF("div"),
     F("exit", List((1, 1))),
     F("exp", List((1, -1))),
     F("fabs", List((1, -1))),
@@ -118,16 +125,13 @@ object DefaultSemantics {
   /** Semantic summaries for common external Java calls.
     */
   def javaFlows: List[FlowSemantic] = List(
-    F("java.lang.String.split:java.lang.String[](java.lang.String)", List((0, 0), (0, -1), (1, 1), (1, -1))),
-    F(
-      "java.lang.String.split:java.lang.String[](java.lang.String,int)",
-      List((0, 0), (0, -1), (1, 1), (1, -1), (2, -1))
-    ),
-    F("java.lang.String.compareTo:int(java.lang.String)", List((0, 0), (0, -1), (0, -1), (1, -1))),
+    PTF("java.lang.String.split:java.lang.String[](java.lang.String)", List((0, 0))),
+    PTF("java.lang.String.split:java.lang.String[](java.lang.String,int)", List((0, 0))),
+    PTF("java.lang.String.compareTo:int(java.lang.String)", List((0, 0))),
     F("java.io.PrintWriter.print:void(java.lang.String)", List((0, 0), (1, 1))),
     F("java.io.PrintWriter.println:void(java.lang.String)", List((0, 0), (1, 1))),
     F("java.io.PrintStream.println:void(java.lang.String)", List((0, 0), (1, 1))),
-    F("java.io.PrintStream.print:void(java.lang.String)", List((0, 0), (0, -1), (1, 1))),
+    PTF("java.io.PrintStream.print:void(java.lang.String)", List((0, 0))),
     F("android.text.TextUtils.isEmpty:boolean(java.lang.String)", List((0, -1), (1, -1))),
     F("java.sql.PreparedStatement.prepareStatement:java.sql.PreparedStatement(java.lang.String)", List((1, -1))),
     F("java.sql.PreparedStatement.prepareStatement:setDouble(int,double)", List((1, 1), (2, 2))),
diff --git a/dataflowengineoss/src/main/scala/io/joern/dataflowengineoss/language/nodemethods/ExpressionMethods.scala b/dataflowengineoss/src/main/scala/io/joern/dataflowengineoss/language/nodemethods/ExpressionMethods.scala
index 9ec3a0946caa..e9d339ae4558 100644
--- a/dataflowengineoss/src/main/scala/io/joern/dataflowengineoss/language/nodemethods/ExpressionMethods.scala
+++ b/dataflowengineoss/src/main/scala/io/joern/dataflowengineoss/language/nodemethods/ExpressionMethods.scala
@@ -11,10 +11,11 @@ class ExpressionMethods[NodeType <: Expression](val node: NodeType) extends AnyV
   def isUsed(implicit semantics: Semantics): Boolean = {
     val s = semanticsForCallByArg
     s.isEmpty || s.exists(_.mappings.exists {
-      case ParamMapping(NamedArg(srcName), _) if node.argumentName.isDefined =>
+      case FlowMapping(ParameterNode(_, Some(srcName)), _) if node.argumentName.isDefined =>
         srcName == node.argumentName.get
-      case ParamMapping(PosArg(srcIndex), _) => srcIndex == node.argumentIndex
-      case _                                 => false
+      case FlowMapping(ParameterNode(srcIndex, _), _)    => srcIndex == node.argumentIndex
+      case PassThroughMapping if node.argumentIndex != 0 => true
+      case _                                             => false
     })
   }
 
@@ -24,10 +25,11 @@ class ExpressionMethods[NodeType <: Expression](val node: NodeType) extends AnyV
     val s = semanticsForCallByArg.l
     s.isEmpty || s.exists { semantic =>
       semantic.mappings.exists {
-        case ParamMapping(_, NamedArg(dstName)) if node.argumentName.isDefined =>
+        case FlowMapping(_, ParameterNode(_, Some(dstName))) if node.argumentName.isDefined =>
           dstName == node.argumentName.get
-        case ParamMapping(_, PosArg(dstIndex)) => dstIndex == node.argumentIndex
-        case _                                 => false
+        case FlowMapping(_, ParameterNode(dstIndex, _))    => dstIndex == node.argumentIndex
+        case PassThroughMapping if node.argumentIndex != 0 => true
+        case _                                             => false
       }
     }
   }
@@ -53,16 +55,17 @@ class ExpressionMethods[NodeType <: Expression](val node: NodeType) extends AnyV
     val s = semanticsForCallByArg.l
     s.isEmpty || s.exists { semantic =>
       semantic.mappings.exists {
-        case ParamMapping(NamedArg(srcName), NamedArg(dstName))
+        case FlowMapping(ParameterNode(_, Some(srcName)), ParameterNode(_, Some(dstName)))
             if node.argumentName.isDefined && tgt.argumentName.isDefined =>
           srcName == node.argumentName.get && dstName == tgt.argumentName.get
-        case ParamMapping(NamedArg(srcName), PosArg(dstIndex)) if node.argumentName.isDefined =>
+        case FlowMapping(ParameterNode(_, Some(srcName)), ParameterNode(dstIndex, _)) if node.argumentName.isDefined =>
           srcName == node.argumentName.get && dstIndex == tgt.argumentIndex
-        case ParamMapping(PosArg(srcIndex), NamedArg(dstName)) if tgt.argumentName.isDefined =>
+        case FlowMapping(ParameterNode(srcIndex, _), ParameterNode(_, Some(dstName))) if tgt.argumentName.isDefined =>
           srcIndex == node.argumentIndex && dstName == tgt.argumentName.get
-        case ParamMapping(PosArg(srcIndex), PosArg(dstIndex)) =>
+        case FlowMapping(ParameterNode(srcIndex, _), ParameterNode(dstIndex, _)) =>
           srcIndex == node.argumentIndex && dstIndex == tgt.argumentIndex
-        case _ => false
+        case PassThroughMapping if tgt.argumentIndex == node.argumentIndex || tgt.argumentIndex == -1 => true
+        case _                                                                                        => false
       }
     }
   }
diff --git a/dataflowengineoss/src/main/scala/io/joern/dataflowengineoss/passes/reachingdef/EdgeValidator.scala b/dataflowengineoss/src/main/scala/io/joern/dataflowengineoss/passes/reachingdef/EdgeValidator.scala
index 968e44933fa3..cfb0ef8ac2f2 100644
--- a/dataflowengineoss/src/main/scala/io/joern/dataflowengineoss/passes/reachingdef/EdgeValidator.scala
+++ b/dataflowengineoss/src/main/scala/io/joern/dataflowengineoss/passes/reachingdef/EdgeValidator.scala
@@ -2,7 +2,7 @@ package io.joern.dataflowengineoss.passes.reachingdef
 
 import io.joern.dataflowengineoss.language._
 import io.joern.dataflowengineoss.queryengine.Engine.isOutputArgOfInternalMethod
-import io.joern.dataflowengineoss.semanticsloader.{ParamMapping, PosArg, Semantics}
+import io.joern.dataflowengineoss.semanticsloader.{FlowMapping, ParameterNode, PassThroughMapping, Semantics}
 import io.shiftleft.codepropertygraph.generated.nodes.{Call, CfgNode, Expression, StoredNode}
 import io.shiftleft.semanticcpg.language._
 
@@ -39,8 +39,9 @@ object EdgeValidator {
       case call: Call =>
         val sem = semantics.forMethod(call.methodFullName)
         sem.isDefined && !sem.get.mappings.exists {
-          case ParamMapping(_, PosArg(dst)) => dst == -1
-          case _                            => false
+          case FlowMapping(_, ParameterNode(dst, _)) => dst == -1
+          case PassThroughMapping                    => true
+          case _                                     => false
         }
       case _ =>
         false
diff --git a/dataflowengineoss/src/main/scala/io/joern/dataflowengineoss/semanticsloader/Parser.scala b/dataflowengineoss/src/main/scala/io/joern/dataflowengineoss/semanticsloader/Parser.scala
index 47c366bdb4a8..8af396a492a0 100644
--- a/dataflowengineoss/src/main/scala/io/joern/dataflowengineoss/semanticsloader/Parser.scala
+++ b/dataflowengineoss/src/main/scala/io/joern/dataflowengineoss/semanticsloader/Parser.scala
@@ -1,6 +1,6 @@
 package io.joern.dataflowengineoss.semanticsloader
 
-import io.joern.dataflowengineoss.SemanticsParser.{MappingContext, SrcContext}
+import io.joern.dataflowengineoss.SemanticsParser.MappingContext
 import io.joern.dataflowengineoss.{SemanticsBaseListener, SemanticsLexer, SemanticsParser}
 import io.shiftleft.codepropertygraph.Cpg
 import org.antlr.v4.runtime.tree.ParseTreeWalker
@@ -8,7 +8,6 @@ import org.antlr.v4.runtime.{CharStream, CharStreams, CommonTokenStream}
 
 import scala.collection.mutable
 import scala.jdk.CollectionConverters._
-import scala.util.Try
 
 object Semantics {
 
@@ -58,14 +57,14 @@ class Semantics private (methodToSemantic: mutable.Map[String, FlowSemantic]) {
       .sortBy(_.methodFullName)
       .map { elem =>
         s"\"${elem.methodFullName}\" " + elem.mappings
-          .collect { case ParamMapping(x, y) => s"$x -> $y" }
+          .collect { case FlowMapping(x, y) => s"$x -> $y" }
           .mkString(" ")
       }
       .mkString("\n")
   }
 
 }
-case class FlowSemantic(methodFullName: String, mappings: List[FlowMapping], regex: Boolean = false)
+case class FlowSemantic(methodFullName: String, mappings: List[FlowPath] = List.empty, regex: Boolean = false)
 
 object FlowSemantic {
 
@@ -73,11 +72,11 @@ object FlowSemantic {
     FlowSemantic(
       methodFullName,
       mappings.map {
-        case (src: Int, dst: Int)       => ParamMapping(src, dst)
-        case (src: String, dst: Int)    => ParamMapping(src, dst)
-        case (src: Int, dst: String)    => ParamMapping(src, dst)
-        case (src: String, dst: String) => ParamMapping(src, dst)
-        case x: ParamMapping            => x
+        case (src: Int, dst: Int)                                 => FlowMapping(src, dst)
+        case (srcIdx: Int, src: String, dst: Int)                 => FlowMapping(srcIdx, src, dst)
+        case (src: Int, dstIdx: Int, dst: String)                 => FlowMapping(src, dstIdx, dst)
+        case (srcIdx: Int, src: String, dstIdx: Int, dst: String) => FlowMapping(srcIdx, src, dstIdx, dst)
+        case x: FlowMapping                                       => x
       },
       regex
     )
@@ -85,29 +84,38 @@ object FlowSemantic {
 
 }
 
-/** Represents how arguments are identified to their respective parameters at a call site.
-  * @tparam P
-  *   a positional argument.
-  * @tparam N
-  *   a named argument.
-  */
-sealed trait FlowArgument[P, N]
+abstract class FlowNode
 
-/** A positional argument where the index of the argument matches the position of the parameter at the callee.
-  * @param idx
-  *   the position or argument index.
+/** Collects parameters and return nodes under a common trait. This trait acknowledges their argument index which is
+  * relevant when a caller wants to coordinate relevant tainted flows through specific arguments and the return flow.
   */
-case class PosArg(idx: Int) extends FlowArgument[PosArg, NamedArg]
+trait ParamOrRetNode extends FlowNode {
+
+  /** Temporary backward compatible idx field.
+    *
+    * @return
+    *   the argument index.
+    */
+  def index: Int
+}
 
-/** A named argument where the name corresponds to the name of the parameter at the callee.
+/** A parameter where the index of the argument matches the position of the parameter at the callee. The name is used to
+  * match named arguments if used instead of positional arguments.
+  *
+  * @param index
+  *   the position or argument index.
   * @param name
   *   the name of the parameter.
   */
-case class NamedArg(name: String) extends FlowArgument[PosArg, NamedArg]
+case class ParameterNode(index: Int, name: Option[String] = None) extends ParamOrRetNode
+
+object ParameterNode {
+  def apply(index: Int, name: String): ParameterNode = ParameterNode(index, Option(name))
+}
 
 /** Represents explicit mappings or special cases.
   */
-sealed trait FlowMapping
+sealed trait FlowPath
 
 /** Maps flow between arguments based on how they interact as parameters at the callee.
   *
@@ -116,23 +124,27 @@ sealed trait FlowMapping
   * @param dst
   *   destination of the flow.
   */
-case class ParamMapping(src: FlowArgument[PosArg, NamedArg], dst: FlowArgument[PosArg, NamedArg]) extends FlowMapping
-object ParamMapping {
-  def apply(from: Int, to: Int): ParamMapping = ParamMapping(PosArg(from), PosArg(to))
+case class FlowMapping(src: FlowNode, dst: FlowNode) extends FlowPath
+
+object FlowMapping {
+  def apply(from: Int, to: Int): FlowMapping = FlowMapping(ParameterNode(from), ParameterNode(to))
 
-  def apply(from: String, to: String): ParamMapping = ParamMapping(NamedArg(from), NamedArg(to))
+  def apply(fromIdx: Int, from: String, toIdx: Int, to: String): FlowMapping =
+    FlowMapping(ParameterNode(fromIdx, from), ParameterNode(toIdx, to))
 
-  def apply(from: String, to: Int): ParamMapping = ParamMapping(NamedArg(from), PosArg(to))
+  def apply(fromIdx: Int, from: String, toIdx: Int): FlowMapping =
+    FlowMapping(ParameterNode(fromIdx, from), ParameterNode(toIdx))
 
-  def apply(from: Int, to: String): ParamMapping = ParamMapping(PosArg(from), NamedArg(to))
+  def apply(from: Int, toIdx: Int, to: String): FlowMapping = FlowMapping(ParameterNode(from), ParameterNode(toIdx, to))
 
 }
 
-/** Represents an instance where parameters are not sanitized, may affect the return value, and do not cross-taint.
+/** Represents an instance where parameters are not sanitized, may affect the return value, and do not cross-taint. e.g.
+  * foo(1, 2) = 1 -> 1, 2 -> 2, 1 -> -1, 2 -> -1
   *
-  * TODO: Implement this for varargs operations such as <operators>.tupleLiteral
+  * The main benefit is that this works for unbounded parameters e.g. VARARGS. Note this does not taint 0 -> 0.
   */
-object PassThroughMapping extends FlowMapping
+object PassThroughMapping extends FlowPath
 
 class Parser() {
 
@@ -158,6 +170,20 @@ class Parser() {
     listener.result.toList
   }
 
+  implicit class AntlrFlowExtensions(val ctx: MappingContext) {
+
+    def isPassThrough: Boolean = Option(ctx.PASSTHROUGH()).isDefined
+
+    def srcIdx: Int = ctx.src().argIdx().NUMBER().getText.toInt
+
+    def srcArgName: Option[String] = Option(ctx.src().argName()).map(_.name().getText)
+
+    def dstIdx: Int = ctx.dst().argIdx().NUMBER().getText.toInt
+
+    def dstArgName: Option[String] = Option(ctx.dst().argName()).map(_.name().getText)
+
+  }
+
   private class Listener extends SemanticsBaseListener {
 
     val result: mutable.ListBuffer[FlowSemantic] = mutable.ListBuffer[FlowSemantic]()
@@ -170,16 +196,15 @@ class Parser() {
       }
     }
 
-    private def ctxToParamMapping(ctx: MappingContext): FlowMapping = {
-      val src =
-        if (ctx.src().argName() != null) NamedArg(ctx.src().argName().name().getText)
-        else PosArg(ctx.src().argIdx().NUMBER().getText.toInt)
-      val dst =
-        if (ctx.dst().argName() != null) NamedArg(ctx.dst().argName().name().getText)
-        else PosArg(ctx.dst().argIdx().NUMBER().getText.toInt)
+    private def ctxToParamMapping(ctx: MappingContext): FlowPath =
+      if (ctx.isPassThrough) {
+        PassThroughMapping
+      } else {
+        val src = ParameterNode(ctx.srcIdx, ctx.srcArgName)
+        val dst = ParameterNode(ctx.dstIdx, ctx.dstArgName)
 
-      ParamMapping(src, dst)
-    }
+        FlowMapping(src, dst)
+      }
 
   }
 


Test patch:
diff --git a/dataflowengineoss/src/test/scala/io/joern/dataflowengineoss/semanticsloader/ParserTests.scala b/dataflowengineoss/src/test/scala/io/joern/dataflowengineoss/semanticsloader/ParserTests.scala
index 3cabb5032f06..2c2e08c9fd18 100644
--- a/dataflowengineoss/src/test/scala/io/joern/dataflowengineoss/semanticsloader/ParserTests.scala
+++ b/dataflowengineoss/src/test/scala/io/joern/dataflowengineoss/semanticsloader/ParserTests.scala
@@ -21,7 +21,10 @@ class ParserTests extends AnyWordSpec with Matchers {
       semantics match {
         case List(x) =>
           x.methodFullName shouldBe "foo"
-          x.mappings shouldBe List(ParamMapping(PosArg(1), PosArg(-1)), ParamMapping(PosArg(2), PosArg(3)))
+          x.mappings shouldBe List(
+            FlowMapping(ParameterNode(1), ParameterNode(-1)),
+            FlowMapping(ParameterNode(2), ParameterNode(3))
+          )
         case _ => fail()
       }
     }
@@ -47,14 +50,15 @@ class ParserTests extends AnyWordSpec with Matchers {
     }
 
     "parse named argument parameters" in new Fixture() {
-      private val semantics = parser.parse("\"foo\" \"param1\"->2 3->\"param2\"")
+      private val semantics = parser.parse("\"foo\" 1 \"param1\"->2 3-> 2 \"param2\"")
       semantics.size shouldBe 1
-      semantics shouldBe List(
-        FlowSemantic(
-          "foo",
-          List(ParamMapping(NamedArg("param1"), PosArg(2)), ParamMapping(PosArg(3), NamedArg("param2")))
-        )
-      )
+      semantics shouldBe List(FlowSemantic("foo", List(FlowMapping(1, "param1", 2), FlowMapping(3, 2, "param2"))))
+    }
+
+    "parse a passthrough mapping" in new Fixture() {
+      private val semantics = parser.parse("\"foo\" PASSTHROUGH 0 -> 0")
+      semantics.size shouldBe 1
+      semantics shouldBe List(FlowSemantic("foo", List(PassThroughMapping, FlowMapping(0, 0))))
     }
   }
 
diff --git a/joern-cli/frontends/pysrc2cpg/src/test/scala/io/joern/pysrc2cpg/dataflow/DataFlowTests.scala b/joern-cli/frontends/pysrc2cpg/src/test/scala/io/joern/pysrc2cpg/dataflow/DataFlowTests.scala
index e212f28dc7e4..378c81833ade 100644
--- a/joern-cli/frontends/pysrc2cpg/src/test/scala/io/joern/pysrc2cpg/dataflow/DataFlowTests.scala
+++ b/joern-cli/frontends/pysrc2cpg/src/test/scala/io/joern/pysrc2cpg/dataflow/DataFlowTests.scala
@@ -336,9 +336,9 @@ class RegexDefinedFlowsDataFlowTests
         FlowSemantic
           .from(
             "requests.py:<module>.post",
-            List((0, 0), (1, 1), ("url", -1), ("body", -1), ("url", "url"), ("body", "body"))
+            List((0, 0), (1, "url", -1), (2, "body", -1), (1, "url", 1, "url"), (2, "body", 2, "body"))
           ),
-        FlowSemantic.from("cross_taint.py:<module>.go", List((0, 0), (1, 1), ("a", "b")))
+        FlowSemantic.from("cross_taint.py:<module>.go", List((0, 0), (1, 1), (1, "a", 2, "b")))
       )
     ) {
 
@@ -474,4 +474,28 @@ class RegexDefinedFlowsDataFlowTests
     }
   }
 
+  "flows via tuple literal" should {
+    val cpg = code("""
+        |a = 1
+        |b = 2
+        |c = 3
+        |
+        |x = (a, b, c)
+        |
+        |sink1(b)
+        |sink2(x)
+        |""".stripMargin)
+    "not cross-taint due to 'pass through' semantics" in {
+      val src = cpg.literal("1").l
+      val snk = cpg.call("sink1").l
+      snk.reachableByFlows(src).size shouldBe 0
+    }
+
+    "taint the return value due to 'pass through' semantics" in {
+      val src = cpg.call.nameExact("<operator>.tupleLiteral").l
+      val snk = cpg.call("sink2").l
+      snk.reachableByFlows(src).size shouldBe 1
+    }
+  }
+
 }