Skip to content

Commit 5076b1a

Browse files
authored
Merge pull request github#16135 from sylwia-budzynska/gradio-model
Python: Add Gradio models
2 parents 4617c05 + 72493a6 commit 5076b1a

File tree

10 files changed

+233
-0
lines changed

10 files changed

+233
-0
lines changed

docs/codeql/reusables/supported-frameworks.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,7 @@ and the CodeQL library pack ``codeql/python-all`` (`changelog <https://github.co
202202
Flask-Admin, Web framework
203203
Tornado, Web framework
204204
Twisted, Web framework
205+
Gradio, Web framework
205206
starlette, Asynchronous Server Gateway Interface (ASGI)
206207
ldap3, Lightweight Directory Access Protocol (LDAP)
207208
python-ldap, Lightweight Directory Access Protocol (LDAP)

python/ql/lib/semmle/python/Frameworks.qll

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ private import semmle.python.frameworks.FastApi
2929
private import semmle.python.frameworks.Flask
3030
private import semmle.python.frameworks.FlaskAdmin
3131
private import semmle.python.frameworks.FlaskSqlAlchemy
32+
private import semmle.python.frameworks.Gradio
3233
private import semmle.python.frameworks.Httpx
3334
private import semmle.python.frameworks.Idna
3435
private import semmle.python.frameworks.Invoke
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/**
2+
* Provides classes modeling security-relevant aspects of the `gradio` PyPI package.
3+
* See https://pypi.org/project/gradio/.
4+
*/
5+
6+
import python
7+
import semmle.python.dataflow.new.RemoteFlowSources
8+
import semmle.python.dataflow.new.TaintTracking
9+
import semmle.python.ApiGraphs
10+
11+
/**
12+
* Provides models for the `gradio` PyPI package.
13+
* See https://pypi.org/project/gradio/.
14+
*/
15+
module Gradio {
16+
/**
17+
* The event handlers, Interface and gradio.ChatInterface classes, which take untrusted data.
18+
*/
19+
private class GradioInput extends API::CallNode {
20+
GradioInput() {
21+
this =
22+
API::moduleImport("gradio")
23+
.getMember([
24+
"Button", "Textbox", "UploadButton", "Slider", "JSON", "HTML", "Markdown", "File",
25+
"AnnotatedImage", "Audio", "BarPlot", "Chatbot", "Checkbox", "CheckboxGroup",
26+
"ClearButton", "Code", "ColorPicker", "Dataframe", "Dataset", "DownloadButton",
27+
"Dropdown", "DuplicateButton", "FileExplorer", "Gallery", "HighlightedText",
28+
"Image", "ImageEditor", "Label", "LinePlot", "LoginButton", "LogoutButton",
29+
"Model3D", "Number", "ParamViewer", "Plot", "Radio", "ScatterPlot", "SimpleImage",
30+
"State", "Video"
31+
])
32+
.getReturn()
33+
.getMember([
34+
"change", "input", "click", "submit", "edit", "clear", "play", "pause", "stop",
35+
"end", "start_recording", "pause_recording", "stop_recording", "focus", "blur",
36+
"upload", "release", "select", "stream", "like", "load", "key_up",
37+
])
38+
.getACall()
39+
or
40+
this = API::moduleImport("gradio").getMember(["Interface", "ChatInterface"]).getACall()
41+
}
42+
}
43+
44+
/**
45+
* The `inputs` parameters in Gradio event handlers, that are lists and are sources of untrusted data.
46+
* This model allows tracking each element list back to source, f.ex. `gr.Textbox(...)`.
47+
*/
48+
private class GradioInputList extends RemoteFlowSource::Range {
49+
GradioInputList() {
50+
exists(GradioInput call |
51+
// limit only to lists of parameters given to `inputs`.
52+
(
53+
(
54+
call.getKeywordParameter("inputs").asSink().asCfgNode() instanceof ListNode
55+
or
56+
call.getParameter(1).asSink().asCfgNode() instanceof ListNode
57+
) and
58+
(
59+
this = call.getKeywordParameter("inputs").getASubscript().getAValueReachingSink()
60+
or
61+
this = call.getParameter(1).getASubscript().getAValueReachingSink()
62+
)
63+
)
64+
)
65+
}
66+
67+
override string getSourceType() { result = "Gradio untrusted input" }
68+
}
69+
70+
/**
71+
* The `inputs` parameters in Gradio event handlers, that are not lists and are sources of untrusted data.
72+
*/
73+
private class GradioInputParameter extends RemoteFlowSource::Range {
74+
GradioInputParameter() {
75+
exists(GradioInput call |
76+
this = call.getParameter(0, "fn").getParameter(_).asSource() and
77+
// exclude lists of parameters given to `inputs`
78+
not call.getKeywordParameter("inputs").asSink().asCfgNode() instanceof ListNode and
79+
not call.getParameter(1).asSink().asCfgNode() instanceof ListNode
80+
)
81+
}
82+
83+
override string getSourceType() { result = "Gradio untrusted input" }
84+
}
85+
86+
/**
87+
* The `inputs` parameters in Gradio decorators to event handlers, that are sources of untrusted data.
88+
*/
89+
private class GradioInputDecorator extends RemoteFlowSource::Range {
90+
GradioInputDecorator() {
91+
exists(GradioInput call |
92+
this = call.getReturn().getACall().getParameter(0).getParameter(_).asSource()
93+
)
94+
}
95+
96+
override string getSourceType() { result = "Gradio untrusted input" }
97+
}
98+
99+
/**
100+
* Extra taint propagation for tracking `inputs` parameters in Gradio event handlers, that are lists.
101+
*/
102+
private class ListTaintStep extends TaintTracking::AdditionalTaintStep {
103+
override predicate step(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) {
104+
exists(GradioInput node |
105+
// handle cases where there are multiple arguments passed as a list to `inputs`
106+
(
107+
(
108+
node.getKeywordParameter("inputs").asSink().asCfgNode() instanceof ListNode
109+
or
110+
node.getParameter(1).asSink().asCfgNode() instanceof ListNode
111+
) and
112+
exists(int i | nodeTo = node.getParameter(0, "fn").getParameter(i).asSource() |
113+
nodeFrom.asCfgNode() =
114+
node.getKeywordParameter("inputs").asSink().asCfgNode().(ListNode).getElement(i)
115+
or
116+
nodeFrom.asCfgNode() =
117+
node.getParameter(1).asSink().asCfgNode().(ListNode).getElement(i)
118+
)
119+
)
120+
)
121+
}
122+
}
123+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
category: minorAnalysis
3+
---
4+
* Added models of `gradio` PyPI package.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
testFailures
2+
failures
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import gradio as gr
2+
3+
4+
with gr.Blocks() as demo:
5+
name = gr.Textbox(label="Name")
6+
output = gr.Textbox(label="Output Box")
7+
# static block - not used as a source
8+
static_block = gr.HTML("""
9+
<div style='height: 100px; width: 800px; background-color: pink;'></div>
10+
""")
11+
greet_btn = gr.Button("Hello")
12+
13+
# decorator
14+
@greet_btn.click(inputs=name, outputs=output)
15+
def greet(name): # $ source=name
16+
return "Hello " + name + "!"
17+
18+
# `click` event handler with keyword arguments
19+
def greet1(name): # $ source=name
20+
return "Hello " + name + "!"
21+
22+
greet1_btn = gr.Button("Hello")
23+
greet1_btn.click(fn=greet1, inputs=name, outputs=output, api_name="greet")
24+
25+
# `click` event handler with positional arguments
26+
def greet2(name): # $ source=name
27+
return "Hello " + name + "!"
28+
29+
greet2_btn = gr.Button("Hello")
30+
greet2_btn.click(fn=greet2, inputs=name, outputs=output, api_name="greet")
31+
32+
33+
demo.launch()
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import python
2+
import semmle.python.dataflow.new.RemoteFlowSources
3+
import TestUtilities.InlineExpectationsTest
4+
private import semmle.python.dataflow.new.internal.PrintNode
5+
6+
module SourceTest implements TestSig {
7+
string getARelevantTag() { result = "source" }
8+
9+
predicate hasActualResult(Location location, string element, string tag, string value) {
10+
exists(location.getFile().getRelativePath()) and
11+
exists(RemoteFlowSource rfs |
12+
location = rfs.getLocation() and
13+
element = rfs.toString() and
14+
value = prettyNode(rfs) and
15+
tag = "source"
16+
)
17+
}
18+
}
19+
20+
import MakeTest<SourceTest>
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
edges
2+
| taint_step_test.py:5:5:5:8 | ControlFlowNode for path | taint_step_test.py:19:43:19:46 | ControlFlowNode for path | provenance | |
3+
| taint_step_test.py:5:12:5:35 | ControlFlowNode for Attribute() | taint_step_test.py:5:5:5:8 | ControlFlowNode for path | provenance | |
4+
| taint_step_test.py:6:5:6:8 | ControlFlowNode for file | taint_step_test.py:19:48:19:51 | ControlFlowNode for file | provenance | |
5+
| taint_step_test.py:6:12:6:35 | ControlFlowNode for Attribute() | taint_step_test.py:6:5:6:8 | ControlFlowNode for file | provenance | |
6+
| taint_step_test.py:11:18:11:21 | ControlFlowNode for path | taint_step_test.py:12:9:12:16 | ControlFlowNode for filepath | provenance | |
7+
| taint_step_test.py:11:18:11:21 | ControlFlowNode for path | taint_step_test.py:12:9:12:16 | ControlFlowNode for filepath | provenance | AdditionalTaintStep |
8+
| taint_step_test.py:11:24:11:27 | ControlFlowNode for file | taint_step_test.py:12:9:12:16 | ControlFlowNode for filepath | provenance | AdditionalTaintStep |
9+
| taint_step_test.py:12:9:12:16 | ControlFlowNode for filepath | taint_step_test.py:13:19:13:26 | ControlFlowNode for filepath | provenance | |
10+
| taint_step_test.py:19:43:19:46 | ControlFlowNode for path | taint_step_test.py:11:18:11:21 | ControlFlowNode for path | provenance | AdditionalTaintStep |
11+
| taint_step_test.py:19:48:19:51 | ControlFlowNode for file | taint_step_test.py:11:24:11:27 | ControlFlowNode for file | provenance | AdditionalTaintStep |
12+
nodes
13+
| taint_step_test.py:5:5:5:8 | ControlFlowNode for path | semmle.label | ControlFlowNode for path |
14+
| taint_step_test.py:5:12:5:35 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
15+
| taint_step_test.py:6:5:6:8 | ControlFlowNode for file | semmle.label | ControlFlowNode for file |
16+
| taint_step_test.py:6:12:6:35 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
17+
| taint_step_test.py:11:18:11:21 | ControlFlowNode for path | semmle.label | ControlFlowNode for path |
18+
| taint_step_test.py:11:24:11:27 | ControlFlowNode for file | semmle.label | ControlFlowNode for file |
19+
| taint_step_test.py:12:9:12:16 | ControlFlowNode for filepath | semmle.label | ControlFlowNode for filepath |
20+
| taint_step_test.py:13:19:13:26 | ControlFlowNode for filepath | semmle.label | ControlFlowNode for filepath |
21+
| taint_step_test.py:19:43:19:46 | ControlFlowNode for path | semmle.label | ControlFlowNode for path |
22+
| taint_step_test.py:19:48:19:51 | ControlFlowNode for file | semmle.label | ControlFlowNode for file |
23+
subpaths
24+
#select
25+
| taint_step_test.py:13:19:13:26 | ControlFlowNode for filepath | taint_step_test.py:5:12:5:35 | ControlFlowNode for Attribute() | taint_step_test.py:13:19:13:26 | ControlFlowNode for filepath | This path depends on a $@. | taint_step_test.py:5:12:5:35 | ControlFlowNode for Attribute() | user-provided value |
26+
| taint_step_test.py:13:19:13:26 | ControlFlowNode for filepath | taint_step_test.py:6:12:6:35 | ControlFlowNode for Attribute() | taint_step_test.py:13:19:13:26 | ControlFlowNode for filepath | This path depends on a $@. | taint_step_test.py:6:12:6:35 | ControlFlowNode for Attribute() | user-provided value |
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import gradio as gr
2+
import os
3+
4+
with gr.Blocks() as demo:
5+
path = gr.Textbox(label="Path") # $ source=gr.Textbox(..)
6+
file = gr.Textbox(label="File") # $ source=gr.Textbox(..)
7+
output = gr.Textbox(label="Output Box")
8+
9+
10+
# path injection sink
11+
def fileread(path, file):
12+
filepath = os.path.join(path, file)
13+
with open(filepath, "r") as f:
14+
return f.read()
15+
16+
17+
# `click` event handler with `inputs` containing a list
18+
greet1_btn = gr.Button("Path for the file to display")
19+
greet1_btn.click(fn=fileread, inputs=[path,file], outputs=output, api_name="fileread")
20+
21+
22+
demo.launch()
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Security/CWE-022/PathInjection.ql

0 commit comments

Comments
 (0)