Skip to content

Commit 251036c

Browse files
authored
Merge pull request #17080 from sylwia-budzynska/streamlit
Python: Add Streamlit models
2 parents f9f57e9 + 9bd00c9 commit 251036c

File tree

10 files changed

+156
-19
lines changed

10 files changed

+156
-19
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ private import semmle.python.frameworks.Simplejson
7373
private import semmle.python.frameworks.SqlAlchemy
7474
private import semmle.python.frameworks.Starlette
7575
private import semmle.python.frameworks.Stdlib
76+
private import semmle.python.frameworks.Streamlit
7677
private import semmle.python.frameworks.Toml
7778
private import semmle.python.frameworks.Torch
7879
private import semmle.python.frameworks.Tornado
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/**
2+
* Provides classes modeling security-relevant aspects of the `streamlit` PyPI package.
3+
* See https://pypi.org/project/streamlit/.
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+
import semmle.python.Concepts
11+
private import semmle.python.frameworks.SqlAlchemy
12+
13+
/**
14+
* Provides models for the `streamlit` PyPI package.
15+
* See https://pypi.org/project/streamlit/.
16+
*/
17+
module Streamlit {
18+
/**
19+
* The calls to the interactive streamlit widgets, which take untrusted input.
20+
*/
21+
private class StreamlitInput extends RemoteFlowSource::Range {
22+
StreamlitInput() {
23+
this =
24+
API::moduleImport("streamlit")
25+
.getMember(["text_input", "text_area", "chat_input"])
26+
.getACall()
27+
}
28+
29+
override string getSourceType() { result = "Streamlit user input" }
30+
}
31+
32+
/**
33+
* The Streamlit SQLConnection class, which is used to create a connection to a SQL Database.
34+
* Streamlit wraps around SQL Alchemy for most database functionality, and adds some on top of it, such as the `query` method.
35+
* Streamlit can also connect to Snowflake and Snowpark databases, but the modeling is not the same, so we need to limit the scope to SQL databases.
36+
* https://docs.streamlit.io/develop/api-reference/connections/st.connections.sqlconnection#:~:text=to%20data.-,st.connections.SQLConnection,-Streamlit%20Version
37+
* We can connect to SQL databases for example with `import streamlit as st; conn = st.connection('pets_db', type='sql')`
38+
*/
39+
private class StreamlitSqlConnection extends API::CallNode {
40+
StreamlitSqlConnection() {
41+
exists(StringLiteral str, API::CallNode n |
42+
str.getText() = "sql" and
43+
n = API::moduleImport("streamlit").getMember("connection").getACall() and
44+
DataFlow::exprNode(str)
45+
.(DataFlow::LocalSourceNode)
46+
.flowsTo([n.getArg(1), n.getArgByName("type")]) and
47+
this = n
48+
)
49+
}
50+
}
51+
52+
/**
53+
* The `query` call that can execute raw queries on a connection to a SQL database.
54+
* https://docs.streamlit.io/develop/api-reference/connections/st.connection
55+
*/
56+
private class QueryMethodCall extends DataFlow::CallCfgNode, SqlExecution::Range {
57+
QueryMethodCall() {
58+
exists(StreamlitSqlConnection s | this = s.getReturn().getMember("query").getACall())
59+
}
60+
61+
override DataFlow::Node getSql() { result in [this.getArg(0), this.getArgByName("sql")] }
62+
}
63+
64+
/**
65+
* The Streamlit SQLConnection.connect() call, which returns a a new sqlalchemy.engine.Connection object.
66+
* Streamlit creates a connection to a SQL database basing off SQL Alchemy, so we can reuse the models that we already have.
67+
*/
68+
private class StreamlitSqlAlchemyConnection extends SqlAlchemy::Connection::InstanceSource {
69+
StreamlitSqlAlchemyConnection() {
70+
exists(StreamlitSqlConnection s | this = s.getReturn().getMember("connect").getACall())
71+
}
72+
}
73+
74+
/**
75+
* The underlying SQLAlchemy Engine, accessed via `st.connection().engine`.
76+
* Streamlit creates an engine to a SQL database basing off SQL Alchemy, so we can reuse the models that we already have.
77+
*/
78+
private class StreamlitSqlAlchemyEngine extends SqlAlchemy::Engine::InstanceSource {
79+
StreamlitSqlAlchemyEngine() {
80+
exists(StreamlitSqlConnection s | this = s.getReturn().getMember("engine").asSource())
81+
}
82+
}
83+
84+
/**
85+
* The SQLAlchemy Session, accessed via `st.connection().session`.
86+
* Streamlit can create a session to a SQL database basing off SQL Alchemy, so we can reuse the models that we already have.
87+
* For example, the modeling for `session` includes an `execute` method, which is used to execute raw SQL queries.
88+
* https://docs.streamlit.io/develop/api-reference/connections/st.connections.sqlconnection#:~:text=SQLConnection.engine-,SQLConnection.session,-Streamlit%20Version
89+
*/
90+
private class StreamlitSqlSession extends SqlAlchemy::Session::InstanceSource {
91+
StreamlitSqlSession() {
92+
exists(StreamlitSqlConnection s | this = s.getReturn().getMember("session").asSource())
93+
}
94+
}
95+
}
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 `streamlit` PyPI package.
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: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,2 @@
11
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>
2+
import experimental.meta.RemoteFlowSourceTest
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
testFailures
2+
failures
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import python
2+
import experimental.meta.ConceptsTest
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
testFailures
2+
failures
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import python
2+
import experimental.meta.RemoteFlowSourceTest
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import streamlit as st
2+
3+
# Streamlit sources
4+
inp = st.text_input("Query the database") # $ source=st.text_input(..)
5+
area = st.text_area("Area") # $ source=st.text_area(..)
6+
chat = st.chat_input("Chat") # $ source=st.chat_input(..)
7+
8+
# Initialize connection.
9+
conn = st.connection("postgresql", type="sql")
10+
11+
# SQL injection sink
12+
q = conn.query("some sql") # $ getSql="some sql"
13+
14+
# SQLAlchemy connection
15+
c = conn.connect()
16+
17+
c.execute("other sql") # $ getSql="other sql"
18+
19+
# SQL Alchemy session
20+
s = conn.session
21+
22+
s.execute("yet another sql") # $ getSql="yet another sql"
23+
24+
# SQL Alchemy engine
25+
e = st.connection("postgresql", type="sql")
26+
27+
e.engine.connect().execute("yet yet another sql") # $ getSql="yet yet another sql"

0 commit comments

Comments
 (0)