Skip to content

Commit 6997618

Browse files
authored
Merge pull request #7127 from jty-team/jty/python/emailInjection
Python: CWE-079 - Add Email injection query
2 parents f7cc46b + 171239b commit 6997618

File tree

16 files changed

+1023
-1
lines changed

16 files changed

+1023
-1
lines changed
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/**
2+
* @name Reflected server-side cross-site scripting
3+
* @description Writing user input directly to a web page
4+
* allows for a cross-site scripting vulnerability.
5+
* @kind path-problem
6+
* @problem.severity error
7+
* @security-severity 2.9
8+
* @sub-severity high
9+
* @id py/reflective-xss
10+
* @tags security
11+
* external/cwe/cwe-079
12+
* external/cwe/cwe-116
13+
*/
14+
15+
// determine precision above
16+
import python
17+
import experimental.semmle.python.security.dataflow.ReflectedXSS
18+
import DataFlow::PathGraph
19+
20+
from ReflectedXssConfiguration config, DataFlow::PathNode source, DataFlow::PathNode sink
21+
where config.hasFlowPath(source, sink)
22+
select sink.getNode(), source, sink, "Cross-site scripting vulnerability due to $@.",
23+
source.getNode(), "a user-provided value"

python/ql/src/experimental/semmle/python/Concepts.qll

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -602,3 +602,77 @@ class JwtDecoding extends DataFlow::Node instanceof JwtDecoding::Range {
602602

603603
/** DEPRECATED: Alias for JwtDecoding */
604604
deprecated class JWTDecoding = JwtDecoding;
605+
606+
/** Provides classes for modeling Email APIs. */
607+
module EmailSender {
608+
/**
609+
* A data-flow node that sends an email.
610+
*
611+
* Extend this class to model new APIs. If you want to refine existing API models,
612+
* extend `EmailSender` instead.
613+
*/
614+
abstract class Range extends DataFlow::Node {
615+
/**
616+
* Gets a data flow node holding the plaintext version of the email body.
617+
*/
618+
abstract DataFlow::Node getPlainTextBody();
619+
620+
/**
621+
* Gets a data flow node holding the html version of the email body.
622+
*/
623+
abstract DataFlow::Node getHtmlBody();
624+
625+
/**
626+
* Gets a data flow node holding the recipients of the email.
627+
*/
628+
abstract DataFlow::Node getTo();
629+
630+
/**
631+
* Gets a data flow node holding the senders of the email.
632+
*/
633+
abstract DataFlow::Node getFrom();
634+
635+
/**
636+
* Gets a data flow node holding the subject of the email.
637+
*/
638+
abstract DataFlow::Node getSubject();
639+
}
640+
}
641+
642+
/**
643+
* A data-flow node that sends an email.
644+
*
645+
* Extend this class to refine existing API models. If you want to model new APIs,
646+
* extend `EmailSender::Range` instead.
647+
*/
648+
class EmailSender extends DataFlow::Node instanceof EmailSender::Range {
649+
/**
650+
* Gets a data flow node holding the plaintext version of the email body.
651+
*/
652+
DataFlow::Node getPlainTextBody() { result = super.getPlainTextBody() }
653+
654+
/**
655+
* Gets a data flow node holding the html version of the email body.
656+
*/
657+
DataFlow::Node getHtmlBody() { result = super.getHtmlBody() }
658+
659+
/**
660+
* Gets a data flow node holding the recipients of the email.
661+
*/
662+
DataFlow::Node getTo() { result = super.getTo() }
663+
664+
/**
665+
* Gets a data flow node holding the senders of the email.
666+
*/
667+
DataFlow::Node getFrom() { result = super.getFrom() }
668+
669+
/**
670+
* Gets a data flow node holding the subject of the email.
671+
*/
672+
DataFlow::Node getSubject() { result = super.getSubject() }
673+
674+
/**
675+
* Gets a data flow node that refers to the HTML body or plaintext body of the email.
676+
*/
677+
DataFlow::Node getABody() { result in [super.getPlainTextBody(), super.getHtmlBody()] }
678+
}

python/ql/src/experimental/semmle/python/Frameworks.qll

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,6 @@ private import experimental.semmle.python.libraries.Python_JWT
1515
private import experimental.semmle.python.libraries.Authlib
1616
private import experimental.semmle.python.libraries.PythonJose
1717
private import experimental.semmle.python.frameworks.CopyFile
18+
private import experimental.semmle.python.frameworks.Sendgrid
19+
private import experimental.semmle.python.libraries.FlaskMail
20+
private import experimental.semmle.python.libraries.SmtpLib

python/ql/src/experimental/semmle/python/frameworks/Django.qll

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ private import semmle.python.frameworks.Django
88
private import semmle.python.dataflow.new.DataFlow
99
private import experimental.semmle.python.Concepts
1010
private import semmle.python.ApiGraphs
11-
import semmle.python.dataflow.new.RemoteFlowSources
1211
private import semmle.python.Concepts
12+
import semmle.python.dataflow.new.RemoteFlowSources
1313

1414
private module ExperimentalPrivateDjango {
1515
private module DjangoMod {
@@ -189,5 +189,90 @@ private module ExperimentalPrivateDjango {
189189
}
190190
}
191191
}
192+
193+
module Email {
194+
/** https://docs.djangoproject.com/en/3.2/topics/email/ */
195+
private API::Node djangoMail() {
196+
result = API::moduleImport("django").getMember("core").getMember("mail")
197+
}
198+
199+
/**
200+
* Gets a call to `django.core.mail.send_mail()`.
201+
*
202+
* Given the following example:
203+
*
204+
* ```py
205+
* send_mail("Subject", "plain-text body", "from@example.com", ["to@example.com"], html_message=django.http.request.GET.get("html"))
206+
* ```
207+
*
208+
* * `this` would be `send_mail("Subject", "plain-text body", "from@example.com", ["to@example.com"], html_message=django.http.request.GET.get("html"))`.
209+
* * `getPlainTextBody()`'s result would be `"plain-text body"`.
210+
* * `getHtmlBody()`'s result would be `django.http.request.GET.get("html")`.
211+
* * `getTo()`'s result would be `["to@example.com"]`.
212+
* * `getFrom()`'s result would be `"from@example.com"`.
213+
* * `getSubject()`'s result would be `"Subject"`.
214+
*/
215+
private class DjangoSendMail extends DataFlow::CallCfgNode, EmailSender::Range {
216+
DjangoSendMail() { this = djangoMail().getMember("send_mail").getACall() }
217+
218+
override DataFlow::Node getPlainTextBody() {
219+
result in [this.getArg(1), this.getArgByName("message")]
220+
}
221+
222+
override DataFlow::Node getHtmlBody() {
223+
result in [this.getArg(8), this.getArgByName("html_message")]
224+
}
225+
226+
override DataFlow::Node getTo() {
227+
result in [this.getArg(3), this.getArgByName("recipient_list")]
228+
}
229+
230+
override DataFlow::Node getFrom() {
231+
result in [this.getArg(2), this.getArgByName("from_email")]
232+
}
233+
234+
override DataFlow::Node getSubject() {
235+
result in [this.getArg(0), this.getArgByName("subject")]
236+
}
237+
}
238+
239+
/**
240+
* Gets a call to `django.core.mail.mail_admins()` or `django.core.mail.mail_managers()`.
241+
*
242+
* Given the following example:
243+
*
244+
* ```py
245+
* mail_admins("Subject", "plain-text body", html_message=django.http.request.GET.get("html"))
246+
* ```
247+
*
248+
* * `this` would be `mail_admins("Subject", "plain-text body", html_message=django.http.request.GET.get("html"))`.
249+
* * `getPlainTextBody()`'s result would be `"plain-text body"`.
250+
* * `getHtmlBody()`'s result would be `django.http.request.GET.get("html")`.
251+
* * `getTo()`'s result would be `none`.
252+
* * `getFrom()`'s result would be `none`.
253+
* * `getSubject()`'s result would be `"Subject"`.
254+
*/
255+
private class DjangoMailInternal extends DataFlow::CallCfgNode, EmailSender::Range {
256+
DjangoMailInternal() {
257+
this = djangoMail().getMember(["mail_admins", "mail_managers"]).getACall()
258+
}
259+
260+
override DataFlow::Node getPlainTextBody() {
261+
result in [this.getArg(1), this.getArgByName("message")]
262+
}
263+
264+
override DataFlow::Node getHtmlBody() {
265+
result in [this.getArg(4), this.getArgByName("html_message")]
266+
}
267+
268+
override DataFlow::Node getTo() { none() }
269+
270+
override DataFlow::Node getFrom() { none() }
271+
272+
override DataFlow::Node getSubject() {
273+
result in [this.getArg(0), this.getArgByName("subject")]
274+
}
275+
}
276+
}
192277
}
193278
}
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
/**
2+
* Provides classes modeling security-relevant aspects of the `sendgrid` PyPI package.
3+
* See https://github.com/sendgrid/sendgrid-python.
4+
*/
5+
6+
private import python
7+
private import semmle.python.dataflow.new.DataFlow
8+
private import experimental.semmle.python.Concepts
9+
private import semmle.python.ApiGraphs
10+
11+
private module Sendgrid {
12+
/** Gets a reference to the `sendgrid` module. */
13+
private API::Node sendgrid() { result = API::moduleImport("sendgrid") }
14+
15+
/** Gets a reference to `sendgrid.helpers.mail` */
16+
private API::Node sendgridMailHelper() {
17+
result = sendgrid().getMember("helpers").getMember("mail")
18+
}
19+
20+
/** Gets a reference to `sendgrid.helpers.mail.Mail` */
21+
private API::Node sendgridMailInstance() { result = sendgridMailHelper().getMember("Mail") }
22+
23+
/** Gets a reference to a `SendGridAPIClient` instance. */
24+
private API::Node sendgridApiClient() {
25+
result = sendgrid().getMember("SendGridAPIClient").getReturn()
26+
}
27+
28+
/** Gets a reference to a `SendGridAPIClient` instance call with `send` or `post`. */
29+
private DataFlow::CallCfgNode sendgridApiSendCall() {
30+
result = sendgridApiClient().getMember("send").getACall()
31+
or
32+
result =
33+
sendgridApiClient()
34+
.getMember("client")
35+
.getMember("mail")
36+
.getMember("send")
37+
.getMember("post")
38+
.getACall()
39+
}
40+
41+
/**
42+
* Gets a reference to `sg.send()` and `sg.client.mail.send.post()`.
43+
*
44+
* Given the following example:
45+
*
46+
* ```py
47+
* from_email = Email("from@example.com")
48+
* to_email = To("to@example.com")
49+
* subject = "Sending with SendGrid is Fun"
50+
* content = Content("text/html", request.args["html_content"])
51+
*
52+
* mail = Mail(from_email, to_email, subject, content)
53+
*
54+
* sg = SendGridAPIClient(api_key='SENDGRID_API_KEY')
55+
* response = sg.client.mail.send.post(request_body=mail.get())
56+
* ```
57+
*
58+
* * `this` would be `sg.client.mail.send.post(request_body=mail.get())`.
59+
* * `getPlainTextBody()`'s result would be `none()`.
60+
* * `getHtmlBody()`'s result would be `request.args["html_content"]`.
61+
* * `getTo()`'s result would be `"to@example.com"`.
62+
* * `getFrom()`'s result would be `"from@example.com"`.
63+
* * `getSubject()`'s result would be `"Sending with SendGrid is Fun"`.
64+
*/
65+
private class SendGridMail extends DataFlow::CallCfgNode, EmailSender::Range {
66+
SendGridMail() { this = sendgridApiSendCall() }
67+
68+
private DataFlow::CallCfgNode getMailCall() {
69+
exists(DataFlow::Node n |
70+
n in [this.getArg(0), this.getArgByName("request_body")] and
71+
result = [n, n.(DataFlow::MethodCallNode).getObject()].getALocalSource()
72+
)
73+
}
74+
75+
private DataFlow::Node sendgridContent(DataFlow::CallCfgNode contentCall, string mime) {
76+
mime in ["text/plain", "text/html", "text/x-amp-html"] and
77+
exists(StrConst mimeNode |
78+
mimeNode.getText() = mime and
79+
DataFlow::exprNode(mimeNode).(DataFlow::LocalSourceNode).flowsTo(contentCall.getArg(0)) and
80+
result = contentCall.getArg(1)
81+
)
82+
}
83+
84+
private DataFlow::Node sendgridWrite(string attributeName) {
85+
attributeName in ["plain_text_content", "html_content", "from_email", "subject"] and
86+
exists(DataFlow::AttrWrite attrWrite |
87+
attrWrite.getObject().getALocalSource() = this.getMailCall() and
88+
attrWrite.getAttributeName() = attributeName and
89+
result = attrWrite.getValue()
90+
)
91+
}
92+
93+
override DataFlow::Node getPlainTextBody() {
94+
result in [
95+
this.getMailCall().getArg(3), this.getMailCall().getArgByName("plain_text_content")
96+
]
97+
or
98+
result in [
99+
this.sendgridContent([
100+
this.getMailCall().getArg(3), this.getMailCall().getArgByName("plain_text_content")
101+
].getALocalSource(), "text/plain"),
102+
this.sendgridContent(sendgridMailInstance().getMember("add_content").getACall(),
103+
"text/plain")
104+
]
105+
or
106+
result = this.sendgridWrite("plain_text_content")
107+
}
108+
109+
override DataFlow::Node getHtmlBody() {
110+
result in [this.getMailCall().getArg(4), this.getMailCall().getArgByName("html_content")]
111+
or
112+
result = this.getMailCall().getAMethodCall("set_html").getArg(0)
113+
or
114+
result =
115+
this.sendgridContent([
116+
this.getMailCall().getArg(4), this.getMailCall().getArgByName("html_content")
117+
].getALocalSource(), ["text/html", "text/x-amp-html"])
118+
or
119+
result = this.sendgridWrite("html_content")
120+
or
121+
exists(KeyValuePair content, Dict generalDict, KeyValuePair typePair, KeyValuePair valuePair |
122+
content.getKey().(StrConst).getText() = "content" and
123+
content.getValue().(List).getAnElt() = generalDict and
124+
// declare KeyValuePairs keys and values
125+
typePair.getKey().(StrConst).getText() = "type" and
126+
typePair.getValue().(StrConst).getText() = ["text/html", "text/x-amp-html"] and
127+
valuePair.getKey().(StrConst).getText() = "value" and
128+
result.asExpr() = valuePair.getValue() and
129+
// correlate generalDict with previously set KeyValuePairs
130+
generalDict.getAnItem() in [typePair, valuePair] and
131+
[this.getArg(0), this.getArgByName("request_body")].getALocalSource().asExpr() =
132+
any(Dict d | d.getAnItem() = content)
133+
)
134+
or
135+
exists(KeyValuePair footer, Dict generalDict, KeyValuePair enablePair, KeyValuePair htmlPair |
136+
footer.getKey().(StrConst).getText() = ["footer", "subscription_tracking"] and
137+
footer.getValue() = generalDict and
138+
// check footer is enabled
139+
enablePair.getKey().(StrConst).getText() = "enable" and
140+
exists(enablePair.getValue().(True)) and
141+
// get html content
142+
htmlPair.getKey().(StrConst).getText() = "html" and
143+
result.asExpr() = htmlPair.getValue() and
144+
// correlate generalDict with previously set KeyValuePairs
145+
generalDict.getAnItem() in [enablePair, htmlPair] and
146+
exists(KeyValuePair k |
147+
k.getKey() =
148+
[this.getArg(0), this.getArgByName("request_body")]
149+
.getALocalSource()
150+
.asExpr()
151+
.(Dict)
152+
.getAKey() and
153+
k.getValue() = any(Dict d | d.getAKey() = footer.getKey())
154+
)
155+
)
156+
}
157+
158+
override DataFlow::Node getTo() {
159+
result in [this.getMailCall().getArg(1), this.getMailCall().getArgByName("to_emails")]
160+
or
161+
result = this.getMailCall().getAMethodCall("To").getArg(0)
162+
or
163+
result =
164+
this.getMailCall()
165+
.getAMethodCall(["to", "add_to", "cc", "add_cc", "bcc", "add_bcc"])
166+
.getArg(0)
167+
}
168+
169+
override DataFlow::Node getFrom() {
170+
result in [this.getMailCall().getArg(0), this.getMailCall().getArgByName("from_email")]
171+
or
172+
result = this.getMailCall().getAMethodCall("Email").getArg(0)
173+
or
174+
result = this.getMailCall().getAMethodCall(["from_email", "set_from"]).getArg(0)
175+
or
176+
result = this.sendgridWrite("from_email")
177+
}
178+
179+
override DataFlow::Node getSubject() {
180+
result in [this.getMailCall().getArg(2), this.getMailCall().getArgByName("subject")]
181+
or
182+
result = this.getMailCall().getAMethodCall(["subject", "set_subject"]).getArg(0)
183+
or
184+
result = this.sendgridWrite("subject")
185+
}
186+
}
187+
}

0 commit comments

Comments
 (0)