Skip to content

Commit 04a68f8

Browse files
authored
Merge pull request #11372 from RasmusWL/getpass
Python: Model `getpass.getpass` as source of passwords
2 parents b281cc8 + 9195b73 commit 04a68f8

File tree

4 files changed

+42
-7
lines changed

4 files changed

+42
-7
lines changed

python/ql/lib/semmle/python/dataflow/new/SensitiveDataSources.qll

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ private import semmle.python.dataflow.new.DataFlow
88
// Need to import `semmle.python.Frameworks` since frameworks can extend `SensitiveDataSource::Range`
99
private import semmle.python.Frameworks
1010
private import semmle.python.security.internal.SensitiveDataHeuristics as SensitiveDataHeuristics
11+
private import semmle.python.ApiGraphs
1112

1213
// We export these explicitly, so we don't also export the `HeuristicNames` module.
1314
class SensitiveDataClassification = SensitiveDataHeuristics::SensitiveDataClassification;
@@ -23,7 +24,17 @@ module SensitiveDataClassification = SensitiveDataHeuristics::SensitiveDataClass
2324
class SensitiveDataSource extends DataFlow::Node {
2425
SensitiveDataSource::Range range;
2526

26-
SensitiveDataSource() { this = range }
27+
SensitiveDataSource() {
28+
this = range and
29+
// ignore sensitive password sources in getpass.py, that can escape through `getpass.getpass()` return value,
30+
// since `getpass.getpass()` is considered a source itself.
31+
not exists(Module getpass |
32+
getpass.getName() = "getpass" and
33+
this.getScope().getEnclosingModule() = getpass and
34+
// do allow this call if we're analyzing getpass.py as part of CPython though
35+
not exists(getpass.getFile().getRelativePath())
36+
)
37+
}
2738

2839
/**
2940
* Gets the classification of the sensitive data.
@@ -312,6 +323,17 @@ private module SensitiveDataModeling {
312323

313324
override SensitiveDataClassification getClassification() { result = classification }
314325
}
326+
327+
/**
328+
* A call to `getpass.getpass`, see https://docs.python.org/3.10/library/getpass.html#getpass.getpass
329+
*/
330+
class GetPassCall extends SensitiveDataSource::Range, API::CallNode {
331+
GetPassCall() { this = API::moduleImport("getpass").getMember("getpass").getACall() }
332+
333+
override SensitiveDataClassification getClassification() {
334+
result = SensitiveDataClassification::password()
335+
}
336+
}
315337
}
316338

317339
predicate sensitiveDataExtraStepForCalls = SensitiveDataModeling::extraStepForCalls/2;
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 modeling of `getpass.getpass` as a source of passwords, which will be an additional source for `py/clear-text-logging-sensitive-data`, `py/clear-text-storage-sensitive-data`, and `py/weak-sensitive-data-hashing`.

python/ql/test/query-tests/Security/CWE-312-CleartextLogging/CleartextLogging.expected

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ edges
44
| test.py:19:16:19:29 | ControlFlowNode for get_password() | test.py:23:58:23:65 | ControlFlowNode for password |
55
| test.py:19:16:19:29 | ControlFlowNode for get_password() | test.py:27:40:27:47 | ControlFlowNode for password |
66
| test.py:19:16:19:29 | ControlFlowNode for get_password() | test.py:30:58:30:65 | ControlFlowNode for password |
7-
| test.py:65:14:68:5 | ControlFlowNode for Dict | test.py:69:11:69:31 | ControlFlowNode for Subscript |
8-
| test.py:67:21:67:37 | ControlFlowNode for Attribute | test.py:65:14:68:5 | ControlFlowNode for Dict |
7+
| test.py:44:9:44:25 | ControlFlowNode for Attribute() | test.py:45:11:45:11 | ControlFlowNode for x |
8+
| test.py:70:14:73:5 | ControlFlowNode for Dict | test.py:74:11:74:31 | ControlFlowNode for Subscript |
9+
| test.py:72:21:72:37 | ControlFlowNode for Attribute | test.py:70:14:73:5 | ControlFlowNode for Dict |
910
nodes
1011
| test.py:19:16:19:29 | ControlFlowNode for get_password() | semmle.label | ControlFlowNode for get_password() |
1112
| test.py:20:48:20:55 | ControlFlowNode for password | semmle.label | ControlFlowNode for password |
@@ -17,9 +18,11 @@ nodes
1718
| test.py:37:11:37:24 | ControlFlowNode for get_password() | semmle.label | ControlFlowNode for get_password() |
1819
| test.py:39:22:39:35 | ControlFlowNode for get_password() | semmle.label | ControlFlowNode for get_password() |
1920
| test.py:40:22:40:35 | ControlFlowNode for get_password() | semmle.label | ControlFlowNode for get_password() |
20-
| test.py:65:14:68:5 | ControlFlowNode for Dict | semmle.label | ControlFlowNode for Dict |
21-
| test.py:67:21:67:37 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
22-
| test.py:69:11:69:31 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript |
21+
| test.py:44:9:44:25 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
22+
| test.py:45:11:45:11 | ControlFlowNode for x | semmle.label | ControlFlowNode for x |
23+
| test.py:70:14:73:5 | ControlFlowNode for Dict | semmle.label | ControlFlowNode for Dict |
24+
| test.py:72:21:72:37 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
25+
| test.py:74:11:74:31 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript |
2326
subpaths
2427
#select
2528
| test.py:20:48:20:55 | ControlFlowNode for password | test.py:19:16:19:29 | ControlFlowNode for get_password() | test.py:20:48:20:55 | ControlFlowNode for password | This expression logs $@ as clear text. | test.py:19:16:19:29 | ControlFlowNode for get_password() | sensitive data (password) |
@@ -31,4 +34,5 @@ subpaths
3134
| test.py:37:11:37:24 | ControlFlowNode for get_password() | test.py:37:11:37:24 | ControlFlowNode for get_password() | test.py:37:11:37:24 | ControlFlowNode for get_password() | This expression logs $@ as clear text. | test.py:37:11:37:24 | ControlFlowNode for get_password() | sensitive data (password) |
3235
| test.py:39:22:39:35 | ControlFlowNode for get_password() | test.py:39:22:39:35 | ControlFlowNode for get_password() | test.py:39:22:39:35 | ControlFlowNode for get_password() | This expression logs $@ as clear text. | test.py:39:22:39:35 | ControlFlowNode for get_password() | sensitive data (password) |
3336
| test.py:40:22:40:35 | ControlFlowNode for get_password() | test.py:40:22:40:35 | ControlFlowNode for get_password() | test.py:40:22:40:35 | ControlFlowNode for get_password() | This expression logs $@ as clear text. | test.py:40:22:40:35 | ControlFlowNode for get_password() | sensitive data (password) |
34-
| test.py:69:11:69:31 | ControlFlowNode for Subscript | test.py:67:21:67:37 | ControlFlowNode for Attribute | test.py:69:11:69:31 | ControlFlowNode for Subscript | This expression logs $@ as clear text. | test.py:67:21:67:37 | ControlFlowNode for Attribute | sensitive data (password) |
37+
| test.py:45:11:45:11 | ControlFlowNode for x | test.py:44:9:44:25 | ControlFlowNode for Attribute() | test.py:45:11:45:11 | ControlFlowNode for x | This expression logs $@ as clear text. | test.py:44:9:44:25 | ControlFlowNode for Attribute() | sensitive data (password) |
38+
| test.py:74:11:74:31 | ControlFlowNode for Subscript | test.py:72:21:72:37 | ControlFlowNode for Attribute | test.py:74:11:74:31 | ControlFlowNode for Subscript | This expression logs $@ as clear text. | test.py:72:21:72:37 | ControlFlowNode for Attribute | sensitive data (password) |

python/ql/test/query-tests/Security/CWE-312-CleartextLogging/test.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ def print_password():
3939
sys.stdout.write(get_password()) # NOT OK
4040
sys.stderr.write(get_password()) # NOT OK
4141

42+
import getpass
43+
44+
x = getpass.getpass()
45+
print(x) # NOT OK
46+
4247

4348
def FPs(account, account_id):
4449
# we assume that any account parameter is sensitive (id/username)

0 commit comments

Comments
 (0)