-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Python: CWE-079 - Add Email injection query #7127
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
63 commits
Select commit
Hold shift + click to select a range
24d4415
Create EmailClients.qll
mrthankyou c3eba25
Add query tests
mrthankyou 20f321e
Remove accidental slash
mrthankyou 48cd506
Change EmailSender structure
jorgectf 4d890dd
Polish flask_mail tests and code
jorgectf 7956b97
Unit tests move and temporary ql
jorgectf 4c9ecf0
Delete testing class-variable
jorgectf ae84df8
Extend ReflectedXSS query
jorgectf c323fbb
Cover Flask-SendMail (Flask-Mail copy)
jorgectf eac5eba
Move tests and qlref to test/
jorgectf 355bb5c
Format Flask.qll
jorgectf 8ae8648
Format ReflectedXSS.qll
jorgectf bf1eb72
Cover `django.core.mail`
jorgectf 9563faf
Add Sendgrid modeling
jorgectf 5e8f995
Extend Sendgrid setters
jorgectf 70d6511
Optimize Flask.qll
jorgectf 7b9cbaf
Move flask_mail to libraries/
jorgectf e0013fc
Fix Concepts.qll dependencies
jorgectf b5ee7c3
Specify `plain-text body`
jorgectf 19a6267
Almost fix `getFlaskMailArgument(...)`
jorgectf 4c2a422
Merge remote-tracking branch 'origin/main' into jty/python/emailInjec…
jorgectf c9634f3
Fix `getFlaskMailArgument()`
jorgectf bf68495
Polish `FlaskMail` qldocs
jorgectf e8e0f0f
Add temporary `.expected`
jorgectf dbf5b24
Polish `Sendgrid.qll` qldoc
jorgectf ba3ea70
Add `Sendgrid` dict data html body modeling
jorgectf 4afcd9d
[mrthankyou] smtplib partial modeling.
jorgectf 3a4e3d5
Remove comments from Python example tests
mrthankyou d9e4df7
Remove unnecessary comment
mrthankyou 3264e7b
Merge branch 'jty/python/emailInjection' of https://github.com/jty-te…
jorgectf 356b071
Cover `MimeType.amp` as a vulnerable mimetype
jorgectf d316974
Add `HtmlContent` additional taint step
jorgectf f4a73fc
Add RFS to `sendgrid` test
jorgectf 5774ce2
Improve `django` test
jorgectf c0a0c5d
Cover `footer` and `subscription_tracking` html injection
jorgectf 5b46b90
Fix additional taint step variables
jorgectf 1393b5b
Add `django` qldocs
jorgectf 33b6f6f
Polish `FlaskMail` qldocs
jorgectf 63eadc8
Polish `sendgrid` modeling
jorgectf dbdf102
Make `EmailSender` an extendable API
jorgectf e7cb762
Add `SmtpLib` to `Frameworks.qll` and minimal fixes
jorgectf 129a81a
Cover `smtplib`
jorgectf 1be823d
Apply suggestions from code review
jorgectf a905205
Merge branch 'github:main' into jty/python/emailInjection
jorgectf 5bd8de1
Fix `smtplib`'s `_subparts` taint config issue
jorgectf f350253
Merge branch 'jty/python/emailInjection' of https://github.com/jty-te…
jorgectf 018aa11
Make `EmailSender` an instance of `EmailSender::Range`
jorgectf 1b9567a
Avoid using `Str_` internal class
jorgectf ede5d41
Update `.expected`
jorgectf 2f2cf2c
Use `StrConst.getText()` instead of `Str_.getS()`
jorgectf 67b672a
Merge remote-tracking branch 'origin/main' into jty/python/emailInjec…
jorgectf 3159d8e
Correlate `SendGridMail` declaration with its predicates
jorgectf 6722671
Refactor `sendgridApiClient` and `sendgridApiSendCall`
jorgectf 6b04344
Refactor `sendgridContent` and `sendgridWrite`
jorgectf 930fbf7
Move `getFlaskMailArgument` inside `FlaskMail` and refactor
jorgectf bbba1a2
Explicitly call `this` in `SendGridMail`
jorgectf 3f43e6e
Fix `FlaskMail`'s `getTo`
jorgectf c155ac6
Add `HtmlEscaping` sanitizer
jorgectf b5734ed
Merge branch 'main' into jty/python/emailInjection
mrthankyou 76c27c6
Merge branch 'main' into jty/python/emailInjection
mrthankyou e577a0e
Update `.expected` tests
jorgectf 897d5c9
Apply suggestions from code review
jorgectf 171239b
Format `FlaskMail.qll` and `Sendgrid.qll`
jorgectf File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
23 changes: 23 additions & 0 deletions
23
python/ql/src/experimental/Security/CWE-079/ReflectedXSS.ql
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 { | ||
|
|
@@ -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
187
python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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") | ||
| } | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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?
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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 :)