Skip to content

Commit c832453

Browse files
author
Alvaro Muñoz
authored
Merge pull request #8 from GitHubSecurityLab/python_packs
Merge field/seclab packs
2 parents 7008cc2 + 6ad804d commit c832453

File tree

138 files changed

+3417
-20
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

138 files changed

+3417
-20
lines changed

.github/workflows/build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ jobs:
1212
strategy:
1313
fail-fast: false
1414
matrix:
15-
language: [ 'csharp', 'go', 'java' ]
15+
language: [ 'csharp', 'go', 'java', 'python' ]
1616

1717
steps:
1818
- uses: actions/checkout@v3

.github/workflows/publish.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ jobs:
1717
strategy:
1818
fail-fast: false
1919
matrix:
20-
language: ["csharp", "java"]
20+
language: ["csharp", "go", "java", "python"]
2121

2222
steps:
2323
- uses: actions/checkout@v3
@@ -54,7 +54,7 @@ jobs:
5454
strategy:
5555
fail-fast: false
5656
matrix:
57-
language: ["csharp", "java"]
57+
language: ["csharp", "go", "java", "python"]
5858

5959
steps:
6060
- uses: actions/checkout@v3

codeql-workspace.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,5 @@ provide:
22
- csharp/**/qlpack.yml
33
- go/**/qlpack.yml
44
- java/**/qlpack.yml
5+
- python/**/qlpack.yml
6+

java/src/suites/java-audit.qls

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
# This is the field security specialist audit pack
2-
31
- description: "GitHub's Community Packs Java Audit Suite"
42

53
# Audit queries

java/src/suites/java-local.qls

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
# See https://help.semmle.com/codeql/codeql-cli/procedures/query-suites.html#filtering-the-queries-in-a-query-suite
2-
# for additional ways to exclude queries
3-
41
- description: "GitHub's Community Packs Java Local Variate Suite"
52

63
- import: codeql-suites/java-security-extended.qls
@@ -15,15 +12,4 @@
1512
- queries: '.'
1613
from: codeql/java-queries
1714
- include:
18-
id:
19-
- java/path-injection-local
20-
- java/command-line-injection-local
21-
- java/xss-local
22-
- java/sql-injection-local
23-
- java/http-response-splitting-local
24-
- java/improper-validation-of-array-construction-local
25-
- java/improper-validation-of-array-index-local
26-
- java/tainted-format-string-local
27-
- java/tainted-arithmetic-local
28-
- java/unvalidated-url-redirection-local
29-
- java/tainted-numeric-cast-local
15+
tags contain: -local

python/lib/codeql-pack.lock.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
lockVersion: 1.0.0
3+
dependencies:
4+
codeql/dataflow:
5+
version: 0.0.3
6+
codeql/mad:
7+
version: 0.1.4
8+
codeql/python-all:
9+
version: 0.10.4
10+
codeql/regex:
11+
version: 0.1.4
12+
codeql/ssa:
13+
version: 0.1.4
14+
codeql/tutorial:
15+
version: 0.1.4
16+
codeql/util:
17+
version: 0.1.4
18+
codeql/yaml:
19+
version: 0.1.4
20+
compiled: false
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
private import python
2+
3+
// password = db.Column(..., server_default=...)
4+
5+
class DBColumn extends Call {
6+
CallNode call;
7+
string name;
8+
ControlFlowNode object;
9+
Name var;
10+
string id;
11+
12+
DBColumn() {
13+
call.getFunction().(AttrNode).getObject(name) = object
14+
and name = "Column"
15+
and call = this.getAFlowNode()
16+
and object.getNode() = var.getVariable().getAnAccess()
17+
and var.getId() = id
18+
}
19+
20+
string getDbId() {
21+
result = id
22+
}
23+
24+
predicate hasStaticDefault() {
25+
exists(DictItem arg |
26+
arg = call.getNode().getANamedArg()
27+
and arg.(Keyword).getArg() in ["server_default", "default"]
28+
and arg.(Keyword).getValue() instanceof ImmutableLiteral
29+
)
30+
}
31+
32+
string assignedToVariable() {
33+
exists(AssignStmt assign, Variable v|
34+
assign.defines(v)
35+
and v.getId() = result
36+
and assign.getValue() = this
37+
)
38+
}
39+
40+
string getColumnName() {
41+
result = call.getNode().getArg(0).(StrConst).getText()
42+
}
43+
}
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
import python
2+
private import semmle.python.dataflow.new.DataFlow
3+
private import semmle.python.dataflow.new.TaintTracking
4+
private import semmle.python.Concepts
5+
private import semmle.python.dataflow.new.RemoteFlowSources
6+
private import semmle.python.dataflow.new.BarrierGuards
7+
private import semmle.python.ApiGraphs
8+
private import DataFlow::PathGraph
9+
private import semmle.python.frameworks.Flask
10+
11+
abstract class CredentialSink extends DataFlow::Node { }
12+
13+
Expr getDictValueByKey(Dict dict, string key) {
14+
exists(KeyValuePair item |
15+
// d = {KEY: VALUE}
16+
item = dict.getAnItem() and
17+
key = item.getKey().(StrConst).getS() and
18+
result = item.getValue()
19+
)
20+
}
21+
22+
Expr getAssignStmtByKey(AssignStmt assign, string key) {
23+
exists(Subscript sub |
24+
// dict['KEY'] = VALUE
25+
sub = assign.getASubExpression() and
26+
// Make sure the keys match
27+
// TODO: What happens if this value itself is not static?
28+
key = sub.getASubExpression().(StrConst).getS() and
29+
// TODO: Only supports static strings, resolve the value??
30+
// result = assign.getASubExpression().(StrConst)
31+
result = sub.getValue()
32+
)
33+
}
34+
35+
Expr getAnyAssignStmtByKey(string key) { result = getAssignStmtByKey(any(AssignStmt a), key) }
36+
37+
// =========================
38+
// Web Frameworks
39+
// =========================
40+
class FlaskCredentialSink extends CredentialSink {
41+
FlaskCredentialSink() {
42+
exists(API::Node node |
43+
exists(AssignStmt stmt |
44+
// app = flask.Flask(__name__)
45+
// app.secret_key = VALUE
46+
node = Flask::FlaskApp::instance().getMember("secret_key") and
47+
stmt = node.getAValueReachingSink().asExpr().getParentNode() and
48+
this = DataFlow::exprNode(stmt.getValue())
49+
)
50+
or
51+
exists(Expr assign, AssignStmt stmt |
52+
// app.config['SECRET_KEY'] = VALUE
53+
assign = getAnyAssignStmtByKey("SECRET_KEY").getParentNode() and
54+
stmt = assign.getParentNode() and
55+
this = DataFlow::exprNode(stmt.getValue())
56+
)
57+
or
58+
// app.config.update(SECRET_KEY=VALUE)
59+
node = Flask::FlaskApp::instance().getMember("config").getMember("update") and
60+
this = DataFlow::exprNode(node.getACall().getArgByName("SECRET_KEY").asExpr())
61+
)
62+
}
63+
}
64+
65+
class DjangoCredentialSink extends CredentialSink {
66+
DjangoCredentialSink() {
67+
// Check Django import is present
68+
exists(API::moduleImport("django")) and
69+
exists(AssignStmt stmt |
70+
// Check is the SECRET_KEY is in the a settings.py file
71+
// Removed "settings/develop.py"
72+
stmt.getLocation().getFile().getBaseName() = ["settings.py", "settings/production.py"] and
73+
(
74+
stmt.getATarget().toString() = "SECRET_KEY" and
75+
this.asExpr() = stmt.getValue()
76+
)
77+
)
78+
}
79+
}
80+
81+
// =========================
82+
// Databases
83+
// =========================
84+
class MySqlSink extends CredentialSink {
85+
MySqlSink() {
86+
this =
87+
API::moduleImport("mysql")
88+
.getMember("connector")
89+
.getMember("connect")
90+
.getACall()
91+
.getArgByName("password")
92+
}
93+
}
94+
95+
class AsyncpgSink extends CredentialSink {
96+
AsyncpgSink() {
97+
this = API::moduleImport("asyncpg").getMember("connect").getACall().getArgByName("password") or
98+
this =
99+
API::moduleImport("asyncpg")
100+
.getMember("connection")
101+
.getMember("Connection")
102+
.getACall()
103+
.getArgByName("password")
104+
}
105+
}
106+
107+
class PsycopgSink extends CredentialSink {
108+
PsycopgSink() {
109+
this = API::moduleImport("psycopg2").getMember("connect").getACall().getArgByName("password")
110+
}
111+
}
112+
113+
class AioredisSink extends CredentialSink {
114+
AioredisSink() {
115+
this =
116+
API::moduleImport("aioredis")
117+
.getMember("create_connection")
118+
.getACall()
119+
.getArgByName("password")
120+
or
121+
this =
122+
API::moduleImport("aioredis").getMember("create_pool").getACall().getArgByName("password")
123+
or
124+
this =
125+
API::moduleImport("aioredis").getMember("create_redis").getACall().getArgByName("password")
126+
or
127+
// redis = await aioredis.create_redis_pool(
128+
// 'redis://localhost', password='sEcRet')
129+
this =
130+
API::moduleImport("aioredis")
131+
.getMember("create_redis_pool")
132+
.getACall()
133+
.getArgByName("password")
134+
or
135+
this =
136+
API::moduleImport("aioredis")
137+
.getMember("sentinel")
138+
.getMember("create_sentinel")
139+
.getACall()
140+
.getArgByName("password")
141+
or
142+
this =
143+
API::moduleImport("aioredis")
144+
.getMember("sentinel")
145+
.getMember("create_sentinel_pool")
146+
.getACall()
147+
.getArgByName("password")
148+
}
149+
}
150+
151+
// =========================
152+
// Utils
153+
// =========================
154+
class RequestsSink extends CredentialSink {
155+
RequestsSink() {
156+
// from requests.auth import HTTPBasicAuth
157+
// auth = HTTPBasicAuth('user', 'mysecretpassword')
158+
this =
159+
API::moduleImport("requests")
160+
.getMember("auth")
161+
.getMember("HTTPBasicAuth")
162+
.getACall()
163+
.getArg(1)
164+
}
165+
}
166+
167+
class PyJwtSink extends CredentialSink {
168+
PyJwtSink() {
169+
// import jwt
170+
// encoded = jwt.encode({"some": "payload"}, "secret", algorithm="HS256")
171+
this = API::moduleImport("jwt").getMember("encode").getACall().getArg(1)
172+
or
173+
// decode = jwt.decode(encoded, "secret", algorithm="HS256")
174+
this = API::moduleImport("jwt").getMember("decode").getACall().getArg(1)
175+
}
176+
}
177+
178+
class PyOtpSink extends CredentialSink {
179+
PyOtpSink() {
180+
// import pyotp
181+
// totp = pyotp.TOTP('base32secret3232')
182+
this = API::moduleImport("pyotp").getMember("TOTP").getACall().getArg(0)
183+
}
184+
}
185+
186+
class Boto3Sink extends CredentialSink {
187+
Boto3Sink() {
188+
// https://docs.min.io/docs/how-to-use-aws-sdk-for-python-with-minio-server.html
189+
exists(DataFlow::CallCfgNode calls |
190+
// s3 = boto3.resource('s3',
191+
// aws_access_key_id='YOUR-ACCESSKEYID',
192+
// aws_secret_access_key='YOUR-SECRETACCESSKEY'
193+
// aws_session_token="YOUR-SESSION-TOKEN"
194+
// )
195+
calls = API::moduleImport("boto3").getMember(["client", "resource"]).getACall() and
196+
(
197+
this = calls.getArgByName("aws_access_key_id") or
198+
this = calls.getArgByName("aws_secret_access_key") or
199+
this = calls.getArgByName("aws_session_token")
200+
)
201+
)
202+
}
203+
}

python/lib/github/Helpers.qll

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import python
2+
import semmle.python.dataflow.new.DataFlow
3+
import semmle.python.dataflow.new.RemoteFlowSources
4+
// Import Sinks
5+
private import semmle.python.security.dataflow.CommandInjectionCustomizations
6+
private import semmle.python.security.dataflow.CodeInjectionCustomizations
7+
private import semmle.python.security.dataflow.ServerSideRequestForgeryCustomizations
8+
private import semmle.python.security.dataflow.SqlInjectionCustomizations
9+
private import semmle.python.security.dataflow.UnsafeDeserializationCustomizations
10+
// Fields Sinks
11+
private import github.HardcodedSecretSinks
12+
private import github.MassAssignment
13+
14+
// Find Node at Location
15+
predicate findByLocation(DataFlow::Node node, string relative_path, int linenumber) {
16+
node.getLocation().getFile().getRelativePath() = relative_path and
17+
node.getLocation().getStartLine() = linenumber
18+
}
19+
20+
// Dangerous Sinks
21+
predicate dangerousSinks(DataFlow::Node sink) {
22+
(
23+
sink instanceof CommandInjection::Sink or
24+
sink instanceof CodeInjection::Sink or
25+
sink instanceof ServerSideRequestForgery::Sink or
26+
sink instanceof SqlInjection::Sink or
27+
sink instanceof UnsafeDeserialization::Sink or
28+
// Fields Query Addtional Sinks
29+
sink instanceof CredentialSink or
30+
sink instanceof MassAssignment::Sinks
31+
) and
32+
sink.getScope().inSource()
33+
}
34+
35+
predicate functionParameters(DataFlow::Node node) {
36+
(
37+
// // Function Call Parameters
38+
node instanceof DataFlow::ParameterNode
39+
or
40+
// Function Call Arguments
41+
node instanceof DataFlow::ArgumentNode
42+
) and
43+
not dangerousSinks(node) and
44+
node.getScope().inSource()
45+
}

0 commit comments

Comments
 (0)