|
| 1 | +/** |
| 2 | + * Provides classes modeling security-relevant aspects of the `sendgrid` PyPI package. |
| 3 | + * See https://github.com/sendgrid/sendgrid-python. |
| 4 | + */ |
| 5 | + |
| 6 | +private import python |
| 7 | +private import semmle.python.dataflow.new.DataFlow |
| 8 | +private import experimental.semmle.python.Concepts |
| 9 | +private import semmle.python.ApiGraphs |
| 10 | + |
| 11 | +private module Sendgrid { |
| 12 | + /** Gets a reference to the `sendgrid` module. */ |
| 13 | + private API::Node sendgrid() { result = API::moduleImport("sendgrid") } |
| 14 | + |
| 15 | + /** Gets a reference to `sendgrid.helpers.mail` */ |
| 16 | + private API::Node sendgridMailHelper() { |
| 17 | + result = sendgrid().getMember("helpers").getMember("mail") |
| 18 | + } |
| 19 | + |
| 20 | + /** Gets a reference to `sendgrid.helpers.mail.Mail` */ |
| 21 | + private API::Node sendgridMailInstance() { result = sendgridMailHelper().getMember("Mail") } |
| 22 | + |
| 23 | + /** Gets a reference to a `SendGridAPIClient` instance. */ |
| 24 | + private API::Node sendgridApiClient() { |
| 25 | + result = sendgrid().getMember("SendGridAPIClient").getReturn() |
| 26 | + } |
| 27 | + |
| 28 | + /** Gets a reference to a `SendGridAPIClient` instance call with `send` or `post`. */ |
| 29 | + private DataFlow::CallCfgNode sendgridApiSendCall() { |
| 30 | + result = sendgridApiClient().getMember("send").getACall() |
| 31 | + or |
| 32 | + result = |
| 33 | + sendgridApiClient() |
| 34 | + .getMember("client") |
| 35 | + .getMember("mail") |
| 36 | + .getMember("send") |
| 37 | + .getMember("post") |
| 38 | + .getACall() |
| 39 | + } |
| 40 | + |
| 41 | + /** |
| 42 | + * Gets a reference to `sg.send()` and `sg.client.mail.send.post()`. |
| 43 | + * |
| 44 | + * Given the following example: |
| 45 | + * |
| 46 | + * ```py |
| 47 | + * from_email = Email("from@example.com") |
| 48 | + * to_email = To("to@example.com") |
| 49 | + * subject = "Sending with SendGrid is Fun" |
| 50 | + * content = Content("text/html", request.args["html_content"]) |
| 51 | + * |
| 52 | + * mail = Mail(from_email, to_email, subject, content) |
| 53 | + * |
| 54 | + * sg = SendGridAPIClient(api_key='SENDGRID_API_KEY') |
| 55 | + * response = sg.client.mail.send.post(request_body=mail.get()) |
| 56 | + * ``` |
| 57 | + * |
| 58 | + * * `this` would be `sg.client.mail.send.post(request_body=mail.get())`. |
| 59 | + * * `getPlainTextBody()`'s result would be `none()`. |
| 60 | + * * `getHtmlBody()`'s result would be `request.args["html_content"]`. |
| 61 | + * * `getTo()`'s result would be `"to@example.com"`. |
| 62 | + * * `getFrom()`'s result would be `"from@example.com"`. |
| 63 | + * * `getSubject()`'s result would be `"Sending with SendGrid is Fun"`. |
| 64 | + */ |
| 65 | + private class SendGridMail extends DataFlow::CallCfgNode, EmailSender::Range { |
| 66 | + SendGridMail() { this = sendgridApiSendCall() } |
| 67 | + |
| 68 | + private DataFlow::CallCfgNode getMailCall() { |
| 69 | + exists(DataFlow::Node n | |
| 70 | + n in [this.getArg(0), this.getArgByName("request_body")] and |
| 71 | + result = [n, n.(DataFlow::MethodCallNode).getObject()].getALocalSource() |
| 72 | + ) |
| 73 | + } |
| 74 | + |
| 75 | + private DataFlow::Node sendgridContent(DataFlow::CallCfgNode contentCall, string mime) { |
| 76 | + mime in ["text/plain", "text/html", "text/x-amp-html"] and |
| 77 | + exists(StrConst mimeNode | |
| 78 | + mimeNode.getText() = mime and |
| 79 | + DataFlow::exprNode(mimeNode).(DataFlow::LocalSourceNode).flowsTo(contentCall.getArg(0)) and |
| 80 | + result = contentCall.getArg(1) |
| 81 | + ) |
| 82 | + } |
| 83 | + |
| 84 | + private DataFlow::Node sendgridWrite(string attributeName) { |
| 85 | + attributeName in ["plain_text_content", "html_content", "from_email", "subject"] and |
| 86 | + exists(DataFlow::AttrWrite attrWrite | |
| 87 | + attrWrite.getObject().getALocalSource() = this.getMailCall() and |
| 88 | + attrWrite.getAttributeName() = attributeName and |
| 89 | + result = attrWrite.getValue() |
| 90 | + ) |
| 91 | + } |
| 92 | + |
| 93 | + override DataFlow::Node getPlainTextBody() { |
| 94 | + result in [ |
| 95 | + this.getMailCall().getArg(3), this.getMailCall().getArgByName("plain_text_content") |
| 96 | + ] |
| 97 | + or |
| 98 | + result in [ |
| 99 | + this.sendgridContent([ |
| 100 | + this.getMailCall().getArg(3), this.getMailCall().getArgByName("plain_text_content") |
| 101 | + ].getALocalSource(), "text/plain"), |
| 102 | + this.sendgridContent(sendgridMailInstance().getMember("add_content").getACall(), |
| 103 | + "text/plain") |
| 104 | + ] |
| 105 | + or |
| 106 | + result = this.sendgridWrite("plain_text_content") |
| 107 | + } |
| 108 | + |
| 109 | + override DataFlow::Node getHtmlBody() { |
| 110 | + result in [this.getMailCall().getArg(4), this.getMailCall().getArgByName("html_content")] |
| 111 | + or |
| 112 | + result = this.getMailCall().getAMethodCall("set_html").getArg(0) |
| 113 | + or |
| 114 | + result = |
| 115 | + this.sendgridContent([ |
| 116 | + this.getMailCall().getArg(4), this.getMailCall().getArgByName("html_content") |
| 117 | + ].getALocalSource(), ["text/html", "text/x-amp-html"]) |
| 118 | + or |
| 119 | + result = this.sendgridWrite("html_content") |
| 120 | + or |
| 121 | + exists(KeyValuePair content, Dict generalDict, KeyValuePair typePair, KeyValuePair valuePair | |
| 122 | + content.getKey().(StrConst).getText() = "content" and |
| 123 | + content.getValue().(List).getAnElt() = generalDict and |
| 124 | + // declare KeyValuePairs keys and values |
| 125 | + typePair.getKey().(StrConst).getText() = "type" and |
| 126 | + typePair.getValue().(StrConst).getText() = ["text/html", "text/x-amp-html"] and |
| 127 | + valuePair.getKey().(StrConst).getText() = "value" and |
| 128 | + result.asExpr() = valuePair.getValue() and |
| 129 | + // correlate generalDict with previously set KeyValuePairs |
| 130 | + generalDict.getAnItem() in [typePair, valuePair] and |
| 131 | + [this.getArg(0), this.getArgByName("request_body")].getALocalSource().asExpr() = |
| 132 | + any(Dict d | d.getAnItem() = content) |
| 133 | + ) |
| 134 | + or |
| 135 | + exists(KeyValuePair footer, Dict generalDict, KeyValuePair enablePair, KeyValuePair htmlPair | |
| 136 | + footer.getKey().(StrConst).getText() = ["footer", "subscription_tracking"] and |
| 137 | + footer.getValue() = generalDict and |
| 138 | + // check footer is enabled |
| 139 | + enablePair.getKey().(StrConst).getText() = "enable" and |
| 140 | + exists(enablePair.getValue().(True)) and |
| 141 | + // get html content |
| 142 | + htmlPair.getKey().(StrConst).getText() = "html" and |
| 143 | + result.asExpr() = htmlPair.getValue() and |
| 144 | + // correlate generalDict with previously set KeyValuePairs |
| 145 | + generalDict.getAnItem() in [enablePair, htmlPair] and |
| 146 | + exists(KeyValuePair k | |
| 147 | + k.getKey() = |
| 148 | + [this.getArg(0), this.getArgByName("request_body")] |
| 149 | + .getALocalSource() |
| 150 | + .asExpr() |
| 151 | + .(Dict) |
| 152 | + .getAKey() and |
| 153 | + k.getValue() = any(Dict d | d.getAKey() = footer.getKey()) |
| 154 | + ) |
| 155 | + ) |
| 156 | + } |
| 157 | + |
| 158 | + override DataFlow::Node getTo() { |
| 159 | + result in [this.getMailCall().getArg(1), this.getMailCall().getArgByName("to_emails")] |
| 160 | + or |
| 161 | + result = this.getMailCall().getAMethodCall("To").getArg(0) |
| 162 | + or |
| 163 | + result = |
| 164 | + this.getMailCall() |
| 165 | + .getAMethodCall(["to", "add_to", "cc", "add_cc", "bcc", "add_bcc"]) |
| 166 | + .getArg(0) |
| 167 | + } |
| 168 | + |
| 169 | + override DataFlow::Node getFrom() { |
| 170 | + result in [this.getMailCall().getArg(0), this.getMailCall().getArgByName("from_email")] |
| 171 | + or |
| 172 | + result = this.getMailCall().getAMethodCall("Email").getArg(0) |
| 173 | + or |
| 174 | + result = this.getMailCall().getAMethodCall(["from_email", "set_from"]).getArg(0) |
| 175 | + or |
| 176 | + result = this.sendgridWrite("from_email") |
| 177 | + } |
| 178 | + |
| 179 | + override DataFlow::Node getSubject() { |
| 180 | + result in [this.getMailCall().getArg(2), this.getMailCall().getArgByName("subject")] |
| 181 | + or |
| 182 | + result = this.getMailCall().getAMethodCall(["subject", "set_subject"]).getArg(0) |
| 183 | + or |
| 184 | + result = this.sendgridWrite("subject") |
| 185 | + } |
| 186 | + } |
| 187 | +} |
0 commit comments