From 24d4415457d65f50b78e39830f88dba3cb8788a9 Mon Sep 17 00:00:00 2001 From: thank_you Date: Mon, 21 Jun 2021 19:01:04 -0400 Subject: [PATCH 01/56] Create EmailClients.qll --- .../semmle/python/EmailClients.qll | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 python/ql/src/experimental/semmle/python/EmailClients.qll diff --git a/python/ql/src/experimental/semmle/python/EmailClients.qll b/python/ql/src/experimental/semmle/python/EmailClients.qll new file mode 100644 index 00000000000..fe6a3a348b5 --- /dev/null +++ b/python/ql/src/experimental/semmle/python/EmailClients.qll @@ -0,0 +1,80 @@ +import python + +/** + * An operation that sends an email. + */ +abstract class EmailSender extends DataFlow::CallCfgNode { + /** + * Gets a data flow node holding the plaintext version of the email body. + */ + abstract ControlFlowNode getPlainTextBody(); + + /** + * Gets a data flow node holding the html version of the email body. + */ + abstract ControlFlowNode 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(); +}\ + +class FlaskMailEmailSender extends EmailSender { + FlaskMailEmailSender() { + this = + API::moduleImport("flask_mail").getMember("Mail").getReturn().getMember("send").getACall() + } + override ControlFlowNode getPlainTextBody() { + exists(API::Node message | + message = API::moduleImport("flask_mail").getMember("Message").getReturn() and + getArg(0) = message.getAUse() and + result = message.getAUse().getALocalSource().asCfgNode().(CallNode).getArgByName("body") + ) + } + override ControlFlowNode getHtmlBody() { + exists(API::Node message | + message = API::moduleImport("flask_mail").getMember("Message").getReturn() and + getArg(0) = message.getAUse() and + result = message.getAUse().getALocalSource().asCfgNode().(CallNode).getArgByName("html") + ) or + exists(API::Node message, DataFlow::AttrWrite htmlAttr | + message = API::moduleImport("flask_mail").getMember("Message").getReturn() and + htmlAttr.getAttributeName() = "html" and + getArg(0) = message.getAUse() and + htmlAttr.getObject() = message.getAUse() and + result = htmlAttr.getValue().asCfgNode() + ) + } + override ControlFlowNode getTo() { + exists(API::Node message | + message = API::moduleImport("flask_mail").getMember("Message").getReturn() and + getArg(0) = message.getAUse() and + result = message.getAUse().getALocalSource().asCfgNode().(CallNode).getArgByName("recipients") + ) + } + override ControlFlowNode getFrom() { + exists(API::Node message | + message = API::moduleImport("flask_mail").getMember("Message").getReturn() and + getArg(0) = message.getAUse() and + result = message.getAUse().getALocalSource().asCfgNode().(CallNode).getArgByName("sender") + ) + } + override ControlFlowNode getSubject() { + exists(API::Node message | + message = API::moduleImport("flask_mail").getMember("Message").getReturn() and + getArg(0) = message.getAUse() and + result = message.getAUse().getALocalSource().asCfgNode().(CallNode).getArgByName("subject") + ) + } +} From c3eba25b0c78300e9dc3d68e1711519369fe1226 Mon Sep 17 00:00:00 2001 From: thank_you Date: Mon, 21 Jun 2021 19:02:20 -0400 Subject: [PATCH 02/56] Add query tests Most of these query tests need to be cleaned up. Also, some of these query tests will fail because no user-tainted data is passing into the email bodies that are generated and sent to a victim user. --- .../Security/CWE-079/flask_mail_bad_1.py | 14 ++ .../Security/CWE-079/flask_mail_bad_2.py | 14 ++ .../CWE-079/flask_mail_bulk_email_bad.py | 16 ++ .../sendgrid_mail_helper_content_bad.py | 16 ++ ...rid_via_mail_send_post_request_body_bad.py | 148 ++++++++++++++++++ .../Security/CWE-079/smtplib_bad_subparts.py | 43 +++++ .../CWE-079/smtplib_bad_via_attach.py | 47 ++++++ 7 files changed, 298 insertions(+) create mode 100644 python/ql/test/experimental/query-tests/Security/CWE-079/flask_mail_bad_1.py create mode 100644 python/ql/test/experimental/query-tests/Security/CWE-079/flask_mail_bad_2.py create mode 100644 python/ql/test/experimental/query-tests/Security/CWE-079/flask_mail_bulk_email_bad.py create mode 100644 python/ql/test/experimental/query-tests/Security/CWE-079/sendgrid_mail_helper_content_bad.py create mode 100644 python/ql/test/experimental/query-tests/Security/CWE-079/sendgrid_via_mail_send_post_request_body_bad.py create mode 100644 python/ql/test/experimental/query-tests/Security/CWE-079/smtplib_bad_subparts.py create mode 100644 python/ql/test/experimental/query-tests/Security/CWE-079/smtplib_bad_via_attach.py diff --git a/python/ql/test/experimental/query-tests/Security/CWE-079/flask_mail_bad_1.py b/python/ql/test/experimental/query-tests/Security/CWE-079/flask_mail_bad_1.py new file mode 100644 index 00000000000..91ffcf8f0be --- /dev/null +++ b/python/ql/test/experimental/query-tests/Security/CWE-079/flask_mail_bad_1.py @@ -0,0 +1,14 @@ +# This tests that the user doesn't pass user-tainted data into the msg.html attribute. +# source: https://pythonhosted.org/Flask-Mail/ +from flask_mail import Message + +@app.route("/") +def index(): + + msg = Message("Hello", + sender="from@example.com", + recipients=["to@example.com"]) + + msg.html = "testing" + + mail.send(msg) diff --git a/python/ql/test/experimental/query-tests/Security/CWE-079/flask_mail_bad_2.py b/python/ql/test/experimental/query-tests/Security/CWE-079/flask_mail_bad_2.py new file mode 100644 index 00000000000..3f0a11a4e74 --- /dev/null +++ b/python/ql/test/experimental/query-tests/Security/CWE-079/flask_mail_bad_2.py @@ -0,0 +1,14 @@ +# This tests that the user doesn't pass user-tainted data into the msg html initialized argument. +# source: https://pythonhosted.org/Flask-Mail/ + +from flask_mail import Message + +@app.route("/") +def index(): + + msg = Message("Hello", + sender="from@example.com", + recipients=["to@example.com"], + html="testing") + + mail.send(msg) diff --git a/python/ql/test/experimental/query-tests/Security/CWE-079/flask_mail_bulk_email_bad.py b/python/ql/test/experimental/query-tests/Security/CWE-079/flask_mail_bulk_email_bad.py new file mode 100644 index 00000000000..b1747762848 --- /dev/null +++ b/python/ql/test/experimental/query-tests/Security/CWE-079/flask_mail_bulk_email_bad.py @@ -0,0 +1,16 @@ +# This tests that the user can't send multiple vulnerable emails. +# source: https://pythonhosted.org/Flask-Mail/ + +from flask_mail import Message + +@app.route("/") +def index(): + with mail.connect() as conn: + for user in users: + message = '...' + subject = "hello, %s" % user.name + msg = Message(recipients=[user.email], + html=message, + subject=subject) + + conn.send(msg) diff --git a/python/ql/test/experimental/query-tests/Security/CWE-079/sendgrid_mail_helper_content_bad.py b/python/ql/test/experimental/query-tests/Security/CWE-079/sendgrid_mail_helper_content_bad.py new file mode 100644 index 00000000000..0f693f5a3e5 --- /dev/null +++ b/python/ql/test/experimental/query-tests/Security/CWE-079/sendgrid_mail_helper_content_bad.py @@ -0,0 +1,16 @@ +# This tests that the developer doesn't pass content via the Content class initializer. +# source:https://github.com/sendgrid/sendgrid-python + +import sendgrid +import os +from sendgrid.helpers.mail import * + +sg = sendgrid.SendGridAPIClient(api_key=os.environ.get('SENDGRID_API_KEY')) +from_email = Email("test@example.com") +to_email = To("test@example.com") +subject = "Sending with SendGrid is Fun" +content = Content("text/html", "and easy to do anywhere, even with Python") # Content can also take the MimeType.html as the first arg here. Need to create a separate example for this. + +mail = Mail(from_email, to_email, subject, content) + +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..ef9c6edfca8 --- /dev/null +++ b/python/ql/test/experimental/query-tests/Security/CWE-079/sendgrid_via_mail_send_post_request_body_bad.py @@ -0,0 +1,148 @@ +# This tests that the developer doesn't pass tainted user data into the mail.send.post() method in the SendGrid library. +# source :https://github.com/sendgrid/sendgrid-python +import sendgrid +import os + + +sg = sendgrid.SendGridAPIClient(os.environ.get('SENDGRID_API_KEY')) + +data = { + "asm": { + "group_id": 1, + "groups_to_display": [ + 1, + 2, + 3 + ] + }, + "attachments": [ + { + "content": "[BASE64 encoded content block here]", + "content_id": "ii_139db99fdb5c3704", + "disposition": "inline", + "filename": "file1.jpg", + "name": "file1", + "type": "jpg" + } + ], + "batch_id": "[YOUR BATCH ID GOES HERE]", + "categories": [ + "category1", + "category2" + ], + "content": [ + { + "type": "text/html", + "value": "

Hello, world!

" + } + ], + "custom_args": { + "New Argument 1": "New Value 1", + "activationAttempt": "1", + "customerAccountNumber": "[CUSTOMER ACCOUNT NUMBER GOES HERE]" + }, + "from": { + "email": "sam.smith@example.com", + "name": "Sam Smith" + }, + "headers": {}, + "ip_pool_name": "[YOUR POOL NAME GOES HERE]", + "mail_settings": { + "bcc": { + "email": "ben.doe@example.com", + "enable": True + }, + "bypass_list_management": { + "enable": True + }, + "footer": { + "enable": True, + "html": "

Thanks
The SendGrid Team

", + "text": "Thanks,/n The SendGrid Team" + }, + "sandbox_mode": { + "enable": False + }, + "spam_check": { + "enable": True, + "post_to_url": "http://example.com/compliance", + "threshold": 3 + } + }, + "personalizations": [ + { + "bcc": [ + { + "email": "sam.doe@example.com", + "name": "Sam Doe" + } + ], + "cc": [ + { + "email": "jane.doe@example.com", + "name": "Jane Doe" + } + ], + "custom_args": { + "New Argument 1": "New Value 1", + "activationAttempt": "1", + "customerAccountNumber": "[CUSTOMER ACCOUNT NUMBER GOES HERE]" + }, + "headers": { + "X-Accept-Language": "en", + "X-Mailer": "MyApp" + }, + "send_at": 1409348513, + "subject": "Hello, World!", + "substitutions": { + "id": "substitutions", + "type": "object" + }, + "to": [ + { + "email": "john.doe@example.com", + "name": "John Doe" + } + ] + } + ], + "reply_to": { + "email": "sam.smith@example.com", + "name": "Sam Smith" + }, + "sections": { + "section": { + ":sectionName1": "section 1 text", + ":sectionName2": "section 2 text" + } + }, + "send_at": 1409348513, + "subject": "Hello, World!", + "template_id": "[YOUR TEMPLATE ID GOES HERE]", + "tracking_settings": { + "click_tracking": { + "enable": True, + "enable_text": True + }, + "ganalytics": { + "enable": True, + "utm_campaign": "[NAME OF YOUR REFERRER SOURCE]", + "utm_content": "[USE THIS SPACE TO DIFFERENTIATE YOUR EMAIL FROM ADS]", + "utm_medium": "[NAME OF YOUR MARKETING MEDIUM e.g. email]", + "utm_name": "[NAME OF YOUR CAMPAIGN]", + "utm_term": "[IDENTIFY PAID KEYWORDS HERE]" + }, + "open_tracking": { + "enable": True, + "substitution_tag": "%opentrack" + }, + "subscription_tracking": { + "enable": True, + "html": "If you would like to unsubscribe and stop receiving these emails <% clickhere %>.", + "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..7bd243bdd5e --- /dev/null +++ b/python/ql/test/experimental/query-tests/Security/CWE-079/smtplib_bad_subparts.py @@ -0,0 +1,43 @@ +# This test checks that the developer doesn't pass a MIMEText instance to a MIMEMultipart initializer via the subparts parameter. + +# source꞉ https꞉//www.programcreek.com/python/example/53141/email.MIMEMultipart.MIMEMultipart +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 = "my@gmail.com" + receiver_email = "your@gmail.com" + password = input("Type your password and press enter:") + + 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, 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..af6de1dda5e --- /dev/null +++ b/python/ql/test/experimental/query-tests/Security/CWE-079/smtplib_bad_via_attach.py @@ -0,0 +1,47 @@ +# This test checks that the developer doesn't pass a MIMEText instance to a MIMEMultipart message. +# source꞉ https꞉//realpython.com/python-send-email/ +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 = "my@gmail.com" + receiver_email = "your@gmail.com" + password = input("Type your password and press enter:") + + 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}" # here is the exploit. passing vulnerable data into the html portion of the email + + # 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, password) + server.sendmail( + sender_email, receiver_email, message.as_string() + ) + + +# if __name__ == "__main__": +# app.run(debug=True) From 20f321e623a6f3fc61fcc6e87628ec63ab449e89 Mon Sep 17 00:00:00 2001 From: thank_you Date: Tue, 22 Jun 2021 13:03:23 -0400 Subject: [PATCH 03/56] Remove accidental slash --- python/ql/src/experimental/semmle/python/EmailClients.qll | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/ql/src/experimental/semmle/python/EmailClients.qll b/python/ql/src/experimental/semmle/python/EmailClients.qll index fe6a3a348b5..6892aefb943 100644 --- a/python/ql/src/experimental/semmle/python/EmailClients.qll +++ b/python/ql/src/experimental/semmle/python/EmailClients.qll @@ -28,7 +28,7 @@ abstract class EmailSender extends DataFlow::CallCfgNode { * Gets a data flow node holding the subject of the email. */ abstract DataFlow::Node getSubject(); -}\ +} class FlaskMailEmailSender extends EmailSender { FlaskMailEmailSender() { From 48cd5062cfad19025c51a709d5128383a7e51ccb Mon Sep 17 00:00:00 2001 From: jorgectf Date: Wed, 23 Jun 2021 00:37:54 +0200 Subject: [PATCH 04/56] Change EmailSender structure --- .../experimental/semmle/python/Concepts.qll | 35 ++++++++ .../semmle/python/EmailClients.qll | 80 ------------------- 2 files changed, 35 insertions(+), 80 deletions(-) delete mode 100644 python/ql/src/experimental/semmle/python/EmailClients.qll diff --git a/python/ql/src/experimental/semmle/python/Concepts.qll b/python/ql/src/experimental/semmle/python/Concepts.qll index 7641ac1becf..6d815e13612 100644 --- a/python/ql/src/experimental/semmle/python/Concepts.qll +++ b/python/ql/src/experimental/semmle/python/Concepts.qll @@ -146,3 +146,38 @@ class LDAPEscape extends DataFlow::Node { */ DataFlow::Node getAnInput() { result = range.getAnInput() } } + +/** + * An operation that sends an email. + */ +abstract class EmailSender extends DataFlow::CallCfgNode { + /** + * 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(); + + /** + * Gets a data flow node that refers to the HTML body or plaintext body of the email. + */ + DataFlow::Node getABody() { result in [getPlainTextBody(), getHtmlBody()] } +} diff --git a/python/ql/src/experimental/semmle/python/EmailClients.qll b/python/ql/src/experimental/semmle/python/EmailClients.qll deleted file mode 100644 index 6892aefb943..00000000000 --- a/python/ql/src/experimental/semmle/python/EmailClients.qll +++ /dev/null @@ -1,80 +0,0 @@ -import python - -/** - * An operation that sends an email. - */ -abstract class EmailSender extends DataFlow::CallCfgNode { - /** - * Gets a data flow node holding the plaintext version of the email body. - */ - abstract ControlFlowNode getPlainTextBody(); - - /** - * Gets a data flow node holding the html version of the email body. - */ - abstract ControlFlowNode 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(); -} - -class FlaskMailEmailSender extends EmailSender { - FlaskMailEmailSender() { - this = - API::moduleImport("flask_mail").getMember("Mail").getReturn().getMember("send").getACall() - } - override ControlFlowNode getPlainTextBody() { - exists(API::Node message | - message = API::moduleImport("flask_mail").getMember("Message").getReturn() and - getArg(0) = message.getAUse() and - result = message.getAUse().getALocalSource().asCfgNode().(CallNode).getArgByName("body") - ) - } - override ControlFlowNode getHtmlBody() { - exists(API::Node message | - message = API::moduleImport("flask_mail").getMember("Message").getReturn() and - getArg(0) = message.getAUse() and - result = message.getAUse().getALocalSource().asCfgNode().(CallNode).getArgByName("html") - ) or - exists(API::Node message, DataFlow::AttrWrite htmlAttr | - message = API::moduleImport("flask_mail").getMember("Message").getReturn() and - htmlAttr.getAttributeName() = "html" and - getArg(0) = message.getAUse() and - htmlAttr.getObject() = message.getAUse() and - result = htmlAttr.getValue().asCfgNode() - ) - } - override ControlFlowNode getTo() { - exists(API::Node message | - message = API::moduleImport("flask_mail").getMember("Message").getReturn() and - getArg(0) = message.getAUse() and - result = message.getAUse().getALocalSource().asCfgNode().(CallNode).getArgByName("recipients") - ) - } - override ControlFlowNode getFrom() { - exists(API::Node message | - message = API::moduleImport("flask_mail").getMember("Message").getReturn() and - getArg(0) = message.getAUse() and - result = message.getAUse().getALocalSource().asCfgNode().(CallNode).getArgByName("sender") - ) - } - override ControlFlowNode getSubject() { - exists(API::Node message | - message = API::moduleImport("flask_mail").getMember("Message").getReturn() and - getArg(0) = message.getAUse() and - result = message.getAUse().getALocalSource().asCfgNode().(CallNode).getArgByName("subject") - ) - } -} From 4d890ddeae60b641ae5d9b0691f68e9313b73ac3 Mon Sep 17 00:00:00 2001 From: jorgectf Date: Wed, 23 Jun 2021 00:38:58 +0200 Subject: [PATCH 05/56] Polish flask_mail tests and code --- .../Security/CWE-079/unit_tests/flask_mail.py | 34 ++++++++ .../experimental/semmle/python/Frameworks.qll | 1 + .../semmle/python/frameworks/Flask.qll | 81 +++++++++++++++++++ 3 files changed, 116 insertions(+) create mode 100644 python/ql/src/experimental/Security/CWE-079/unit_tests/flask_mail.py create mode 100644 python/ql/src/experimental/semmle/python/frameworks/Flask.qll diff --git a/python/ql/src/experimental/Security/CWE-079/unit_tests/flask_mail.py b/python/ql/src/experimental/Security/CWE-079/unit_tests/flask_mail.py new file mode 100644 index 00000000000..7f02b6db10f --- /dev/null +++ b/python/ql/src/experimental/Security/CWE-079/unit_tests/flask_mail.py @@ -0,0 +1,34 @@ +# https://pythonhosted.org/Flask-Mail/ +# https://github.com/mattupstate/flask-mail/blob/1709c70d839a7cc7b1f7eeb97333b71cd420fe32/flask_mail.py#L239 + +# tmp: this test cover RFS to any part of the message, but can be shortened to a specific part (body&html) once we decide the objective of the query. +from flask_mail import Mail, Message + +app = Flask(__name__) +mail = Mail(app) + +@app.route("/send") +def send(): + msg = Message(subject=request.args["subject"], + sender=request.args["sender"], + recipients=list(request.args["recipient"]), + body=request.args["body"], + html=request.args["html"]) + + # The message can contain a body and/or HTML: + msg.body = "test" + msg.html = "test" + + mail.send(msg) + +@app.route("/connect") +def connect(): + """ + Minimal example to test mail.connect() usage + """ + with mail.connect() as conn: + msg = Message(subject=request.args["subject"], + sender=request.args["sender"], + recipients=list(request.args["recipient"]), + body=request.args["html"]) + conn.send(msg) diff --git a/python/ql/src/experimental/semmle/python/Frameworks.qll b/python/ql/src/experimental/semmle/python/Frameworks.qll index 5a77fc63a7d..75df222a060 100644 --- a/python/ql/src/experimental/semmle/python/Frameworks.qll +++ b/python/ql/src/experimental/semmle/python/Frameworks.qll @@ -4,3 +4,4 @@ private import experimental.semmle.python.frameworks.Stdlib private import experimental.semmle.python.frameworks.LDAP +private import experimental.semmle.python.frameworks.Flask diff --git a/python/ql/src/experimental/semmle/python/frameworks/Flask.qll b/python/ql/src/experimental/semmle/python/frameworks/Flask.qll new file mode 100644 index 00000000000..88e0fbef4f0 --- /dev/null +++ b/python/ql/src/experimental/semmle/python/frameworks/Flask.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 + +private module Flask { + private API::Node flaskMail() { result = API::moduleImport("flask_mail") } + + private API::Node flaskMailInstance() { result = flaskMail().getMember("Mail").getReturn() } + + private DataFlow::CallCfgNode flaskMessageCall() { + result = flaskMail().getMember("Message").getACall() + } + + private class FlaskMail extends DataFlow::CallCfgNode, EmailSender { + /** A message variable to avoid multiple results in case consequential results are needed */ + DataFlow::CallCfgNode message; + + FlaskMail() { + this = + [flaskMailInstance(), flaskMailInstance().getMember("connect").getReturn()] + .getMember("send") + .getACall() + } + + override DataFlow::Node getPlainTextBody() { + result in [flaskMessageCall().getArg(2), flaskMessageCall().getArgByName("body")] + or + exists(DataFlow::AttrWrite bodyWrite | + bodyWrite.getObject().getALocalSource() = flaskMessageCall() and + bodyWrite.getAttributeName() = "body" and + result = bodyWrite.getValue() + ) + } + + override DataFlow::Node getHtmlBody() { + result in [flaskMessageCall().getArg(3), flaskMessageCall().getArgByName("html")] + or + exists(DataFlow::AttrWrite bodyWrite | + bodyWrite.getObject().getALocalSource() = flaskMessageCall() and + bodyWrite.getAttributeName() = "html" and + result = bodyWrite.getValue() + ) + } + + override DataFlow::Node getTo() { + result in [flaskMessageCall().getArg(1), flaskMessageCall().getArgByName("recipients")] + or + exists(DataFlow::AttrWrite bodyWrite | + bodyWrite.getObject().getALocalSource() = flaskMessageCall() and + bodyWrite.getAttributeName() = "recipients" and + result = bodyWrite.getValue() + ) + } + + override DataFlow::Node getFrom() { + result in [flaskMessageCall().getArg(5), flaskMessageCall().getArgByName("sender")] + or + exists(DataFlow::AttrWrite bodyWrite | + bodyWrite.getObject().getALocalSource() = flaskMessageCall() and + bodyWrite.getAttributeName() = "sender" and + result = bodyWrite.getValue() + ) + } + + override DataFlow::Node getSubject() { + result in [flaskMessageCall().getArg(0), flaskMessageCall().getArgByName("subject")] + or + exists(DataFlow::AttrWrite bodyWrite | + bodyWrite.getObject().getALocalSource() = flaskMessageCall() and + bodyWrite.getAttributeName() = "subject" and + result = bodyWrite.getValue() + ) + } + } +} From 7956b97ac36d50eae381e0f295120f3dbf0f48bb Mon Sep 17 00:00:00 2001 From: jorgectf Date: Wed, 23 Jun 2021 00:40:05 +0200 Subject: [PATCH 06/56] Unit tests move and temporary ql --- .../ql/src/experimental/Security/CWE-079/test.ql | 3 +++ .../sendgrid_mail_helper_content_bad.py | 0 ...ndgrid_via_mail_send_post_request_body_bad.py | 0 .../CWE-079/unit_tests}/smtplib_bad_subparts.py | 0 .../unit_tests}/smtplib_bad_via_attach.py | 0 .../Security/CWE-079/unit_tests/test.actual | 1 + .../Security/CWE-079/unit_tests/test.qlref | 1 + .../query-tests/Security/CWE-079/.gitkeep | 0 .../Security/CWE-079/flask_mail_bad_1.py | 14 -------------- .../Security/CWE-079/flask_mail_bad_2.py | 14 -------------- .../CWE-079/flask_mail_bulk_email_bad.py | 16 ---------------- 11 files changed, 5 insertions(+), 44 deletions(-) create mode 100644 python/ql/src/experimental/Security/CWE-079/test.ql rename python/ql/{test/experimental/query-tests/Security/CWE-079 => src/experimental/Security/CWE-079/unit_tests}/sendgrid_mail_helper_content_bad.py (100%) rename python/ql/{test/experimental/query-tests/Security/CWE-079 => src/experimental/Security/CWE-079/unit_tests}/sendgrid_via_mail_send_post_request_body_bad.py (100%) rename python/ql/{test/experimental/query-tests/Security/CWE-079 => src/experimental/Security/CWE-079/unit_tests}/smtplib_bad_subparts.py (100%) rename python/ql/{test/experimental/query-tests/Security/CWE-079 => src/experimental/Security/CWE-079/unit_tests}/smtplib_bad_via_attach.py (100%) create mode 100644 python/ql/src/experimental/Security/CWE-079/unit_tests/test.actual create mode 100644 python/ql/src/experimental/Security/CWE-079/unit_tests/test.qlref create mode 100644 python/ql/test/experimental/query-tests/Security/CWE-079/.gitkeep delete mode 100644 python/ql/test/experimental/query-tests/Security/CWE-079/flask_mail_bad_1.py delete mode 100644 python/ql/test/experimental/query-tests/Security/CWE-079/flask_mail_bad_2.py delete mode 100644 python/ql/test/experimental/query-tests/Security/CWE-079/flask_mail_bulk_email_bad.py diff --git a/python/ql/src/experimental/Security/CWE-079/test.ql b/python/ql/src/experimental/Security/CWE-079/test.ql new file mode 100644 index 00000000000..e30d45c0c3f --- /dev/null +++ b/python/ql/src/experimental/Security/CWE-079/test.ql @@ -0,0 +1,3 @@ +select "1" +// void query to run and generate unit_tests.testproj database to test +// until we decide the objective of the query diff --git a/python/ql/test/experimental/query-tests/Security/CWE-079/sendgrid_mail_helper_content_bad.py b/python/ql/src/experimental/Security/CWE-079/unit_tests/sendgrid_mail_helper_content_bad.py similarity index 100% rename from python/ql/test/experimental/query-tests/Security/CWE-079/sendgrid_mail_helper_content_bad.py rename to python/ql/src/experimental/Security/CWE-079/unit_tests/sendgrid_mail_helper_content_bad.py diff --git a/python/ql/test/experimental/query-tests/Security/CWE-079/sendgrid_via_mail_send_post_request_body_bad.py b/python/ql/src/experimental/Security/CWE-079/unit_tests/sendgrid_via_mail_send_post_request_body_bad.py similarity index 100% rename from python/ql/test/experimental/query-tests/Security/CWE-079/sendgrid_via_mail_send_post_request_body_bad.py rename to python/ql/src/experimental/Security/CWE-079/unit_tests/sendgrid_via_mail_send_post_request_body_bad.py diff --git a/python/ql/test/experimental/query-tests/Security/CWE-079/smtplib_bad_subparts.py b/python/ql/src/experimental/Security/CWE-079/unit_tests/smtplib_bad_subparts.py similarity index 100% rename from python/ql/test/experimental/query-tests/Security/CWE-079/smtplib_bad_subparts.py rename to python/ql/src/experimental/Security/CWE-079/unit_tests/smtplib_bad_subparts.py diff --git a/python/ql/test/experimental/query-tests/Security/CWE-079/smtplib_bad_via_attach.py b/python/ql/src/experimental/Security/CWE-079/unit_tests/smtplib_bad_via_attach.py similarity index 100% rename from python/ql/test/experimental/query-tests/Security/CWE-079/smtplib_bad_via_attach.py rename to python/ql/src/experimental/Security/CWE-079/unit_tests/smtplib_bad_via_attach.py diff --git a/python/ql/src/experimental/Security/CWE-079/unit_tests/test.actual b/python/ql/src/experimental/Security/CWE-079/unit_tests/test.actual new file mode 100644 index 00000000000..2a4f078a25f --- /dev/null +++ b/python/ql/src/experimental/Security/CWE-079/unit_tests/test.actual @@ -0,0 +1 @@ +| 1 | diff --git a/python/ql/src/experimental/Security/CWE-079/unit_tests/test.qlref b/python/ql/src/experimental/Security/CWE-079/unit_tests/test.qlref new file mode 100644 index 00000000000..01c9dd06163 --- /dev/null +++ b/python/ql/src/experimental/Security/CWE-079/unit_tests/test.qlref @@ -0,0 +1 @@ +experimental/Security/CWE-079/test.ql diff --git a/python/ql/test/experimental/query-tests/Security/CWE-079/.gitkeep b/python/ql/test/experimental/query-tests/Security/CWE-079/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/python/ql/test/experimental/query-tests/Security/CWE-079/flask_mail_bad_1.py b/python/ql/test/experimental/query-tests/Security/CWE-079/flask_mail_bad_1.py deleted file mode 100644 index 91ffcf8f0be..00000000000 --- a/python/ql/test/experimental/query-tests/Security/CWE-079/flask_mail_bad_1.py +++ /dev/null @@ -1,14 +0,0 @@ -# This tests that the user doesn't pass user-tainted data into the msg.html attribute. -# source: https://pythonhosted.org/Flask-Mail/ -from flask_mail import Message - -@app.route("/") -def index(): - - msg = Message("Hello", - sender="from@example.com", - recipients=["to@example.com"]) - - msg.html = "testing" - - mail.send(msg) diff --git a/python/ql/test/experimental/query-tests/Security/CWE-079/flask_mail_bad_2.py b/python/ql/test/experimental/query-tests/Security/CWE-079/flask_mail_bad_2.py deleted file mode 100644 index 3f0a11a4e74..00000000000 --- a/python/ql/test/experimental/query-tests/Security/CWE-079/flask_mail_bad_2.py +++ /dev/null @@ -1,14 +0,0 @@ -# This tests that the user doesn't pass user-tainted data into the msg html initialized argument. -# source: https://pythonhosted.org/Flask-Mail/ - -from flask_mail import Message - -@app.route("/") -def index(): - - msg = Message("Hello", - sender="from@example.com", - recipients=["to@example.com"], - html="testing") - - mail.send(msg) diff --git a/python/ql/test/experimental/query-tests/Security/CWE-079/flask_mail_bulk_email_bad.py b/python/ql/test/experimental/query-tests/Security/CWE-079/flask_mail_bulk_email_bad.py deleted file mode 100644 index b1747762848..00000000000 --- a/python/ql/test/experimental/query-tests/Security/CWE-079/flask_mail_bulk_email_bad.py +++ /dev/null @@ -1,16 +0,0 @@ -# This tests that the user can't send multiple vulnerable emails. -# source: https://pythonhosted.org/Flask-Mail/ - -from flask_mail import Message - -@app.route("/") -def index(): - with mail.connect() as conn: - for user in users: - message = '...' - subject = "hello, %s" % user.name - msg = Message(recipients=[user.email], - html=message, - subject=subject) - - conn.send(msg) From 4c9ecf0d9bc9aa5cd217effbdd89eaf1a33a72ac Mon Sep 17 00:00:00 2001 From: jorgectf Date: Wed, 23 Jun 2021 00:52:34 +0200 Subject: [PATCH 07/56] Delete testing class-variable --- python/ql/src/experimental/semmle/python/frameworks/Flask.qll | 3 --- 1 file changed, 3 deletions(-) diff --git a/python/ql/src/experimental/semmle/python/frameworks/Flask.qll b/python/ql/src/experimental/semmle/python/frameworks/Flask.qll index 88e0fbef4f0..5073d121880 100644 --- a/python/ql/src/experimental/semmle/python/frameworks/Flask.qll +++ b/python/ql/src/experimental/semmle/python/frameworks/Flask.qll @@ -18,9 +18,6 @@ private module Flask { } private class FlaskMail extends DataFlow::CallCfgNode, EmailSender { - /** A message variable to avoid multiple results in case consequential results are needed */ - DataFlow::CallCfgNode message; - FlaskMail() { this = [flaskMailInstance(), flaskMailInstance().getMember("connect").getReturn()] From ae84df817ac30a64663ae00f42ca1166a508c3ba Mon Sep 17 00:00:00 2001 From: jorgectf Date: Wed, 23 Jun 2021 17:08:28 +0200 Subject: [PATCH 08/56] Extend ReflectedXSS query --- .../Security/CWE-079/ReflectedXSS.ql | 23 +++++++++++++++ .../src/experimental/Security/CWE-079/test.ql | 3 -- .../CWE-079/unit_tests/ReflectedXSS.qlref | 1 + .../Security/CWE-079/unit_tests/flask_mail.py | 21 +++++++------- .../Security/CWE-079/unit_tests/test.actual | 1 - .../Security/CWE-079/unit_tests/test.qlref | 1 - .../python/security/dataflow/ReflectedXSS.qll | 29 +++++++++++++++++++ 7 files changed, 63 insertions(+), 16 deletions(-) create mode 100644 python/ql/src/experimental/Security/CWE-079/ReflectedXSS.ql delete mode 100644 python/ql/src/experimental/Security/CWE-079/test.ql create mode 100644 python/ql/src/experimental/Security/CWE-079/unit_tests/ReflectedXSS.qlref delete mode 100644 python/ql/src/experimental/Security/CWE-079/unit_tests/test.actual delete mode 100644 python/ql/src/experimental/Security/CWE-079/unit_tests/test.qlref create mode 100644 python/ql/src/experimental/semmle/python/security/dataflow/ReflectedXSS.qll 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/Security/CWE-079/test.ql b/python/ql/src/experimental/Security/CWE-079/test.ql deleted file mode 100644 index e30d45c0c3f..00000000000 --- a/python/ql/src/experimental/Security/CWE-079/test.ql +++ /dev/null @@ -1,3 +0,0 @@ -select "1" -// void query to run and generate unit_tests.testproj database to test -// until we decide the objective of the query diff --git a/python/ql/src/experimental/Security/CWE-079/unit_tests/ReflectedXSS.qlref b/python/ql/src/experimental/Security/CWE-079/unit_tests/ReflectedXSS.qlref new file mode 100644 index 00000000000..dec87309b29 --- /dev/null +++ b/python/ql/src/experimental/Security/CWE-079/unit_tests/ReflectedXSS.qlref @@ -0,0 +1 @@ +experimental/Security/CWE-079/ReflectedXSS.ql diff --git a/python/ql/src/experimental/Security/CWE-079/unit_tests/flask_mail.py b/python/ql/src/experimental/Security/CWE-079/unit_tests/flask_mail.py index 7f02b6db10f..8b1494b539f 100644 --- a/python/ql/src/experimental/Security/CWE-079/unit_tests/flask_mail.py +++ b/python/ql/src/experimental/Security/CWE-079/unit_tests/flask_mail.py @@ -1,7 +1,6 @@ # https://pythonhosted.org/Flask-Mail/ # https://github.com/mattupstate/flask-mail/blob/1709c70d839a7cc7b1f7eeb97333b71cd420fe32/flask_mail.py#L239 -# tmp: this test cover RFS to any part of the message, but can be shortened to a specific part (body&html) once we decide the objective of the query. from flask_mail import Mail, Message app = Flask(__name__) @@ -9,15 +8,15 @@ mail = Mail(app) @app.route("/send") def send(): - msg = Message(subject=request.args["subject"], - sender=request.args["sender"], - recipients=list(request.args["recipient"]), - body=request.args["body"], + msg = Message(subject="Subject", + sender="from@example.com", + recipients=["to@example.com"], + body="body", html=request.args["html"]) # The message can contain a body and/or HTML: - msg.body = "test" - msg.html = "test" + msg.body = "body" + msg.html = request.args["html"] mail.send(msg) @@ -27,8 +26,8 @@ def connect(): Minimal example to test mail.connect() usage """ with mail.connect() as conn: - msg = Message(subject=request.args["subject"], - sender=request.args["sender"], - recipients=list(request.args["recipient"]), - body=request.args["html"]) + msg = Message(subject="Subject", + sender="from@example.com", + recipients=["to@example.com"], + html=request.args["html"]) conn.send(msg) diff --git a/python/ql/src/experimental/Security/CWE-079/unit_tests/test.actual b/python/ql/src/experimental/Security/CWE-079/unit_tests/test.actual deleted file mode 100644 index 2a4f078a25f..00000000000 --- a/python/ql/src/experimental/Security/CWE-079/unit_tests/test.actual +++ /dev/null @@ -1 +0,0 @@ -| 1 | diff --git a/python/ql/src/experimental/Security/CWE-079/unit_tests/test.qlref b/python/ql/src/experimental/Security/CWE-079/unit_tests/test.qlref deleted file mode 100644 index 01c9dd06163..00000000000 --- a/python/ql/src/experimental/Security/CWE-079/unit_tests/test.qlref +++ /dev/null @@ -1 +0,0 @@ -experimental/Security/CWE-079/test.ql 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..eb8a3144ecf --- /dev/null +++ b/python/ql/src/experimental/semmle/python/security/dataflow/ReflectedXSS.qll @@ -0,0 +1,29 @@ +/** + * 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 + +/** + * 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 + } +} From c323fbbf3ca9dcf37c896ed450ba9e2954bca5b6 Mon Sep 17 00:00:00 2001 From: jorgectf Date: Wed, 23 Jun 2021 17:26:14 +0200 Subject: [PATCH 09/56] Cover Flask-SendMail (Flask-Mail copy) --- .../semmle/python/frameworks/Flask.qll | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/python/ql/src/experimental/semmle/python/frameworks/Flask.qll b/python/ql/src/experimental/semmle/python/frameworks/Flask.qll index 5073d121880..258a41068a1 100644 --- a/python/ql/src/experimental/semmle/python/frameworks/Flask.qll +++ b/python/ql/src/experimental/semmle/python/frameworks/Flask.qll @@ -9,19 +9,23 @@ private import experimental.semmle.python.Concepts private import semmle.python.ApiGraphs private module Flask { - private API::Node flaskMail() { result = API::moduleImport("flask_mail") } + /** https://pythonhosted.org/Flask-Mail/#module-flask_mail */ + private API::Node flaskMail() { result = API::moduleImport(["flask_mail", "flask_sendmail", "flask.ext.sendmail"]) } private API::Node flaskMailInstance() { result = flaskMail().getMember("Mail").getReturn() } + private DataFlow::CallCfgNode flaskMessageInstance() { + result = flaskMail().getMember("Message") + } private DataFlow::CallCfgNode flaskMessageCall() { - result = flaskMail().getMember("Message").getACall() + result = flaskMessageInstance().getACall() } private class FlaskMail extends DataFlow::CallCfgNode, EmailSender { FlaskMail() { this = [flaskMailInstance(), flaskMailInstance().getMember("connect").getReturn()] - .getMember("send") + .getMember(["send", "send_message"]) .getACall() } @@ -52,7 +56,9 @@ private module Flask { bodyWrite.getObject().getALocalSource() = flaskMessageCall() and bodyWrite.getAttributeName() = "recipients" and result = bodyWrite.getValue() - ) + ) or + /** https://pythonhosted.org/Flask-Mail/#flask_mail.Message.add_recipient */ + result = flaskMessageInstance().getMember("add_recipient").getACall().getArg(0) } override DataFlow::Node getFrom() { From eac5eba9d24ee5b3d093b8014ae53a62181c35ca Mon Sep 17 00:00:00 2001 From: jorgectf Date: Wed, 23 Jun 2021 18:36:44 +0200 Subject: [PATCH 10/56] Move tests and qlref to test/ --- .../ql/test/experimental/query-tests/Security/CWE-079/.gitkeep | 0 .../query-tests/Security/CWE-079}/ReflectedXSS.qlref | 0 .../experimental/query-tests/Security/CWE-079}/flask_mail.py | 1 + .../Security/CWE-079}/sendgrid_mail_helper_content_bad.py | 0 .../CWE-079}/sendgrid_via_mail_send_post_request_body_bad.py | 0 .../query-tests/Security/CWE-079}/smtplib_bad_subparts.py | 0 .../query-tests/Security/CWE-079}/smtplib_bad_via_attach.py | 0 7 files changed, 1 insertion(+) delete mode 100644 python/ql/test/experimental/query-tests/Security/CWE-079/.gitkeep rename python/ql/{src/experimental/Security/CWE-079/unit_tests => test/experimental/query-tests/Security/CWE-079}/ReflectedXSS.qlref (100%) rename python/ql/{src/experimental/Security/CWE-079/unit_tests => test/experimental/query-tests/Security/CWE-079}/flask_mail.py (96%) rename python/ql/{src/experimental/Security/CWE-079/unit_tests => test/experimental/query-tests/Security/CWE-079}/sendgrid_mail_helper_content_bad.py (100%) rename python/ql/{src/experimental/Security/CWE-079/unit_tests => test/experimental/query-tests/Security/CWE-079}/sendgrid_via_mail_send_post_request_body_bad.py (100%) rename python/ql/{src/experimental/Security/CWE-079/unit_tests => test/experimental/query-tests/Security/CWE-079}/smtplib_bad_subparts.py (100%) rename python/ql/{src/experimental/Security/CWE-079/unit_tests => test/experimental/query-tests/Security/CWE-079}/smtplib_bad_via_attach.py (100%) diff --git a/python/ql/test/experimental/query-tests/Security/CWE-079/.gitkeep b/python/ql/test/experimental/query-tests/Security/CWE-079/.gitkeep deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/python/ql/src/experimental/Security/CWE-079/unit_tests/ReflectedXSS.qlref b/python/ql/test/experimental/query-tests/Security/CWE-079/ReflectedXSS.qlref similarity index 100% rename from python/ql/src/experimental/Security/CWE-079/unit_tests/ReflectedXSS.qlref rename to python/ql/test/experimental/query-tests/Security/CWE-079/ReflectedXSS.qlref diff --git a/python/ql/src/experimental/Security/CWE-079/unit_tests/flask_mail.py b/python/ql/test/experimental/query-tests/Security/CWE-079/flask_mail.py similarity index 96% rename from python/ql/src/experimental/Security/CWE-079/unit_tests/flask_mail.py rename to python/ql/test/experimental/query-tests/Security/CWE-079/flask_mail.py index 8b1494b539f..a2b19786057 100644 --- a/python/ql/src/experimental/Security/CWE-079/unit_tests/flask_mail.py +++ b/python/ql/test/experimental/query-tests/Security/CWE-079/flask_mail.py @@ -1,6 +1,7 @@ # https://pythonhosted.org/Flask-Mail/ # https://github.com/mattupstate/flask-mail/blob/1709c70d839a7cc7b1f7eeb97333b71cd420fe32/flask_mail.py#L239 +from flask import request, Flask from flask_mail import Mail, Message app = Flask(__name__) diff --git a/python/ql/src/experimental/Security/CWE-079/unit_tests/sendgrid_mail_helper_content_bad.py b/python/ql/test/experimental/query-tests/Security/CWE-079/sendgrid_mail_helper_content_bad.py similarity index 100% rename from python/ql/src/experimental/Security/CWE-079/unit_tests/sendgrid_mail_helper_content_bad.py rename to python/ql/test/experimental/query-tests/Security/CWE-079/sendgrid_mail_helper_content_bad.py diff --git a/python/ql/src/experimental/Security/CWE-079/unit_tests/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 similarity index 100% rename from python/ql/src/experimental/Security/CWE-079/unit_tests/sendgrid_via_mail_send_post_request_body_bad.py rename to python/ql/test/experimental/query-tests/Security/CWE-079/sendgrid_via_mail_send_post_request_body_bad.py diff --git a/python/ql/src/experimental/Security/CWE-079/unit_tests/smtplib_bad_subparts.py b/python/ql/test/experimental/query-tests/Security/CWE-079/smtplib_bad_subparts.py similarity index 100% rename from python/ql/src/experimental/Security/CWE-079/unit_tests/smtplib_bad_subparts.py rename to python/ql/test/experimental/query-tests/Security/CWE-079/smtplib_bad_subparts.py diff --git a/python/ql/src/experimental/Security/CWE-079/unit_tests/smtplib_bad_via_attach.py b/python/ql/test/experimental/query-tests/Security/CWE-079/smtplib_bad_via_attach.py similarity index 100% rename from python/ql/src/experimental/Security/CWE-079/unit_tests/smtplib_bad_via_attach.py rename to python/ql/test/experimental/query-tests/Security/CWE-079/smtplib_bad_via_attach.py From 355bb5c73407c77d5b29d4751cc45943531d9cc7 Mon Sep 17 00:00:00 2001 From: jorgectf Date: Wed, 23 Jun 2021 18:37:11 +0200 Subject: [PATCH 11/56] Format Flask.qll --- .../semmle/python/frameworks/Flask.qll | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/python/ql/src/experimental/semmle/python/frameworks/Flask.qll b/python/ql/src/experimental/semmle/python/frameworks/Flask.qll index 258a41068a1..6e5d86e37cd 100644 --- a/python/ql/src/experimental/semmle/python/frameworks/Flask.qll +++ b/python/ql/src/experimental/semmle/python/frameworks/Flask.qll @@ -10,16 +10,15 @@ private import semmle.python.ApiGraphs private module Flask { /** https://pythonhosted.org/Flask-Mail/#module-flask_mail */ - private API::Node flaskMail() { result = API::moduleImport(["flask_mail", "flask_sendmail", "flask.ext.sendmail"]) } + private API::Node flaskMail() { + result = API::moduleImport(["flask_mail", "flask_sendmail", "flask.ext.sendmail"]) + } private API::Node flaskMailInstance() { result = flaskMail().getMember("Mail").getReturn() } - private DataFlow::CallCfgNode flaskMessageInstance() { - result = flaskMail().getMember("Message") - } - private DataFlow::CallCfgNode flaskMessageCall() { - result = flaskMessageInstance().getACall() - } + private API::Node flaskMessageInstance() { result = flaskMail().getMember("Message") } + + private DataFlow::CallCfgNode flaskMessageCall() { result = flaskMessageInstance().getACall() } private class FlaskMail extends DataFlow::CallCfgNode, EmailSender { FlaskMail() { @@ -56,8 +55,9 @@ private module Flask { bodyWrite.getObject().getALocalSource() = flaskMessageCall() and bodyWrite.getAttributeName() = "recipients" and result = bodyWrite.getValue() - ) or - /** https://pythonhosted.org/Flask-Mail/#flask_mail.Message.add_recipient */ + ) + or + // https://pythonhosted.org/Flask-Mail/#flask_mail.Message.add_recipient result = flaskMessageInstance().getMember("add_recipient").getACall().getArg(0) } From 8ae864827a7d0bd35eaa92f2ebcfc62dcb8fee2e Mon Sep 17 00:00:00 2001 From: jorgectf Date: Wed, 23 Jun 2021 18:37:33 +0200 Subject: [PATCH 12/56] Format ReflectedXSS.qll --- .../semmle/python/security/dataflow/ReflectedXSS.qll | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/python/ql/src/experimental/semmle/python/security/dataflow/ReflectedXSS.qll b/python/ql/src/experimental/semmle/python/security/dataflow/ReflectedXSS.qll index eb8a3144ecf..36042a4b400 100644 --- a/python/ql/src/experimental/semmle/python/security/dataflow/ReflectedXSS.qll +++ b/python/ql/src/experimental/semmle/python/security/dataflow/ReflectedXSS.qll @@ -19,9 +19,7 @@ class ReflectedXssConfiguration extends TaintTracking::Configuration { override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource } - override predicate isSink(DataFlow::Node sink) { - sink = any(EmailSender email).getHtmlBody() - } + override predicate isSink(DataFlow::Node sink) { sink = any(EmailSender email).getHtmlBody() } override predicate isSanitizerGuard(DataFlow::BarrierGuard guard) { guard instanceof StringConstCompare From bf1eb7238e4932262efccb48497b03cf6c5fb80f Mon Sep 17 00:00:00 2001 From: jorgectf Date: Wed, 23 Jun 2021 18:37:55 +0200 Subject: [PATCH 13/56] Cover `django.core.mail` --- .../experimental/semmle/python/Frameworks.qll | 1 + .../semmle/python/frameworks/Django.qll | 63 +++++++++++++++++++ .../Security/CWE-079/django_mail.py | 24 +++++++ 3 files changed, 88 insertions(+) create mode 100644 python/ql/src/experimental/semmle/python/frameworks/Django.qll create mode 100644 python/ql/test/experimental/query-tests/Security/CWE-079/django_mail.py diff --git a/python/ql/src/experimental/semmle/python/Frameworks.qll b/python/ql/src/experimental/semmle/python/Frameworks.qll index 75df222a060..523561d1631 100644 --- a/python/ql/src/experimental/semmle/python/Frameworks.qll +++ b/python/ql/src/experimental/semmle/python/Frameworks.qll @@ -5,3 +5,4 @@ private import experimental.semmle.python.frameworks.Stdlib private import experimental.semmle.python.frameworks.LDAP private import experimental.semmle.python.frameworks.Flask +private import experimental.semmle.python.frameworks.Django diff --git a/python/ql/src/experimental/semmle/python/frameworks/Django.qll b/python/ql/src/experimental/semmle/python/frameworks/Django.qll new file mode 100644 index 00000000000..a8d4dfbf63e --- /dev/null +++ b/python/ql/src/experimental/semmle/python/frameworks/Django.qll @@ -0,0 +1,63 @@ +/** + * Provides classes modeling security-relevant aspects of the `django` PyPI package. + * See https://www.djangoproject.com/. + */ + +private import python +private import semmle.python.dataflow.new.DataFlow +private import experimental.semmle.python.Concepts +private import semmle.python.ApiGraphs + +private module Django { + private API::Node django() { result = API::moduleImport("django") } + + /** https://docs.djangoproject.com/en/3.2/topics/email/ */ + private API::Node djangoMail() { result = django().getMember("core").getMember("mail") } + + private class DjangoSendMail extends DataFlow::CallCfgNode, EmailSender { + 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")] + } + } + + /** https://github.com/django/django/blob/ca9872905559026af82000e46cde6f7dedc897b6/django/core/mail/__init__.py#L90-L121 */ + private class DjangoMailInternal extends DataFlow::CallCfgNode, EmailSender { + 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/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..e4be8d12872 --- /dev/null +++ b/python/ql/test/experimental/query-tests/Security/CWE-079/django_mail.py @@ -0,0 +1,24 @@ +# https://data-flair.training/blogs/django-send-email/ +# Using flask for RFS and django.core.mail as email library + +from flask import request, Flask +from django.core.mail import send_mail, mail_admins, mail_managers + +app = Flask(__name__) + +@app.route("/send") +def send(): + """ + https://github.com/django/django/blob/ca9872905559026af82000e46cde6f7dedc897b6/django/core/mail/__init__.py#L38 + + Apparently there's no html_message in send_mass_mail: https://github.com/django/django/blob/ca9872905559026af82000e46cde6f7dedc897b6/django/core/mail/__init__.py#L64 + """ + send_mail("Subject", "body", "from@example.com", ["to@example.com"], html_message=request.args("html")) + +@app.route("/internal") +def internal(): + """ + https://github.com/django/django/blob/ca9872905559026af82000e46cde6f7dedc897b6/django/core/mail/__init__.py#L90-L121 + """ + mail_admins("Subject", "body", html_message=request.args("html")) + mail_managers("Subject", "body", html_message=request.args("html")) From 9563faf918903937765b4cf308c453e1c27ef307 Mon Sep 17 00:00:00 2001 From: jorgectf Date: Wed, 23 Jun 2021 20:53:17 +0200 Subject: [PATCH 14/56] Add Sendgrid modeling --- .../experimental/semmle/python/Frameworks.qll | 1 + .../semmle/python/frameworks/Sendgrid.qll | 137 ++++++++++++++++++ .../Security/CWE-079/sendgrid_mail.py | 33 +++++ .../sendgrid_mail_helper_content_bad.py | 16 -- 4 files changed, 171 insertions(+), 16 deletions(-) create mode 100644 python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll create mode 100644 python/ql/test/experimental/query-tests/Security/CWE-079/sendgrid_mail.py delete mode 100644 python/ql/test/experimental/query-tests/Security/CWE-079/sendgrid_mail_helper_content_bad.py diff --git a/python/ql/src/experimental/semmle/python/Frameworks.qll b/python/ql/src/experimental/semmle/python/Frameworks.qll index 523561d1631..6542bbc2e9b 100644 --- a/python/ql/src/experimental/semmle/python/Frameworks.qll +++ b/python/ql/src/experimental/semmle/python/Frameworks.qll @@ -6,3 +6,4 @@ private import experimental.semmle.python.frameworks.Stdlib private import experimental.semmle.python.frameworks.LDAP private import experimental.semmle.python.frameworks.Flask private import experimental.semmle.python.frameworks.Django +private import experimental.semmle.python.frameworks.Sendgrid 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..ce7b3698de0 --- /dev/null +++ b/python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll @@ -0,0 +1,137 @@ +/** + * 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 { + private API::Node sendgrid() { result = API::moduleImport("sendgrid") } + + private API::Node sendgridMailHelper() { + result = sendgrid().getMember("helpers").getMember("mail") + } + + private API::Node sendgridMailInstance() { result = sendgridMailHelper().getMember("Mail") } + + private DataFlow::CallCfgNode sendgridMailCall() { result = sendgridMailInstance().getACall() } + + /** Gets a reference to a `SendGridAPIClient` instance. */ + private DataFlow::LocalSourceNode sendgridApiClient(DataFlow::TypeTracker t) { + t.start() and + result.(DataFlow::AttrRead).getObject*().getALocalSource() = + sendgrid().getMember("SendGridAPIClient").getReturn().getAUse() + or + exists(DataFlow::TypeTracker t2 | result = sendgridApiClient(t2).track(t2, t)) + } + + /** Gets a reference to a `SendGridAPIClient` instance use. */ + private DataFlow::Node sendgridApiClient() { + sendgridApiClient(DataFlow::TypeTracker::end()).flowsTo(result) + } + + /** Gets a reference to a `SendGridAPIClient` instance call with `send` or `post`. */ + private DataFlow::Node sendgridApiSendCall() { + result = sendgridApiClient() and + result.(DataFlow::AttrRead).getAttributeName() in ["send", "post"] + } + + /** + * https://github.com/sendgrid/sendgrid-python + * https://github.com/sendgrid/sendgrid-python/blob/cf0924c35c37bbec8e5ca39e963a55f54f0eec11/sendgrid/helpers/mail/mail.py#L20 + */ + private class SendGridMail extends DataFlow::CallCfgNode, EmailSender { + SendGridMail() { this.getFunction() = sendgridApiSendCall() } + + override DataFlow::Node getPlainTextBody() { + result in [ + sendgridMailCall().getArg(3), sendgridMailCall().getArgByName("plain_text_content") + ] + or + exists(DataFlow::CallCfgNode contentCall, StrConst mime | + contentCall = sendgridMailHelper().getMember("Content").getACall() and + mime.getText() = "text/plain" and + DataFlow::exprNode(mime).(DataFlow::LocalSourceNode).flowsTo(contentCall.getArg(0)) and + result = contentCall.getArg(1) + ) + or + exists(DataFlow::CallCfgNode addContentCall, StrConst mime | + addContentCall = sendgridMailInstance().getMember("add_content").getACall() and + mime.getText() = "text/plain" and + DataFlow::exprNode(mime).(DataFlow::LocalSourceNode).flowsTo(addContentCall.getArg(1)) and + result = addContentCall.getArg(0) + ) + or + exists(DataFlow::AttrWrite bodyWrite | + bodyWrite.getObject().getALocalSource() = sendgridMailCall() and + bodyWrite.getAttributeName() = "plain_text_content" and + result = bodyWrite.getValue() + ) + } + + override DataFlow::Node getHtmlBody() { + result in [sendgridMailCall().getArg(4), sendgridMailCall().getArgByName("html_content")] + or + exists(DataFlow::CallCfgNode contentCall, StrConst mime | + contentCall = sendgridMailHelper().getMember("Content").getACall() and + mime.getText() = "text/html" and + DataFlow::exprNode(mime).(DataFlow::LocalSourceNode).flowsTo(contentCall.getArg(0)) and + result = contentCall.getArg(1) + ) + or + exists(DataFlow::CallCfgNode addContentCall, StrConst mime | + addContentCall = sendgridMailInstance().getMember("add_content").getACall() and + mime.getText() = "text/html" and + DataFlow::exprNode(mime).(DataFlow::LocalSourceNode).flowsTo(addContentCall.getArg(1)) and + result = addContentCall.getArg(0) + ) + or + exists(DataFlow::AttrWrite htmlWrite | + htmlWrite.getObject().getALocalSource() = sendgridMailCall() and + htmlWrite.getAttributeName() = "html_content" and + result = htmlWrite.getValue() + ) + } + + override DataFlow::Node getTo() { + result in [sendgridMailCall().getArg(1), sendgridMailCall().getArgByName("to_emails")] + or + result = sendgridMailHelper().getMember("To").getACall().getArg(0) + or + result = + sendgridMailInstance() + .getMember(["to", "add_to", "cc", "add_cc", "bcc", "add_bcc"]) + .getACall() + .getArg(0) + } + + override DataFlow::Node getFrom() { + result in [sendgridMailCall().getArg(0), sendgridMailCall().getArgByName("from_email")] + or + result = sendgridMailHelper().getMember("Email").getACall().getArg(0) + or + result = sendgridMailInstance().getMember("from_email").getACall().getArg(0) + or + exists(DataFlow::AttrWrite fromWrite | + fromWrite.getObject().getALocalSource() = sendgridMailCall() and + fromWrite.getAttributeName() = "from_email" and + result = fromWrite.getValue() + ) + } + + override DataFlow::Node getSubject() { + result in [sendgridMailCall().getArg(2), sendgridMailCall().getArgByName("subject")] + or + result = sendgridMailInstance().getMember("subject").getACall().getArg(0) + or + exists(DataFlow::AttrWrite subjectWrite | + subjectWrite.getObject().getALocalSource() = sendgridMailCall() and + subjectWrite.getAttributeName() = "subject" and + result = subjectWrite.getValue() + ) + } + } +} 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..75c11f38159 --- /dev/null +++ b/python/ql/test/experimental/query-tests/Security/CWE-079/sendgrid_mail.py @@ -0,0 +1,33 @@ +# https://www.twilio.com/blog/how-to-send-emails-in-python-with-sendgrid + +from flask import request, Flask +from sendgrid import SendGridAPIClient +from sendgrid.helpers.mail import Mail, Email, To, Content, MimeType + +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_post") +def send_post(): + from_email = Email("test@example.com") + to_email = To("test@example.com") + subject = "Sending with SendGrid is Fun" + content = Content("text/html", request.args["html_content"]) + + # https://github.com/sendgrid/sendgrid-python/blob/cf0924c35c37bbec8e5ca39e963a55f54f0eec11/sendgrid/helpers/mail/mime_type.py#L1 + content = Content(MimeType.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()) diff --git a/python/ql/test/experimental/query-tests/Security/CWE-079/sendgrid_mail_helper_content_bad.py b/python/ql/test/experimental/query-tests/Security/CWE-079/sendgrid_mail_helper_content_bad.py deleted file mode 100644 index 0f693f5a3e5..00000000000 --- a/python/ql/test/experimental/query-tests/Security/CWE-079/sendgrid_mail_helper_content_bad.py +++ /dev/null @@ -1,16 +0,0 @@ -# This tests that the developer doesn't pass content via the Content class initializer. -# source:https://github.com/sendgrid/sendgrid-python - -import sendgrid -import os -from sendgrid.helpers.mail import * - -sg = sendgrid.SendGridAPIClient(api_key=os.environ.get('SENDGRID_API_KEY')) -from_email = Email("test@example.com") -to_email = To("test@example.com") -subject = "Sending with SendGrid is Fun" -content = Content("text/html", "and easy to do anywhere, even with Python") # Content can also take the MimeType.html as the first arg here. Need to create a separate example for this. - -mail = Mail(from_email, to_email, subject, content) - -response = sg.client.mail.send.post(request_body=mail.get()) From 5e8f9959ef2114cd707160b2b28490590c649f32 Mon Sep 17 00:00:00 2001 From: jorgectf Date: Wed, 23 Jun 2021 20:56:48 +0200 Subject: [PATCH 15/56] Extend Sendgrid setters --- .../src/experimental/semmle/python/frameworks/Sendgrid.qll | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll b/python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll index ce7b3698de0..49c69f56274 100644 --- a/python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll +++ b/python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll @@ -75,6 +75,8 @@ private module Sendgrid { override DataFlow::Node getHtmlBody() { result in [sendgridMailCall().getArg(4), sendgridMailCall().getArgByName("html_content")] or + result = sendgridMailInstance().getMember("set_html").getACall().getArg(0) + or exists(DataFlow::CallCfgNode contentCall, StrConst mime | contentCall = sendgridMailHelper().getMember("Content").getACall() and mime.getText() = "text/html" and @@ -113,7 +115,7 @@ private module Sendgrid { or result = sendgridMailHelper().getMember("Email").getACall().getArg(0) or - result = sendgridMailInstance().getMember("from_email").getACall().getArg(0) + result = sendgridMailInstance().getMember(["from_email", "set_from"]).getACall().getArg(0) or exists(DataFlow::AttrWrite fromWrite | fromWrite.getObject().getALocalSource() = sendgridMailCall() and @@ -125,7 +127,7 @@ private module Sendgrid { override DataFlow::Node getSubject() { result in [sendgridMailCall().getArg(2), sendgridMailCall().getArgByName("subject")] or - result = sendgridMailInstance().getMember("subject").getACall().getArg(0) + result = sendgridMailInstance().getMember(["subject", "set_subject"]).getACall().getArg(0) or exists(DataFlow::AttrWrite subjectWrite | subjectWrite.getObject().getALocalSource() = sendgridMailCall() and From 70d651184b7497791bc2d88fc4669cca46bbf317 Mon Sep 17 00:00:00 2001 From: jorgectf Date: Wed, 23 Jun 2021 21:21:45 +0200 Subject: [PATCH 16/56] Optimize Flask.qll --- .../semmle/python/frameworks/Flask.qll | 61 ++++++------------- 1 file changed, 17 insertions(+), 44 deletions(-) diff --git a/python/ql/src/experimental/semmle/python/frameworks/Flask.qll b/python/ql/src/experimental/semmle/python/frameworks/Flask.qll index 6e5d86e37cd..d7b1a68e8f5 100644 --- a/python/ql/src/experimental/semmle/python/frameworks/Flask.qll +++ b/python/ql/src/experimental/semmle/python/frameworks/Flask.qll @@ -20,6 +20,18 @@ private module Flask { private DataFlow::CallCfgNode flaskMessageCall() { result = flaskMessageInstance().getACall() } + private DataFlow::Node getFlaskMailArgument(int argumentPosition, string argumentName) { + result in [ + flaskMessageCall().getArg(argumentPosition), flaskMessageCall().getArgByName(argumentName) + ] + or + exists(DataFlow::AttrWrite write | + write.getObject().getALocalSource() = flaskMessageCall() and + write.getAttributeName() = argumentName and + result = write.getValue() + ) + } + private class FlaskMail extends DataFlow::CallCfgNode, EmailSender { FlaskMail() { this = @@ -28,57 +40,18 @@ private module Flask { .getACall() } - override DataFlow::Node getPlainTextBody() { - result in [flaskMessageCall().getArg(2), flaskMessageCall().getArgByName("body")] - or - exists(DataFlow::AttrWrite bodyWrite | - bodyWrite.getObject().getALocalSource() = flaskMessageCall() and - bodyWrite.getAttributeName() = "body" and - result = bodyWrite.getValue() - ) - } + override DataFlow::Node getPlainTextBody() { result = getFlaskMailArgument(2, "body") } - override DataFlow::Node getHtmlBody() { - result in [flaskMessageCall().getArg(3), flaskMessageCall().getArgByName("html")] - or - exists(DataFlow::AttrWrite bodyWrite | - bodyWrite.getObject().getALocalSource() = flaskMessageCall() and - bodyWrite.getAttributeName() = "html" and - result = bodyWrite.getValue() - ) - } + override DataFlow::Node getHtmlBody() { result = getFlaskMailArgument(3, "html") } override DataFlow::Node getTo() { - result in [flaskMessageCall().getArg(1), flaskMessageCall().getArgByName("recipients")] + result = getFlaskMailArgument(1, "recipients") or - exists(DataFlow::AttrWrite bodyWrite | - bodyWrite.getObject().getALocalSource() = flaskMessageCall() and - bodyWrite.getAttributeName() = "recipients" and - result = bodyWrite.getValue() - ) - or - // https://pythonhosted.org/Flask-Mail/#flask_mail.Message.add_recipient result = flaskMessageInstance().getMember("add_recipient").getACall().getArg(0) } - override DataFlow::Node getFrom() { - result in [flaskMessageCall().getArg(5), flaskMessageCall().getArgByName("sender")] - or - exists(DataFlow::AttrWrite bodyWrite | - bodyWrite.getObject().getALocalSource() = flaskMessageCall() and - bodyWrite.getAttributeName() = "sender" and - result = bodyWrite.getValue() - ) - } + override DataFlow::Node getFrom() { result = getFlaskMailArgument(5, "sender") } - override DataFlow::Node getSubject() { - result in [flaskMessageCall().getArg(0), flaskMessageCall().getArgByName("subject")] - or - exists(DataFlow::AttrWrite bodyWrite | - bodyWrite.getObject().getALocalSource() = flaskMessageCall() and - bodyWrite.getAttributeName() = "subject" and - result = bodyWrite.getValue() - ) - } + override DataFlow::Node getSubject() { result = getFlaskMailArgument(0, "subject") } } } From 7b9cbafd623dd3dc17cf3800f9379aea74f613c8 Mon Sep 17 00:00:00 2001 From: jorgectf Date: Wed, 23 Jun 2021 21:28:11 +0200 Subject: [PATCH 17/56] Move flask_mail to libraries/ --- .../python/{frameworks/Flask.qll => libraries/Flask-Mail.qll} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename python/ql/src/experimental/semmle/python/{frameworks/Flask.qll => libraries/Flask-Mail.qll} (98%) diff --git a/python/ql/src/experimental/semmle/python/frameworks/Flask.qll b/python/ql/src/experimental/semmle/python/libraries/Flask-Mail.qll similarity index 98% rename from python/ql/src/experimental/semmle/python/frameworks/Flask.qll rename to python/ql/src/experimental/semmle/python/libraries/Flask-Mail.qll index d7b1a68e8f5..26152005bdc 100644 --- a/python/ql/src/experimental/semmle/python/frameworks/Flask.qll +++ b/python/ql/src/experimental/semmle/python/libraries/Flask-Mail.qll @@ -8,7 +8,7 @@ private import semmle.python.dataflow.new.DataFlow private import experimental.semmle.python.Concepts private import semmle.python.ApiGraphs -private module Flask { +private module FlaskMail { /** https://pythonhosted.org/Flask-Mail/#module-flask_mail */ private API::Node flaskMail() { result = API::moduleImport(["flask_mail", "flask_sendmail", "flask.ext.sendmail"]) From e0013fcdbb9ea1e8d5b18950853cbfe9a0ca4787 Mon Sep 17 00:00:00 2001 From: jorgectf Date: Wed, 23 Jun 2021 21:29:35 +0200 Subject: [PATCH 18/56] Fix Concepts.qll dependencies --- python/ql/src/experimental/semmle/python/Frameworks.qll | 2 +- .../semmle/python/libraries/{Flask-Mail.qll => FlaskMail.qll} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename python/ql/src/experimental/semmle/python/libraries/{Flask-Mail.qll => FlaskMail.qll} (100%) diff --git a/python/ql/src/experimental/semmle/python/Frameworks.qll b/python/ql/src/experimental/semmle/python/Frameworks.qll index 6542bbc2e9b..15808bb74ed 100644 --- a/python/ql/src/experimental/semmle/python/Frameworks.qll +++ b/python/ql/src/experimental/semmle/python/Frameworks.qll @@ -4,6 +4,6 @@ private import experimental.semmle.python.frameworks.Stdlib private import experimental.semmle.python.frameworks.LDAP -private import experimental.semmle.python.frameworks.Flask +private import experimental.semmle.python.libraries.FlaskMail private import experimental.semmle.python.frameworks.Django private import experimental.semmle.python.frameworks.Sendgrid diff --git a/python/ql/src/experimental/semmle/python/libraries/Flask-Mail.qll b/python/ql/src/experimental/semmle/python/libraries/FlaskMail.qll similarity index 100% rename from python/ql/src/experimental/semmle/python/libraries/Flask-Mail.qll rename to python/ql/src/experimental/semmle/python/libraries/FlaskMail.qll From b5ee7c30328e28320297ea39caba9b11e7c1e893 Mon Sep 17 00:00:00 2001 From: jorgectf Date: Tue, 29 Jun 2021 17:28:20 +0200 Subject: [PATCH 19/56] Specify `plain-text body` --- .../query-tests/Security/CWE-079/django_mail.py | 6 +++--- .../experimental/query-tests/Security/CWE-079/flask_mail.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) 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 index e4be8d12872..a051af71d02 100644 --- 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 @@ -13,12 +13,12 @@ def send(): Apparently there's no html_message in send_mass_mail: https://github.com/django/django/blob/ca9872905559026af82000e46cde6f7dedc897b6/django/core/mail/__init__.py#L64 """ - send_mail("Subject", "body", "from@example.com", ["to@example.com"], html_message=request.args("html")) + send_mail("Subject", "plain-text body", "from@example.com", ["to@example.com"], html_message=request.args("html")) @app.route("/internal") def internal(): """ https://github.com/django/django/blob/ca9872905559026af82000e46cde6f7dedc897b6/django/core/mail/__init__.py#L90-L121 """ - mail_admins("Subject", "body", html_message=request.args("html")) - mail_managers("Subject", "body", html_message=request.args("html")) + mail_admins("Subject", "plain-text body", html_message=request.args("html")) + mail_managers("Subject", "plain-text body", html_message=request.args("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 index a2b19786057..c5de32c6f94 100644 --- 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 @@ -12,11 +12,11 @@ def send(): msg = Message(subject="Subject", sender="from@example.com", recipients=["to@example.com"], - body="body", + body="plain-text body", html=request.args["html"]) # The message can contain a body and/or HTML: - msg.body = "body" + msg.body = "plain-text body" msg.html = request.args["html"] mail.send(msg) From 19a626742a5495a8df049b4433af2b86e87c3f68 Mon Sep 17 00:00:00 2001 From: jorgectf Date: Tue, 29 Jun 2021 17:28:45 +0200 Subject: [PATCH 20/56] Almost fix `getFlaskMailArgument(...)` --- .../ql/src/experimental/semmle/python/libraries/FlaskMail.qll | 3 +++ 1 file changed, 3 insertions(+) diff --git a/python/ql/src/experimental/semmle/python/libraries/FlaskMail.qll b/python/ql/src/experimental/semmle/python/libraries/FlaskMail.qll index 26152005bdc..8c0f155f5ff 100644 --- a/python/ql/src/experimental/semmle/python/libraries/FlaskMail.qll +++ b/python/ql/src/experimental/semmle/python/libraries/FlaskMail.qll @@ -21,6 +21,9 @@ private module FlaskMail { private DataFlow::CallCfgNode flaskMessageCall() { result = flaskMessageInstance().getACall() } private DataFlow::Node getFlaskMailArgument(int argumentPosition, string argumentName) { + // 'argumentPosition' is not bound to a value. + argumentName in ["body", "html", "recipients", "sender", "subject"] and + argumentPosition in [0 .. 5] and result in [ flaskMessageCall().getArg(argumentPosition), flaskMessageCall().getArgByName(argumentName) ] From c9634f3c6faa0d5af133e0f9732355eed581e36f Mon Sep 17 00:00:00 2001 From: jorgectf Date: Thu, 28 Oct 2021 13:54:14 +0200 Subject: [PATCH 21/56] Fix `getFlaskMailArgument()` --- .../ql/src/experimental/semmle/python/libraries/FlaskMail.qll | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/python/ql/src/experimental/semmle/python/libraries/FlaskMail.qll b/python/ql/src/experimental/semmle/python/libraries/FlaskMail.qll index 8c0f155f5ff..6223782d440 100644 --- a/python/ql/src/experimental/semmle/python/libraries/FlaskMail.qll +++ b/python/ql/src/experimental/semmle/python/libraries/FlaskMail.qll @@ -20,10 +20,8 @@ private module FlaskMail { private DataFlow::CallCfgNode flaskMessageCall() { result = flaskMessageInstance().getACall() } + bindingset[argumentPosition, argumentName] private DataFlow::Node getFlaskMailArgument(int argumentPosition, string argumentName) { - // 'argumentPosition' is not bound to a value. - argumentName in ["body", "html", "recipients", "sender", "subject"] and - argumentPosition in [0 .. 5] and result in [ flaskMessageCall().getArg(argumentPosition), flaskMessageCall().getArgByName(argumentName) ] From bf6849510216781f82f96f2abef72a90c1f08bb6 Mon Sep 17 00:00:00 2001 From: jorgectf Date: Thu, 28 Oct 2021 14:21:43 +0200 Subject: [PATCH 22/56] Polish `FlaskMail` qldocs --- .../semmle/python/libraries/FlaskMail.qll | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/python/ql/src/experimental/semmle/python/libraries/FlaskMail.qll b/python/ql/src/experimental/semmle/python/libraries/FlaskMail.qll index 6223782d440..641b9b1e010 100644 --- a/python/ql/src/experimental/semmle/python/libraries/FlaskMail.qll +++ b/python/ql/src/experimental/semmle/python/libraries/FlaskMail.qll @@ -8,18 +8,25 @@ 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 { - /** https://pythonhosted.org/Flask-Mail/#module-flask_mail */ + /** 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 reference to `flask_mail.Message`, `flask_sendmail.Message` and `flask.ext.sendmail.Message`. */ private API::Node flaskMessageInstance() { result = flaskMail().getMember("Message") } + /** Gets a call to `flask_mail.Message`, `flask_sendmail.Message` and `flask.ext.sendmail.Message`. */ private DataFlow::CallCfgNode flaskMessageCall() { result = flaskMessageInstance().getACall() } + /** + * Gets a reference to an argument from `flask_mail.Message`, `flask_sendmail.Message` and `flask.ext.sendmail.Message`. + */ bindingset[argumentPosition, argumentName] private DataFlow::Node getFlaskMailArgument(int argumentPosition, string argumentName) { result in [ @@ -33,6 +40,27 @@ private module FlaskMail { ) } + /** + * 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 { FlaskMail() { this = From e8e0f0fea806da69f72ee4d6720dfd8e8d8ae714 Mon Sep 17 00:00:00 2001 From: jorgectf Date: Thu, 28 Oct 2021 14:22:14 +0200 Subject: [PATCH 23/56] Add temporary `.expected` --- .../semmle/python/frameworks/Sendgrid.qll | 4 ++ .../Security/CWE-079/ReflectedXSS.expected | 37 +++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 python/ql/test/experimental/query-tests/Security/CWE-079/ReflectedXSS.expected diff --git a/python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll b/python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll index 49c69f56274..fb7b4a609c5 100644 --- a/python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll +++ b/python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll @@ -9,14 +9,18 @@ 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 call to `sendgrid.helpers.mail.Mail()`. */ private DataFlow::CallCfgNode sendgridMailCall() { result = sendgridMailInstance().getACall() } /** Gets a reference to a `SendGridAPIClient` instance. */ 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..abfc23f011c --- /dev/null +++ b/python/ql/test/experimental/query-tests/Security/CWE-079/ReflectedXSS.expected @@ -0,0 +1,37 @@ +edges +| flask_mail.py:16:22:16:28 | ControlFlowNode for request | flask_mail.py:16:22:16:33 | ControlFlowNode for Attribute | +| flask_mail.py:16:22:16:28 | ControlFlowNode for request | flask_mail.py:20:14:20:20 | ControlFlowNode for request | +| flask_mail.py:16:22:16:28 | ControlFlowNode for request | flask_mail.py:20:14:20:25 | ControlFlowNode for Attribute | +| flask_mail.py:16:22:16:33 | ControlFlowNode for Attribute | flask_mail.py:16:22:16:41 | ControlFlowNode for Subscript | +| flask_mail.py:20:14:20:20 | ControlFlowNode for request | flask_mail.py:20:14:20:25 | ControlFlowNode for Attribute | +| flask_mail.py:20:14:20:25 | ControlFlowNode for Attribute | flask_mail.py:20:14:20:33 | ControlFlowNode for Subscript | +| flask_mail.py:33:24:33:30 | ControlFlowNode for request | flask_mail.py:33:24:33:35 | ControlFlowNode for Attribute | +| flask_mail.py:33:24:33:35 | ControlFlowNode for Attribute | flask_mail.py:33:24:33:43 | ControlFlowNode for Subscript | +| sendgrid_mail.py:15:20:15:26 | ControlFlowNode for request | sendgrid_mail.py:15:20:15:31 | ControlFlowNode for Attribute | +| sendgrid_mail.py:15:20:15:31 | ControlFlowNode for Attribute | sendgrid_mail.py:15:20:15:47 | ControlFlowNode for Subscript | +| sendgrid_mail.py:25:34:25:40 | ControlFlowNode for request | sendgrid_mail.py:25:34:25:45 | ControlFlowNode for Attribute | +| sendgrid_mail.py:25:34:25:45 | ControlFlowNode for Attribute | sendgrid_mail.py:25:34:25:61 | ControlFlowNode for Subscript | +nodes +| flask_mail.py:16:22:16:28 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| flask_mail.py:16:22:16:33 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | +| flask_mail.py:16:22:16:41 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | +| flask_mail.py:20:14:20:20 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| flask_mail.py:20:14:20:25 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | +| flask_mail.py:20:14:20:33 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | +| flask_mail.py:33:24:33:30 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| flask_mail.py:33:24:33:35 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | +| flask_mail.py:33:24:33:43 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | +| sendgrid_mail.py:15:20:15:26 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| sendgrid_mail.py:15:20:15:31 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | +| sendgrid_mail.py:15:20:15:47 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | +| sendgrid_mail.py:25:34:25:40 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| sendgrid_mail.py:25:34:25:45 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | +| sendgrid_mail.py:25:34:25:61 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | +subpaths +#select +| flask_mail.py:16:22:16:41 | ControlFlowNode for Subscript | flask_mail.py:16:22:16:28 | ControlFlowNode for request | flask_mail.py:16:22:16:41 | ControlFlowNode for Subscript | Cross-site scripting vulnerability due to $@. | flask_mail.py:16:22:16:28 | ControlFlowNode for request | a user-provided value | +| flask_mail.py:20:14:20:33 | ControlFlowNode for Subscript | flask_mail.py:16:22:16:28 | ControlFlowNode for request | flask_mail.py:20:14:20:33 | ControlFlowNode for Subscript | Cross-site scripting vulnerability due to $@. | flask_mail.py:16:22:16:28 | ControlFlowNode for request | a user-provided value | +| flask_mail.py:20:14:20:33 | ControlFlowNode for Subscript | flask_mail.py:20:14:20:20 | ControlFlowNode for request | flask_mail.py:20:14:20:33 | ControlFlowNode for Subscript | Cross-site scripting vulnerability due to $@. | flask_mail.py:20:14:20:20 | ControlFlowNode for request | a user-provided value | +| flask_mail.py:33:24:33:43 | ControlFlowNode for Subscript | flask_mail.py:33:24:33:30 | ControlFlowNode for request | flask_mail.py:33:24:33:43 | ControlFlowNode for Subscript | Cross-site scripting vulnerability due to $@. | flask_mail.py:33:24:33:30 | ControlFlowNode for request | a user-provided value | +| sendgrid_mail.py:15:20:15:47 | ControlFlowNode for Subscript | sendgrid_mail.py:15:20:15:26 | ControlFlowNode for request | sendgrid_mail.py:15:20:15:47 | ControlFlowNode for Subscript | Cross-site scripting vulnerability due to $@. | sendgrid_mail.py:15:20:15:26 | ControlFlowNode for request | a user-provided value | +| sendgrid_mail.py:25:34:25:61 | ControlFlowNode for Subscript | sendgrid_mail.py:25:34:25:40 | ControlFlowNode for request | sendgrid_mail.py:25:34:25:61 | ControlFlowNode for Subscript | Cross-site scripting vulnerability due to $@. | sendgrid_mail.py:25:34:25:40 | ControlFlowNode for request | a user-provided value | From dbf5b24b860cf8fe7aa31a960454622f3aa26829 Mon Sep 17 00:00:00 2001 From: jorgectf Date: Thu, 28 Oct 2021 18:26:35 +0200 Subject: [PATCH 24/56] Polish `Sendgrid.qll` qldoc --- .../semmle/python/frameworks/Sendgrid.qll | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll b/python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll index fb7b4a609c5..e5a9c42d9dd 100644 --- a/python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll +++ b/python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll @@ -44,8 +44,28 @@ private module Sendgrid { } /** - * https://github.com/sendgrid/sendgrid-python - * https://github.com/sendgrid/sendgrid-python/blob/cf0924c35c37bbec8e5ca39e963a55f54f0eec11/sendgrid/helpers/mail/mail.py#L20 + * 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 { SendGridMail() { this.getFunction() = sendgridApiSendCall() } From ba3ea700f57a25f0187baca710ea29abba5019eb Mon Sep 17 00:00:00 2001 From: jorgectf Date: Thu, 28 Oct 2021 18:47:54 +0200 Subject: [PATCH 25/56] Add `Sendgrid` dict data html body modeling --- .../semmle/python/frameworks/Sendgrid.qll | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll b/python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll index e5a9c42d9dd..b8d7ed66aeb 100644 --- a/python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll +++ b/python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll @@ -120,6 +120,19 @@ private module Sendgrid { htmlWrite.getAttributeName() = "html_content" and result = htmlWrite.getValue() ) + or + exists(KeyValuePair content, Dict generalDict, KeyValuePair typePair, KeyValuePair valuePair | + // find L33 + content.getKey().(Str_).getS() = "content" and + content.getValue().(List).getAnElt() = generalDict and + // declare KeyValuePairs keys and values + typePair.getKey().(Str_).getS() = "type" and + typePair.getValue().(Str_).getS() = "text/html" and + valuePair.getKey().(Str_).getS() = "value" and + result.asExpr() = valuePair.getValue() and + // since the pairs' keys are already set, this will set the items accordingly + generalDict.getAnItem() in [typePair, valuePair] + ) } override DataFlow::Node getTo() { From 4afcd9d207c6f26838ea014774bfb6bfcd62e72d Mon Sep 17 00:00:00 2001 From: jorgectf Date: Thu, 28 Oct 2021 19:18:59 +0200 Subject: [PATCH 26/56] [mrthankyou] smtplib partial modeling. --- .../semmle/python/libraries/SmtpLib.qll | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 python/ql/src/experimental/semmle/python/libraries/SmtpLib.qll 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..f57b2ada261 --- /dev/null +++ b/python/ql/src/experimental/semmle/python/libraries/SmtpLib.qll @@ -0,0 +1,90 @@ +private import python +private import semmle.python.dataflow.new.DataFlow +private import experimental.semmle.python.Concepts +private import semmle.python.ApiGraphs + +module SmtpLib { + private API::Node smtpLib() { result = API::moduleImport("smtplib") } + + private API::Node smtpConnectionInstance() { result = smtpLib().getMember("SMTP_SSL") } + + API::Node smtpMimeMultipartInstance() { + result = API::moduleImport("email.mime.multipart").getMember("MIMEMultipart") + } + + API::Node smtpMimeTextInstance() { + result = API::moduleImport("email.mime.text").getMember("MIMEText") + } + + DataFlow::Node smtpMimeTextHTMLInstance() { + // select SmtpLib::smtpMimeTextInstance().getAUse().getALocalSource().getACall() + exists(API::Node mimeTextInstance, DataFlow::CallCfgNode callNode | + mimeTextInstance = smtpMimeTextInstance().getReturn() and + callNode = mimeTextInstance.getACall() and + callNode.getArg(1).asExpr().(Unicode).getText() = "html" and + result = callNode + ) + } + + class SmtpLibSendMail extends DataFlow::CallCfgNode, EmailSender { + SmtpLibSendMail() { this = smtpConnectionInstance().getMember("sendmail").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")] + } + } +} + +// MIMEMultipart has two ways it can add tainted data: +// MIMEMultipart(_subparts=(part1, part2)) +// or +// message = MIMEMultipart("alternative") +// message.attach(part1) +// +// +// select SmtpLib::smtpMimeTextHTMLInstance() +// select API::moduleImport("email.mime.multipart") +// .getMember("MIMEMultipart") +// .getACall() +// .getArgByName("_subparts") +// +// from DataFlow::Node arg1 +// where +// arg1 = +// API::moduleImport("email.mime.multipart") +// .getMember("MIMEMultipart") +// .getReturn() +// .getMember("attach") +// .getACall() +// .getArg(0) +// +// select SmtpLib::smtpMimeTextHTMLInstance() //.getReturn() +// +//.getArg(1) //.getAUse() +// +// Work on the smtpMimeTextHTMLInstance function +from DataFlow::CallCfgNode result1 +where + exists(API::Node mimeTextInstance, DataFlow::CallCfgNode callNode | + mimeTextInstance = SmtpLib::smtpMimeTextInstance().getReturn() and + callNode = mimeTextInstance.getACall() and + callNode.getArg(1).asExpr().(Unicode).getText() = "html" and + result1 = callNode + ) +select result1 From 3a4e3d5146b40e4516473d96b0be58a7c37915a4 Mon Sep 17 00:00:00 2001 From: thank_you Date: Sat, 30 Oct 2021 14:00:51 -0400 Subject: [PATCH 27/56] Remove comments from Python example tests Besides removing comments, I also reduced the complexity of some of the Python code examples. --- .../Security/CWE-079/django_mail.py | 8 +- .../Security/CWE-079/flask_mail.py | 4 +- .../Security/CWE-079/sendgrid_mail.py | 3 - ...rid_via_mail_send_post_request_body_bad.py | 104 ------------------ .../Security/CWE-079/smtplib_bad_subparts.py | 9 +- .../CWE-079/smtplib_bad_via_attach.py | 10 +- 6 files changed, 12 insertions(+), 126 deletions(-) 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 index a051af71d02..4df6369110f 100644 --- 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 @@ -1,6 +1,3 @@ -# https://data-flair.training/blogs/django-send-email/ -# Using flask for RFS and django.core.mail as email library - from flask import request, Flask from django.core.mail import send_mail, mail_admins, mail_managers @@ -9,15 +6,18 @@ app = Flask(__name__) @app.route("/send") def send(): """ + 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 - Apparently there's no html_message in send_mass_mail: https://github.com/django/django/blob/ca9872905559026af82000e46cde6f7dedc897b6/django/core/mail/__init__.py#L64 + 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=request.args("html")) @app.route("/internal") def internal(): """ + 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=request.args("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 index c5de32c6f94..e8bdcc93634 100644 --- 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 @@ -1,6 +1,3 @@ -# https://pythonhosted.org/Flask-Mail/ -# https://github.com/mattupstate/flask-mail/blob/1709c70d839a7cc7b1f7eeb97333b71cd420fe32/flask_mail.py#L239 - from flask import request, Flask from flask_mail import Mail, Message @@ -17,6 +14,7 @@ def send(): # 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) 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 index 75c11f38159..8c2424c765f 100644 --- 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 @@ -1,5 +1,3 @@ -# https://www.twilio.com/blog/how-to-send-emails-in-python-with-sendgrid - from flask import request, Flask from sendgrid import SendGridAPIClient from sendgrid.helpers.mail import Mail, Email, To, Content, MimeType @@ -24,7 +22,6 @@ def send_post(): subject = "Sending with SendGrid is Fun" content = Content("text/html", request.args["html_content"]) - # https://github.com/sendgrid/sendgrid-python/blob/cf0924c35c37bbec8e5ca39e963a55f54f0eec11/sendgrid/helpers/mail/mime_type.py#L1 content = Content(MimeType.html, request.args["html_content"]) mail = Mail(from_email, to_email, subject, content) 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 index ef9c6edfca8..fd0627c612c 100644 --- 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 @@ -1,5 +1,4 @@ # This tests that the developer doesn't pass tainted user data into the mail.send.post() method in the SendGrid library. -# source :https://github.com/sendgrid/sendgrid-python import sendgrid import os @@ -7,135 +6,32 @@ import os sg = sendgrid.SendGridAPIClient(os.environ.get('SENDGRID_API_KEY')) data = { - "asm": { - "group_id": 1, - "groups_to_display": [ - 1, - 2, - 3 - ] - }, - "attachments": [ - { - "content": "[BASE64 encoded content block here]", - "content_id": "ii_139db99fdb5c3704", - "disposition": "inline", - "filename": "file1.jpg", - "name": "file1", - "type": "jpg" - } - ], - "batch_id": "[YOUR BATCH ID GOES HERE]", - "categories": [ - "category1", - "category2" - ], "content": [ { "type": "text/html", "value": "

Hello, world!

" } ], - "custom_args": { - "New Argument 1": "New Value 1", - "activationAttempt": "1", - "customerAccountNumber": "[CUSTOMER ACCOUNT NUMBER GOES HERE]" - }, "from": { "email": "sam.smith@example.com", "name": "Sam Smith" }, "headers": {}, - "ip_pool_name": "[YOUR POOL NAME GOES HERE]", "mail_settings": { - "bcc": { - "email": "ben.doe@example.com", - "enable": True - }, - "bypass_list_management": { - "enable": True - }, "footer": { "enable": True, "html": "

Thanks
The SendGrid Team

", "text": "Thanks,/n The SendGrid Team" }, - "sandbox_mode": { - "enable": False - }, - "spam_check": { - "enable": True, - "post_to_url": "http://example.com/compliance", - "threshold": 3 - } }, - "personalizations": [ - { - "bcc": [ - { - "email": "sam.doe@example.com", - "name": "Sam Doe" - } - ], - "cc": [ - { - "email": "jane.doe@example.com", - "name": "Jane Doe" - } - ], - "custom_args": { - "New Argument 1": "New Value 1", - "activationAttempt": "1", - "customerAccountNumber": "[CUSTOMER ACCOUNT NUMBER GOES HERE]" - }, - "headers": { - "X-Accept-Language": "en", - "X-Mailer": "MyApp" - }, - "send_at": 1409348513, - "subject": "Hello, World!", - "substitutions": { - "id": "substitutions", - "type": "object" - }, - "to": [ - { - "email": "john.doe@example.com", - "name": "John Doe" - } - ] - } - ], "reply_to": { "email": "sam.smith@example.com", "name": "Sam Smith" }, - "sections": { - "section": { - ":sectionName1": "section 1 text", - ":sectionName2": "section 2 text" - } - }, "send_at": 1409348513, "subject": "Hello, World!", "template_id": "[YOUR TEMPLATE ID GOES HERE]", "tracking_settings": { - "click_tracking": { - "enable": True, - "enable_text": True - }, - "ganalytics": { - "enable": True, - "utm_campaign": "[NAME OF YOUR REFERRER SOURCE]", - "utm_content": "[USE THIS SPACE TO DIFFERENTIATE YOUR EMAIL FROM ADS]", - "utm_medium": "[NAME OF YOUR MARKETING MEDIUM e.g. email]", - "utm_name": "[NAME OF YOUR CAMPAIGN]", - "utm_term": "[IDENTIFY PAID KEYWORDS HERE]" - }, - "open_tracking": { - "enable": True, - "substitution_tag": "%opentrack" - }, "subscription_tracking": { "enable": True, "html": "If you would like to unsubscribe and stop receiving these emails <% clickhere %>.", 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 index 7bd243bdd5e..f9cec87c45f 100644 --- 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 @@ -1,6 +1,4 @@ # This test checks that the developer doesn't pass a MIMEText instance to a MIMEMultipart initializer via the subparts parameter. - -# source꞉ https꞉//www.programcreek.com/python/example/53141/email.MIMEMultipart.MIMEMultipart from flask import Flask, request import json import smtplib, ssl @@ -11,9 +9,8 @@ app = Flask(__name__) @app.route("/") def email_person(): - sender_email = "my@gmail.com" - receiver_email = "your@gmail.com" - password = input("Type your password and press enter:") + sender_email = "sender@gmail.com" + receiver_email = "receiver@example.com" name = request.args['search'] # Create the plain-text and HTML version of your message @@ -33,7 +30,7 @@ def email_person(): context = ssl.create_default_context() server = smtplib.SMTP_SSL("smtp.gmail.com", 465, context=context); - server.login(sender_email, password) + server.login(sender_email, "SERVER_PASSWORD") server.sendmail( sender_email, receiver_email, message.as_string() ) 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 index af6de1dda5e..48a228b0bc6 100644 --- 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 @@ -1,5 +1,4 @@ # This test checks that the developer doesn't pass a MIMEText instance to a MIMEMultipart message. -# source꞉ https꞉//realpython.com/python-send-email/ from flask import Flask, request import json import smtplib, ssl @@ -10,9 +9,8 @@ app = Flask(__name__) @app.route("/") def email_person(): - sender_email = "my@gmail.com" - receiver_email = "your@gmail.com" - password = input("Type your password and press enter:") + sender_email = "sender@gmail.com" + receiver_email = "receiver@example.com" message = MIMEMultipart("alternative") message["Subject"] = "multipart test" @@ -22,7 +20,7 @@ def email_person(): name = request.args['name'] # Create the plain-text and HTML version of your message text = "hello there" - html = f"hello {name}" # here is the exploit. passing vulnerable data into the html portion of the email + html = f"hello {name}" # Turn these into plain/html MIMEText objects part1 = MIMEText(text, "plain") @@ -37,7 +35,7 @@ def email_person(): context = ssl.create_default_context() server = smtplib.SMTP_SSL("smtp.gmail.com", 465, context=context) - server.login(sender_email, password) + server.login(sender_email, "SERVER_PASSWORD") server.sendmail( sender_email, receiver_email, message.as_string() ) From d9e4df7f9778a9b02ccc639ff9a6864c77f7f4cf Mon Sep 17 00:00:00 2001 From: thank_you Date: Sat, 30 Oct 2021 14:00:58 -0400 Subject: [PATCH 28/56] Remove unnecessary comment --- python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll | 1 - 1 file changed, 1 deletion(-) diff --git a/python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll b/python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll index b8d7ed66aeb..17484c968ec 100644 --- a/python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll +++ b/python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll @@ -122,7 +122,6 @@ private module Sendgrid { ) or exists(KeyValuePair content, Dict generalDict, KeyValuePair typePair, KeyValuePair valuePair | - // find L33 content.getKey().(Str_).getS() = "content" and content.getValue().(List).getAnElt() = generalDict and // declare KeyValuePairs keys and values From 356b07112a1b3323d8c28f439ca0bd113582f114 Mon Sep 17 00:00:00 2001 From: jorgectf Date: Sat, 30 Oct 2021 21:19:22 +0200 Subject: [PATCH 29/56] Cover `MimeType.amp` as a vulnerable mimetype --- .../src/experimental/semmle/python/frameworks/Sendgrid.qll | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll b/python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll index 17484c968ec..abaf8dc740d 100644 --- a/python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll +++ b/python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll @@ -103,14 +103,14 @@ private module Sendgrid { or exists(DataFlow::CallCfgNode contentCall, StrConst mime | contentCall = sendgridMailHelper().getMember("Content").getACall() and - mime.getText() = "text/html" and + mime.getText() = ["text/html", "text/x-amp-html"] and DataFlow::exprNode(mime).(DataFlow::LocalSourceNode).flowsTo(contentCall.getArg(0)) and result = contentCall.getArg(1) ) or exists(DataFlow::CallCfgNode addContentCall, StrConst mime | addContentCall = sendgridMailInstance().getMember("add_content").getACall() and - mime.getText() = "text/html" and + mime.getText() = ["text/html", "text/x-amp-html"] and DataFlow::exprNode(mime).(DataFlow::LocalSourceNode).flowsTo(addContentCall.getArg(1)) and result = addContentCall.getArg(0) ) @@ -126,7 +126,7 @@ private module Sendgrid { content.getValue().(List).getAnElt() = generalDict and // declare KeyValuePairs keys and values typePair.getKey().(Str_).getS() = "type" and - typePair.getValue().(Str_).getS() = "text/html" and + typePair.getValue().(Str_).getS() = ["text/html", "text/x-amp-html"] and valuePair.getKey().(Str_).getS() = "value" and result.asExpr() = valuePair.getValue() and // since the pairs' keys are already set, this will set the items accordingly From d316974157c363824c778b27d5d1de51e975f00f Mon Sep 17 00:00:00 2001 From: jorgectf Date: Mon, 8 Nov 2021 10:23:50 +0100 Subject: [PATCH 30/56] Add `HtmlContent` additional taint step --- .../python/security/dataflow/ReflectedXSS.qll | 14 ++++++ .../Security/CWE-079/sendgrid_mail.py | 46 ++++++++++++------- 2 files changed, 44 insertions(+), 16 deletions(-) diff --git a/python/ql/src/experimental/semmle/python/security/dataflow/ReflectedXSS.qll b/python/ql/src/experimental/semmle/python/security/dataflow/ReflectedXSS.qll index 36042a4b400..4ea5013f2a8 100644 --- a/python/ql/src/experimental/semmle/python/security/dataflow/ReflectedXSS.qll +++ b/python/ql/src/experimental/semmle/python/security/dataflow/ReflectedXSS.qll @@ -9,6 +9,7 @@ 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.ApiGraphs /** * A taint-tracking configuration for detecting reflected server-side cross-site @@ -24,4 +25,17 @@ class ReflectedXssConfiguration extends TaintTracking::Configuration { override predicate isSanitizerGuard(DataFlow::BarrierGuard guard) { guard instanceof StringConstCompare } + + override predicate isAdditionalTaintStep(DataFlow::Node nodeTo, DataFlow::Node nodeFrom) { + exists(DataFlow::CallCfgNode htmlContentCall | + htmlContentCall = + API::moduleImport("sendgrid") + .getMember("helpers") + .getMember("mail") + .getMember("HtmlContent") + .getACall() and + nodeFrom = htmlContentCall and + nodeTo = htmlContentCall.getArg(0) + ) + } } 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 index 8c2424c765f..4c577e178c4 100644 --- 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 @@ -1,30 +1,44 @@ from flask import request, Flask from sendgrid import SendGridAPIClient -from sendgrid.helpers.mail import Mail, Email, To, Content, MimeType +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"]) + 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) - 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" - content = Content("text/html", request.args["html_content"]) + from_email = Email("test@example.com") + to_email = To("test@example.com") + subject = "Sending with SendGrid is Fun" + content = Content("text/html", request.args["html_content"]) - content = Content(MimeType.html, request.args["html_content"]) + content = Content(MimeType.html, request.args["html_content"]) - mail = Mail(from_email, to_email, subject, 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()) + sg = SendGridAPIClient(api_key='SENDGRID_API_KEY') + response = sg.client.mail.send.post(request_body=mail.get()) From f4a73fcc591d877003e9963f087d2473568bfa9d Mon Sep 17 00:00:00 2001 From: jorgectf Date: Mon, 8 Nov 2021 10:33:57 +0100 Subject: [PATCH 31/56] Add RFS to `sendgrid` test --- ...rid_via_mail_send_post_request_body_bad.py | 74 ++++++++++--------- 1 file changed, 39 insertions(+), 35 deletions(-) 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 index fd0627c612c..fca641057da 100644 --- 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 @@ -1,44 +1,48 @@ -# This tests that the developer doesn't pass tainted user data into the mail.send.post() method in the SendGrid library. import sendgrid import os +from flask import request, Flask + +app = Flask(__name__) -sg = sendgrid.SendGridAPIClient(os.environ.get('SENDGRID_API_KEY')) +@app.route("/sendgrid") +def send(): + sg = sendgrid.SendGridAPIClient(os.environ.get('SENDGRID_API_KEY')) -data = { - "content": [ - { - "type": "text/html", - "value": "

Hello, world!

" - } - ], - "from": { - "email": "sam.smith@example.com", - "name": "Sam Smith" - }, - "headers": {}, - "mail_settings": { - "footer": { - "enable": True, - "html": "

Thanks
The SendGrid Team

", - "text": "Thanks,/n The SendGrid Team" + data = { + "content": [ + { + "type": "text/html", + "value": "{}".format(request.args["html_content"]) + } + ], + "from": { + "email": "sam.smith@example.com", + "name": "Sam Smith" }, - }, - "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": "If you would like to unsubscribe and stop receiving these emails <% clickhere %>.", - "substitution_tag": "<%click here%>", - "text": "If you would like to unsubscribe and stop receiving these emails <% click here %>." + "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) + response = sg.client.mail.send.post(request_body=data) From 5774ce247996eec36c48b534c11a3d99b3be9bcf Mon Sep 17 00:00:00 2001 From: jorgectf Date: Mon, 8 Nov 2021 10:34:16 +0100 Subject: [PATCH 32/56] Improve `django` test --- .../Security/CWE-079/django_mail.py | 39 ++++++++++--------- 1 file changed, 20 insertions(+), 19 deletions(-) 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 index 4df6369110f..178e8decc79 100644 --- 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 @@ -1,24 +1,25 @@ -from flask import request, Flask +import django.http from django.core.mail import send_mail, mail_admins, mail_managers -app = Flask(__name__) -@app.route("/send") -def send(): - """ - 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 +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=request.args("html")) + 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")) -@app.route("/internal") -def internal(): - """ - 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=request.args("html")) - mail_managers("Subject", "plain-text body", html_message=request.args("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")) From c0a0c5d811271f40087cec434d30f636439582d3 Mon Sep 17 00:00:00 2001 From: jorgectf Date: Mon, 8 Nov 2021 10:51:11 +0100 Subject: [PATCH 33/56] Cover `footer` and `subscription_tracking` html injection --- .../semmle/python/frameworks/Sendgrid.qll | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll b/python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll index abaf8dc740d..45ca81a9f4a 100644 --- a/python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll +++ b/python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll @@ -129,9 +129,37 @@ private module Sendgrid { typePair.getValue().(Str_).getS() = ["text/html", "text/x-amp-html"] and valuePair.getKey().(Str_).getS() = "value" and result.asExpr() = valuePair.getValue() and - // since the pairs' keys are already set, this will set the items accordingly + // correlate generalDict with previously set KeyValuePairs generalDict.getAnItem() in [typePair, valuePair] ) + or + exists(KeyValuePair footer, Dict generalDict, KeyValuePair enablePair, KeyValuePair htmlPair | + footer.getKey().(Str_).getS() = "footer" and + footer.getValue().(Dict) = generalDict and + // check footer is enabled + enablePair.getKey().(Str_).getS() = "enable" and + exists(enablePair.getValue().(True)) and + // get html content + htmlPair.getKey().(Str_).getS() = "html" and + result.asExpr() = htmlPair.getValue() and + // correlate generalDict with previously set KeyValuePairs + generalDict.getAnItem() in [enablePair, htmlPair] + ) + or + exists( + KeyValuePair subTracking, Dict generalDict, KeyValuePair enablePair, KeyValuePair htmlPair + | + subTracking.getKey().(Str_).getS() = "subscription_tracking" and + subTracking.getValue().(Dict) = generalDict and + // check subscription tracking is enabled + enablePair.getKey().(Str_).getS() = "enable" and + exists(enablePair.getValue().(True)) and + // get html content + htmlPair.getKey().(Str_).getS() = "html" and + result.asExpr() = htmlPair.getValue() and + // correlate generalDict with previously set KeyValuePairs + generalDict.getAnItem() in [enablePair, htmlPair] + ) } override DataFlow::Node getTo() { From 5b46b90e1021d5a529a02acbf71f789cbc5e860e Mon Sep 17 00:00:00 2001 From: jorgectf Date: Tue, 9 Nov 2021 14:41:35 +0100 Subject: [PATCH 34/56] Fix additional taint step variables --- .../semmle/python/security/dataflow/ReflectedXSS.qll | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/python/ql/src/experimental/semmle/python/security/dataflow/ReflectedXSS.qll b/python/ql/src/experimental/semmle/python/security/dataflow/ReflectedXSS.qll index 4ea5013f2a8..c9996e461f8 100644 --- a/python/ql/src/experimental/semmle/python/security/dataflow/ReflectedXSS.qll +++ b/python/ql/src/experimental/semmle/python/security/dataflow/ReflectedXSS.qll @@ -26,7 +26,7 @@ class ReflectedXssConfiguration extends TaintTracking::Configuration { guard instanceof StringConstCompare } - override predicate isAdditionalTaintStep(DataFlow::Node nodeTo, DataFlow::Node nodeFrom) { + override predicate isAdditionalTaintStep(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) { exists(DataFlow::CallCfgNode htmlContentCall | htmlContentCall = API::moduleImport("sendgrid") @@ -34,8 +34,8 @@ class ReflectedXssConfiguration extends TaintTracking::Configuration { .getMember("mail") .getMember("HtmlContent") .getACall() and - nodeFrom = htmlContentCall and - nodeTo = htmlContentCall.getArg(0) + nodeTo = htmlContentCall and + nodeFrom = htmlContentCall.getArg(0) ) } } From 1393b5b1572535f55f286b28563cc8ddb29a5730 Mon Sep 17 00:00:00 2001 From: jorgectf Date: Sat, 13 Nov 2021 02:11:45 +0100 Subject: [PATCH 35/56] Add `django` qldocs --- .../semmle/python/frameworks/Django.qll | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/python/ql/src/experimental/semmle/python/frameworks/Django.qll b/python/ql/src/experimental/semmle/python/frameworks/Django.qll index 21a68401e32..1674203fbd6 100644 --- a/python/ql/src/experimental/semmle/python/frameworks/Django.qll +++ b/python/ql/src/experimental/semmle/python/frameworks/Django.qll @@ -96,6 +96,22 @@ private module PrivateDjango { 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 { DjangoSendMail() { this = djangoMail().getMember("send_mail").getACall() } @@ -120,7 +136,22 @@ private module PrivateDjango { } } - /** https://github.com/django/django/blob/ca9872905559026af82000e46cde6f7dedc897b6/django/core/mail/__init__.py#L90-L121 */ + /** + * 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 { DjangoMailInternal() { this = djangoMail().getMember(["mail_admins", "mail_managers"]).getACall() From 33b6f6fe617236986897618c2cadcccc06146923 Mon Sep 17 00:00:00 2001 From: jorgectf Date: Sat, 13 Nov 2021 02:12:22 +0100 Subject: [PATCH 36/56] Polish `FlaskMail` qldocs --- .../src/experimental/semmle/python/libraries/FlaskMail.qll | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/python/ql/src/experimental/semmle/python/libraries/FlaskMail.qll b/python/ql/src/experimental/semmle/python/libraries/FlaskMail.qll index 641b9b1e010..c8b44ac01f0 100644 --- a/python/ql/src/experimental/semmle/python/libraries/FlaskMail.qll +++ b/python/ql/src/experimental/semmle/python/libraries/FlaskMail.qll @@ -26,6 +26,12 @@ private module FlaskMail { /** * Gets a reference to an argument from `flask_mail.Message`, `flask_sendmail.Message` and `flask.ext.sendmail.Message`. + * + * Usage example: + * + * ```codeql + * DataFlow::Node getPlainTextBody() { result = getFlaskMailArgument(2, "body") } + * ``` */ bindingset[argumentPosition, argumentName] private DataFlow::Node getFlaskMailArgument(int argumentPosition, string argumentName) { From 63eadc844189a420f7624613861ff8dc28279f33 Mon Sep 17 00:00:00 2001 From: jorgectf Date: Sat, 13 Nov 2021 02:12:58 +0100 Subject: [PATCH 37/56] Polish `sendgrid` modeling --- .../semmle/python/frameworks/Sendgrid.qll | 93 +++++++------------ 1 file changed, 31 insertions(+), 62 deletions(-) diff --git a/python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll b/python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll index 45ca81a9f4a..7d467501877 100644 --- a/python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll +++ b/python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll @@ -43,6 +43,22 @@ private module Sendgrid { result.(DataFlow::AttrRead).getAttributeName() in ["send", "post"] } + private DataFlow::Node sendgridContent(DataFlow::CallCfgNode contentCall, string mime) { + 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) { + exists(DataFlow::AttrWrite attrWrite | + attrWrite.getObject().getALocalSource() = sendgridMailCall() and + attrWrite.getAttributeName() = attributeName and + result = attrWrite.getValue() + ) + } + /** * Gets a reference to `sg.send()` and `sg.client.mail.send.post()`. * @@ -75,25 +91,12 @@ private module Sendgrid { sendgridMailCall().getArg(3), sendgridMailCall().getArgByName("plain_text_content") ] or - exists(DataFlow::CallCfgNode contentCall, StrConst mime | - contentCall = sendgridMailHelper().getMember("Content").getACall() and - mime.getText() = "text/plain" and - DataFlow::exprNode(mime).(DataFlow::LocalSourceNode).flowsTo(contentCall.getArg(0)) and - result = contentCall.getArg(1) - ) + result in [ + sendgridContent(sendgridMailHelper().getMember("Content").getACall(), "text/plain"), + sendgridContent(sendgridMailInstance().getMember("add_content").getACall(), "text/plain") + ] or - exists(DataFlow::CallCfgNode addContentCall, StrConst mime | - addContentCall = sendgridMailInstance().getMember("add_content").getACall() and - mime.getText() = "text/plain" and - DataFlow::exprNode(mime).(DataFlow::LocalSourceNode).flowsTo(addContentCall.getArg(1)) and - result = addContentCall.getArg(0) - ) - or - exists(DataFlow::AttrWrite bodyWrite | - bodyWrite.getObject().getALocalSource() = sendgridMailCall() and - bodyWrite.getAttributeName() = "plain_text_content" and - result = bodyWrite.getValue() - ) + result = sendgridWrite("plain_text_content") } override DataFlow::Node getHtmlBody() { @@ -101,25 +104,14 @@ private module Sendgrid { or result = sendgridMailInstance().getMember("set_html").getACall().getArg(0) or - exists(DataFlow::CallCfgNode contentCall, StrConst mime | - contentCall = sendgridMailHelper().getMember("Content").getACall() and - mime.getText() = ["text/html", "text/x-amp-html"] and - DataFlow::exprNode(mime).(DataFlow::LocalSourceNode).flowsTo(contentCall.getArg(0)) and - result = contentCall.getArg(1) - ) + result in [ + sendgridContent(sendgridMailHelper().getMember("Content").getACall(), + ["text/html", "text/x-amp-html"]), + sendgridContent(sendgridMailInstance().getMember("add_content").getACall(), + ["text/html", "text/x-amp-html"]) + ] or - exists(DataFlow::CallCfgNode addContentCall, StrConst mime | - addContentCall = sendgridMailInstance().getMember("add_content").getACall() and - mime.getText() = ["text/html", "text/x-amp-html"] and - DataFlow::exprNode(mime).(DataFlow::LocalSourceNode).flowsTo(addContentCall.getArg(1)) and - result = addContentCall.getArg(0) - ) - or - exists(DataFlow::AttrWrite htmlWrite | - htmlWrite.getObject().getALocalSource() = sendgridMailCall() and - htmlWrite.getAttributeName() = "html_content" and - result = htmlWrite.getValue() - ) + result = sendgridWrite("html_content") or exists(KeyValuePair content, Dict generalDict, KeyValuePair typePair, KeyValuePair valuePair | content.getKey().(Str_).getS() = "content" and @@ -134,7 +126,7 @@ private module Sendgrid { ) or exists(KeyValuePair footer, Dict generalDict, KeyValuePair enablePair, KeyValuePair htmlPair | - footer.getKey().(Str_).getS() = "footer" and + footer.getKey().(Str_).getS() = ["footer", "subscription_tracking"] and footer.getValue().(Dict) = generalDict and // check footer is enabled enablePair.getKey().(Str_).getS() = "enable" and @@ -145,21 +137,6 @@ private module Sendgrid { // correlate generalDict with previously set KeyValuePairs generalDict.getAnItem() in [enablePair, htmlPair] ) - or - exists( - KeyValuePair subTracking, Dict generalDict, KeyValuePair enablePair, KeyValuePair htmlPair - | - subTracking.getKey().(Str_).getS() = "subscription_tracking" and - subTracking.getValue().(Dict) = generalDict and - // check subscription tracking is enabled - enablePair.getKey().(Str_).getS() = "enable" and - exists(enablePair.getValue().(True)) and - // get html content - htmlPair.getKey().(Str_).getS() = "html" and - result.asExpr() = htmlPair.getValue() and - // correlate generalDict with previously set KeyValuePairs - generalDict.getAnItem() in [enablePair, htmlPair] - ) } override DataFlow::Node getTo() { @@ -181,11 +158,7 @@ private module Sendgrid { or result = sendgridMailInstance().getMember(["from_email", "set_from"]).getACall().getArg(0) or - exists(DataFlow::AttrWrite fromWrite | - fromWrite.getObject().getALocalSource() = sendgridMailCall() and - fromWrite.getAttributeName() = "from_email" and - result = fromWrite.getValue() - ) + result = sendgridWrite("from_email") } override DataFlow::Node getSubject() { @@ -193,11 +166,7 @@ private module Sendgrid { or result = sendgridMailInstance().getMember(["subject", "set_subject"]).getACall().getArg(0) or - exists(DataFlow::AttrWrite subjectWrite | - subjectWrite.getObject().getALocalSource() = sendgridMailCall() and - subjectWrite.getAttributeName() = "subject" and - result = subjectWrite.getValue() - ) + result = sendgridWrite("subject") } } } From dbdf102ea677f6810bc5f97523dde326486de8a0 Mon Sep 17 00:00:00 2001 From: jorgectf Date: Sat, 13 Nov 2021 14:23:11 +0100 Subject: [PATCH 38/56] Make `EmailSender` an extendable API --- .../experimental/semmle/python/Concepts.qll | 59 ++++++++++++++++--- 1 file changed, 51 insertions(+), 8 deletions(-) diff --git a/python/ql/src/experimental/semmle/python/Concepts.qll b/python/ql/src/experimental/semmle/python/Concepts.qll index 3359c600975..9cdfb028ae4 100644 --- a/python/ql/src/experimental/semmle/python/Concepts.qll +++ b/python/ql/src/experimental/semmle/python/Concepts.qll @@ -297,37 +297,80 @@ class HeaderDeclaration extends DataFlow::Node { DataFlow::Node getValueArg() { result = range.getValueArg() } } +/** 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(); + } +} + /** - * An operation that sends an email. + * 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. */ -abstract class EmailSender extends DataFlow::CallCfgNode { +class EmailSender extends DataFlow::Node { + EmailSender::Range range; + + EmailSender() { this = range } + /** * Gets a data flow node holding the plaintext version of the email body. */ - abstract DataFlow::Node getPlainTextBody(); + DataFlow::Node getPlainTextBody() { result = range.getPlainTextBody() } /** * Gets a data flow node holding the html version of the email body. */ - abstract DataFlow::Node getHtmlBody(); + DataFlow::Node getHtmlBody() { result = range.getHtmlBody() } /** * Gets a data flow node holding the recipients of the email. */ - abstract DataFlow::Node getTo(); + DataFlow::Node getTo() { result = range.getTo() } /** * Gets a data flow node holding the senders of the email. */ - abstract DataFlow::Node getFrom(); + DataFlow::Node getFrom() { result = range.getFrom() } /** * Gets a data flow node holding the subject of the email. */ - abstract DataFlow::Node getSubject(); + DataFlow::Node getSubject() { result = range.getSubject() } /** * Gets a data flow node that refers to the HTML body or plaintext body of the email. */ - DataFlow::Node getABody() { result in [getPlainTextBody(), getHtmlBody()] } + DataFlow::Node getABody() { result in [range.getPlainTextBody(), range.getHtmlBody()] } } From e7cb7629470692068639d5a50ef5db11d215a2c9 Mon Sep 17 00:00:00 2001 From: jorgectf Date: Sat, 13 Nov 2021 14:24:02 +0100 Subject: [PATCH 39/56] Add `SmtpLib` to `Frameworks.qll` and minimal fixes --- python/ql/src/experimental/semmle/python/Frameworks.qll | 3 ++- .../ql/src/experimental/semmle/python/frameworks/Django.qll | 6 +++--- .../src/experimental/semmle/python/frameworks/Sendgrid.qll | 2 +- .../src/experimental/semmle/python/libraries/FlaskMail.qll | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/python/ql/src/experimental/semmle/python/Frameworks.qll b/python/ql/src/experimental/semmle/python/Frameworks.qll index 40bb588c455..554e102021a 100644 --- a/python/ql/src/experimental/semmle/python/Frameworks.qll +++ b/python/ql/src/experimental/semmle/python/Frameworks.qll @@ -9,6 +9,7 @@ private import experimental.semmle.python.frameworks.Werkzeug private import experimental.semmle.python.frameworks.LDAP private import experimental.semmle.python.frameworks.NoSQL private import experimental.semmle.python.frameworks.Log -private import experimental.semmle.python.libraries.FlaskMail private import experimental.semmle.python.frameworks.Django 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 1674203fbd6..b5808230407 100644 --- a/python/ql/src/experimental/semmle/python/frameworks/Django.qll +++ b/python/ql/src/experimental/semmle/python/frameworks/Django.qll @@ -8,7 +8,7 @@ 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.dataflow.new.RemoteFlowSources private module PrivateDjango { private module django { @@ -112,7 +112,7 @@ private module PrivateDjango { * * `getFrom()`'s result would be `"from@example.com"`. * * `getSubject()`'s result would be `"Subject"`. */ - private class DjangoSendMail extends DataFlow::CallCfgNode, EmailSender { + private class DjangoSendMail extends DataFlow::CallCfgNode, EmailSender::Range { DjangoSendMail() { this = djangoMail().getMember("send_mail").getACall() } override DataFlow::Node getPlainTextBody() { @@ -152,7 +152,7 @@ private module PrivateDjango { * * `getFrom()`'s result would be `none`. * * `getSubject()`'s result would be `"Subject"`. */ - private class DjangoMailInternal extends DataFlow::CallCfgNode, EmailSender { + private class DjangoMailInternal extends DataFlow::CallCfgNode, EmailSender::Range { DjangoMailInternal() { this = djangoMail().getMember(["mail_admins", "mail_managers"]).getACall() } diff --git a/python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll b/python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll index 7d467501877..797f7030e8d 100644 --- a/python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll +++ b/python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll @@ -83,7 +83,7 @@ private module Sendgrid { * * `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 { + private class SendGridMail extends DataFlow::CallCfgNode, EmailSender::Range { SendGridMail() { this.getFunction() = sendgridApiSendCall() } override DataFlow::Node getPlainTextBody() { diff --git a/python/ql/src/experimental/semmle/python/libraries/FlaskMail.qll b/python/ql/src/experimental/semmle/python/libraries/FlaskMail.qll index c8b44ac01f0..e4fe2e7f357 100644 --- a/python/ql/src/experimental/semmle/python/libraries/FlaskMail.qll +++ b/python/ql/src/experimental/semmle/python/libraries/FlaskMail.qll @@ -67,7 +67,7 @@ private module FlaskMail { * * `getFrom()`'s result would be `"from@example.com"`. * * `getSubject()`'s result would be `"Subject"`. */ - private class FlaskMail extends DataFlow::CallCfgNode, EmailSender { + private class FlaskMail extends DataFlow::CallCfgNode, EmailSender::Range { FlaskMail() { this = [flaskMailInstance(), flaskMailInstance().getMember("connect").getReturn()] From 129a81a2f8a6ed14b9e47b3d28242fa625a1543f Mon Sep 17 00:00:00 2001 From: jorgectf Date: Sat, 13 Nov 2021 14:24:40 +0100 Subject: [PATCH 40/56] Cover `smtplib` --- .../semmle/python/libraries/SmtpLib.qll | 213 ++++++++++++------ .../Security/CWE-079/smtplib_bad_subparts.py | 8 +- 2 files changed, 155 insertions(+), 66 deletions(-) diff --git a/python/ql/src/experimental/semmle/python/libraries/SmtpLib.qll b/python/ql/src/experimental/semmle/python/libraries/SmtpLib.qll index f57b2ada261..37fd8a73635 100644 --- a/python/ql/src/experimental/semmle/python/libraries/SmtpLib.qll +++ b/python/ql/src/experimental/semmle/python/libraries/SmtpLib.qll @@ -2,89 +2,176 @@ 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.TaintTracking module SmtpLib { - private API::Node smtpLib() { result = API::moduleImport("smtplib") } - - private API::Node smtpConnectionInstance() { result = smtpLib().getMember("SMTP_SSL") } - - API::Node smtpMimeMultipartInstance() { - result = API::moduleImport("email.mime.multipart").getMember("MIMEMultipart") + /** Gets a reference to `smtplib.SMTP_SSL` */ + private API::Node smtpConnectionInstance() { + result = API::moduleImport("smtplib").getMember("SMTP_SSL") } - API::Node smtpMimeTextInstance() { - result = API::moduleImport("email.mime.text").getMember("MIMEText") + /** Gets a reference to `email.mime.multipart.MIMEMultipart` */ + private API::Node smtpMimeMultipartInstance() { + result = + API::moduleImport("email").getMember("mime").getMember("multipart").getMember("MIMEMultipart") } - DataFlow::Node smtpMimeTextHTMLInstance() { - // select SmtpLib::smtpMimeTextInstance().getAUse().getALocalSource().getACall() - exists(API::Node mimeTextInstance, DataFlow::CallCfgNode callNode | - mimeTextInstance = smtpMimeTextInstance().getReturn() and - callNode = mimeTextInstance.getACall() and - callNode.getArg(1).asExpr().(Unicode).getText() = "html" and - result = callNode + /** 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().(Str_).getS() = 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 TaintTracking::Configuration { + SMTPMessageConfig() { this = "SMTPMessageConfig" } + + override predicate isSource(DataFlow::Node source) { source = mimeText(_) } + + override predicate isSink(DataFlow::Node sink) { + sink = smtpMimeMultipartInstance().getACall().getArgByName("_subparts") + } + } + + /** + * Using `MimeText` call, gets the content argument whose type argument equals `mimetype`. + * This call flow sinto `MIMEMultipart`'s `_subparts` argument or `.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 + DataFlow::flowsTo(source, sink.(DataFlow::CallCfgNode).getArgByName("_subparts"), + any(SMTPMessageConfig a)) + 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) ) } - class SmtpLibSendMail extends DataFlow::CallCfgNode, EmailSender { - SmtpLibSendMail() { this = smtpConnectionInstance().getMember("sendmail").getACall() } + /** + * 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().(Str_).getS() = index and + result.asCfgNode() = def.getValue() + ) + } - override DataFlow::Node getPlainTextBody() { - result in [this.getArg(1), this.getArgByName("message")] + /** + * 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 getHtmlBody() { - result in [this.getArg(8), this.getArgByName("html_message")] - } + override DataFlow::Node getPlainTextBody() { result = getSmtpMessage(this, "plain") } + + override DataFlow::Node getHtmlBody() { result = getSmtpMessage(this, "html") } override DataFlow::Node getTo() { - result in [this.getArg(3), this.getArgByName("recipient_list")] + result in [this.getArg(1), getSMTPSubscriptByIndex(this, "To")] } override DataFlow::Node getFrom() { - result in [this.getArg(2), this.getArgByName("from_email")] + result in [this.getArg(0), getSMTPSubscriptByIndex(this, "From")] } override DataFlow::Node getSubject() { - result in [this.getArg(0), this.getArgByName("subject")] + result in [this.getArg(2), getSMTPSubscriptByIndex(this, "Subject")] } } } - -// MIMEMultipart has two ways it can add tainted data: -// MIMEMultipart(_subparts=(part1, part2)) -// or -// message = MIMEMultipart("alternative") -// message.attach(part1) -// -// -// select SmtpLib::smtpMimeTextHTMLInstance() -// select API::moduleImport("email.mime.multipart") -// .getMember("MIMEMultipart") -// .getACall() -// .getArgByName("_subparts") -// -// from DataFlow::Node arg1 -// where -// arg1 = -// API::moduleImport("email.mime.multipart") -// .getMember("MIMEMultipart") -// .getReturn() -// .getMember("attach") -// .getACall() -// .getArg(0) -// -// select SmtpLib::smtpMimeTextHTMLInstance() //.getReturn() -// -//.getArg(1) //.getAUse() -// -// Work on the smtpMimeTextHTMLInstance function -from DataFlow::CallCfgNode result1 -where - exists(API::Node mimeTextInstance, DataFlow::CallCfgNode callNode | - mimeTextInstance = SmtpLib::smtpMimeTextInstance().getReturn() and - callNode = mimeTextInstance.getACall() and - callNode.getArg(1).asExpr().(Unicode).getText() = "html" and - result1 = callNode - ) -select result1 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 index f9cec87c45f..209bd889393 100644 --- 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 @@ -1,12 +1,14 @@ # 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, ssl +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" @@ -28,8 +30,8 @@ def email_person(): # Create secure connection with server and send email context = ssl.create_default_context() - server = smtplib.SMTP_SSL("smtp.gmail.com", 465, context=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() From 1be823d5e77524e0b0f1a9dcf67316da139538ec Mon Sep 17 00:00:00 2001 From: Jorge <46056498+jorgectf@users.noreply.github.com> Date: Mon, 15 Nov 2021 16:41:51 +0100 Subject: [PATCH 41/56] Apply suggestions from code review Co-authored-by: ${sleep,5} <52643283+mrthankyou@users.noreply.github.com> --- python/ql/src/experimental/semmle/python/Concepts.qll | 2 +- .../ql/src/experimental/semmle/python/libraries/SmtpLib.qll | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/python/ql/src/experimental/semmle/python/Concepts.qll b/python/ql/src/experimental/semmle/python/Concepts.qll index 9cdfb028ae4..2a51db3c809 100644 --- a/python/ql/src/experimental/semmle/python/Concepts.qll +++ b/python/ql/src/experimental/semmle/python/Concepts.qll @@ -334,7 +334,7 @@ module EmailSender { } /** - * A data-flow node that sends an email.. + * 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. diff --git a/python/ql/src/experimental/semmle/python/libraries/SmtpLib.qll b/python/ql/src/experimental/semmle/python/libraries/SmtpLib.qll index 37fd8a73635..b9490916ee9 100644 --- a/python/ql/src/experimental/semmle/python/libraries/SmtpLib.qll +++ b/python/ql/src/experimental/semmle/python/libraries/SmtpLib.qll @@ -42,8 +42,8 @@ module SmtpLib { } /** - * Using `MimeText` call, gets the content argument whose type argument equals `mimetype`. - * This call flow sinto `MIMEMultipart`'s `_subparts` argument or `.attach()` method call, + * 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")`: From 5bd8de1514f39430d2ba444121a7403464cbf9be Mon Sep 17 00:00:00 2001 From: jorgectf Date: Mon, 15 Nov 2021 23:04:17 +0100 Subject: [PATCH 42/56] Fix `smtplib`'s `_subparts` taint config issue --- .../src/experimental/semmle/python/libraries/SmtpLib.qll | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/python/ql/src/experimental/semmle/python/libraries/SmtpLib.qll b/python/ql/src/experimental/semmle/python/libraries/SmtpLib.qll index b9490916ee9..0285fac4249 100644 --- a/python/ql/src/experimental/semmle/python/libraries/SmtpLib.qll +++ b/python/ql/src/experimental/semmle/python/libraries/SmtpLib.qll @@ -2,7 +2,7 @@ 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.TaintTracking +private import semmle.python.dataflow.new.TaintTracking2 module SmtpLib { /** Gets a reference to `smtplib.SMTP_SSL` */ @@ -31,7 +31,7 @@ module SmtpLib { * argument. Used because of the impossibility to get local source nodes from `_subparts`' * `(List|Tuple)` elements. */ - private class SMTPMessageConfig extends TaintTracking::Configuration { + private class SMTPMessageConfig extends TaintTracking2::Configuration { SMTPMessageConfig() { this = "SMTPMessageConfig" } override predicate isSource(DataFlow::Node source) { source = mimeText(_) } @@ -87,8 +87,8 @@ module SmtpLib { sink = [sendCall.getArg(2), sendCall.getArg(2).(DataFlow::MethodCallNode).getObject()] .getALocalSource() and - DataFlow::flowsTo(source, sink.(DataFlow::CallCfgNode).getArgByName("_subparts"), - any(SMTPMessageConfig a)) + any(SMTPMessageConfig a) + .hasFlow(source, sink.(DataFlow::CallCfgNode).getArgByName("_subparts")) or // via .attach() sink = smtpMimeMultipartInstance().getReturn().getMember("attach").getACall() and From 018aa11bb65c1fbb6775999c891212538fbe90a7 Mon Sep 17 00:00:00 2001 From: jorgectf Date: Tue, 16 Nov 2021 13:17:43 +0100 Subject: [PATCH 43/56] Make `EmailSender` an instance of `EmailSender::Range` --- .../experimental/semmle/python/Concepts.qll | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/python/ql/src/experimental/semmle/python/Concepts.qll b/python/ql/src/experimental/semmle/python/Concepts.qll index 2a51db3c809..59b57836261 100644 --- a/python/ql/src/experimental/semmle/python/Concepts.qll +++ b/python/ql/src/experimental/semmle/python/Concepts.qll @@ -339,38 +339,34 @@ module EmailSender { * Extend this class to refine existing API models. If you want to model new APIs, * extend `EmailSender::Range` instead. */ -class EmailSender extends DataFlow::Node { - EmailSender::Range range; - - EmailSender() { this = range } - +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 = range.getPlainTextBody() } + DataFlow::Node getPlainTextBody() { result = super.getPlainTextBody() } /** * Gets a data flow node holding the html version of the email body. */ - DataFlow::Node getHtmlBody() { result = range.getHtmlBody() } + DataFlow::Node getHtmlBody() { result = super.getHtmlBody() } /** * Gets a data flow node holding the recipients of the email. */ - DataFlow::Node getTo() { result = range.getTo() } + DataFlow::Node getTo() { result = super.getTo() } /** * Gets a data flow node holding the senders of the email. */ - DataFlow::Node getFrom() { result = range.getFrom() } + DataFlow::Node getFrom() { result = super.getFrom() } /** * Gets a data flow node holding the subject of the email. */ - DataFlow::Node getSubject() { result = range.getSubject() } + 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 [range.getPlainTextBody(), range.getHtmlBody()] } + DataFlow::Node getABody() { result in [super.getPlainTextBody(), super.getHtmlBody()] } } From 1b9567a1d840c158ce53caaf97a509f92a7a2b6d Mon Sep 17 00:00:00 2001 From: jorgectf Date: Sun, 19 Dec 2021 19:56:58 +0100 Subject: [PATCH 44/56] Avoid using `Str_` internal class --- .../semmle/python/frameworks/Sendgrid.qll | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll b/python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll index 797f7030e8d..72a8e1ec960 100644 --- a/python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll +++ b/python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll @@ -114,25 +114,25 @@ private module Sendgrid { result = sendgridWrite("html_content") or exists(KeyValuePair content, Dict generalDict, KeyValuePair typePair, KeyValuePair valuePair | - content.getKey().(Str_).getS() = "content" and + content.getKey().(StrConst).getS() = "content" and content.getValue().(List).getAnElt() = generalDict and // declare KeyValuePairs keys and values - typePair.getKey().(Str_).getS() = "type" and - typePair.getValue().(Str_).getS() = ["text/html", "text/x-amp-html"] and - valuePair.getKey().(Str_).getS() = "value" and + typePair.getKey().(StrConst).getS() = "type" and + typePair.getValue().(StrConst).getS() = ["text/html", "text/x-amp-html"] and + valuePair.getKey().(StrConst).getS() = "value" and result.asExpr() = valuePair.getValue() and // correlate generalDict with previously set KeyValuePairs generalDict.getAnItem() in [typePair, valuePair] ) or exists(KeyValuePair footer, Dict generalDict, KeyValuePair enablePair, KeyValuePair htmlPair | - footer.getKey().(Str_).getS() = ["footer", "subscription_tracking"] and + footer.getKey().(StrConst).getS() = ["footer", "subscription_tracking"] and footer.getValue().(Dict) = generalDict and // check footer is enabled - enablePair.getKey().(Str_).getS() = "enable" and + enablePair.getKey().(StrConst).getS() = "enable" and exists(enablePair.getValue().(True)) and // get html content - htmlPair.getKey().(Str_).getS() = "html" and + htmlPair.getKey().(StrConst).getS() = "html" and result.asExpr() = htmlPair.getValue() and // correlate generalDict with previously set KeyValuePairs generalDict.getAnItem() in [enablePair, htmlPair] From ede5d412acc221eb2937c7030c0db8616310d98e Mon Sep 17 00:00:00 2001 From: jorgectf Date: Sun, 19 Dec 2021 19:57:08 +0100 Subject: [PATCH 45/56] Update `.expected` --- .../Security/CWE-079/ReflectedXSS.expected | 129 +++++++++++++----- 1 file changed, 96 insertions(+), 33 deletions(-) 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 index abfc23f011c..f2d54d254a5 100644 --- a/python/ql/test/experimental/query-tests/Security/CWE-079/ReflectedXSS.expected +++ b/python/ql/test/experimental/query-tests/Security/CWE-079/ReflectedXSS.expected @@ -1,37 +1,100 @@ edges -| flask_mail.py:16:22:16:28 | ControlFlowNode for request | flask_mail.py:16:22:16:33 | ControlFlowNode for Attribute | -| flask_mail.py:16:22:16:28 | ControlFlowNode for request | flask_mail.py:20:14:20:20 | ControlFlowNode for request | -| flask_mail.py:16:22:16:28 | ControlFlowNode for request | flask_mail.py:20:14:20:25 | ControlFlowNode for Attribute | -| flask_mail.py:16:22:16:33 | ControlFlowNode for Attribute | flask_mail.py:16:22:16:41 | ControlFlowNode for Subscript | -| flask_mail.py:20:14:20:20 | ControlFlowNode for request | flask_mail.py:20:14:20:25 | ControlFlowNode for Attribute | -| flask_mail.py:20:14:20:25 | ControlFlowNode for Attribute | flask_mail.py:20:14:20:33 | ControlFlowNode for Subscript | -| flask_mail.py:33:24:33:30 | ControlFlowNode for request | flask_mail.py:33:24:33:35 | ControlFlowNode for Attribute | -| flask_mail.py:33:24:33:35 | ControlFlowNode for Attribute | flask_mail.py:33:24:33:43 | ControlFlowNode for Subscript | -| sendgrid_mail.py:15:20:15:26 | ControlFlowNode for request | sendgrid_mail.py:15:20:15:31 | ControlFlowNode for Attribute | -| sendgrid_mail.py:15:20:15:31 | ControlFlowNode for Attribute | sendgrid_mail.py:15:20:15:47 | ControlFlowNode for Subscript | -| sendgrid_mail.py:25:34:25:40 | ControlFlowNode for request | sendgrid_mail.py:25:34:25:45 | ControlFlowNode for Attribute | -| sendgrid_mail.py:25:34:25:45 | ControlFlowNode for Attribute | sendgrid_mail.py:25:34:25:61 | ControlFlowNode for Subscript | +| 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:20 | ControlFlowNode for request | +| 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:36:37:42 | ControlFlowNode for request | sendgrid_mail.py:37:36:37:47 | ControlFlowNode for Attribute | +| sendgrid_mail.py:37:36:37:47 | ControlFlowNode for Attribute | sendgrid_mail.py:37:36:37:63 | 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:56 | ControlFlowNode for request | +| 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:56 | ControlFlowNode for request | +| 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:56 | ControlFlowNode for request | +| 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 -| flask_mail.py:16:22:16:28 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| flask_mail.py:16:22:16:33 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | -| flask_mail.py:16:22:16:41 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | -| flask_mail.py:20:14:20:20 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| flask_mail.py:20:14:20:25 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | -| flask_mail.py:20:14:20:33 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | -| flask_mail.py:33:24:33:30 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| flask_mail.py:33:24:33:35 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | -| flask_mail.py:33:24:33:43 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | -| sendgrid_mail.py:15:20:15:26 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| sendgrid_mail.py:15:20:15:31 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | -| sendgrid_mail.py:15:20:15:47 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | -| sendgrid_mail.py:25:34:25:40 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| sendgrid_mail.py:25:34:25:45 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | -| sendgrid_mail.py:25:34:25:61 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript | +| 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:36:37:42 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| sendgrid_mail.py:37:36:37:47 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | +| sendgrid_mail.py:37:36:37:63 | 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 -| flask_mail.py:16:22:16:41 | ControlFlowNode for Subscript | flask_mail.py:16:22:16:28 | ControlFlowNode for request | flask_mail.py:16:22:16:41 | ControlFlowNode for Subscript | Cross-site scripting vulnerability due to $@. | flask_mail.py:16:22:16:28 | ControlFlowNode for request | a user-provided value | -| flask_mail.py:20:14:20:33 | ControlFlowNode for Subscript | flask_mail.py:16:22:16:28 | ControlFlowNode for request | flask_mail.py:20:14:20:33 | ControlFlowNode for Subscript | Cross-site scripting vulnerability due to $@. | flask_mail.py:16:22:16:28 | ControlFlowNode for request | a user-provided value | -| flask_mail.py:20:14:20:33 | ControlFlowNode for Subscript | flask_mail.py:20:14:20:20 | ControlFlowNode for request | flask_mail.py:20:14:20:33 | ControlFlowNode for Subscript | Cross-site scripting vulnerability due to $@. | flask_mail.py:20:14:20:20 | ControlFlowNode for request | a user-provided value | -| flask_mail.py:33:24:33:43 | ControlFlowNode for Subscript | flask_mail.py:33:24:33:30 | ControlFlowNode for request | flask_mail.py:33:24:33:43 | ControlFlowNode for Subscript | Cross-site scripting vulnerability due to $@. | flask_mail.py:33:24:33:30 | ControlFlowNode for request | a user-provided value | -| sendgrid_mail.py:15:20:15:47 | ControlFlowNode for Subscript | sendgrid_mail.py:15:20:15:26 | ControlFlowNode for request | sendgrid_mail.py:15:20:15:47 | ControlFlowNode for Subscript | Cross-site scripting vulnerability due to $@. | sendgrid_mail.py:15:20:15:26 | ControlFlowNode for request | a user-provided value | -| sendgrid_mail.py:25:34:25:61 | ControlFlowNode for Subscript | sendgrid_mail.py:25:34:25:40 | ControlFlowNode for request | sendgrid_mail.py:25:34:25:61 | ControlFlowNode for Subscript | Cross-site scripting vulnerability due to $@. | sendgrid_mail.py:25:34:25:40 | ControlFlowNode for request | a user-provided value | +| 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:36:37:63 | ControlFlowNode for Subscript | sendgrid_mail.py:37:36:37:42 | ControlFlowNode for request | sendgrid_mail.py:37:36:37:63 | ControlFlowNode for Subscript | Cross-site scripting vulnerability due to $@. | sendgrid_mail.py:37:36:37:42 | 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 | From 2f2cf2c1f6fc636c71b4a9b78e2ce6e994c7e0eb Mon Sep 17 00:00:00 2001 From: jorgectf Date: Sat, 26 Feb 2022 01:19:50 +0100 Subject: [PATCH 46/56] Use `StrConst.getText()` instead of `Str_.getS()` --- .../semmle/python/frameworks/Sendgrid.qll | 14 +++++++------- .../semmle/python/libraries/SmtpLib.qll | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll b/python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll index 72a8e1ec960..de8c6b2eda7 100644 --- a/python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll +++ b/python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll @@ -114,25 +114,25 @@ private module Sendgrid { result = sendgridWrite("html_content") or exists(KeyValuePair content, Dict generalDict, KeyValuePair typePair, KeyValuePair valuePair | - content.getKey().(StrConst).getS() = "content" and + content.getKey().(StrConst).getText() = "content" and content.getValue().(List).getAnElt() = generalDict and // declare KeyValuePairs keys and values - typePair.getKey().(StrConst).getS() = "type" and - typePair.getValue().(StrConst).getS() = ["text/html", "text/x-amp-html"] and - valuePair.getKey().(StrConst).getS() = "value" and + 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] ) or exists(KeyValuePair footer, Dict generalDict, KeyValuePair enablePair, KeyValuePair htmlPair | - footer.getKey().(StrConst).getS() = ["footer", "subscription_tracking"] and + footer.getKey().(StrConst).getText() = ["footer", "subscription_tracking"] and footer.getValue().(Dict) = generalDict and // check footer is enabled - enablePair.getKey().(StrConst).getS() = "enable" and + enablePair.getKey().(StrConst).getText() = "enable" and exists(enablePair.getValue().(True)) and // get html content - htmlPair.getKey().(StrConst).getS() = "html" and + htmlPair.getKey().(StrConst).getText() = "html" and result.asExpr() = htmlPair.getValue() and // correlate generalDict with previously set KeyValuePairs generalDict.getAnItem() in [enablePair, htmlPair] diff --git a/python/ql/src/experimental/semmle/python/libraries/SmtpLib.qll b/python/ql/src/experimental/semmle/python/libraries/SmtpLib.qll index 0285fac4249..8d69bcb1a62 100644 --- a/python/ql/src/experimental/semmle/python/libraries/SmtpLib.qll +++ b/python/ql/src/experimental/semmle/python/libraries/SmtpLib.qll @@ -23,7 +23,7 @@ module SmtpLib { private DataFlow::CallCfgNode mimeText(string mimetype) { result = smtpMimeTextInstance().getACall() and - [result.getArg(1), result.getArgByName("_subtype")].asExpr().(Str_).getS() = mimetype + [result.getArg(1), result.getArgByName("_subtype")].asExpr().(StrConst).getText() = mimetype } /** @@ -123,7 +123,7 @@ module SmtpLib { DataFlow::exprNode(sub.getObject()).getALocalSource() = [sendCall.getArg(2), sendCall.getArg(2).(DataFlow::MethodCallNode).getObject()] .getALocalSource() and - sub.getIndex().(Str_).getS() = index and + sub.getIndex().(StrConst).getText() = index and result.asCfgNode() = def.getValue() ) } From 3159d8e211a1dd874e9d2d0420a2c6764ad88709 Mon Sep 17 00:00:00 2001 From: jorgectf Date: Thu, 3 Mar 2022 04:33:10 +0100 Subject: [PATCH 47/56] Correlate `SendGridMail` declaration with its predicates --- .../semmle/python/frameworks/Sendgrid.qll | 73 ++++++++++++------- .../Security/CWE-079/ReflectedXSS.expected | 12 +-- .../Security/CWE-079/sendgrid_mail.py | 21 +++++- 3 files changed, 68 insertions(+), 38 deletions(-) diff --git a/python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll b/python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll index de8c6b2eda7..65caa24e1b1 100644 --- a/python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll +++ b/python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll @@ -51,9 +51,9 @@ private module Sendgrid { ) } - private DataFlow::Node sendgridWrite(string attributeName) { + private DataFlow::Node sendgridWrite(DataFlow::CallCfgNode mailCall, string attributeName) { exists(DataFlow::AttrWrite attrWrite | - attrWrite.getObject().getALocalSource() = sendgridMailCall() and + attrWrite.getObject().getALocalSource() = mailCall and attrWrite.getAttributeName() = attributeName and result = attrWrite.getValue() ) @@ -86,32 +86,39 @@ private module Sendgrid { private class SendGridMail extends DataFlow::CallCfgNode, EmailSender::Range { SendGridMail() { this.getFunction() = sendgridApiSendCall() } + DataFlow::CallCfgNode getMailCall() { + exists(DataFlow::Node n | + n in [this.getArg(0), this.getArgByName("request_body")] and + result = [n, n.(DataFlow::MethodCallNode).getObject()].getALocalSource() + ) + } + override DataFlow::Node getPlainTextBody() { result in [ - sendgridMailCall().getArg(3), sendgridMailCall().getArgByName("plain_text_content") + this.getMailCall().getArg(3), this.getMailCall().getArgByName("plain_text_content") ] or result in [ - sendgridContent(sendgridMailHelper().getMember("Content").getACall(), "text/plain"), + sendgridContent([ + this.getMailCall().getArg(3), this.getMailCall().getArgByName("plain_text_content") + ].getALocalSource(), "text/plain"), sendgridContent(sendgridMailInstance().getMember("add_content").getACall(), "text/plain") ] or - result = sendgridWrite("plain_text_content") + result = sendgridWrite(this.getMailCall(), "plain_text_content") } override DataFlow::Node getHtmlBody() { - result in [sendgridMailCall().getArg(4), sendgridMailCall().getArgByName("html_content")] + result in [this.getMailCall().getArg(4), this.getMailCall().getArgByName("html_content")] or - result = sendgridMailInstance().getMember("set_html").getACall().getArg(0) + result = this.getMailCall().getAMethodCall("set_html").getArg(0) or - result in [ - sendgridContent(sendgridMailHelper().getMember("Content").getACall(), - ["text/html", "text/x-amp-html"]), - sendgridContent(sendgridMailInstance().getMember("add_content").getACall(), - ["text/html", "text/x-amp-html"]) - ] + result = + sendgridContent([ + this.getMailCall().getArg(4), this.getMailCall().getArgByName("html_content") + ].getALocalSource(), ["text/html", "text/x-amp-html"]) or - result = sendgridWrite("html_content") + result = sendgridWrite(this.getMailCall(), "html_content") or exists(KeyValuePair content, Dict generalDict, KeyValuePair typePair, KeyValuePair valuePair | content.getKey().(StrConst).getText() = "content" and @@ -122,7 +129,9 @@ private module Sendgrid { valuePair.getKey().(StrConst).getText() = "value" and result.asExpr() = valuePair.getValue() and // correlate generalDict with previously set KeyValuePairs - generalDict.getAnItem() in [typePair, valuePair] + 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 | @@ -135,38 +144,46 @@ private module Sendgrid { htmlPair.getKey().(StrConst).getText() = "html" and result.asExpr() = htmlPair.getValue() and // correlate generalDict with previously set KeyValuePairs - generalDict.getAnItem() in [enablePair, htmlPair] + 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 [sendgridMailCall().getArg(1), sendgridMailCall().getArgByName("to_emails")] + result in [this.getMailCall().getArg(1), this.getMailCall().getArgByName("to_emails")] or - result = sendgridMailHelper().getMember("To").getACall().getArg(0) + result = this.getMailCall().getAMethodCall("To").getArg(0) or result = - sendgridMailInstance() - .getMember(["to", "add_to", "cc", "add_cc", "bcc", "add_bcc"]) - .getACall() + this.getMailCall() + .getAMethodCall(["to", "add_to", "cc", "add_cc", "bcc", "add_bcc"]) .getArg(0) } override DataFlow::Node getFrom() { - result in [sendgridMailCall().getArg(0), sendgridMailCall().getArgByName("from_email")] + result in [this.getMailCall().getArg(0), this.getMailCall().getArgByName("from_email")] or - result = sendgridMailHelper().getMember("Email").getACall().getArg(0) + result = this.getMailCall().getAMethodCall("Email").getArg(0) or - result = sendgridMailInstance().getMember(["from_email", "set_from"]).getACall().getArg(0) + result = this.getMailCall().getAMethodCall(["from_email", "set_from"]).getArg(0) or - result = sendgridWrite("from_email") + result = sendgridWrite(this.getMailCall(), "from_email") } override DataFlow::Node getSubject() { - result in [sendgridMailCall().getArg(2), sendgridMailCall().getArgByName("subject")] + result in [this.getMailCall().getArg(2), this.getMailCall().getArgByName("subject")] or - result = sendgridMailInstance().getMember(["subject", "set_subject"]).getACall().getArg(0) + result = this.getMailCall().getAMethodCall(["subject", "set_subject"]).getArg(0) or - result = sendgridWrite("subject") + result = sendgridWrite(this.getMailCall(), "subject") } } } 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 index f2d54d254a5..094e7b9974e 100644 --- a/python/ql/test/experimental/query-tests/Security/CWE-079/ReflectedXSS.expected +++ b/python/ql/test/experimental/query-tests/Security/CWE-079/ReflectedXSS.expected @@ -12,8 +12,8 @@ edges | 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:36:37:42 | ControlFlowNode for request | sendgrid_mail.py:37:36:37:47 | ControlFlowNode for Attribute | -| sendgrid_mail.py:37:36:37:47 | ControlFlowNode for Attribute | sendgrid_mail.py:37:36:37:63 | ControlFlowNode for Subscript | +| 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:56 | ControlFlowNode for request | | 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 | @@ -55,9 +55,9 @@ nodes | 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:36:37:42 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | -| sendgrid_mail.py:37:36:37:47 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | -| sendgrid_mail.py:37:36:37:63 | 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 | @@ -89,7 +89,7 @@ subpaths | 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:36:37:63 | ControlFlowNode for Subscript | sendgrid_mail.py:37:36:37:42 | ControlFlowNode for request | sendgrid_mail.py:37:36:37:63 | ControlFlowNode for Subscript | Cross-site scripting vulnerability due to $@. | sendgrid_mail.py:37:36:37:42 | 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 | 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 index 4c577e178c4..e10e8a030a8 100644 --- 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 @@ -34,11 +34,24 @@ def send_post(): from_email = Email("test@example.com") to_email = To("test@example.com") subject = "Sending with SendGrid is Fun" - content = Content("text/html", request.args["html_content"]) + html_content = Content("text/html", request.args["html_content"]) + plain_content = Content("text/plain", request.args["plain_content"]) - content = Content(MimeType.html, request.args["html_content"]) - - mail = Mail(from_email, to_email, subject, 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()) From 6722671541e8ddff86b3505a400bb46e19f87ee4 Mon Sep 17 00:00:00 2001 From: jorgectf Date: Tue, 8 Mar 2022 18:24:38 +0100 Subject: [PATCH 48/56] Refactor `sendgridApiClient` and `sendgridApiSendCall` Co-authored-by: yoff --- .../semmle/python/frameworks/Sendgrid.qll | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll b/python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll index 65caa24e1b1..2a5ea173011 100644 --- a/python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll +++ b/python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll @@ -24,23 +24,21 @@ private module Sendgrid { private DataFlow::CallCfgNode sendgridMailCall() { result = sendgridMailInstance().getACall() } /** Gets a reference to a `SendGridAPIClient` instance. */ - private DataFlow::LocalSourceNode sendgridApiClient(DataFlow::TypeTracker t) { - t.start() and - result.(DataFlow::AttrRead).getObject*().getALocalSource() = - sendgrid().getMember("SendGridAPIClient").getReturn().getAUse() - or - exists(DataFlow::TypeTracker t2 | result = sendgridApiClient(t2).track(t2, t)) - } - - /** Gets a reference to a `SendGridAPIClient` instance use. */ - private DataFlow::Node sendgridApiClient() { - sendgridApiClient(DataFlow::TypeTracker::end()).flowsTo(result) + private API::Node sendgridApiClient() { + result = sendgrid().getMember("SendGridAPIClient").getReturn() } /** Gets a reference to a `SendGridAPIClient` instance call with `send` or `post`. */ - private DataFlow::Node sendgridApiSendCall() { - result = sendgridApiClient() and - result.(DataFlow::AttrRead).getAttributeName() in ["send", "post"] + private DataFlow::CallCfgNode sendgridApiSendCall() { + result = sendgridApiClient().getMember("send").getACall() + or + result = + sendgridApiClient() + .getMember("client") + .getMember("mail") + .getMember("send") + .getMember("post") + .getACall() } private DataFlow::Node sendgridContent(DataFlow::CallCfgNode contentCall, string mime) { From 6b043446554d5a908a6f3970e34ac7b87131394e Mon Sep 17 00:00:00 2001 From: jorgectf Date: Tue, 8 Mar 2022 18:26:20 +0100 Subject: [PATCH 49/56] Refactor `sendgridContent` and `sendgridWrite` Move the predicates inside `SendGridMail`. See https://github.com/github/codeql/pull/7127#discussion_r821574462 --- .../semmle/python/frameworks/Sendgrid.qll | 46 ++++++++++--------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll b/python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll index 2a5ea173011..ec7577af909 100644 --- a/python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll +++ b/python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll @@ -41,22 +41,6 @@ private module Sendgrid { .getACall() } - private DataFlow::Node sendgridContent(DataFlow::CallCfgNode contentCall, string mime) { - 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(DataFlow::CallCfgNode mailCall, string attributeName) { - exists(DataFlow::AttrWrite attrWrite | - attrWrite.getObject().getALocalSource() = mailCall and - attrWrite.getAttributeName() = attributeName and - result = attrWrite.getValue() - ) - } - /** * Gets a reference to `sg.send()` and `sg.client.mail.send.post()`. * @@ -82,15 +66,33 @@ private module Sendgrid { * * `getSubject()`'s result would be `"Sending with SendGrid is Fun"`. */ private class SendGridMail extends DataFlow::CallCfgNode, EmailSender::Range { - SendGridMail() { this.getFunction() = sendgridApiSendCall() } + SendGridMail() { this = sendgridApiSendCall() } - DataFlow::CallCfgNode getMailCall() { + 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") @@ -103,7 +105,7 @@ private module Sendgrid { sendgridContent(sendgridMailInstance().getMember("add_content").getACall(), "text/plain") ] or - result = sendgridWrite(this.getMailCall(), "plain_text_content") + result = sendgridWrite("plain_text_content") } override DataFlow::Node getHtmlBody() { @@ -116,7 +118,7 @@ private module Sendgrid { this.getMailCall().getArg(4), this.getMailCall().getArgByName("html_content") ].getALocalSource(), ["text/html", "text/x-amp-html"]) or - result = sendgridWrite(this.getMailCall(), "html_content") + result = sendgridWrite("html_content") or exists(KeyValuePair content, Dict generalDict, KeyValuePair typePair, KeyValuePair valuePair | content.getKey().(StrConst).getText() = "content" and @@ -173,7 +175,7 @@ private module Sendgrid { or result = this.getMailCall().getAMethodCall(["from_email", "set_from"]).getArg(0) or - result = sendgridWrite(this.getMailCall(), "from_email") + result = sendgridWrite("from_email") } override DataFlow::Node getSubject() { @@ -181,7 +183,7 @@ private module Sendgrid { or result = this.getMailCall().getAMethodCall(["subject", "set_subject"]).getArg(0) or - result = sendgridWrite(this.getMailCall(), "subject") + result = sendgridWrite("subject") } } } From 930fbf777c45045840801d32d880da7709993d7f Mon Sep 17 00:00:00 2001 From: jorgectf Date: Tue, 8 Mar 2022 18:38:32 +0100 Subject: [PATCH 50/56] Move `getFlaskMailArgument` inside `FlaskMail` and refactor --- .../semmle/python/libraries/FlaskMail.qll | 52 ++++++++----------- 1 file changed, 22 insertions(+), 30 deletions(-) diff --git a/python/ql/src/experimental/semmle/python/libraries/FlaskMail.qll b/python/ql/src/experimental/semmle/python/libraries/FlaskMail.qll index e4fe2e7f357..58e923faa82 100644 --- a/python/ql/src/experimental/semmle/python/libraries/FlaskMail.qll +++ b/python/ql/src/experimental/semmle/python/libraries/FlaskMail.qll @@ -21,31 +21,6 @@ private module FlaskMail { /** Gets a reference to `flask_mail.Message`, `flask_sendmail.Message` and `flask.ext.sendmail.Message`. */ private API::Node flaskMessageInstance() { result = flaskMail().getMember("Message") } - /** Gets a call to `flask_mail.Message`, `flask_sendmail.Message` and `flask.ext.sendmail.Message`. */ - private DataFlow::CallCfgNode flaskMessageCall() { result = flaskMessageInstance().getACall() } - - /** - * Gets a reference to an argument from `flask_mail.Message`, `flask_sendmail.Message` and `flask.ext.sendmail.Message`. - * - * Usage example: - * - * ```codeql - * DataFlow::Node getPlainTextBody() { result = getFlaskMailArgument(2, "body") } - * ``` - */ - bindingset[argumentPosition, argumentName] - private DataFlow::Node getFlaskMailArgument(int argumentPosition, string argumentName) { - result in [ - flaskMessageCall().getArg(argumentPosition), flaskMessageCall().getArgByName(argumentName) - ] - or - exists(DataFlow::AttrWrite write | - write.getObject().getALocalSource() = flaskMessageCall() and - write.getAttributeName() = argumentName and - result = write.getValue() - ) - } - /** * Gets a call to `mail.send()`. * @@ -75,18 +50,35 @@ private module FlaskMail { .getACall() } - override DataFlow::Node getPlainTextBody() { result = getFlaskMailArgument(2, "body") } + private DataFlow::CallCfgNode getMessage() { result = this.getArg(0).getALocalSource() } - override DataFlow::Node getHtmlBody() { result = getFlaskMailArgument(3, "html") } + 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 = getFlaskMailArgument(1, "recipients") + result = this.getFlaskMailArgument(1, "recipients") or result = flaskMessageInstance().getMember("add_recipient").getACall().getArg(0) } - override DataFlow::Node getFrom() { result = getFlaskMailArgument(5, "sender") } + override DataFlow::Node getFrom() { result = this.getFlaskMailArgument(5, "sender") } - override DataFlow::Node getSubject() { result = getFlaskMailArgument(0, "subject") } + override DataFlow::Node getSubject() { result = this.getFlaskMailArgument(0, "subject") } } } From bbba1a21c4c289ca00baa6275b7dc3c53580a328 Mon Sep 17 00:00:00 2001 From: jorgectf Date: Tue, 8 Mar 2022 18:40:20 +0100 Subject: [PATCH 51/56] Explicitly call `this` in `SendGridMail` --- .../semmle/python/frameworks/Sendgrid.qll | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll b/python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll index ec7577af909..8bb64d4b72b 100644 --- a/python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll +++ b/python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll @@ -99,13 +99,14 @@ private module Sendgrid { ] or result in [ - sendgridContent([ + this.sendgridContent([ this.getMailCall().getArg(3), this.getMailCall().getArgByName("plain_text_content") ].getALocalSource(), "text/plain"), - sendgridContent(sendgridMailInstance().getMember("add_content").getACall(), "text/plain") + this.sendgridContent(sendgridMailInstance().getMember("add_content").getACall(), + "text/plain") ] or - result = sendgridWrite("plain_text_content") + result = this.sendgridWrite("plain_text_content") } override DataFlow::Node getHtmlBody() { @@ -114,11 +115,11 @@ private module Sendgrid { result = this.getMailCall().getAMethodCall("set_html").getArg(0) or result = - sendgridContent([ + this.sendgridContent([ this.getMailCall().getArg(4), this.getMailCall().getArgByName("html_content") ].getALocalSource(), ["text/html", "text/x-amp-html"]) or - result = sendgridWrite("html_content") + result = this.sendgridWrite("html_content") or exists(KeyValuePair content, Dict generalDict, KeyValuePair typePair, KeyValuePair valuePair | content.getKey().(StrConst).getText() = "content" and @@ -175,7 +176,7 @@ private module Sendgrid { or result = this.getMailCall().getAMethodCall(["from_email", "set_from"]).getArg(0) or - result = sendgridWrite("from_email") + result = this.sendgridWrite("from_email") } override DataFlow::Node getSubject() { @@ -183,7 +184,7 @@ private module Sendgrid { or result = this.getMailCall().getAMethodCall(["subject", "set_subject"]).getArg(0) or - result = sendgridWrite("subject") + result = this.sendgridWrite("subject") } } } From 3f43e6ef548eb25e5ef0ce71a78f844b6f09c853 Mon Sep 17 00:00:00 2001 From: jorgectf Date: Tue, 8 Mar 2022 18:45:53 +0100 Subject: [PATCH 52/56] Fix `FlaskMail`'s `getTo` --- .../ql/src/experimental/semmle/python/libraries/FlaskMail.qll | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/ql/src/experimental/semmle/python/libraries/FlaskMail.qll b/python/ql/src/experimental/semmle/python/libraries/FlaskMail.qll index 58e923faa82..bfe3b943b24 100644 --- a/python/ql/src/experimental/semmle/python/libraries/FlaskMail.qll +++ b/python/ql/src/experimental/semmle/python/libraries/FlaskMail.qll @@ -74,7 +74,7 @@ private module FlaskMail { override DataFlow::Node getTo() { result = this.getFlaskMailArgument(1, "recipients") or - result = flaskMessageInstance().getMember("add_recipient").getACall().getArg(0) + result = this.getMessage().getAMethodCall("add_recipient").getACall().getArg(0) } override DataFlow::Node getFrom() { result = this.getFlaskMailArgument(5, "sender") } From c155ac6e7ada78eba0a1c40f04c9ca408ddde6ec Mon Sep 17 00:00:00 2001 From: jorgectf Date: Thu, 10 Mar 2022 00:47:04 +0100 Subject: [PATCH 53/56] Add `HtmlEscaping` sanitizer --- .../semmle/python/security/dataflow/ReflectedXSS.qll | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/python/ql/src/experimental/semmle/python/security/dataflow/ReflectedXSS.qll b/python/ql/src/experimental/semmle/python/security/dataflow/ReflectedXSS.qll index c9996e461f8..58ca3c5b4e6 100644 --- a/python/ql/src/experimental/semmle/python/security/dataflow/ReflectedXSS.qll +++ b/python/ql/src/experimental/semmle/python/security/dataflow/ReflectedXSS.qll @@ -9,6 +9,7 @@ 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 /** @@ -26,6 +27,10 @@ class ReflectedXssConfiguration extends TaintTracking::Configuration { 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 = From e577a0e8360137001cbb43c1d039e5ce6b8f1e7d Mon Sep 17 00:00:00 2001 From: jorgectf Date: Fri, 27 May 2022 00:13:40 +0200 Subject: [PATCH 54/56] Update `.expected` tests --- .../query-tests/Security/CWE-079/ReflectedXSS.expected | 4 ---- 1 file changed, 4 deletions(-) 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 index 094e7b9974e..f787dfa43fc 100644 --- a/python/ql/test/experimental/query-tests/Security/CWE-079/ReflectedXSS.expected +++ b/python/ql/test/experimental/query-tests/Security/CWE-079/ReflectedXSS.expected @@ -1,6 +1,5 @@ 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:20 | ControlFlowNode for request | | 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 | @@ -15,14 +14,11 @@ edges | 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:56 | ControlFlowNode for request | | 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:56 | ControlFlowNode for request | | 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:56 | ControlFlowNode for request | | 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() | From 897d5c947186b0d80039bf1975453698ed446b22 Mon Sep 17 00:00:00 2001 From: Jorge <46056498+jorgectf@users.noreply.github.com> Date: Wed, 1 Jun 2022 12:44:08 +0200 Subject: [PATCH 55/56] Apply suggestions from code review Co-authored-by: yoff --- .../ql/src/experimental/semmle/python/frameworks/Django.qll | 2 +- .../ql/src/experimental/semmle/python/frameworks/Sendgrid.qll | 4 +--- .../ql/src/experimental/semmle/python/libraries/FlaskMail.qll | 2 -- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/python/ql/src/experimental/semmle/python/frameworks/Django.qll b/python/ql/src/experimental/semmle/python/frameworks/Django.qll index 79ebaaaa9ad..6853f9c3f6a 100644 --- a/python/ql/src/experimental/semmle/python/frameworks/Django.qll +++ b/python/ql/src/experimental/semmle/python/frameworks/Django.qll @@ -190,7 +190,7 @@ private module ExperimentalPrivateDjango { } } - module email { + module Email { /** https://docs.djangoproject.com/en/3.2/topics/email/ */ private API::Node djangoMail() { result = API::moduleImport("django").getMember("core").getMember("mail") diff --git a/python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll b/python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll index 8bb64d4b72b..bde91a1bc47 100644 --- a/python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll +++ b/python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll @@ -20,8 +20,6 @@ private module Sendgrid { /** Gets a reference to `sendgrid.helpers.mail.Mail` */ private API::Node sendgridMailInstance() { result = sendgridMailHelper().getMember("Mail") } - /** Gets a call to `sendgrid.helpers.mail.Mail()`. */ - private DataFlow::CallCfgNode sendgridMailCall() { result = sendgridMailInstance().getACall() } /** Gets a reference to a `SendGridAPIClient` instance. */ private API::Node sendgridApiClient() { @@ -137,7 +135,7 @@ private module Sendgrid { or exists(KeyValuePair footer, Dict generalDict, KeyValuePair enablePair, KeyValuePair htmlPair | footer.getKey().(StrConst).getText() = ["footer", "subscription_tracking"] and - footer.getValue().(Dict) = generalDict and + footer.getValue() = generalDict and // check footer is enabled enablePair.getKey().(StrConst).getText() = "enable" and exists(enablePair.getValue().(True)) and diff --git a/python/ql/src/experimental/semmle/python/libraries/FlaskMail.qll b/python/ql/src/experimental/semmle/python/libraries/FlaskMail.qll index bfe3b943b24..d7f84d3132d 100644 --- a/python/ql/src/experimental/semmle/python/libraries/FlaskMail.qll +++ b/python/ql/src/experimental/semmle/python/libraries/FlaskMail.qll @@ -18,8 +18,6 @@ private module FlaskMail { /** 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 reference to `flask_mail.Message`, `flask_sendmail.Message` and `flask.ext.sendmail.Message`. */ - private API::Node flaskMessageInstance() { result = flaskMail().getMember("Message") } /** * Gets a call to `mail.send()`. From 171239b78fa2146d51b3fbaf224f28a5489be729 Mon Sep 17 00:00:00 2001 From: jorgectf Date: Fri, 3 Jun 2022 18:27:45 +0200 Subject: [PATCH 56/56] Format `FlaskMail.qll` and `Sendgrid.qll` --- python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll | 1 - python/ql/src/experimental/semmle/python/libraries/FlaskMail.qll | 1 - 2 files changed, 2 deletions(-) diff --git a/python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll b/python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll index bde91a1bc47..2914c59d755 100644 --- a/python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll +++ b/python/ql/src/experimental/semmle/python/frameworks/Sendgrid.qll @@ -20,7 +20,6 @@ private module Sendgrid { /** 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() diff --git a/python/ql/src/experimental/semmle/python/libraries/FlaskMail.qll b/python/ql/src/experimental/semmle/python/libraries/FlaskMail.qll index d7f84d3132d..7659a0f45b2 100644 --- a/python/ql/src/experimental/semmle/python/libraries/FlaskMail.qll +++ b/python/ql/src/experimental/semmle/python/libraries/FlaskMail.qll @@ -18,7 +18,6 @@ private module FlaskMail { /** 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()`. *