Skip to content

Commit 9b73bbf

Browse files
committed
Python: Add keyword argument support
and a fair bit of refactoring
1 parent d6d13f8 commit 9b73bbf

File tree

3 files changed

+72
-63
lines changed

3 files changed

+72
-63
lines changed

python/ql/lib/semmle/python/frameworks/PyMongo.qll

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -109,22 +109,34 @@ private module PyMongo {
109109
*
110110
* `mongo.db.user.find({'name': safe_search})` would be a collection method call.
111111
*/
112-
private class MongoCollectionCall extends DataFlow::CallCfgNode, NoSqlExecution::Range {
112+
private class MongoCollectionCall extends API::CallNode, NoSqlExecution::Range {
113113
MongoCollectionCall() {
114114
this = mongoCollection().getMember(mongoCollectionMethodName()).getACall()
115115
}
116116

117-
override DataFlow::Node getQuery() { result = this.getArg(0) }
117+
/** Gets the argument that specifies the NoSQL query to be executed, as an API::node */
118+
pragma[inline]
119+
API::Node getQueryAsApiNode() {
120+
// 'filter' is allowed keyword in pymongo, see https://pymongo.readthedocs.io/en/stable/api/pymongo/collection.html#pymongo.collection.Collection.find
121+
result = this.getParameter(0, "filter")
122+
}
123+
124+
override DataFlow::Node getQuery() { result = this.getQueryAsApiNode().asSink() }
118125

119126
override predicate interpretsDict() { any() }
120127

121128
override predicate vulnerableToStrings() { none() }
122129
}
123130

131+
/**
132+
* See https://pymongo.readthedocs.io/en/stable/api/pymongo/collection.html#pymongo.collection.Collection.aggregate
133+
*/
124134
private class MongoCollectionAggregation extends API::CallNode, NoSqlExecution::Range {
125135
MongoCollectionAggregation() { this = mongoCollection().getMember("aggregate").getACall() }
126136

127-
override DataFlow::Node getQuery() { result = this.getParameter(0).getASubscript().asSink() }
137+
override DataFlow::Node getQuery() {
138+
result = this.getParameter(0, "pipeline").getASubscript().asSink()
139+
}
128140

129141
override predicate interpretsDict() { any() }
130142

@@ -157,8 +169,7 @@ private module PyMongo {
157169

158170
WhereQueryOperator() {
159171
exists(API::Node dictionary |
160-
dictionary =
161-
mongoCollection().getMember(mongoCollectionMethodName()).getACall().getParameter(0) and
172+
dictionary = any(MongoCollectionCall c).getQueryAsApiNode() and
162173
query = dictionary.getSubscript("$where").asSink() and
163174
this = dictionary.getAValueReachingSink()
164175
)
@@ -184,12 +195,7 @@ private module PyMongo {
184195
FunctionQueryOperator() {
185196
exists(API::Node dictionary |
186197
dictionary =
187-
mongoCollection()
188-
.getMember(mongoCollectionMethodName())
189-
.getACall()
190-
.getParameter(0)
191-
.getASubscript*()
192-
.getSubscript("$function") and
198+
any(MongoCollectionCall c).getQueryAsApiNode().getASubscript*().getSubscript("$function") and
193199
query = dictionary.getSubscript("body").asSink() and
194200
this = dictionary.getAValueReachingSink()
195201
)
@@ -285,12 +291,7 @@ private module PyMongo {
285291
private class EqualityOperator extends DataFlow::Node, NoSqlSanitizer::Range {
286292
EqualityOperator() {
287293
this =
288-
mongoCollection()
289-
.getMember(mongoCollectionMethodName())
290-
.getParameter(0)
291-
.getASubscript*()
292-
.getSubscript("$eq")
293-
.asSink()
294+
any(MongoCollectionCall c).getQueryAsApiNode().getASubscript*().getSubscript("$eq").asSink()
294295
}
295296

296297
override DataFlow::Node getAnInput() { result = this }

python/ql/test/query-tests/Security/CWE-943-NoSqlInjection/NoSqlInjection.expected

Lines changed: 52 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,34 @@
11
edges
22
| PoC/server.py:1:26:1:32 | ControlFlowNode for ImportMember | PoC/server.py:1:26:1:32 | GSSA Variable request |
33
| PoC/server.py:1:26:1:32 | GSSA Variable request | PoC/server.py:26:21:26:27 | ControlFlowNode for request |
4-
| PoC/server.py:1:26:1:32 | GSSA Variable request | PoC/server.py:42:14:42:20 | ControlFlowNode for request |
5-
| PoC/server.py:1:26:1:32 | GSSA Variable request | PoC/server.py:51:14:51:20 | ControlFlowNode for request |
6-
| PoC/server.py:1:26:1:32 | GSSA Variable request | PoC/server.py:76:14:76:20 | ControlFlowNode for request |
7-
| PoC/server.py:1:26:1:32 | GSSA Variable request | PoC/server.py:96:14:96:20 | ControlFlowNode for request |
4+
| PoC/server.py:1:26:1:32 | GSSA Variable request | PoC/server.py:43:14:43:20 | ControlFlowNode for request |
5+
| PoC/server.py:1:26:1:32 | GSSA Variable request | PoC/server.py:52:14:52:20 | ControlFlowNode for request |
6+
| PoC/server.py:1:26:1:32 | GSSA Variable request | PoC/server.py:77:14:77:20 | ControlFlowNode for request |
7+
| PoC/server.py:1:26:1:32 | GSSA Variable request | PoC/server.py:98:14:98:20 | ControlFlowNode for request |
88
| PoC/server.py:26:5:26:17 | SSA variable author_string | PoC/server.py:27:25:27:37 | ControlFlowNode for author_string |
99
| PoC/server.py:26:21:26:27 | ControlFlowNode for request | PoC/server.py:26:5:26:17 | SSA variable author_string |
1010
| PoC/server.py:27:5:27:10 | SSA variable author | PoC/server.py:30:27:30:44 | ControlFlowNode for Dict |
11+
| PoC/server.py:27:5:27:10 | SSA variable author | PoC/server.py:31:34:31:51 | ControlFlowNode for Dict |
1112
| PoC/server.py:27:14:27:38 | ControlFlowNode for Attribute() | PoC/server.py:27:5:27:10 | SSA variable author |
1213
| PoC/server.py:27:25:27:37 | ControlFlowNode for author_string | PoC/server.py:27:14:27:38 | ControlFlowNode for Attribute() |
13-
| PoC/server.py:42:5:42:10 | SSA variable author | PoC/server.py:46:38:46:67 | ControlFlowNode for BinaryExpr |
14-
| PoC/server.py:42:14:42:20 | ControlFlowNode for request | PoC/server.py:42:5:42:10 | SSA variable author |
15-
| PoC/server.py:46:38:46:67 | ControlFlowNode for BinaryExpr | PoC/server.py:46:27:46:68 | ControlFlowNode for Dict |
16-
| PoC/server.py:51:5:51:10 | SSA variable author | PoC/server.py:53:17:53:70 | ControlFlowNode for BinaryExpr |
17-
| PoC/server.py:51:14:51:20 | ControlFlowNode for request | PoC/server.py:51:5:51:10 | SSA variable author |
18-
| PoC/server.py:52:5:52:10 | SSA variable search | PoC/server.py:60:27:60:58 | ControlFlowNode for Dict |
19-
| PoC/server.py:52:14:56:5 | ControlFlowNode for Dict | PoC/server.py:52:5:52:10 | SSA variable search |
20-
| PoC/server.py:53:17:53:70 | ControlFlowNode for BinaryExpr | PoC/server.py:52:14:56:5 | ControlFlowNode for Dict |
21-
| PoC/server.py:76:5:76:10 | SSA variable author | PoC/server.py:79:23:79:101 | ControlFlowNode for BinaryExpr |
22-
| PoC/server.py:76:14:76:20 | ControlFlowNode for request | PoC/server.py:76:5:76:10 | SSA variable author |
23-
| PoC/server.py:77:5:77:15 | SSA variable accumulator | PoC/server.py:83:5:83:9 | SSA variable group |
24-
| PoC/server.py:77:19:82:5 | ControlFlowNode for Dict | PoC/server.py:77:5:77:15 | SSA variable accumulator |
25-
| PoC/server.py:79:23:79:101 | ControlFlowNode for BinaryExpr | PoC/server.py:77:19:82:5 | ControlFlowNode for Dict |
26-
| PoC/server.py:83:5:83:9 | SSA variable group | PoC/server.py:90:29:90:47 | ControlFlowNode for Dict |
27-
| PoC/server.py:96:5:96:10 | SSA variable author | PoC/server.py:97:5:97:10 | SSA variable mapper |
28-
| PoC/server.py:96:14:96:20 | ControlFlowNode for request | PoC/server.py:96:5:96:10 | SSA variable author |
29-
| PoC/server.py:97:5:97:10 | SSA variable mapper | PoC/server.py:100:9:100:14 | ControlFlowNode for mapper |
14+
| PoC/server.py:43:5:43:10 | SSA variable author | PoC/server.py:47:38:47:67 | ControlFlowNode for BinaryExpr |
15+
| PoC/server.py:43:14:43:20 | ControlFlowNode for request | PoC/server.py:43:5:43:10 | SSA variable author |
16+
| PoC/server.py:47:38:47:67 | ControlFlowNode for BinaryExpr | PoC/server.py:47:27:47:68 | ControlFlowNode for Dict |
17+
| PoC/server.py:52:5:52:10 | SSA variable author | PoC/server.py:54:17:54:70 | ControlFlowNode for BinaryExpr |
18+
| PoC/server.py:52:14:52:20 | ControlFlowNode for request | PoC/server.py:52:5:52:10 | SSA variable author |
19+
| PoC/server.py:53:5:53:10 | SSA variable search | PoC/server.py:61:27:61:58 | ControlFlowNode for Dict |
20+
| PoC/server.py:53:14:57:5 | ControlFlowNode for Dict | PoC/server.py:53:5:53:10 | SSA variable search |
21+
| PoC/server.py:54:17:54:70 | ControlFlowNode for BinaryExpr | PoC/server.py:53:14:57:5 | ControlFlowNode for Dict |
22+
| PoC/server.py:77:5:77:10 | SSA variable author | PoC/server.py:80:23:80:101 | ControlFlowNode for BinaryExpr |
23+
| PoC/server.py:77:14:77:20 | ControlFlowNode for request | PoC/server.py:77:5:77:10 | SSA variable author |
24+
| PoC/server.py:78:5:78:15 | SSA variable accumulator | PoC/server.py:84:5:84:9 | SSA variable group |
25+
| PoC/server.py:78:19:83:5 | ControlFlowNode for Dict | PoC/server.py:78:5:78:15 | SSA variable accumulator |
26+
| PoC/server.py:80:23:80:101 | ControlFlowNode for BinaryExpr | PoC/server.py:78:19:83:5 | ControlFlowNode for Dict |
27+
| PoC/server.py:84:5:84:9 | SSA variable group | PoC/server.py:91:29:91:47 | ControlFlowNode for Dict |
28+
| PoC/server.py:84:5:84:9 | SSA variable group | PoC/server.py:92:38:92:56 | ControlFlowNode for Dict |
29+
| PoC/server.py:98:5:98:10 | SSA variable author | PoC/server.py:99:5:99:10 | SSA variable mapper |
30+
| PoC/server.py:98:14:98:20 | ControlFlowNode for request | PoC/server.py:98:5:98:10 | SSA variable author |
31+
| PoC/server.py:99:5:99:10 | SSA variable mapper | PoC/server.py:102:9:102:14 | ControlFlowNode for mapper |
3032
| flask_mongoengine_bad.py:1:26:1:32 | ControlFlowNode for ImportMember | flask_mongoengine_bad.py:1:26:1:32 | GSSA Variable request |
3133
| flask_mongoengine_bad.py:1:26:1:32 | GSSA Variable request | flask_mongoengine_bad.py:19:21:19:27 | ControlFlowNode for request |
3234
| flask_mongoengine_bad.py:1:26:1:32 | GSSA Variable request | flask_mongoengine_bad.py:26:21:26:27 | ControlFlowNode for request |
@@ -123,27 +125,29 @@ nodes
123125
| PoC/server.py:27:14:27:38 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
124126
| PoC/server.py:27:25:27:37 | ControlFlowNode for author_string | semmle.label | ControlFlowNode for author_string |
125127
| PoC/server.py:30:27:30:44 | ControlFlowNode for Dict | semmle.label | ControlFlowNode for Dict |
126-
| PoC/server.py:42:5:42:10 | SSA variable author | semmle.label | SSA variable author |
127-
| PoC/server.py:42:14:42:20 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
128-
| PoC/server.py:46:27:46:68 | ControlFlowNode for Dict | semmle.label | ControlFlowNode for Dict |
129-
| PoC/server.py:46:38:46:67 | ControlFlowNode for BinaryExpr | semmle.label | ControlFlowNode for BinaryExpr |
130-
| PoC/server.py:51:5:51:10 | SSA variable author | semmle.label | SSA variable author |
131-
| PoC/server.py:51:14:51:20 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
132-
| PoC/server.py:52:5:52:10 | SSA variable search | semmle.label | SSA variable search |
133-
| PoC/server.py:52:14:56:5 | ControlFlowNode for Dict | semmle.label | ControlFlowNode for Dict |
134-
| PoC/server.py:53:17:53:70 | ControlFlowNode for BinaryExpr | semmle.label | ControlFlowNode for BinaryExpr |
135-
| PoC/server.py:60:27:60:58 | ControlFlowNode for Dict | semmle.label | ControlFlowNode for Dict |
136-
| PoC/server.py:76:5:76:10 | SSA variable author | semmle.label | SSA variable author |
137-
| PoC/server.py:76:14:76:20 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
138-
| PoC/server.py:77:5:77:15 | SSA variable accumulator | semmle.label | SSA variable accumulator |
139-
| PoC/server.py:77:19:82:5 | ControlFlowNode for Dict | semmle.label | ControlFlowNode for Dict |
140-
| PoC/server.py:79:23:79:101 | ControlFlowNode for BinaryExpr | semmle.label | ControlFlowNode for BinaryExpr |
141-
| PoC/server.py:83:5:83:9 | SSA variable group | semmle.label | SSA variable group |
142-
| PoC/server.py:90:29:90:47 | ControlFlowNode for Dict | semmle.label | ControlFlowNode for Dict |
143-
| PoC/server.py:96:5:96:10 | SSA variable author | semmle.label | SSA variable author |
144-
| PoC/server.py:96:14:96:20 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
145-
| PoC/server.py:97:5:97:10 | SSA variable mapper | semmle.label | SSA variable mapper |
146-
| PoC/server.py:100:9:100:14 | ControlFlowNode for mapper | semmle.label | ControlFlowNode for mapper |
128+
| PoC/server.py:31:34:31:51 | ControlFlowNode for Dict | semmle.label | ControlFlowNode for Dict |
129+
| PoC/server.py:43:5:43:10 | SSA variable author | semmle.label | SSA variable author |
130+
| PoC/server.py:43:14:43:20 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
131+
| PoC/server.py:47:27:47:68 | ControlFlowNode for Dict | semmle.label | ControlFlowNode for Dict |
132+
| PoC/server.py:47:38:47:67 | ControlFlowNode for BinaryExpr | semmle.label | ControlFlowNode for BinaryExpr |
133+
| PoC/server.py:52:5:52:10 | SSA variable author | semmle.label | SSA variable author |
134+
| PoC/server.py:52:14:52:20 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
135+
| PoC/server.py:53:5:53:10 | SSA variable search | semmle.label | SSA variable search |
136+
| PoC/server.py:53:14:57:5 | ControlFlowNode for Dict | semmle.label | ControlFlowNode for Dict |
137+
| PoC/server.py:54:17:54:70 | ControlFlowNode for BinaryExpr | semmle.label | ControlFlowNode for BinaryExpr |
138+
| PoC/server.py:61:27:61:58 | ControlFlowNode for Dict | semmle.label | ControlFlowNode for Dict |
139+
| PoC/server.py:77:5:77:10 | SSA variable author | semmle.label | SSA variable author |
140+
| PoC/server.py:77:14:77:20 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
141+
| PoC/server.py:78:5:78:15 | SSA variable accumulator | semmle.label | SSA variable accumulator |
142+
| PoC/server.py:78:19:83:5 | ControlFlowNode for Dict | semmle.label | ControlFlowNode for Dict |
143+
| PoC/server.py:80:23:80:101 | ControlFlowNode for BinaryExpr | semmle.label | ControlFlowNode for BinaryExpr |
144+
| PoC/server.py:84:5:84:9 | SSA variable group | semmle.label | SSA variable group |
145+
| PoC/server.py:91:29:91:47 | ControlFlowNode for Dict | semmle.label | ControlFlowNode for Dict |
146+
| PoC/server.py:92:38:92:56 | ControlFlowNode for Dict | semmle.label | ControlFlowNode for Dict |
147+
| PoC/server.py:98:5:98:10 | SSA variable author | semmle.label | SSA variable author |
148+
| PoC/server.py:98:14:98:20 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
149+
| PoC/server.py:99:5:99:10 | SSA variable mapper | semmle.label | SSA variable mapper |
150+
| PoC/server.py:102:9:102:14 | ControlFlowNode for mapper | semmle.label | ControlFlowNode for mapper |
147151
| flask_mongoengine_bad.py:1:26:1:32 | ControlFlowNode for ImportMember | semmle.label | ControlFlowNode for ImportMember |
148152
| flask_mongoengine_bad.py:1:26:1:32 | GSSA Variable request | semmle.label | GSSA Variable request |
149153
| flask_mongoengine_bad.py:19:5:19:17 | SSA variable unsafe_search | semmle.label | SSA variable unsafe_search |
@@ -238,10 +242,12 @@ nodes
238242
subpaths
239243
#select
240244
| PoC/server.py:30:27:30:44 | ControlFlowNode for Dict | PoC/server.py:1:26:1:32 | ControlFlowNode for ImportMember | PoC/server.py:30:27:30:44 | ControlFlowNode for Dict | This NoSQL query contains an unsanitized $@. | PoC/server.py:1:26:1:32 | ControlFlowNode for ImportMember | user-provided value |
241-
| PoC/server.py:46:27:46:68 | ControlFlowNode for Dict | PoC/server.py:1:26:1:32 | ControlFlowNode for ImportMember | PoC/server.py:46:27:46:68 | ControlFlowNode for Dict | This NoSQL query contains an unsanitized $@. | PoC/server.py:1:26:1:32 | ControlFlowNode for ImportMember | user-provided value |
242-
| PoC/server.py:60:27:60:58 | ControlFlowNode for Dict | PoC/server.py:1:26:1:32 | ControlFlowNode for ImportMember | PoC/server.py:60:27:60:58 | ControlFlowNode for Dict | This NoSQL query contains an unsanitized $@. | PoC/server.py:1:26:1:32 | ControlFlowNode for ImportMember | user-provided value |
243-
| PoC/server.py:90:29:90:47 | ControlFlowNode for Dict | PoC/server.py:1:26:1:32 | ControlFlowNode for ImportMember | PoC/server.py:90:29:90:47 | ControlFlowNode for Dict | This NoSQL query contains an unsanitized $@. | PoC/server.py:1:26:1:32 | ControlFlowNode for ImportMember | user-provided value |
244-
| PoC/server.py:100:9:100:14 | ControlFlowNode for mapper | PoC/server.py:1:26:1:32 | ControlFlowNode for ImportMember | PoC/server.py:100:9:100:14 | ControlFlowNode for mapper | This NoSQL query contains an unsanitized $@. | PoC/server.py:1:26:1:32 | ControlFlowNode for ImportMember | user-provided value |
245+
| PoC/server.py:31:34:31:51 | ControlFlowNode for Dict | PoC/server.py:1:26:1:32 | ControlFlowNode for ImportMember | PoC/server.py:31:34:31:51 | ControlFlowNode for Dict | This NoSQL query contains an unsanitized $@. | PoC/server.py:1:26:1:32 | ControlFlowNode for ImportMember | user-provided value |
246+
| PoC/server.py:47:27:47:68 | ControlFlowNode for Dict | PoC/server.py:1:26:1:32 | ControlFlowNode for ImportMember | PoC/server.py:47:27:47:68 | ControlFlowNode for Dict | This NoSQL query contains an unsanitized $@. | PoC/server.py:1:26:1:32 | ControlFlowNode for ImportMember | user-provided value |
247+
| PoC/server.py:61:27:61:58 | ControlFlowNode for Dict | PoC/server.py:1:26:1:32 | ControlFlowNode for ImportMember | PoC/server.py:61:27:61:58 | ControlFlowNode for Dict | This NoSQL query contains an unsanitized $@. | PoC/server.py:1:26:1:32 | ControlFlowNode for ImportMember | user-provided value |
248+
| PoC/server.py:91:29:91:47 | ControlFlowNode for Dict | PoC/server.py:1:26:1:32 | ControlFlowNode for ImportMember | PoC/server.py:91:29:91:47 | ControlFlowNode for Dict | This NoSQL query contains an unsanitized $@. | PoC/server.py:1:26:1:32 | ControlFlowNode for ImportMember | user-provided value |
249+
| PoC/server.py:92:38:92:56 | ControlFlowNode for Dict | PoC/server.py:1:26:1:32 | ControlFlowNode for ImportMember | PoC/server.py:92:38:92:56 | ControlFlowNode for Dict | This NoSQL query contains an unsanitized $@. | PoC/server.py:1:26:1:32 | ControlFlowNode for ImportMember | user-provided value |
250+
| PoC/server.py:102:9:102:14 | ControlFlowNode for mapper | PoC/server.py:1:26:1:32 | ControlFlowNode for ImportMember | PoC/server.py:102:9:102:14 | ControlFlowNode for mapper | This NoSQL query contains an unsanitized $@. | PoC/server.py:1:26:1:32 | ControlFlowNode for ImportMember | user-provided value |
245251
| flask_mongoengine_bad.py:22:34:22:44 | ControlFlowNode for json_search | flask_mongoengine_bad.py:1:26:1:32 | ControlFlowNode for ImportMember | flask_mongoengine_bad.py:22:34:22:44 | ControlFlowNode for json_search | This NoSQL query contains an unsanitized $@. | flask_mongoengine_bad.py:1:26:1:32 | ControlFlowNode for ImportMember | user-provided value |
246252
| flask_mongoengine_bad.py:30:39:30:59 | ControlFlowNode for Dict | flask_mongoengine_bad.py:1:26:1:32 | ControlFlowNode for ImportMember | flask_mongoengine_bad.py:30:39:30:59 | ControlFlowNode for Dict | This NoSQL query contains an unsanitized $@. | flask_mongoengine_bad.py:1:26:1:32 | ControlFlowNode for ImportMember | user-provided value |
247253
| flask_pymongo_bad.py:14:31:14:51 | ControlFlowNode for Dict | flask_pymongo_bad.py:1:26:1:32 | ControlFlowNode for ImportMember | flask_pymongo_bad.py:14:31:14:51 | ControlFlowNode for Dict | This NoSQL query contains an unsanitized $@. | flask_pymongo_bad.py:1:26:1:32 | ControlFlowNode for ImportMember | user-provided value |

python/ql/test/query-tests/Security/CWE-943-NoSqlInjection/PoC/server.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ def as_dict():
2828
# Use {"$ne": 1} as author
2929
# Found by http://127.0.0.1:5000/dict?author={%22$ne%22:1}
3030
post = posts.find_one({'author': author}) # $ result=BAD
31+
post = posts.find_one(filter={'author': author}) # $ result=BAD
3132
return show_post(post, author)
3233

3334
@app.route('/dictHardened', methods=['GET'])
@@ -88,6 +89,7 @@ def by_group():
8889
# making the query `this.author === "" | "a" === "a"`
8990
# Found by http://127.0.0.1:5000/byGroup?author=%22%20|%20%22a%22%20===%20%22a
9091
post = posts.aggregate([{ "$group": group }]).next() # $ result=BAD
92+
post = posts.aggregate(pipeline=[{ "$group": group }]).next() # $ result=BAD
9193
return show_post(post, author)
9294

9395
# works with pymongo 3.9, `map_reduce` is removed in pymongo 4.0

0 commit comments

Comments
 (0)