--dataflow-traces
- Accepted by
opengrep scan,opengrep ci
Adds to each finding of a taint rule the path the tainted data took from the source to the sink. Without the flag, the text output shows only the sink.
Reading a trace
A trace is a list of steps, one per line, each with a label and the line of code it refers to:
| Label | Step |
|---|---|
source | The code that a source of the rule matches. |
through | A variable that holds the tainted data. |
through call | A call that the data goes through, entering the called function by an argument and leaving it by its return value or by a field of an argument. |
source in call | A call to a function that contains the source. |
sink in call | A call to a function that contains the sink. |
param | The parameter by which the data enters a called function. |
return | The return by which the data leaves a called function. |
sink | The code that a sink of the rule matches. |
The steps inside a called function are drawn under its call, one level deeper. A function called within a called function is nested in turn. These steps appear only when the analysis follows calls: with the rule option taint_intrafile, the flag --taint-intrafile, or the cross-file analysis of --enable-interfile. Otherwise, a call appears as a single through step.
Examples
The path from source to sink
rules: - id: input-to-eval mode: taint pattern-sources: - pattern: input(...) pattern-sinks: - pattern: eval(...) message: input reaches eval languages: [python] severity: ERRORx = input()y = xeval(y)$ opengrep scan -q --config rule.yaml --dataflow-traces app.py
app.py
error input-to-eval
input reaches eval
3: eval(y)
│
├─ source 1: x = input()
├─ through 1: x = input()
├─ through 2: y = x
└─ sink 3: eval(y)Through a function call
rules: - id: sql-injection mode: taint pattern-sources: - pattern: request.args.get(...) pattern-sinks: - pattern: cursor.execute(...) message: user input reaches a SQL query languages: [python] severity: ERRORdef build_query(name): query = "SELECT * FROM users WHERE name = '" + name + "'" return querydef search(): name = request.args.get("name") query = build_query(name) cursor.execute(query)$ opengrep scan -q --config sql.yaml --dataflow-traces search.py
search.py
error sql-injection
user input reaches a SQL query
8: cursor.execute(query)
│
├─ source 6: name = request.args.get("name")
├─ through 6: name = request.args.get("name")
├─ through 7: query = build_query(name)
└─ sink 8: cursor.execute(query)
$ opengrep scan -q --config sql.yaml --dataflow-traces --taint-intrafile search.py
search.py
error sql-injection
user input reaches a SQL query
8: cursor.execute(query)
│
├─ source 6: name = request.args.get("name")
├─ through 6: name = request.args.get("name")
├─ through call 7: query = build_query(name)
│ ├─ param 1: def build_query(name):
│ ├─ through 2: query = "SELECT * FROM users WHERE name = '" + name + "'"
│ └─ return 3: return query
├─ through 7: query = build_query(name)
└─ sink 8: cursor.execute(query)The first scan treats the call to build_query as a single step. With --taint-intrafile, the trace also shows the path inside the function.
A source and a sink in called functions
rules: - id: sql-injection mode: taint pattern-sources: - pattern: request.args.get(...) pattern-sinks: - pattern: cursor.execute(...) message: user input reaches a SQL query languages: [python] severity: ERRORdef get_name(): name = request.args.get("name") return namedef run_query(name): sql = "SELECT * FROM users WHERE name = '" + name + "'" cursor.execute(sql)def search(): run_query(get_name())$ opengrep scan -q --config sql.yaml --dataflow-traces --taint-intrafile calls.py
calls.py
error sql-injection
user input reaches a SQL query
7: cursor.execute(sql)
│
├─ source in call 10: run_query(get_name())
│ ├─ source 2: name = request.args.get("name")
│ ├─ through 2: name = request.args.get("name")
│ └─ return 3: return name
└─ sink in call 10: run_query(get_name())
├─ param 5: def run_query(name):
├─ through 6: sql = "SELECT * FROM users WHERE name = '" + name + "'"
└─ sink 7: cursor.execute(sql)The finding is reported at the sink, inside run_query. The trace starts at the call in search that connects the two functions.
JSON schema
In the JSON output, each result of a taint rule holds its trace in extra.dataflow_trace, with or without the flag. The trace is a list of steps in execution order, and each step is an object with these fields:
| Field | Value |
|---|---|
kind | The kind of step: source, through, through_call, source_in_call, sink_in_call, parameter, return or sink. These are the labels of the text output, with _ for a space and parameter for param. |
location | The path of the file, and the start and end of the code. Each has a line and a col, counted from 1, and an offset from the start of the file, counted from 0. end is the position after the last character. |
content | The code at the location, as written in the file; for a call, the name of the called function. |
children | For a call, the steps inside the called function, in the same form; for other steps, an empty list. |
A trace in the JSON output
rules: - id: input-to-eval mode: taint options: taint_intrafile: true pattern-sources: - pattern: input(...) pattern-sinks: - pattern: eval(...) message: input reaches eval languages: [python] severity: ERRORdef run(code): eval(code)run(input())$ opengrep scan -q --config rule.yaml --dataflow-traces app.py
app.py
error input-to-eval
input reaches eval
2: eval(code)
│
├─ source 4: run(input())
└─ sink in call 4: run(input())
├─ param 1: def run(code):
└─ sink 2: eval(code)
$ opengrep scan --config rule.yaml --json app.py | jq .
{
"version": "X.Y.Z",
"results": [
{
"check_id": "input-to-eval",
"path": "app.py",
"start": {
"line": 2,
"col": 5,
"offset": 19
},
"end": {
"line": 2,
"col": 15,
"offset": 29
},
"extra": {
"metavars": {},
"message": "input reaches eval",
"metadata": {},
"severity": "ERROR",
"fingerprint": "d41d752bf0b1ea72530217bd06cc03b7d24d71fb0d4af0ec9374a71b66caf9c82c17a25f10ae71f05a05fe5fc45177fabbd13b8dd7b991b62d833d75e641d928_0",
"lines": " eval(code)",
"is_ignored": false,
"validation_state": "NO_VALIDATOR",
"dataflow_trace": [
{
"kind": "source",
"location": {
"path": "app.py",
"start": {
"line": 4,
"col": 5,
"offset": 35
},
"end": {
"line": 4,
"col": 12,
"offset": 42
}
},
"content": "input()",
"children": []
},
{
"kind": "sink_in_call",
"location": {
"path": "app.py",
"start": {
"line": 4,
"col": 1,
"offset": 31
},
"end": {
"line": 4,
"col": 4,
"offset": 34
}
},
"content": "run",
"children": [
{
"kind": "parameter",
"location": {
"path": "app.py",
"start": {
"line": 1,
"col": 9,
"offset": 8
},
"end": {
"line": 1,
"col": 13,
"offset": 12
}
},
"content": "code",
"children": []
},
{
"kind": "sink",
"location": {
"path": "app.py",
"start": {
"line": 2,
"col": 5,
"offset": 19
},
"end": {
"line": 2,
"col": 15,
"offset": 29
}
},
"content": "eval(code)",
"children": []
}
]
}
],
"engine_kind": "Opengrep"
}
}
],
"errors": [],
"paths": {
"scanned": [
"app.py"
]
},
"interfile_languages_used": [],
"skipped_rules": []
}SARIF schema
With the flag, each result of a taint rule in the SARIF output has a codeFlows list with one code flow. The results of a rule that runs across files with --enable-interfile have it without the flag as well.
The message of the code flow names the file and line of the source and of the finding, as in Untrusted dataflow from app.py:4 to app.py:2. The code flow has one thread flow, whose locations are the steps of the trace in execution order, each call followed by the steps inside the called function. Each location has these fields:
| Field | Value |
|---|---|
nestingLevel | 0 for a step of the trace, one more for each call that the step is inside. |
kinds | ["call"] for a call, ["enter"] for a parameter and ["exit"] for a return; absent for the other steps. |
location.message.text | The kind of step, the code at the location and its file and line, as in Source: 'input()' @ 'app.py:4'; for a call, the code is the name of the called function. The kind is Source: for a source, Call: for a call, Sink: for a sink and Propagator : for the other steps. |
location.physicalLocation | The file, in artifactLocation.uri, and the region of the code: startLine, startColumn, endLine and endColumn, the code in snippet.text, and the same message. |
A trace in the SARIF output
rules: - id: input-to-eval mode: taint options: taint_intrafile: true pattern-sources: - pattern: input(...) pattern-sinks: - pattern: eval(...) message: input reaches eval languages: [python] severity: ERRORdef run(code): eval(code)run(input())$ opengrep scan --config rule.yaml --sarif --dataflow-traces app.py | jq .
{
"version": "2.1.0",
"runs": [
{
"invocations": [
{
"executionSuccessful": true,
"toolExecutionNotifications": []
}
],
"results": [
{
"codeFlows": [
{
"message": {
"text": "Untrusted dataflow from app.py:4 to app.py:2"
},
"threadFlows": [
{
"locations": [
{
"location": {
"message": {
"text": "Source: 'input()' @ 'app.py:4'"
},
"physicalLocation": {
"artifactLocation": {
"uri": "app.py"
},
"region": {
"endColumn": 12,
"endLine": 4,
"message": {
"text": "Source: 'input()' @ 'app.py:4'"
},
"snippet": {
"text": "input()"
},
"startColumn": 5,
"startLine": 4
}
}
},
"nestingLevel": 0
},
{
"kinds": [
"call"
],
"location": {
"message": {
"text": "Call: 'run' @ 'app.py:4'"
},
"physicalLocation": {
"artifactLocation": {
"uri": "app.py"
},
"region": {
"endColumn": 4,
"endLine": 4,
"message": {
"text": "Call: 'run' @ 'app.py:4'"
},
"snippet": {
"text": "run"
},
"startColumn": 1,
"startLine": 4
}
}
},
"nestingLevel": 0
},
{
"kinds": [
"enter"
],
"location": {
"message": {
"text": "Propagator : 'code' @ 'app.py:1'"
},
"physicalLocation": {
"artifactLocation": {
"uri": "app.py"
},
"region": {
"endColumn": 13,
"endLine": 1,
"message": {
"text": "Propagator : 'code' @ 'app.py:1'"
},
"snippet": {
"text": "code"
},
"startColumn": 9,
"startLine": 1
}
}
},
"nestingLevel": 1
},
{
"location": {
"message": {
"text": "Sink: 'eval(code)' @ 'app.py:2'"
},
"physicalLocation": {
"artifactLocation": {
"uri": "app.py"
},
"region": {
"endColumn": 15,
"endLine": 2,
"message": {
"text": "Sink: 'eval(code)' @ 'app.py:2'"
},
"snippet": {
"text": "eval(code)"
},
"startColumn": 5,
"startLine": 2
}
}
},
"nestingLevel": 1
}
]
}
]
}
],
"fingerprints": {
"matchBasedId/v1": "d41d752bf0b1ea72530217bd06cc03b7d24d71fb0d4af0ec9374a71b66caf9c82c17a25f10ae71f05a05fe5fc45177fabbd13b8dd7b991b62d833d75e641d928_0"
},
"locations": [
{
"physicalLocation": {
"artifactLocation": {
"uri": "app.py",
"uriBaseId": "%SRCROOT%"
},
"region": {
"endColumn": 15,
"endLine": 2,
"snippet": {
"text": " eval(code)"
},
"startColumn": 5,
"startLine": 2
}
}
}
],
"message": {
"text": "input reaches eval"
},
"properties": {},
"ruleId": "input-to-eval"
}
],
"tool": {
"driver": {
"name": "Opengrep",
"rules": [
{
"defaultConfiguration": {
"level": "error"
},
"fullDescription": {
"text": "input reaches eval"
},
"help": {
"markdown": "input reaches eval",
"text": "input reaches eval"
},
"id": "input-to-eval",
"name": "input-to-eval",
"properties": {
"precision": "very-high",
"tags": []
},
"shortDescription": {
"text": "Opengrep Finding: input-to-eval"
}
}
],
"semanticVersion": "X.Y.Z"
}
}
}
],
"$schema": "https://docs.oasis-open.org/sarif/sarif/v2.1.0/os/schemas/sarif-schema-2.1.0.json"
}