Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
24d4415
Create EmailClients.qll
mrthankyou Jun 21, 2021
c3eba25
Add query tests
mrthankyou Jun 21, 2021
20f321e
Remove accidental slash
mrthankyou Jun 22, 2021
48cd506
Change EmailSender structure
jorgectf Jun 22, 2021
4d890dd
Polish flask_mail tests and code
jorgectf Jun 22, 2021
7956b97
Unit tests move and temporary ql
jorgectf Jun 22, 2021
4c9ecf0
Delete testing class-variable
jorgectf Jun 22, 2021
ae84df8
Extend ReflectedXSS query
jorgectf Jun 23, 2021
c323fbb
Cover Flask-SendMail (Flask-Mail copy)
jorgectf Jun 23, 2021
eac5eba
Move tests and qlref to test/
jorgectf Jun 23, 2021
355bb5c
Format Flask.qll
jorgectf Jun 23, 2021
8ae8648
Format ReflectedXSS.qll
jorgectf Jun 23, 2021
bf1eb72
Cover `django.core.mail`
jorgectf Jun 23, 2021
9563faf
Add Sendgrid modeling
jorgectf Jun 23, 2021
5e8f995
Extend Sendgrid setters
jorgectf Jun 23, 2021
70d6511
Optimize Flask.qll
jorgectf Jun 23, 2021
7b9cbaf
Move flask_mail to libraries/
jorgectf Jun 23, 2021
e0013fc
Fix Concepts.qll dependencies
jorgectf Jun 23, 2021
b5ee7c3
Specify `plain-text body`
jorgectf Jun 29, 2021
19a6267
Almost fix `getFlaskMailArgument(...)`
jorgectf Jun 29, 2021
4c2a422
Merge remote-tracking branch 'origin/main' into jty/python/emailInjec…
jorgectf Oct 28, 2021
c9634f3
Fix `getFlaskMailArgument()`
jorgectf Oct 28, 2021
bf68495
Polish `FlaskMail` qldocs
jorgectf Oct 28, 2021
e8e0f0f
Add temporary `.expected`
jorgectf Oct 28, 2021
dbf5b24
Polish `Sendgrid.qll` qldoc
jorgectf Oct 28, 2021
ba3ea70
Add `Sendgrid` dict data html body modeling
jorgectf Oct 28, 2021
4afcd9d
[mrthankyou] smtplib partial modeling.
jorgectf Oct 28, 2021
3a4e3d5
Remove comments from Python example tests
mrthankyou Oct 30, 2021
d9e4df7
Remove unnecessary comment
mrthankyou Oct 30, 2021
3264e7b
Merge branch 'jty/python/emailInjection' of https://github.com/jty-te…
jorgectf Oct 30, 2021
356b071
Cover `MimeType.amp` as a vulnerable mimetype
jorgectf Oct 30, 2021
d316974
Add `HtmlContent` additional taint step
jorgectf Nov 8, 2021
f4a73fc
Add RFS to `sendgrid` test
jorgectf Nov 8, 2021
5774ce2
Improve `django` test
jorgectf Nov 8, 2021
c0a0c5d
Cover `footer` and `subscription_tracking` html injection
jorgectf Nov 8, 2021
5b46b90
Fix additional taint step variables
jorgectf Nov 9, 2021
1393b5b
Add `django` qldocs
jorgectf Nov 13, 2021
33b6f6f
Polish `FlaskMail` qldocs
jorgectf Nov 13, 2021
63eadc8
Polish `sendgrid` modeling
jorgectf Nov 13, 2021
dbdf102
Make `EmailSender` an extendable API
jorgectf Nov 13, 2021
e7cb762
Add `SmtpLib` to `Frameworks.qll` and minimal fixes
jorgectf Nov 13, 2021
129a81a
Cover `smtplib`
jorgectf Nov 13, 2021
1be823d
Apply suggestions from code review
jorgectf Nov 15, 2021
a905205
Merge branch 'github:main' into jty/python/emailInjection
jorgectf Nov 15, 2021
5bd8de1
Fix `smtplib`'s `_subparts` taint config issue
jorgectf Nov 15, 2021
f350253
Merge branch 'jty/python/emailInjection' of https://github.com/jty-te…
jorgectf Nov 15, 2021
018aa11
Make `EmailSender` an instance of `EmailSender::Range`
jorgectf Nov 16, 2021
1b9567a
Avoid using `Str_` internal class
jorgectf Dec 19, 2021
ede5d41
Update `.expected`
jorgectf Dec 19, 2021
2f2cf2c
Use `StrConst.getText()` instead of `Str_.getS()`
jorgectf Feb 26, 2022
67b672a
Merge remote-tracking branch 'origin/main' into jty/python/emailInjec…
jorgectf Feb 26, 2022
3159d8e
Correlate `SendGridMail` declaration with its predicates
jorgectf Mar 3, 2022
6722671
Refactor `sendgridApiClient` and `sendgridApiSendCall`
jorgectf Mar 8, 2022
6b04344
Refactor `sendgridContent` and `sendgridWrite`
jorgectf Mar 8, 2022
930fbf7
Move `getFlaskMailArgument` inside `FlaskMail` and refactor
jorgectf Mar 8, 2022
bbba1a2
Explicitly call `this` in `SendGridMail`
jorgectf Mar 8, 2022
3f43e6e
Fix `FlaskMail`'s `getTo`
jorgectf Mar 8, 2022
c155ac6
Add `HtmlEscaping` sanitizer
jorgectf Mar 9, 2022
b5734ed
Merge branch 'main' into jty/python/emailInjection
mrthankyou Apr 20, 2022
76c27c6
Merge branch 'main' into jty/python/emailInjection
mrthankyou May 26, 2022
e577a0e
Update `.expected` tests
jorgectf May 26, 2022
897d5c9
Apply suggestions from code review
jorgectf Jun 1, 2022
171239b
Format `FlaskMail.qll` and `Sendgrid.qll`
jorgectf Jun 3, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions python/ql/src/experimental/Security/CWE-079/ReflectedXSS.ql
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* @name Reflected server-side cross-site scripting
* @description Writing user input directly to a web page
* allows for a cross-site scripting vulnerability.
* @kind path-problem
* @problem.severity error
* @security-severity 2.9
* @sub-severity high
* @id py/reflective-xss
* @tags security
* external/cwe/cwe-079
* external/cwe/cwe-116
*/

// determine precision above
import python
import experimental.semmle.python.security.dataflow.ReflectedXSS
import DataFlow::PathGraph

from ReflectedXssConfiguration config, DataFlow::PathNode source, DataFlow::PathNode sink
where config.hasFlowPath(source, sink)
select sink.getNode(), source, sink, "Cross-site scripting vulnerability due to $@.",
source.getNode(), "a user-provided value"
74 changes: 74 additions & 0 deletions python/ql/src/experimental/semmle/python/Concepts.qll
Original file line number Diff line number Diff line change
Expand Up @@ -569,3 +569,77 @@ class JwtDecoding extends DataFlow::Node instanceof JwtDecoding::Range {

/** DEPRECATED: Alias for JwtDecoding */
deprecated class JWTDecoding = JwtDecoding;

/** Provides classes for modeling Email APIs. */
module EmailSender {
/**
* A data-flow node that sends an email.
*
* Extend this class to model new APIs. If you want to refine existing API models,
* extend `EmailSender` instead.
*/
abstract class Range extends DataFlow::Node {
/**
* Gets a data flow node holding the plaintext version of the email body.
*/
abstract DataFlow::Node getPlainTextBody();

/**
* Gets a data flow node holding the html version of the email body.
*/
abstract DataFlow::Node getHtmlBody();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to be the only field used in the query. Is there a reason to have and to model the rest?

Copy link
Contributor

@jorgectf jorgectf Mar 2, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just in case they are needed for a different query in the future :)


/**
* Gets a data flow node holding the recipients of the email.
*/
abstract DataFlow::Node getTo();

/**
* Gets a data flow node holding the senders of the email.
*/
abstract DataFlow::Node getFrom();

/**
* Gets a data flow node holding the subject of the email.
*/
abstract DataFlow::Node getSubject();
}
}

/**
* A data-flow node that sends an email.
*
* Extend this class to refine existing API models. If you want to model new APIs,
* extend `EmailSender::Range` instead.
*/
class EmailSender extends DataFlow::Node instanceof EmailSender::Range {
/**
* Gets a data flow node holding the plaintext version of the email body.
*/
DataFlow::Node getPlainTextBody() { result = super.getPlainTextBody() }

/**
* Gets a data flow node holding the html version of the email body.
*/
DataFlow::Node getHtmlBody() { result = super.getHtmlBody() }

/**
* Gets a data flow node holding the recipients of the email.
*/
DataFlow::Node getTo() { result = super.getTo() }

/**
* Gets a data flow node holding the senders of the email.
*/
DataFlow::Node getFrom() { result = super.getFrom() }

/**
* Gets a data flow node holding the subject of the email.
*/
DataFlow::Node getSubject() { result = super.getSubject() }

/**
* Gets a data flow node that refers to the HTML body or plaintext body of the email.
*/
DataFlow::Node getABody() { result in [super.getPlainTextBody(), super.getHtmlBody()] }
}
3 changes: 3 additions & 0 deletions python/ql/src/experimental/semmle/python/Frameworks.qll
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,6 @@ private import experimental.semmle.python.libraries.Python_JWT
private import experimental.semmle.python.libraries.Authlib
private import experimental.semmle.python.libraries.PythonJose
private import experimental.semmle.python.frameworks.CopyFile
private import experimental.semmle.python.frameworks.Sendgrid
private import experimental.semmle.python.libraries.FlaskMail
private import experimental.semmle.python.libraries.SmtpLib
87 changes: 86 additions & 1 deletion python/ql/src/experimental/semmle/python/frameworks/Django.qll
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ private import semmle.python.frameworks.Django
private import semmle.python.dataflow.new.DataFlow
private import experimental.semmle.python.Concepts
private import semmle.python.ApiGraphs
import semmle.python.dataflow.new.RemoteFlowSources
private import semmle.python.Concepts
import semmle.python.dataflow.new.RemoteFlowSources

private module ExperimentalPrivateDjango {
private module DjangoMod {
Expand Down Expand Up @@ -189,5 +189,90 @@ private module ExperimentalPrivateDjango {
}
}
}

module Email {
/** https://docs.djangoproject.com/en/3.2/topics/email/ */
private API::Node djangoMail() {
result = API::moduleImport("django").getMember("core").getMember("mail")
}

/**
* Gets a call to `django.core.mail.send_mail()`.
*
* Given the following example:
*
* ```py
* send_mail("Subject", "plain-text body", "[email protected]", ["[email protected]"], html_message=django.http.request.GET.get("html"))
* ```
*
* * `this` would be `send_mail("Subject", "plain-text body", "[email protected]", ["[email protected]"], html_message=django.http.request.GET.get("html"))`.
* * `getPlainTextBody()`'s result would be `"plain-text body"`.
* * `getHtmlBody()`'s result would be `django.http.request.GET.get("html")`.
* * `getTo()`'s result would be `["[email protected]"]`.
* * `getFrom()`'s result would be `"[email protected]"`.
* * `getSubject()`'s result would be `"Subject"`.
*/
private class DjangoSendMail extends DataFlow::CallCfgNode, EmailSender::Range {
DjangoSendMail() { this = djangoMail().getMember("send_mail").getACall() }

override DataFlow::Node getPlainTextBody() {
result in [this.getArg(1), this.getArgByName("message")]
}

override DataFlow::Node getHtmlBody() {
result in [this.getArg(8), this.getArgByName("html_message")]
}

override DataFlow::Node getTo() {
result in [this.getArg(3), this.getArgByName("recipient_list")]
}

override DataFlow::Node getFrom() {
result in [this.getArg(2), this.getArgByName("from_email")]
}

override DataFlow::Node getSubject() {
result in [this.getArg(0), this.getArgByName("subject")]
}
}

/**
* Gets a call to `django.core.mail.mail_admins()` or `django.core.mail.mail_managers()`.
*
* Given the following example:
*
* ```py
* mail_admins("Subject", "plain-text body", html_message=django.http.request.GET.get("html"))
* ```
*
* * `this` would be `mail_admins("Subject", "plain-text body", html_message=django.http.request.GET.get("html"))`.
* * `getPlainTextBody()`'s result would be `"plain-text body"`.
* * `getHtmlBody()`'s result would be `django.http.request.GET.get("html")`.
* * `getTo()`'s result would be `none`.
* * `getFrom()`'s result would be `none`.
* * `getSubject()`'s result would be `"Subject"`.
*/
private class DjangoMailInternal extends DataFlow::CallCfgNode, EmailSender::Range {
DjangoMailInternal() {
this = djangoMail().getMember(["mail_admins", "mail_managers"]).getACall()
}

override DataFlow::Node getPlainTextBody() {
result in [this.getArg(1), this.getArgByName("message")]
}

override DataFlow::Node getHtmlBody() {
result in [this.getArg(4), this.getArgByName("html_message")]
}

override DataFlow::Node getTo() { none() }

override DataFlow::Node getFrom() { none() }

override DataFlow::Node getSubject() {
result in [this.getArg(0), this.getArgByName("subject")]
}
}
}
}
}
187 changes: 187 additions & 0 deletions python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
/**
* Provides classes modeling security-relevant aspects of the `sendgrid` PyPI package.
* See https://github.com/sendgrid/sendgrid-python.
*/

private import python
private import semmle.python.dataflow.new.DataFlow
private import experimental.semmle.python.Concepts
private import semmle.python.ApiGraphs

private module Sendgrid {
/** Gets a reference to the `sendgrid` module. */
private API::Node sendgrid() { result = API::moduleImport("sendgrid") }

/** Gets a reference to `sendgrid.helpers.mail` */
private API::Node sendgridMailHelper() {
result = sendgrid().getMember("helpers").getMember("mail")
}

/** Gets a reference to `sendgrid.helpers.mail.Mail` */
private API::Node sendgridMailInstance() { result = sendgridMailHelper().getMember("Mail") }

/** Gets a reference to a `SendGridAPIClient` instance. */
private API::Node sendgridApiClient() {
result = sendgrid().getMember("SendGridAPIClient").getReturn()
}

/** Gets a reference to a `SendGridAPIClient` instance call with `send` or `post`. */
private DataFlow::CallCfgNode sendgridApiSendCall() {
result = sendgridApiClient().getMember("send").getACall()
or
result =
sendgridApiClient()
.getMember("client")
.getMember("mail")
.getMember("send")
.getMember("post")
.getACall()
}

/**
* Gets a reference to `sg.send()` and `sg.client.mail.send.post()`.
*
* Given the following example:
*
* ```py
* from_email = Email("[email protected]")
* to_email = To("[email protected]")
* subject = "Sending with SendGrid is Fun"
* content = Content("text/html", request.args["html_content"])
*
* mail = Mail(from_email, to_email, subject, content)
*
* sg = SendGridAPIClient(api_key='SENDGRID_API_KEY')
* response = sg.client.mail.send.post(request_body=mail.get())
* ```
*
* * `this` would be `sg.client.mail.send.post(request_body=mail.get())`.
* * `getPlainTextBody()`'s result would be `none()`.
* * `getHtmlBody()`'s result would be `request.args["html_content"]`.
* * `getTo()`'s result would be `"[email protected]"`.
* * `getFrom()`'s result would be `"[email protected]"`.
* * `getSubject()`'s result would be `"Sending with SendGrid is Fun"`.
*/
private class SendGridMail extends DataFlow::CallCfgNode, EmailSender::Range {
SendGridMail() { this = sendgridApiSendCall() }

private DataFlow::CallCfgNode getMailCall() {
exists(DataFlow::Node n |
n in [this.getArg(0), this.getArgByName("request_body")] and
result = [n, n.(DataFlow::MethodCallNode).getObject()].getALocalSource()
)
}

private DataFlow::Node sendgridContent(DataFlow::CallCfgNode contentCall, string mime) {
mime in ["text/plain", "text/html", "text/x-amp-html"] and
exists(StrConst mimeNode |
mimeNode.getText() = mime and
DataFlow::exprNode(mimeNode).(DataFlow::LocalSourceNode).flowsTo(contentCall.getArg(0)) and
result = contentCall.getArg(1)
)
}

private DataFlow::Node sendgridWrite(string attributeName) {
attributeName in ["plain_text_content", "html_content", "from_email", "subject"] and
exists(DataFlow::AttrWrite attrWrite |
attrWrite.getObject().getALocalSource() = this.getMailCall() and
attrWrite.getAttributeName() = attributeName and
result = attrWrite.getValue()
)
}

override DataFlow::Node getPlainTextBody() {
result in [
this.getMailCall().getArg(3), this.getMailCall().getArgByName("plain_text_content")
]
or
result in [
this.sendgridContent([
this.getMailCall().getArg(3), this.getMailCall().getArgByName("plain_text_content")
].getALocalSource(), "text/plain"),
this.sendgridContent(sendgridMailInstance().getMember("add_content").getACall(),
"text/plain")
]
or
result = this.sendgridWrite("plain_text_content")
}

override DataFlow::Node getHtmlBody() {
result in [this.getMailCall().getArg(4), this.getMailCall().getArgByName("html_content")]
or
result = this.getMailCall().getAMethodCall("set_html").getArg(0)
or
result =
this.sendgridContent([
this.getMailCall().getArg(4), this.getMailCall().getArgByName("html_content")
].getALocalSource(), ["text/html", "text/x-amp-html"])
or
result = this.sendgridWrite("html_content")
or
exists(KeyValuePair content, Dict generalDict, KeyValuePair typePair, KeyValuePair valuePair |
content.getKey().(StrConst).getText() = "content" and
content.getValue().(List).getAnElt() = generalDict and
// declare KeyValuePairs keys and values
typePair.getKey().(StrConst).getText() = "type" and
typePair.getValue().(StrConst).getText() = ["text/html", "text/x-amp-html"] and
valuePair.getKey().(StrConst).getText() = "value" and
result.asExpr() = valuePair.getValue() and
// correlate generalDict with previously set KeyValuePairs
generalDict.getAnItem() in [typePair, valuePair] and
[this.getArg(0), this.getArgByName("request_body")].getALocalSource().asExpr() =
any(Dict d | d.getAnItem() = content)
)
or
exists(KeyValuePair footer, Dict generalDict, KeyValuePair enablePair, KeyValuePair htmlPair |
footer.getKey().(StrConst).getText() = ["footer", "subscription_tracking"] and
footer.getValue() = generalDict and
// check footer is enabled
enablePair.getKey().(StrConst).getText() = "enable" and
exists(enablePair.getValue().(True)) and
// get html content
htmlPair.getKey().(StrConst).getText() = "html" and
result.asExpr() = htmlPair.getValue() and
// correlate generalDict with previously set KeyValuePairs
generalDict.getAnItem() in [enablePair, htmlPair] and
exists(KeyValuePair k |
k.getKey() =
[this.getArg(0), this.getArgByName("request_body")]
.getALocalSource()
.asExpr()
.(Dict)
.getAKey() and
k.getValue() = any(Dict d | d.getAKey() = footer.getKey())
)
)
}

override DataFlow::Node getTo() {
result in [this.getMailCall().getArg(1), this.getMailCall().getArgByName("to_emails")]
or
result = this.getMailCall().getAMethodCall("To").getArg(0)
or
result =
this.getMailCall()
.getAMethodCall(["to", "add_to", "cc", "add_cc", "bcc", "add_bcc"])
.getArg(0)
}

override DataFlow::Node getFrom() {
result in [this.getMailCall().getArg(0), this.getMailCall().getArgByName("from_email")]
or
result = this.getMailCall().getAMethodCall("Email").getArg(0)
or
result = this.getMailCall().getAMethodCall(["from_email", "set_from"]).getArg(0)
or
result = this.sendgridWrite("from_email")
}

override DataFlow::Node getSubject() {
result in [this.getMailCall().getArg(2), this.getMailCall().getArgByName("subject")]
or
result = this.getMailCall().getAMethodCall(["subject", "set_subject"]).getArg(0)
or
result = this.sendgridWrite("subject")
}
}
}
Loading