Skip to content

Commit e3e0307

Browse files
committed
V1
1 parent 5259a6e commit e3e0307

File tree

11 files changed

+447
-0
lines changed

11 files changed

+447
-0
lines changed
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<!DOCTYPE qhelp PUBLIC "-//Semmle//qhelp//EN" "qhelp.dtd">
2+
<qhelp>
3+
4+
<overview>
5+
<p>
6+
Flask and Django require a Securely signed key for singing the session cookies. most of the time developers rely on load hardcoded secret keys from a config file or python code. this proves that the way of hardcoded secret can make problems when you forgot to change the constant secret keys.
7+
</p>
8+
</overview>
9+
<recommendation>
10+
<p>
11+
In Flask Consider using a secure random generator with <a href="https://docs.python.org/3/library/secrets.html#secrets.token_hex">Python standard secrets library</a>
12+
</p>
13+
<p>
14+
In Django Consider using a secure random generator with "get_random_secret_key()"" method from "django.core.management.utils".
15+
</p>
16+
</recommendation>
17+
<example>
18+
19+
<sample src="example_bad.py" />
20+
21+
<sample src="example_good.py" />
22+
23+
</example>
24+
<references>
25+
<li>
26+
<a href="https://flask.palletsprojects.com/en/2.3.x/config/#SECRET_KEY">Flask Documentation</a>
27+
</li>
28+
<li>
29+
<a href="https://docs.djangoproject.com/en/4.2/ref/settings/#secret-key">Django Documentation</a>
30+
</li>
31+
<li>
32+
<a href="https://flask-jwt-extended.readthedocs.io/en/stable/basic_usage.html#basic-usage">Flask-JWT-Extended Documentation</a>
33+
</li>
34+
<li>
35+
<a href="https://www.horizon3.ai/cve-2023-27524-insecure-default-configuration-in-apache-superset-leads-to-remote-code-execution/">CVE-2023-27524 - Apache Superset had multiple CVEs related to this kind of Vulnerability</a>
36+
</li>
37+
<li>
38+
<a href="https://nvd.nist.gov/vuln/detail/CVE-2020-17526">CVE-2020-17526 - Apache Airflow had multiple CVEs related to this kind of Vulnerability</a>
39+
</li>
40+
<li>
41+
<a href="https://nvd.nist.gov/vuln/detail/CVE-2021-41192">CVE-2021-41192 - Redash was assigning a environment variable with a default value which it was assigning the default secrect if the environment variable does not exists</a>
42+
</li>
43+
44+
</references>
45+
</qhelp>
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
/**
2+
* @name Initializing SECRET_KEY of Flask application with Constant value
3+
* @description Initializing SECRET_KEY of Flask application with Constant value
4+
* files can lead to Authentication bypass
5+
* @kind path-problem
6+
* @id py/ConstantSecretKey
7+
* @problem.severity error
8+
* @security-severity 8.5
9+
* @precision high
10+
* @tags security
11+
* experimental
12+
* external/cwe/cwe-287
13+
*/
14+
15+
import python
16+
import experimental.semmle.python.Concepts
17+
import semmle.python.dataflow.new.DataFlow
18+
import semmle.python.ApiGraphs
19+
import semmle.python.dataflow.new.TaintTracking
20+
21+
/**
22+
* `flask.Flask()`
23+
*/
24+
API::Node flaskInstance() { result = API::moduleImport("flask").getMember("Flask").getASubclass*() }
25+
26+
/**
27+
* with using flask-session package, there is no jwt exists in cookies in user side
28+
* ```python
29+
*import os
30+
*from flask import Flask, session
31+
*app = Flask(__name__)
32+
* ```
33+
*/
34+
module FlaskConstantSecretKeyConfig implements DataFlow::ConfigSig {
35+
/**
36+
* Sources are Constants that without any Tainting reach the Sinks.
37+
* Also Sources can be the default value of getenv or similar methods
38+
* in a case that no value is assigned to Desired SECRET_KEY environment variable
39+
*/
40+
predicate isSource(DataFlow::Node source) {
41+
(
42+
source.asExpr().isConstant()
43+
or
44+
exists(API::Node cn |
45+
cn =
46+
[
47+
API::moduleImport("configparser")
48+
.getMember(["ConfigParser", "RawConfigParser"])
49+
.getReturn(),
50+
// legacy API https://docs.python.org/3/library/configparser.html#legacy-api-examples
51+
API::moduleImport("configparser")
52+
.getMember(["ConfigParser", "RawConfigParser"])
53+
.getReturn()
54+
.getMember("get")
55+
.getReturn()
56+
] and
57+
source = cn.asSource()
58+
)
59+
or
60+
exists(API::CallNode cn |
61+
cn =
62+
[
63+
API::moduleImport("os").getMember("getenv").getACall(),
64+
API::moduleImport("os").getMember("environ").getMember("get").getACall()
65+
] and
66+
(
67+
// this can be ideal if we assume that best security practice is that
68+
// we don't get SECRET_KEY from env and we always assign a secure generated random string to it
69+
cn.getNumArgument() = 1
70+
or
71+
cn.getNumArgument() = 2 and
72+
DataFlow::localFlow(any(DataFlow::Node n | n.asExpr().isConstant()), cn.getArg(1))
73+
) and
74+
source.asExpr() = cn.asExpr()
75+
)
76+
or
77+
exists(DataFlow::LocalSourceNode lsn |
78+
lsn = API::moduleImport("os").getMember("environ").getASubscript().asSource() and
79+
source.asExpr() = lsn.asExpr()
80+
)
81+
) and
82+
not source.getScope().getLocation().getFile().inStdlib()
83+
}
84+
85+
/**
86+
* Sinks are one of the following kinds, some of them are directly connected to a flask Instance like
87+
* ```python
88+
* app.config['SECRET_KEY'] = 'CHANGEME1'
89+
* app.secret_key = 'CHANGEME2'
90+
* app.config.update(SECRET_KEY="CHANGEME3")
91+
* app.config.from_mapping(SECRET_KEY="CHANGEME4")
92+
* ```
93+
* other Sinks are SECRET_KEY Constants Variables that are defined in seperate files or a class in those files like:
94+
* ```python
95+
* app.config.from_pyfile("config.py")
96+
* app.config.from_object('config.Config')
97+
*```
98+
* we find these files with `FromObjectFileName` DataFlow Configuration
99+
* note that "JWT_SECRET_KEY" is same as "SECRET_KEY" but it is belong to popular flask-jwt-extended library
100+
*/
101+
predicate isSink(DataFlow::Node sink) {
102+
(
103+
exists(API::Node n |
104+
n = flaskInstance() and
105+
flask_sessionSanitizer(n.getReturn().asSource())
106+
|
107+
sink =
108+
[
109+
n.getReturn().getAMember().getSubscript(["SECRET_KEY", "JWT_SECRET_KEY"]).asSink(),
110+
n.getReturn().getMember(["SECRET_KEY", "JWT_SECRET_KEY"]).asSink(),
111+
n.getReturn()
112+
.getMember("config")
113+
.getMember(["update", "from_mapping"])
114+
.getACall()
115+
.getArgByName(["SECRET_KEY", "JWT_SECRET_KEY"])
116+
]
117+
)
118+
or
119+
// this query checks for Django SecretKey too
120+
if exists(API::moduleImport("django"))
121+
then
122+
exists(AssignStmt e | e.getTarget(0).toString() = "SECRET_KEY" |
123+
sink.asExpr() = e.getValue()
124+
// and sanitizer(e.getTarget(0))
125+
)
126+
else
127+
exists(SecretKeyAssignStmt e |
128+
sink.asExpr() = e.getValue()
129+
// | sanitizer(e.getTarget(0))
130+
)
131+
) and
132+
not sink.getScope().getLocation().getFile().inStdlib()
133+
}
134+
}
135+
136+
// using flask_session library is safe
137+
predicate flask_sessionSanitizer(DataFlow::Node source) {
138+
not DataFlow::localFlow(source,
139+
API::moduleImport("flask_session").getMember("Session").getACall().getArg(0))
140+
}
141+
142+
// *it seems that sanitizer have a lot of performance issues*
143+
// for case check whether SECRECT_KEY is empty or not
144+
predicate sanitizer(Expr sourceExpr) {
145+
exists(DataFlow::Node source, DataFlow::Node sink, If i |
146+
source.asExpr() = sourceExpr and
147+
DataFlow::localFlow(source, sink)
148+
|
149+
not i.getASubExpression().getAChildNode*().(Compare) = sink.asExpr() and
150+
not sink.getScope().getLocation().getFile().inStdlib() and
151+
not source.getScope().getLocation().getFile().inStdlib() and
152+
not i.getScope().getLocation().getFile().inStdlib()
153+
)
154+
}
155+
156+
/**
157+
* Assignments like `SECRET_KEY = ConstantValue`
158+
* which ConstantValue will be found by another DataFlow Configuration
159+
* and `SECRET_KEY` location must be a argument of `from_object` or `from_pyfile` methods
160+
* the argument/location value will be found by another Taint Tracking Configuration.
161+
*/
162+
class SecretKeyAssignStmt extends AssignStmt {
163+
SecretKeyAssignStmt() {
164+
exists(
165+
string configFileName, string fileNamehelper, DataFlow::Node n1, FromObjectFileName config
166+
|
167+
config.hasFlow(n1, _) and
168+
n1.asExpr().isConstant() and
169+
fileNamehelper = n1.asExpr().(StrConst).getS() and
170+
// because of `from_object` we want first part of `Config.AClassName` which `Config` is a python file name
171+
configFileName = fileNamehelper.splitAt(".") and
172+
// after spliting, don't look at %py% pattern
173+
configFileName != "py"
174+
|
175+
this.getLocation().getFile().getShortName().matches("%" + configFileName + "%") and
176+
this.getTarget(0).toString() = ["SECRET_KEY", "JWT_SECRET_KEY"]
177+
) and
178+
not this.getScope().getLocation().getFile().inStdlib()
179+
}
180+
}
181+
182+
/**
183+
* we have some file name that telling us the SECRET_KEY location
184+
* which have determined by these two methods
185+
* `app.config.from_pyfile("configFileName.py")` or `app.config.from_object("configFileName.ClassName")`
186+
* this is a helper configuration that help us skip the SECRET_KEY variables that are not related to Flask.
187+
*/
188+
class FromObjectFileName extends TaintTracking::Configuration {
189+
FromObjectFileName() { this = "FromObjectFileName" }
190+
191+
override predicate isSource(DataFlow::Node source) {
192+
source.asExpr().isConstant() and
193+
not source.getScope().getLocation().getFile().inStdlib()
194+
}
195+
196+
override predicate isSink(DataFlow::Node sink) {
197+
exists(API::Node n |
198+
n = flaskInstance() and
199+
flask_sessionSanitizer(n.getReturn().asSource())
200+
|
201+
sink =
202+
n.getReturn()
203+
.getMember("config")
204+
.getMember(["from_object", "from_pyfile"])
205+
.getACall()
206+
.getArg(0)
207+
) and
208+
not sink.getScope().getLocation().getFile().inStdlib()
209+
}
210+
}
211+
212+
module FlaskConstantSecretKey = TaintTracking::Global<FlaskConstantSecretKeyConfig>;
213+
214+
import FlaskConstantSecretKey::PathGraph
215+
216+
from FlaskConstantSecretKey::PathNode source, FlaskConstantSecretKey::PathNode sink
217+
where FlaskConstantSecretKey::flowPath(source, sink)
218+
select sink, source, sink, "The SECRET_KEY config variable has assigned by $@.", source,
219+
" this constant String"
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from flask import Flask, session
2+
3+
app = Flask(__name__)
4+
aConstant = 'CHANGEME1'
5+
app.config['SECRET_KEY'] = aConstant
6+
7+
@app.route('/')
8+
def DEB_EX():
9+
if 'logged_in' not in session:
10+
session['logged_in'] = False
11+
if session['logged_in']:
12+
return app.secret_key
13+
else:
14+
return app.secret_key, 403
15+
16+
17+
if __name__ == '__main__':
18+
app.run()
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from flask import Flask, session
2+
from secrets import token_hex
3+
4+
app = Flask(__name__)
5+
app.config['SECRET_KEY'] = token_hex(64)
6+
7+
8+
@app.route('/')
9+
def DEB_EX():
10+
if 'logged_in' not in session:
11+
session['logged_in'] = False
12+
if session['logged_in']:
13+
return app.secret_key
14+
else:
15+
return app.secret_key, 403
16+
17+
18+
if __name__ == '__main__':
19+
app.run()
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
edges
2+
| app_unsafe.py:4:13:4:23 | ControlFlowNode for Str | app_unsafe.py:5:28:5:36 | ControlFlowNode for aConstant |
3+
| app_unsafe.py:4:13:4:23 | ControlFlowNode for Str | app_unsafe.py:7:30:7:38 | ControlFlowNode for aConstant |
4+
| app_unsafe.py:4:13:4:23 | ControlFlowNode for Str | app_unsafe.py:8:36:8:44 | ControlFlowNode for aConstant |
5+
| config.py:7:1:7:9 | GSSA Variable aConstant | config.py:12:18:12:26 | ControlFlowNode for aConstant |
6+
| config.py:7:1:7:9 | GSSA Variable aConstant | config.py:12:18:12:26 | ControlFlowNode for aConstant |
7+
| config.py:7:1:7:9 | GSSA Variable aConstant | config.py:17:38:17:46 | ControlFlowNode for aConstant |
8+
| config.py:7:1:7:9 | GSSA Variable aConstant | config.py:18:43:18:51 | ControlFlowNode for aConstant |
9+
| config.py:7:13:7:23 | ControlFlowNode for Str | config.py:7:1:7:9 | GSSA Variable aConstant |
10+
| config.py:12:18:12:26 | ControlFlowNode for aConstant | config.py:17:38:17:46 | ControlFlowNode for aConstant |
11+
| config.py:12:18:12:26 | ControlFlowNode for aConstant | config.py:18:43:18:51 | ControlFlowNode for aConstant |
12+
| config.py:17:38:17:46 | ControlFlowNode for aConstant | config.py:17:18:17:47 | ControlFlowNode for Attribute() |
13+
| config.py:17:38:17:46 | ControlFlowNode for aConstant | config.py:18:43:18:51 | ControlFlowNode for aConstant |
14+
| config.py:17:38:17:46 | ControlFlowNode for aConstant | file:///usr/lib/python3.10/os.py:771:17:771:23 | ControlFlowNode for default |
15+
| config.py:18:43:18:51 | ControlFlowNode for aConstant | config.py:18:18:18:52 | ControlFlowNode for Attribute() |
16+
| file:///usr/lib/python3.10/os.py:771:17:771:23 | ControlFlowNode for default | file:///usr/lib/python3.10/os.py:775:29:775:35 | ControlFlowNode for default |
17+
| file:///usr/lib/python3.10/os.py:775:29:775:35 | ControlFlowNode for default | file:///usr/lib/python3.10/os.py:775:12:775:36 | ControlFlowNode for Attribute() |
18+
nodes
19+
| app_unsafe.py:4:13:4:23 | ControlFlowNode for Str | semmle.label | ControlFlowNode for Str |
20+
| app_unsafe.py:5:28:5:36 | ControlFlowNode for aConstant | semmle.label | ControlFlowNode for aConstant |
21+
| app_unsafe.py:7:30:7:38 | ControlFlowNode for aConstant | semmle.label | ControlFlowNode for aConstant |
22+
| app_unsafe.py:8:36:8:44 | ControlFlowNode for aConstant | semmle.label | ControlFlowNode for aConstant |
23+
| config2.py:5:14:5:24 | ControlFlowNode for Str | semmle.label | ControlFlowNode for Str |
24+
| config.py:7:1:7:9 | GSSA Variable aConstant | semmle.label | GSSA Variable aConstant |
25+
| config.py:7:13:7:23 | ControlFlowNode for Str | semmle.label | ControlFlowNode for Str |
26+
| config.py:11:18:11:38 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
27+
| config.py:12:18:12:26 | ControlFlowNode for aConstant | semmle.label | ControlFlowNode for aConstant |
28+
| config.py:12:18:12:26 | ControlFlowNode for aConstant | semmle.label | ControlFlowNode for aConstant |
29+
| config.py:13:18:13:36 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
30+
| config.py:14:18:14:41 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
31+
| config.py:17:18:17:47 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
32+
| config.py:17:38:17:46 | ControlFlowNode for aConstant | semmle.label | ControlFlowNode for aConstant |
33+
| config.py:18:18:18:52 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
34+
| config.py:18:43:18:51 | ControlFlowNode for aConstant | semmle.label | ControlFlowNode for aConstant |
35+
| config.py:19:18:19:37 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript |
36+
| file:///usr/lib/python3.10/os.py:771:17:771:23 | ControlFlowNode for default | semmle.label | ControlFlowNode for default |
37+
| file:///usr/lib/python3.10/os.py:775:12:775:36 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
38+
| file:///usr/lib/python3.10/os.py:775:29:775:35 | ControlFlowNode for default | semmle.label | ControlFlowNode for default |
39+
subpaths
40+
| config.py:17:38:17:46 | ControlFlowNode for aConstant | file:///usr/lib/python3.10/os.py:771:17:771:23 | ControlFlowNode for default | file:///usr/lib/python3.10/os.py:775:12:775:36 | ControlFlowNode for Attribute() | config.py:17:18:17:47 | ControlFlowNode for Attribute() |
41+
#select
42+
| app_unsafe.py:5:28:5:36 | ControlFlowNode for aConstant | app_unsafe.py:4:13:4:23 | ControlFlowNode for Str | app_unsafe.py:5:28:5:36 | ControlFlowNode for aConstant | The SECRET_KEY config variable has assigned by $@. | app_unsafe.py:4:13:4:23 | ControlFlowNode for Str | this constant String |
43+
| app_unsafe.py:7:30:7:38 | ControlFlowNode for aConstant | app_unsafe.py:4:13:4:23 | ControlFlowNode for Str | app_unsafe.py:7:30:7:38 | ControlFlowNode for aConstant | The SECRET_KEY config variable has assigned by $@. | app_unsafe.py:4:13:4:23 | ControlFlowNode for Str | this constant String |
44+
| app_unsafe.py:8:36:8:44 | ControlFlowNode for aConstant | app_unsafe.py:4:13:4:23 | ControlFlowNode for Str | app_unsafe.py:8:36:8:44 | ControlFlowNode for aConstant | The SECRET_KEY config variable has assigned by $@. | app_unsafe.py:4:13:4:23 | ControlFlowNode for Str | this constant String |
45+
| config2.py:5:14:5:24 | ControlFlowNode for Str | config2.py:5:14:5:24 | ControlFlowNode for Str | config2.py:5:14:5:24 | ControlFlowNode for Str | The SECRET_KEY config variable has assigned by $@. | config2.py:5:14:5:24 | ControlFlowNode for Str | this constant String |
46+
| config.py:11:18:11:38 | ControlFlowNode for Attribute() | config.py:11:18:11:38 | ControlFlowNode for Attribute() | config.py:11:18:11:38 | ControlFlowNode for Attribute() | The SECRET_KEY config variable has assigned by $@. | config.py:11:18:11:38 | ControlFlowNode for Attribute() | this constant String |
47+
| config.py:12:18:12:26 | ControlFlowNode for aConstant | config.py:7:13:7:23 | ControlFlowNode for Str | config.py:12:18:12:26 | ControlFlowNode for aConstant | The SECRET_KEY config variable has assigned by $@. | config.py:7:13:7:23 | ControlFlowNode for Str | this constant String |
48+
| config.py:13:18:13:36 | ControlFlowNode for Attribute() | config.py:13:18:13:36 | ControlFlowNode for Attribute() | config.py:13:18:13:36 | ControlFlowNode for Attribute() | The SECRET_KEY config variable has assigned by $@. | config.py:13:18:13:36 | ControlFlowNode for Attribute() | this constant String |
49+
| config.py:14:18:14:41 | ControlFlowNode for Attribute() | config.py:14:18:14:41 | ControlFlowNode for Attribute() | config.py:14:18:14:41 | ControlFlowNode for Attribute() | The SECRET_KEY config variable has assigned by $@. | config.py:14:18:14:41 | ControlFlowNode for Attribute() | this constant String |
50+
| config.py:17:18:17:47 | ControlFlowNode for Attribute() | config.py:7:13:7:23 | ControlFlowNode for Str | config.py:17:18:17:47 | ControlFlowNode for Attribute() | The SECRET_KEY config variable has assigned by $@. | config.py:7:13:7:23 | ControlFlowNode for Str | this constant String |
51+
| config.py:18:18:18:52 | ControlFlowNode for Attribute() | config.py:7:13:7:23 | ControlFlowNode for Str | config.py:18:18:18:52 | ControlFlowNode for Attribute() | The SECRET_KEY config variable has assigned by $@. | config.py:7:13:7:23 | ControlFlowNode for Str | this constant String |
52+
| config.py:19:18:19:37 | ControlFlowNode for Subscript | config.py:19:18:19:37 | ControlFlowNode for Subscript | config.py:19:18:19:37 | ControlFlowNode for Subscript | The SECRET_KEY config variable has assigned by $@. | config.py:19:18:19:37 | ControlFlowNode for Subscript | this constant String |
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
experimental/Security/CWE-287-ConstantSecretKey/ConstantSecretKey.ql
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from flask import Flask, session
2+
from flask_session import Session
3+
4+
app = Flask(__name__)
5+
app.config['SECRET_KEY'] = 'CHANGEME'
6+
7+
Session(app)
8+
9+
10+
@app.route('/')
11+
def index():
12+
if 'logged_in' not in session:
13+
session['logged_in'] = False
14+
15+
if session['logged_in']:
16+
return '<h1>You are logged in!</h1>'
17+
else:
18+
return '<h1>Access Denied</h1>', 403
19+
20+
21+
if __name__ == '__main__':
22+
app.run()

0 commit comments

Comments
 (0)