diff --git a/python/ql/src/experimental/Security/CWE-079/ReflectedXSS.ql b/python/ql/src/experimental/Security/CWE-079/ReflectedXSS.ql new file mode 100644 index 00000000000..008650c86e7 --- /dev/null +++ b/python/ql/src/experimental/Security/CWE-079/ReflectedXSS.ql @@ -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" diff --git a/python/ql/src/experimental/semmle/python/Concepts.qll b/python/ql/src/experimental/semmle/python/Concepts.qll index 305ba22ce79..433d89dbad1 100644 --- a/python/ql/src/experimental/semmle/python/Concepts.qll +++ b/python/ql/src/experimental/semmle/python/Concepts.qll @@ -602,3 +602,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(); + + /** + * 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()] } +} diff --git a/python/ql/src/experimental/semmle/python/Frameworks.qll b/python/ql/src/experimental/semmle/python/Frameworks.qll index 359dc91162b..98fb86f6d92 100644 --- a/python/ql/src/experimental/semmle/python/Frameworks.qll +++ b/python/ql/src/experimental/semmle/python/Frameworks.qll @@ -15,3 +15,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 diff --git a/python/ql/src/experimental/semmle/python/frameworks/Django.qll b/python/ql/src/experimental/semmle/python/frameworks/Django.qll index 03462649792..6853f9c3f6a 100644 --- a/python/ql/src/experimental/semmle/python/frameworks/Django.qll +++ b/python/ql/src/experimental/semmle/python/frameworks/Django.qll @@ -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", "from@example.com", ["to@example.com"], html_message=django.http.request.GET.get("html")) + * ``` + * + * * `this` would be `send_mail("Subject", "plain-text body", "from@example.com", ["to@example.com"], 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 `["to@example.com"]`. + * * `getFrom()`'s result would be `"from@example.com"`. + * * `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")] + } + } + } } } diff --git a/python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll b/python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll new file mode 100644 index 00000000000..2914c59d755 --- /dev/null +++ b/python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll @@ -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("from@example.com") + * to_email = To("to@example.com") + * 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 `"to@example.com"`. + * * `getFrom()`'s result would be `"from@example.com"`. + * * `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") + } + } +} diff --git a/python/ql/src/experimental/semmle/python/libraries/FlaskMail.qll b/python/ql/src/experimental/semmle/python/libraries/FlaskMail.qll new file mode 100644 index 00000000000..7659a0f45b2 --- /dev/null +++ b/python/ql/src/experimental/semmle/python/libraries/FlaskMail.qll @@ -0,0 +1,81 @@ +/** + * Provides classes modeling security-relevant aspects of the `flask` PyPI package. + * See https://flask.palletsprojects.com/en/1.1.x/. + */ + +private import python +private import semmle.python.dataflow.new.DataFlow +private import experimental.semmle.python.Concepts +private import semmle.python.ApiGraphs + +/** https://pythonhosted.org/Flask-Mail/#module-flask_mail */ +private module FlaskMail { + /** Gets a reference to `flask_mail`, `flask_sendmail` and `flask.ext.sendmail`. */ + private API::Node flaskMail() { + result = API::moduleImport(["flask_mail", "flask_sendmail", "flask.ext.sendmail"]) + } + + /** Gets a reference to `flask_mail.Mail()`, `flask_sendmail.Mail()` and `flask.ext.sendmail.Mail()`. */ + private API::Node flaskMailInstance() { result = flaskMail().getMember("Mail").getReturn() } + + /** + * Gets a call to `mail.send()`. + * + * Given the following example: + * + * ```py + * msg = Message(subject="Subject", + * sender="from@example.com", + * recipients=["to@example.com"], + * body="plain-text body", + * html=request.args["html"]) + * mail.send(msg) + * ``` + * + * * `this` would be `mail.send(msg)`. + * * `getPlainTextBody()`'s result would be `"plain-text body"`. + * * `getHtmlBody()`'s result would be `request.args["html"]`. + * * `getTo()`'s result would be `["to@example.com"]`. + * * `getFrom()`'s result would be `"from@example.com"`. + * * `getSubject()`'s result would be `"Subject"`. + */ + private class FlaskMail extends DataFlow::CallCfgNode, EmailSender::Range { + FlaskMail() { + this = + [flaskMailInstance(), flaskMailInstance().getMember("connect").getReturn()] + .getMember(["send", "send_message"]) + .getACall() + } + + private DataFlow::CallCfgNode getMessage() { result = this.getArg(0).getALocalSource() } + + bindingset[argumentPosition] + private DataFlow::Node getFlaskMailArgument(int argumentPosition, string argumentName) { + argumentPosition in [[0 .. 3], 5] and + argumentName in ["body", "html", "recipients", "sender", "subject"] and + result in [ + this.getMessage().getArg(argumentPosition), this.getMessage().getArgByName(argumentName) + ] + or + exists(DataFlow::AttrWrite write | + write.getObject().getALocalSource() = this.getMessage() and + write.getAttributeName() = argumentName and + result = write.getValue() + ) + } + + override DataFlow::Node getPlainTextBody() { result = this.getFlaskMailArgument(2, "body") } + + override DataFlow::Node getHtmlBody() { result = this.getFlaskMailArgument(3, "html") } + + override DataFlow::Node getTo() { + result = this.getFlaskMailArgument(1, "recipients") + or + result = this.getMessage().getAMethodCall("add_recipient").getACall().getArg(0) + } + + override DataFlow::Node getFrom() { result = this.getFlaskMailArgument(5, "sender") } + + override DataFlow::Node getSubject() { result = this.getFlaskMailArgument(0, "subject") } + } +} diff --git a/python/ql/src/experimental/semmle/python/libraries/SmtpLib.qll b/python/ql/src/experimental/semmle/python/libraries/SmtpLib.qll new file mode 100644 index 00000000000..8d69bcb1a62 --- /dev/null +++ b/python/ql/src/experimental/semmle/python/libraries/SmtpLib.qll @@ -0,0 +1,177 @@ +private import python +private import semmle.python.dataflow.new.DataFlow +private import experimental.semmle.python.Concepts +private import semmle.python.ApiGraphs +private import semmle.python.dataflow.new.TaintTracking2 + +module SmtpLib { + /** Gets a reference to `smtplib.SMTP_SSL` */ + private API::Node smtpConnectionInstance() { + result = API::moduleImport("smtplib").getMember("SMTP_SSL") + } + + /** Gets a reference to `email.mime.multipart.MIMEMultipart` */ + private API::Node smtpMimeMultipartInstance() { + result = + API::moduleImport("email").getMember("mime").getMember("multipart").getMember("MIMEMultipart") + } + + /** Gets a reference to `email.mime.text.MIMEText` */ + private API::Node smtpMimeTextInstance() { + result = API::moduleImport("email").getMember("mime").getMember("text").getMember("MIMEText") + } + + private DataFlow::CallCfgNode mimeText(string mimetype) { + result = smtpMimeTextInstance().getACall() and + [result.getArg(1), result.getArgByName("_subtype")].asExpr().(StrConst).getText() = mimetype + } + + /** + * Gets flow from `MIMEText()` to `MIMEMultipart(_subparts=(part1, part2))`'s `_subparts` + * argument. Used because of the impossibility to get local source nodes from `_subparts`' + * `(List|Tuple)` elements. + */ + private class SMTPMessageConfig extends TaintTracking2::Configuration { + SMTPMessageConfig() { this = "SMTPMessageConfig" } + + override predicate isSource(DataFlow::Node source) { source = mimeText(_) } + + override predicate isSink(DataFlow::Node sink) { + sink = smtpMimeMultipartInstance().getACall().getArgByName("_subparts") + } + } + + /** + * Using the `MimeText` call retrieves the content argument whose type argument equals `mimetype`. + * This call flows into `MIMEMultipart`'s `_subparts` argument or the `.attach()` method call + * and both local source nodes correlate to `smtp`'s `sendmail` call 3rd argument's local source. + * + * Given the following example with `getSmtpMessage(any(SmtpLibSendMail s), "html")`: + * + * ```py + * part1 = MIMEText(text, "plain") + * part2 = MIMEText(html, "html") + * message = MIMEMultipart(_subparts=(part1, part2)) + * server.sendmail(sender_email, receiver_email, message.as_string()) + * ``` + * + * * `source` would be `MIMEText(text, "html")`. + * * `sink` would be `MIMEMultipart(_subparts=(part1, part2))`. + * * Then `message` local source node is correlated to `sink`. + * * Then the flow from `source` to `_subparts` is checked. + * + * Given the following example with `getSmtpMessage(any(SmtpLibSendMail s), "html")`: + * + * ```py + * part1 = MIMEText(text, "plain") + * part2 = MIMEText(html, "html") + * message = MIMEMultipart("alternative") + * message.attach(part1) + * message.attach(part2) + * server.sendmail(sender_email, receiver_email, message.as_string()) + * ``` + * + * * `source` would be `MIMEText(text, "html")`. + * * `sink` would be `message.attach(part2)`. + * * Then `sink`'s object (`message`) local source is correlated to `server.sendmail` + * 3rd argument local source (`MIMEMultipart("alternative")`). + * * Then the flow from `source` to `sink` 1st argument is checked. + */ + bindingset[mimetype] + private DataFlow::Node getSmtpMessage(DataFlow::CallCfgNode sendCall, string mimetype) { + exists(DataFlow::Node source, DataFlow::Node sink | + source = mimeText(mimetype) and + ( + // via _subparts + sink = smtpMimeMultipartInstance().getACall() and + sink = + [sendCall.getArg(2), sendCall.getArg(2).(DataFlow::MethodCallNode).getObject()] + .getALocalSource() and + any(SMTPMessageConfig a) + .hasFlow(source, sink.(DataFlow::CallCfgNode).getArgByName("_subparts")) + or + // via .attach() + sink = smtpMimeMultipartInstance().getReturn().getMember("attach").getACall() and + sink.(DataFlow::MethodCallNode).getObject().getALocalSource() = + [sendCall.getArg(2), sendCall.getArg(2).(DataFlow::MethodCallNode).getObject()] + .getALocalSource() and + source.(DataFlow::CallCfgNode).flowsTo(sink.(DataFlow::CallCfgNode).getArg(0)) + ) and + result = source.(DataFlow::CallCfgNode).getArg(0) + ) + } + + /** + * Gets a message subscript write by correlating subscript's object local source with + * `smtp`'s `sendmail` call 3rd argument's local source. + * + * Given the following example with `getSMTPSubscriptByIndex(any(SmtpLibSendMail s), "Subject")`: + * + * ```py + * message = MIMEMultipart("alternative") + * message["Subject"] = "multipart test" + * server.sendmail(sender_email, receiver_email, message.as_string()) + * ``` + * + * * `def` would be `message["Subject"]` (`DefinitionNode`) + * * `sub` would be `message["Subject"]` (`Subscript`) + * * `result` would be `"multipart test"` + */ + private DataFlow::Node getSMTPSubscriptByIndex(DataFlow::CallCfgNode sendCall, string index) { + exists(DefinitionNode def, Subscript sub | + sub = def.getNode() and + DataFlow::exprNode(sub.getObject()).getALocalSource() = + [sendCall.getArg(2), sendCall.getArg(2).(DataFlow::MethodCallNode).getObject()] + .getALocalSource() and + sub.getIndex().(StrConst).getText() = index and + result.asCfgNode() = def.getValue() + ) + } + + /** + * Gets a reference to `smtplib.SMTP_SSL().sendmail()`. + * + * Given the following example: + * + * ```py + * part1 = MIMEText(text, "plain") + * part2 = MIMEText(html, "html") + * + * message = MIMEMultipart(_subparts=(part1, part2)) + * message["Subject"] = "multipart test" + * message["From"] = sender_email + * message["To"] = receiver_email + * + * server.login(sender_email, "SERVER_PASSWORD") + * server.sendmail(sender_email, receiver_email, message.as_string()) + * ``` + * + * * `this` would be `server.sendmail(sender_email, receiver_email, message.as_string())`. + * * `getPlainTextBody()`'s result would be `text`. + * * `getHtmlBody()`'s result would be `html`. + * * `getTo()`'s result would be `receiver_email`. + * * `getFrom()`'s result would be `sender_email`. + * * `getSubject()`'s result would be `"multipart test"`. + */ + private class SmtpLibSendMail extends DataFlow::CallCfgNode, EmailSender::Range { + SmtpLibSendMail() { + this = smtpConnectionInstance().getReturn().getMember("sendmail").getACall() + } + + override DataFlow::Node getPlainTextBody() { result = getSmtpMessage(this, "plain") } + + override DataFlow::Node getHtmlBody() { result = getSmtpMessage(this, "html") } + + override DataFlow::Node getTo() { + result in [this.getArg(1), getSMTPSubscriptByIndex(this, "To")] + } + + override DataFlow::Node getFrom() { + result in [this.getArg(0), getSMTPSubscriptByIndex(this, "From")] + } + + override DataFlow::Node getSubject() { + result in [this.getArg(2), getSMTPSubscriptByIndex(this, "Subject")] + } + } +} diff --git a/python/ql/src/experimental/semmle/python/security/dataflow/ReflectedXSS.qll b/python/ql/src/experimental/semmle/python/security/dataflow/ReflectedXSS.qll new file mode 100644 index 00000000000..58ca3c5b4e6 --- /dev/null +++ b/python/ql/src/experimental/semmle/python/security/dataflow/ReflectedXSS.qll @@ -0,0 +1,46 @@ +/** + * Provides a taint-tracking configuration for detecting reflected server-side + * cross-site scripting vulnerabilities. + */ + +import python +import semmle.python.dataflow.new.DataFlow +import semmle.python.dataflow.new.TaintTracking +import semmle.python.dataflow.new.RemoteFlowSources +import semmle.python.dataflow.new.BarrierGuards +import experimental.semmle.python.Concepts +import semmle.python.Concepts +import semmle.python.ApiGraphs + +/** + * A taint-tracking configuration for detecting reflected server-side cross-site + * scripting vulnerabilities. + */ +class ReflectedXssConfiguration extends TaintTracking::Configuration { + ReflectedXssConfiguration() { this = "ReflectedXssConfiguration" } + + override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource } + + override predicate isSink(DataFlow::Node sink) { sink = any(EmailSender email).getHtmlBody() } + + override predicate isSanitizerGuard(DataFlow::BarrierGuard guard) { + guard instanceof StringConstCompare + } + + override predicate isSanitizer(DataFlow::Node sanitizer) { + sanitizer = any(HtmlEscaping esc).getOutput() + } + + override predicate isAdditionalTaintStep(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) { + exists(DataFlow::CallCfgNode htmlContentCall | + htmlContentCall = + API::moduleImport("sendgrid") + .getMember("helpers") + .getMember("mail") + .getMember("HtmlContent") + .getACall() and + nodeTo = htmlContentCall and + nodeFrom = htmlContentCall.getArg(0) + ) + } +} diff --git a/python/ql/test/experimental/query-tests/Security/CWE-079/ReflectedXSS.expected b/python/ql/test/experimental/query-tests/Security/CWE-079/ReflectedXSS.expected new file mode 100644 index 00000000000..f787dfa43fc --- /dev/null +++ b/python/ql/test/experimental/query-tests/Security/CWE-079/ReflectedXSS.expected @@ -0,0 +1,96 @@ +edges +| flask_mail.py:13:22:13:28 | ControlFlowNode for request | flask_mail.py:13:22:13:33 | ControlFlowNode for Attribute | +| flask_mail.py:13:22:13:28 | ControlFlowNode for request | flask_mail.py:18:14:18:25 | ControlFlowNode for Attribute | +| flask_mail.py:13:22:13:33 | ControlFlowNode for Attribute | flask_mail.py:13:22:13:41 | ControlFlowNode for Subscript | +| flask_mail.py:18:14:18:20 | ControlFlowNode for request | flask_mail.py:18:14:18:25 | ControlFlowNode for Attribute | +| flask_mail.py:18:14:18:25 | ControlFlowNode for Attribute | flask_mail.py:18:14:18:33 | ControlFlowNode for Subscript | +| flask_mail.py:31:24:31:30 | ControlFlowNode for request | flask_mail.py:31:24:31:35 | ControlFlowNode for Attribute | +| flask_mail.py:31:24:31:35 | ControlFlowNode for Attribute | flask_mail.py:31:24:31:43 | ControlFlowNode for Subscript | +| sendgrid_mail.py:14:22:14:28 | ControlFlowNode for request | sendgrid_mail.py:14:22:14:33 | ControlFlowNode for Attribute | +| sendgrid_mail.py:14:22:14:33 | ControlFlowNode for Attribute | sendgrid_mail.py:14:22:14:49 | ControlFlowNode for Subscript | +| sendgrid_mail.py:26:34:26:40 | ControlFlowNode for request | sendgrid_mail.py:26:34:26:45 | ControlFlowNode for Attribute | +| sendgrid_mail.py:26:34:26:45 | ControlFlowNode for Attribute | sendgrid_mail.py:26:34:26:61 | ControlFlowNode for Subscript | +| sendgrid_mail.py:26:34:26:61 | ControlFlowNode for Subscript | sendgrid_mail.py:26:22:26:62 | ControlFlowNode for HtmlContent() | +| sendgrid_mail.py:37:41:37:47 | ControlFlowNode for request | sendgrid_mail.py:37:41:37:52 | ControlFlowNode for Attribute | +| sendgrid_mail.py:37:41:37:52 | ControlFlowNode for Attribute | sendgrid_mail.py:37:41:37:68 | ControlFlowNode for Subscript | +| sendgrid_via_mail_send_post_request_body_bad.py:16:51:16:57 | ControlFlowNode for request | sendgrid_via_mail_send_post_request_body_bad.py:16:51:16:62 | ControlFlowNode for Attribute | +| sendgrid_via_mail_send_post_request_body_bad.py:16:51:16:57 | ControlFlowNode for request | sendgrid_via_mail_send_post_request_body_bad.py:27:50:27:61 | ControlFlowNode for Attribute | +| sendgrid_via_mail_send_post_request_body_bad.py:16:51:16:57 | ControlFlowNode for request | sendgrid_via_mail_send_post_request_body_bad.py:41:50:41:61 | ControlFlowNode for Attribute | +| sendgrid_via_mail_send_post_request_body_bad.py:16:51:16:62 | ControlFlowNode for Attribute | sendgrid_via_mail_send_post_request_body_bad.py:16:51:16:78 | ControlFlowNode for Subscript | +| sendgrid_via_mail_send_post_request_body_bad.py:16:51:16:78 | ControlFlowNode for Subscript | sendgrid_via_mail_send_post_request_body_bad.py:16:26:16:79 | ControlFlowNode for Attribute() | +| sendgrid_via_mail_send_post_request_body_bad.py:27:50:27:56 | ControlFlowNode for request | sendgrid_via_mail_send_post_request_body_bad.py:27:50:27:61 | ControlFlowNode for Attribute | +| sendgrid_via_mail_send_post_request_body_bad.py:27:50:27:56 | ControlFlowNode for request | sendgrid_via_mail_send_post_request_body_bad.py:41:50:41:61 | ControlFlowNode for Attribute | +| sendgrid_via_mail_send_post_request_body_bad.py:27:50:27:61 | ControlFlowNode for Attribute | sendgrid_via_mail_send_post_request_body_bad.py:27:50:27:76 | ControlFlowNode for Subscript | +| sendgrid_via_mail_send_post_request_body_bad.py:27:50:27:76 | ControlFlowNode for Subscript | sendgrid_via_mail_send_post_request_body_bad.py:27:25:27:77 | ControlFlowNode for Attribute() | +| sendgrid_via_mail_send_post_request_body_bad.py:41:50:41:56 | ControlFlowNode for request | sendgrid_via_mail_send_post_request_body_bad.py:41:50:41:61 | ControlFlowNode for Attribute | +| sendgrid_via_mail_send_post_request_body_bad.py:41:50:41:61 | ControlFlowNode for Attribute | sendgrid_via_mail_send_post_request_body_bad.py:41:50:41:78 | ControlFlowNode for Subscript | +| sendgrid_via_mail_send_post_request_body_bad.py:41:50:41:78 | ControlFlowNode for Subscript | sendgrid_via_mail_send_post_request_body_bad.py:41:25:41:79 | ControlFlowNode for Attribute() | +| smtplib_bad_subparts.py:17:12:17:18 | ControlFlowNode for request | smtplib_bad_subparts.py:17:12:17:23 | ControlFlowNode for Attribute | +| smtplib_bad_subparts.py:17:12:17:23 | ControlFlowNode for Attribute | smtplib_bad_subparts.py:17:12:17:33 | ControlFlowNode for Subscript | +| smtplib_bad_subparts.py:17:12:17:33 | ControlFlowNode for Subscript | smtplib_bad_subparts.py:24:22:24:25 | ControlFlowNode for html | +| smtplib_bad_via_attach.py:20:12:20:18 | ControlFlowNode for request | smtplib_bad_via_attach.py:20:12:20:23 | ControlFlowNode for Attribute | +| smtplib_bad_via_attach.py:20:12:20:23 | ControlFlowNode for Attribute | smtplib_bad_via_attach.py:20:12:20:31 | ControlFlowNode for Subscript | +| smtplib_bad_via_attach.py:20:12:20:31 | ControlFlowNode for Subscript | smtplib_bad_via_attach.py:27:22:27:25 | ControlFlowNode for html | +nodes +| django_mail.py:14:48:14:82 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() | +| django_mail.py:23:30:23:64 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() | +| django_mail.py:25:32:25:66 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() | +| flask_mail.py:13:22:13:28 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| flask_mail.py:13:22:13:33 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | +| flask_mail.py:13:22:13:41 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | +| flask_mail.py:18:14:18:20 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| flask_mail.py:18:14:18:25 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | +| flask_mail.py:18:14:18:33 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | +| flask_mail.py:31:24:31:30 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| flask_mail.py:31:24:31:35 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | +| flask_mail.py:31:24:31:43 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | +| sendgrid_mail.py:14:22:14:28 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| sendgrid_mail.py:14:22:14:33 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | +| sendgrid_mail.py:14:22:14:49 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | +| sendgrid_mail.py:26:22:26:62 | ControlFlowNode for HtmlContent() | semmle.label | ControlFlowNode for HtmlContent() | +| sendgrid_mail.py:26:34:26:40 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| sendgrid_mail.py:26:34:26:45 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | +| sendgrid_mail.py:26:34:26:61 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | +| sendgrid_mail.py:37:41:37:47 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| sendgrid_mail.py:37:41:37:52 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | +| sendgrid_mail.py:37:41:37:68 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | +| sendgrid_via_mail_send_post_request_body_bad.py:16:26:16:79 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() | +| sendgrid_via_mail_send_post_request_body_bad.py:16:51:16:57 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| sendgrid_via_mail_send_post_request_body_bad.py:16:51:16:62 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | +| sendgrid_via_mail_send_post_request_body_bad.py:16:51:16:78 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | +| sendgrid_via_mail_send_post_request_body_bad.py:27:25:27:77 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() | +| sendgrid_via_mail_send_post_request_body_bad.py:27:50:27:56 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| sendgrid_via_mail_send_post_request_body_bad.py:27:50:27:61 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | +| sendgrid_via_mail_send_post_request_body_bad.py:27:50:27:76 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | +| sendgrid_via_mail_send_post_request_body_bad.py:41:25:41:79 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() | +| sendgrid_via_mail_send_post_request_body_bad.py:41:50:41:56 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| sendgrid_via_mail_send_post_request_body_bad.py:41:50:41:61 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | +| sendgrid_via_mail_send_post_request_body_bad.py:41:50:41:78 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | +| smtplib_bad_subparts.py:17:12:17:18 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| smtplib_bad_subparts.py:17:12:17:23 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | +| smtplib_bad_subparts.py:17:12:17:33 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | +| smtplib_bad_subparts.py:24:22:24:25 | ControlFlowNode for html | semmle.label | ControlFlowNode for html | +| smtplib_bad_via_attach.py:20:12:20:18 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| smtplib_bad_via_attach.py:20:12:20:23 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | +| smtplib_bad_via_attach.py:20:12:20:31 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | +| smtplib_bad_via_attach.py:27:22:27:25 | ControlFlowNode for html | semmle.label | ControlFlowNode for html | +subpaths +#select +| django_mail.py:14:48:14:82 | ControlFlowNode for Attribute() | django_mail.py:14:48:14:82 | ControlFlowNode for Attribute() | django_mail.py:14:48:14:82 | ControlFlowNode for Attribute() | Cross-site scripting vulnerability due to $@. | django_mail.py:14:48:14:82 | ControlFlowNode for Attribute() | a user-provided value | +| django_mail.py:23:30:23:64 | ControlFlowNode for Attribute() | django_mail.py:23:30:23:64 | ControlFlowNode for Attribute() | django_mail.py:23:30:23:64 | ControlFlowNode for Attribute() | Cross-site scripting vulnerability due to $@. | django_mail.py:23:30:23:64 | ControlFlowNode for Attribute() | a user-provided value | +| django_mail.py:25:32:25:66 | ControlFlowNode for Attribute() | django_mail.py:25:32:25:66 | ControlFlowNode for Attribute() | django_mail.py:25:32:25:66 | ControlFlowNode for Attribute() | Cross-site scripting vulnerability due to $@. | django_mail.py:25:32:25:66 | ControlFlowNode for Attribute() | a user-provided value | +| flask_mail.py:13:22:13:41 | ControlFlowNode for Subscript | flask_mail.py:13:22:13:28 | ControlFlowNode for request | flask_mail.py:13:22:13:41 | ControlFlowNode for Subscript | Cross-site scripting vulnerability due to $@. | flask_mail.py:13:22:13:28 | ControlFlowNode for request | a user-provided value | +| flask_mail.py:18:14:18:33 | ControlFlowNode for Subscript | flask_mail.py:13:22:13:28 | ControlFlowNode for request | flask_mail.py:18:14:18:33 | ControlFlowNode for Subscript | Cross-site scripting vulnerability due to $@. | flask_mail.py:13:22:13:28 | ControlFlowNode for request | a user-provided value | +| flask_mail.py:18:14:18:33 | ControlFlowNode for Subscript | flask_mail.py:18:14:18:20 | ControlFlowNode for request | flask_mail.py:18:14:18:33 | ControlFlowNode for Subscript | Cross-site scripting vulnerability due to $@. | flask_mail.py:18:14:18:20 | ControlFlowNode for request | a user-provided value | +| flask_mail.py:31:24:31:43 | ControlFlowNode for Subscript | flask_mail.py:31:24:31:30 | ControlFlowNode for request | flask_mail.py:31:24:31:43 | ControlFlowNode for Subscript | Cross-site scripting vulnerability due to $@. | flask_mail.py:31:24:31:30 | ControlFlowNode for request | a user-provided value | +| sendgrid_mail.py:14:22:14:49 | ControlFlowNode for Subscript | sendgrid_mail.py:14:22:14:28 | ControlFlowNode for request | sendgrid_mail.py:14:22:14:49 | ControlFlowNode for Subscript | Cross-site scripting vulnerability due to $@. | sendgrid_mail.py:14:22:14:28 | ControlFlowNode for request | a user-provided value | +| sendgrid_mail.py:26:22:26:62 | ControlFlowNode for HtmlContent() | sendgrid_mail.py:26:34:26:40 | ControlFlowNode for request | sendgrid_mail.py:26:22:26:62 | ControlFlowNode for HtmlContent() | Cross-site scripting vulnerability due to $@. | sendgrid_mail.py:26:34:26:40 | ControlFlowNode for request | a user-provided value | +| sendgrid_mail.py:37:41:37:68 | ControlFlowNode for Subscript | sendgrid_mail.py:37:41:37:47 | ControlFlowNode for request | sendgrid_mail.py:37:41:37:68 | ControlFlowNode for Subscript | Cross-site scripting vulnerability due to $@. | sendgrid_mail.py:37:41:37:47 | ControlFlowNode for request | a user-provided value | +| sendgrid_via_mail_send_post_request_body_bad.py:16:26:16:79 | ControlFlowNode for Attribute() | sendgrid_via_mail_send_post_request_body_bad.py:16:51:16:57 | ControlFlowNode for request | sendgrid_via_mail_send_post_request_body_bad.py:16:26:16:79 | ControlFlowNode for Attribute() | Cross-site scripting vulnerability due to $@. | sendgrid_via_mail_send_post_request_body_bad.py:16:51:16:57 | ControlFlowNode for request | a user-provided value | +| sendgrid_via_mail_send_post_request_body_bad.py:27:25:27:77 | ControlFlowNode for Attribute() | sendgrid_via_mail_send_post_request_body_bad.py:16:51:16:57 | ControlFlowNode for request | sendgrid_via_mail_send_post_request_body_bad.py:27:25:27:77 | ControlFlowNode for Attribute() | Cross-site scripting vulnerability due to $@. | sendgrid_via_mail_send_post_request_body_bad.py:16:51:16:57 | ControlFlowNode for request | a user-provided value | +| sendgrid_via_mail_send_post_request_body_bad.py:27:25:27:77 | ControlFlowNode for Attribute() | sendgrid_via_mail_send_post_request_body_bad.py:27:50:27:56 | ControlFlowNode for request | sendgrid_via_mail_send_post_request_body_bad.py:27:25:27:77 | ControlFlowNode for Attribute() | Cross-site scripting vulnerability due to $@. | sendgrid_via_mail_send_post_request_body_bad.py:27:50:27:56 | ControlFlowNode for request | a user-provided value | +| sendgrid_via_mail_send_post_request_body_bad.py:41:25:41:79 | ControlFlowNode for Attribute() | sendgrid_via_mail_send_post_request_body_bad.py:16:51:16:57 | ControlFlowNode for request | sendgrid_via_mail_send_post_request_body_bad.py:41:25:41:79 | ControlFlowNode for Attribute() | Cross-site scripting vulnerability due to $@. | sendgrid_via_mail_send_post_request_body_bad.py:16:51:16:57 | ControlFlowNode for request | a user-provided value | +| sendgrid_via_mail_send_post_request_body_bad.py:41:25:41:79 | ControlFlowNode for Attribute() | sendgrid_via_mail_send_post_request_body_bad.py:27:50:27:56 | ControlFlowNode for request | sendgrid_via_mail_send_post_request_body_bad.py:41:25:41:79 | ControlFlowNode for Attribute() | Cross-site scripting vulnerability due to $@. | sendgrid_via_mail_send_post_request_body_bad.py:27:50:27:56 | ControlFlowNode for request | a user-provided value | +| sendgrid_via_mail_send_post_request_body_bad.py:41:25:41:79 | ControlFlowNode for Attribute() | sendgrid_via_mail_send_post_request_body_bad.py:41:50:41:56 | ControlFlowNode for request | sendgrid_via_mail_send_post_request_body_bad.py:41:25:41:79 | ControlFlowNode for Attribute() | Cross-site scripting vulnerability due to $@. | sendgrid_via_mail_send_post_request_body_bad.py:41:50:41:56 | ControlFlowNode for request | a user-provided value | +| smtplib_bad_subparts.py:24:22:24:25 | ControlFlowNode for html | smtplib_bad_subparts.py:17:12:17:18 | ControlFlowNode for request | smtplib_bad_subparts.py:24:22:24:25 | ControlFlowNode for html | Cross-site scripting vulnerability due to $@. | smtplib_bad_subparts.py:17:12:17:18 | ControlFlowNode for request | a user-provided value | +| smtplib_bad_via_attach.py:27:22:27:25 | ControlFlowNode for html | smtplib_bad_via_attach.py:20:12:20:18 | ControlFlowNode for request | smtplib_bad_via_attach.py:27:22:27:25 | ControlFlowNode for html | Cross-site scripting vulnerability due to $@. | smtplib_bad_via_attach.py:20:12:20:18 | ControlFlowNode for request | a user-provided value | diff --git a/python/ql/test/experimental/query-tests/Security/CWE-079/ReflectedXSS.qlref b/python/ql/test/experimental/query-tests/Security/CWE-079/ReflectedXSS.qlref new file mode 100644 index 00000000000..dec87309b29 --- /dev/null +++ b/python/ql/test/experimental/query-tests/Security/CWE-079/ReflectedXSS.qlref @@ -0,0 +1 @@ +experimental/Security/CWE-079/ReflectedXSS.ql diff --git a/python/ql/test/experimental/query-tests/Security/CWE-079/django_mail.py b/python/ql/test/experimental/query-tests/Security/CWE-079/django_mail.py new file mode 100644 index 00000000000..178e8decc79 --- /dev/null +++ b/python/ql/test/experimental/query-tests/Security/CWE-079/django_mail.py @@ -0,0 +1,25 @@ +import django.http +from django.core.mail import send_mail, mail_admins, mail_managers + + +def django_response(request): + """ + The Django.core.mail#send_mail function source code can be found in the link below: + https://github.com/django/django/blob/ca9872905559026af82000e46cde6f7dedc897b6/django/core/mail/__init__.py#L38 + + send_mass_mail does not provide html_message as an argument to it's function. See the link below for more info: + https://github.com/django/django/blob/ca9872905559026af82000e46cde6f7dedc897b6/django/core/mail/__init__.py#L64 + """ + send_mail("Subject", "plain-text body", "from@example.com", + ["to@example.com"], html_message=django.http.request.GET.get("html")) + + +def django_response(request): + """ + The Django.core.mail#mail_admins and Django.core.mail#mail_managers functions source code can be found in the link below: + https://github.com/django/django/blob/ca9872905559026af82000e46cde6f7dedc897b6/django/core/mail/__init__.py#L90-L121 + """ + mail_admins("Subject", "plain-text body", + html_message=django.http.request.GET.get("html")) + mail_managers("Subject", "plain-text body", + html_message=django.http.request.GET.get("html")) diff --git a/python/ql/test/experimental/query-tests/Security/CWE-079/flask_mail.py b/python/ql/test/experimental/query-tests/Security/CWE-079/flask_mail.py new file mode 100644 index 00000000000..e8bdcc93634 --- /dev/null +++ b/python/ql/test/experimental/query-tests/Security/CWE-079/flask_mail.py @@ -0,0 +1,32 @@ +from flask import request, Flask +from flask_mail import Mail, Message + +app = Flask(__name__) +mail = Mail(app) + +@app.route("/send") +def send(): + msg = Message(subject="Subject", + sender="from@example.com", + recipients=["to@example.com"], + body="plain-text body", + html=request.args["html"]) + + # The message can contain a body and/or HTML: + msg.body = "plain-text body" + # The email's HTML can be set via msg.html or as an initialize argument when creating a Message object. + msg.html = request.args["html"] + + mail.send(msg) + +@app.route("/connect") +def connect(): + """ + Minimal example to test mail.connect() usage + """ + with mail.connect() as conn: + msg = Message(subject="Subject", + sender="from@example.com", + recipients=["to@example.com"], + html=request.args["html"]) + conn.send(msg) diff --git a/python/ql/test/experimental/query-tests/Security/CWE-079/sendgrid_mail.py b/python/ql/test/experimental/query-tests/Security/CWE-079/sendgrid_mail.py new file mode 100644 index 00000000000..e10e8a030a8 --- /dev/null +++ b/python/ql/test/experimental/query-tests/Security/CWE-079/sendgrid_mail.py @@ -0,0 +1,57 @@ +from flask import request, Flask +from sendgrid import SendGridAPIClient +from sendgrid.helpers.mail import Mail, Email, To, Content, MimeType, HtmlContent + +app = Flask(__name__) + + +@app.route("/send") +def send(): + message = Mail( + from_email='from_email@example.com', + to_emails='to@example.com', + subject='Sending with Twilio SendGrid is Fun', + html_content=request.args["html_content"]) + + sg = SendGridAPIClient('SENDGRID_API_KEY') + sg.send(message) + + +@app.route("/send-HtmlContent") +def send(): + message = Mail( + from_email='from_email@example.com', + to_emails='to@example.com', + subject='Sending with Twilio SendGrid is Fun', + html_content=HtmlContent(request.args["html_content"])) + + sg = SendGridAPIClient('SENDGRID_API_KEY') + sg.send(message) + + +@app.route("/send_post") +def send_post(): + from_email = Email("test@example.com") + to_email = To("test@example.com") + subject = "Sending with SendGrid is Fun" + html_content = Content("text/html", request.args["html_content"]) + plain_content = Content("text/plain", request.args["plain_content"]) + + mail = Mail(from_email, to_email, subject, plain_content, html_content) + + sg = SendGridAPIClient(api_key='SENDGRID_API_KEY') + response = sg.client.mail.send.post(request_body=mail.get()) + + +@app.route("/send_post2") +def send_post2(): + from_email = Email("test@example.com") + to_email = To("test@example.com") + subject = "Sending with SendGrid is Fun" + html_content = Content(MimeType.html, request.args["html_content"]) + plain_content = Content(MimeType.text, request.args["plain_content"]) + + mail = Mail(from_email, to_email, subject, plain_content, html_content) + + sg = SendGridAPIClient(api_key='SENDGRID_API_KEY') + response = sg.client.mail.send.post(request_body=mail.get()) diff --git a/python/ql/test/experimental/query-tests/Security/CWE-079/sendgrid_via_mail_send_post_request_body_bad.py b/python/ql/test/experimental/query-tests/Security/CWE-079/sendgrid_via_mail_send_post_request_body_bad.py new file mode 100644 index 00000000000..fca641057da --- /dev/null +++ b/python/ql/test/experimental/query-tests/Security/CWE-079/sendgrid_via_mail_send_post_request_body_bad.py @@ -0,0 +1,48 @@ +import sendgrid +import os +from flask import request, Flask + +app = Flask(__name__) + + +@app.route("/sendgrid") +def send(): + sg = sendgrid.SendGridAPIClient(os.environ.get('SENDGRID_API_KEY')) + + data = { + "content": [ + { + "type": "text/html", + "value": "{}".format(request.args["html_content"]) + } + ], + "from": { + "email": "sam.smith@example.com", + "name": "Sam Smith" + }, + "headers": {}, + "mail_settings": { + "footer": { + "enable": True, + "html": "{}".format(request.args["html_footer"]), + "text": "Thanks,/n The SendGrid Team" + }, + }, + "reply_to": { + "email": "sam.smith@example.com", + "name": "Sam Smith" + }, + "send_at": 1409348513, + "subject": "Hello, World!", + "template_id": "[YOUR TEMPLATE ID GOES HERE]", + "tracking_settings": { + "subscription_tracking": { + "enable": True, + "html": "{}".format(request.args["html_tracking"]), + "substitution_tag": "<%click here%>", + "text": "If you would like to unsubscribe and stop receiving these emails <% click here %>." + } + } + } + + response = sg.client.mail.send.post(request_body=data) diff --git a/python/ql/test/experimental/query-tests/Security/CWE-079/smtplib_bad_subparts.py b/python/ql/test/experimental/query-tests/Security/CWE-079/smtplib_bad_subparts.py new file mode 100644 index 00000000000..209bd889393 --- /dev/null +++ b/python/ql/test/experimental/query-tests/Security/CWE-079/smtplib_bad_subparts.py @@ -0,0 +1,42 @@ +# This test checks that the developer doesn't pass a MIMEText instance to a MIMEMultipart initializer via the subparts parameter. +from flask import Flask, request +import json +import smtplib +import ssl +from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart + +app = Flask(__name__) + + +@app.route("/") +def email_person(): + sender_email = "sender@gmail.com" + receiver_email = "receiver@example.com" + + name = request.args['search'] + # Create the plain-text and HTML version of your message + text = "hello there" + html = f"hello {name}" + + # Turn these into plain/html MIMEText objects + part1 = MIMEText(text, "plain") + part2 = MIMEText(html, "html") + + message = MIMEMultipart(_subparts=(part1, part2)) + message["Subject"] = "multipart test" + message["From"] = sender_email + message["To"] = receiver_email + + # Create secure connection with server and send email + context = ssl.create_default_context() + server = smtplib.SMTP_SSL("smtp.gmail.com", 465, context=context) + + server.login(sender_email, "SERVER_PASSWORD") + server.sendmail( + sender_email, receiver_email, message.as_string() + ) + + +# if __name__ == "__main__": +# app.run(debug=True) diff --git a/python/ql/test/experimental/query-tests/Security/CWE-079/smtplib_bad_via_attach.py b/python/ql/test/experimental/query-tests/Security/CWE-079/smtplib_bad_via_attach.py new file mode 100644 index 00000000000..48a228b0bc6 --- /dev/null +++ b/python/ql/test/experimental/query-tests/Security/CWE-079/smtplib_bad_via_attach.py @@ -0,0 +1,45 @@ +# This test checks that the developer doesn't pass a MIMEText instance to a MIMEMultipart message. +from flask import Flask, request +import json +import smtplib, ssl +from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart + +app = Flask(__name__) + +@app.route("/") +def email_person(): + sender_email = "sender@gmail.com" + receiver_email = "receiver@example.com" + + message = MIMEMultipart("alternative") + message["Subject"] = "multipart test" + message["From"] = sender_email + message["To"] = receiver_email + + name = request.args['name'] + # Create the plain-text and HTML version of your message + text = "hello there" + html = f"hello {name}" + + # Turn these into plain/html MIMEText objects + part1 = MIMEText(text, "plain") + part2 = MIMEText(html, "html") + + # Add HTML/plain-text parts to MIMEMultipart message + # The email client will try to render the last part first + message.attach(part1) + message.attach(part2) + + # Create secure connection with server and send email + context = ssl.create_default_context() + server = smtplib.SMTP_SSL("smtp.gmail.com", 465, context=context) + + server.login(sender_email, "SERVER_PASSWORD") + server.sendmail( + sender_email, receiver_email, message.as_string() + ) + + +# if __name__ == "__main__": +# app.run(debug=True)