Skip to content
This repository was archived by the owner on Jan 5, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions ql/src/experimental/CWE-640/EmailBad.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package main

import (
"net/http"
"net/smtp"
)

func mail(w http.ResponseWriter, r *http.Request) {
host := r.Header.Get("Host")
token := backend.getUserSecretResetToken(email)
body := "Click to reset password: " + host + "/" + token
smtp.SendMail("test.test", nil, "from@from.com", nil, []byte(body))
}
13 changes: 13 additions & 0 deletions ql/src/experimental/CWE-640/EmailGood.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package main

import (
"net/http"
"net/smtp"
)

func mailGood(w http.ResponseWriter, r *http.Request) {
host := config.Get("Host")
token := backend.getUserSecretResetToken(email)
body := "Click to reset password: " + host + "/" + token
smtp.SendMail("test.test", nil, "from@from.com", nil, []byte(body))
}
48 changes: 48 additions & 0 deletions ql/src/experimental/CWE-640/EmailInjection.qhelp
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<!DOCTYPE qhelp SYSTEM "qhelp.dtd">
<qhelp>
<overview>
<p>
Using untrusted input to construct an email induces multiple security
vulnerabilities. For instance, inclusion of an untrusted input in a email body
may allow an attacker to conduct Cross Site Scripting (XSS) Attacks. While
inclusion of an HTTP Header in the email body may allow a full account
compromise as shown in the example below.
</p>
</overview>
<recommendation>
<p>
Any data which is passed to an email subject or body must be sanitized before use.
</p>
</recommendation>
<example>
<p>
In the following example snippet, the
<code>host</code>
field is user controlled.
</p>
<p>
A malicious user can send an HTTP request to the targeted web site,
but with a Host header that refers to his own web site. This means the
emails will be sent out to potential victims, originating from a server
they trust, but with links leading to a malicious web site.
</p>
<p>
If the email contains a password reset link, and should the victim click
the link, the secret reset token will be leaked to the attacker. Using the
leaked token, the attacker can then construct the real reset link and use it to
change the victim's password.
</p>
<sample src="EmailBad.go" />
<p>
One way to prevent this is to load the host name from a trusted configuration file instead.
</p>
<sample src="EmailGood.go" />
</example>
<references>
<li>
OWASP
<a href="https://owasp.org/www-community/attacks/Content_Spoofing">Content Spoofing</a>
.
</li>
</references>
</qhelp>
19 changes: 19 additions & 0 deletions ql/src/experimental/CWE-640/EmailInjection.ql
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* @name Email content injection
* @description Incorporating untrusted input directly into an email message can enable
* content spoofing, which in turn may lead to information leaks and other
* security issues.
* @id go/email-injection
* @kind path-problem
* @problem.severity error
* @tags security
* external/cwe/cwe-640
*/

import go
import DataFlow::PathGraph
import EmailInjection::EmailInjection

from DataFlow::PathNode source, DataFlow::PathNode sink, Configuration config
where config.hasFlowPath(source, sink)
select sink, source, sink, "Email content may contain $@.", source.getNode(), "untrusted input"
29 changes: 29 additions & 0 deletions ql/src/experimental/CWE-640/EmailInjection.qll
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* Provides a taint-tracking configuration for reasoning about
* server-side email-injection vulnerabilities.
*
* Note, for performance reasons: only import this file if
* `EmailInjection::Configuration` is needed, otherwise
* `EmailInjectionCustomizations` should be imported instead.
*/

import go

/**
* Provides a taint-tracking configuration for reasoning about
* email-injection vulnerabilities.
*/
module EmailInjection {
import EmailInjectionCustomizations::EmailInjection

/**
* A taint-tracking configuration for reasoning about email-injection vulnerabilities.
*/
class Configuration extends TaintTracking::Configuration {
Configuration() { this = "Email Injection" }

override predicate isSource(DataFlow::Node source) { source instanceof Source }

override predicate isSink(DataFlow::Node sink) { sink instanceof Sink }
}
}
30 changes: 30 additions & 0 deletions ql/src/experimental/CWE-640/EmailInjectionCustomizations.qll
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/** Provides classes for reasoning about email-injection vulnerabilities. */

import go

/**
* Provides a library for reasoning about email-injection vulnerabilities.
*/
module EmailInjection {
/**
* A data-flow node that should be considered a source of untrusted data for email-injection vulnerabilities.
*/
abstract class Source extends DataFlow::Node { }

/**
* A data-flow node that should be considered a sink for email-injection vulnerabilities.
*/
abstract class Sink extends DataFlow::Node { }

/** A source of untrusted data, considered as a taint source for email injection. */
class UntrustedFlowSourceAsSource extends Source {
UntrustedFlowSourceAsSource() { this instanceof UntrustedFlowSource }
}

/**
* A data-flow node that becomes part of an email considered as a taint sink for email injection.
*/
class MailDataAsSink extends Sink {
MailDataAsSink() { this instanceof MailData }
}
}
1 change: 1 addition & 0 deletions ql/src/go.qll
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import semmle.go.dataflow.DataFlow
import semmle.go.dataflow.GlobalValueNumbering
import semmle.go.dataflow.TaintTracking
import semmle.go.dataflow.SSA
import semmle.go.frameworks.Email
import semmle.go.frameworks.HTTP
import semmle.go.frameworks.SystemCommandExecutors
import semmle.go.frameworks.SQL
Expand Down
110 changes: 110 additions & 0 deletions ql/src/semmle/go/frameworks/Email.qll
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/** Provides classes for working with email-related APIs. */

import go

/**
* A data-flow node that represents data written to an email.
* Data in this case includes the email headers and the mail body
*
* Extend this class to refine existing API models. If you want to model new APIs,
* extend `MailDataCall::Range` instead.
*/
class MailData extends DataFlow::Node {
Copy link
Author

Choose a reason for hiding this comment

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

I have removed the Function suffix and renamed this as MailData.

MailDataCall::Range self;

MailData() { this = self.getData() }
}

/** Provides classes for working with calls which write data to an email. */
module MailDataCall {
/**
* A data-flow node that represents a call which writes data to an email.
* Data in this case refers to email headers and the mail body
*
*/
abstract class Range extends DataFlow::CallNode {
/** Gets data written to an email connection. */
abstract DataFlow::Node getData();
}

/** Get the package name `github.com/sendgrid/sendgrid-go/helpers/mail`. */
bindingset[result]
private string sendgridMail() { result = "github.com/sendgrid/sendgrid-go/helpers/mail" }

/** A Client.Data expression string used in an API function of the net/smtp package. */
private class SmtpData extends Range {
SmtpData() {
// func (c *Client) Data() (io.WriteCloser, error)
this.getTarget().(Method).hasQualifiedName("net/smtp", "Client", "Data")
}

override DataFlow::Node getData() {
exists(DataFlow::CallNode write, DataFlow::Node writer, int i |
this.getResult(0) = writer and
(
write.getTarget().hasQualifiedName("fmt", "Fprintf")
or
write.getTarget().hasQualifiedName("io", "WriteString")
) and
writer.getASuccessor*() = write.getArgument(0) and
i > 0 and
write.getArgument(i) = result
)
}
}

/** A send mail expression string used in an API function of the net/smtp package. */
private class SmtpSendMail extends Range {
SmtpSendMail() {
// func SendMail(addr string, a Auth, from string, to []string, msg []byte) error
this.getTarget().hasQualifiedName("net/smtp", "SendMail")
}

override DataFlow::Node getData() { result = this.getArgument(4) }
}

/** A call to `NewSingleEmail` API function of the Sendgrid mail package. */
private class SendGridSingleEmail extends Range {
SendGridSingleEmail() {
// func NewSingleEmail(from *Email, subject string, to *Email, plainTextContent string, htmlContent string) *SGMailV3
this.getTarget().hasQualifiedName(sendgridMail(), "NewSingleEmail")
}

override DataFlow::Node getData() { result = this.getArgument([1, 3, 4]) }
}

/* Gets the value of the `i`-th content parameter of the given `call` */
private DataFlow::Node getContent(DataFlow::CallNode call, int i) {
exists(DataFlow::CallNode cn, DataFlow::Node content |
// func NewContent(contentType string, value string) *Content
cn.getTarget().hasQualifiedName(sendgridMail(), "NewContent") and
cn.getResult() = content and
content.getASuccessor*() = call.getArgument(i) and
result = cn.getArgument(1)
)
}

/** A call to `NewV3MailInit` API function of the Sendgrid mail package. */
private class SendGridV3Init extends Range {
SendGridV3Init() {
// func NewV3MailInit(from *Email, subject string, to *Email, content ...*Content) *SGMailV3
this.getTarget().hasQualifiedName(sendgridMail(), "NewV3MailInit")
}

override DataFlow::Node getData() {
exists(int i | result = getContent(this, i) and i >= 3)
or
result = this.getArgument(1)
}
}

/** A call to `AddContent` API function of the Sendgrid mail package. */
private class SendGridAddContent extends Range {
SendGridAddContent() {
// func (s *SGMailV3) AddContent(c ...*Content) *SGMailV3
this.getTarget().(Method).hasQualifiedName(sendgridMail(), "SGMailV3", "AddContent")
}

override DataFlow::Node getData() { result = getContent(this, _) }
}
}
37 changes: 37 additions & 0 deletions ql/test/experimental/CWE-640/EmailInjection.expected
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
edges
| email.go:24:10:24:17 | selection of Header : Header | email.go:27:56:27:67 | type conversion |
| email.go:34:21:34:31 | call to Referer : string | email.go:36:57:36:78 | type conversion |
| email.go:42:21:42:31 | call to Referer : string | email.go:46:25:46:38 | untrustedInput |
| email.go:51:21:51:31 | call to Referer : string | email.go:57:46:57:59 | untrustedInput |
| email.go:51:21:51:31 | call to Referer : string | email.go:58:52:58:65 | untrustedInput |
| email.go:63:21:63:31 | call to Referer : string | email.go:65:47:65:60 | untrustedInput |
| email.go:73:21:73:31 | call to Referer : string | email.go:79:47:79:60 | untrustedInput |
| email.go:87:21:87:31 | call to Referer : string | email.go:94:37:94:50 | untrustedInput |
| email.go:87:21:87:31 | call to Referer : string | email.go:96:48:96:61 | untrustedInput |
nodes
| email.go:24:10:24:17 | selection of Header : Header | semmle.label | selection of Header : Header |
| email.go:27:56:27:67 | type conversion | semmle.label | type conversion |
| email.go:34:21:34:31 | call to Referer : string | semmle.label | call to Referer : string |
| email.go:36:57:36:78 | type conversion | semmle.label | type conversion |
| email.go:42:21:42:31 | call to Referer : string | semmle.label | call to Referer : string |
| email.go:46:25:46:38 | untrustedInput | semmle.label | untrustedInput |
| email.go:51:21:51:31 | call to Referer : string | semmle.label | call to Referer : string |
| email.go:57:46:57:59 | untrustedInput | semmle.label | untrustedInput |
| email.go:58:52:58:65 | untrustedInput | semmle.label | untrustedInput |
| email.go:63:21:63:31 | call to Referer : string | semmle.label | call to Referer : string |
| email.go:65:47:65:60 | untrustedInput | semmle.label | untrustedInput |
| email.go:73:21:73:31 | call to Referer : string | semmle.label | call to Referer : string |
| email.go:79:47:79:60 | untrustedInput | semmle.label | untrustedInput |
| email.go:87:21:87:31 | call to Referer : string | semmle.label | call to Referer : string |
| email.go:94:37:94:50 | untrustedInput | semmle.label | untrustedInput |
| email.go:96:48:96:61 | untrustedInput | semmle.label | untrustedInput |
#select
| email.go:27:56:27:67 | type conversion | email.go:24:10:24:17 | selection of Header : Header | email.go:27:56:27:67 | type conversion | Email content may contain $@. | email.go:24:10:24:17 | selection of Header | untrusted input |
| email.go:36:57:36:78 | type conversion | email.go:34:21:34:31 | call to Referer : string | email.go:36:57:36:78 | type conversion | Email content may contain $@. | email.go:34:21:34:31 | call to Referer | untrusted input |
| email.go:46:25:46:38 | untrustedInput | email.go:42:21:42:31 | call to Referer : string | email.go:46:25:46:38 | untrustedInput | Email content may contain $@. | email.go:42:21:42:31 | call to Referer | untrusted input |
| email.go:57:46:57:59 | untrustedInput | email.go:51:21:51:31 | call to Referer : string | email.go:57:46:57:59 | untrustedInput | Email content may contain $@. | email.go:51:21:51:31 | call to Referer | untrusted input |
| email.go:58:52:58:65 | untrustedInput | email.go:51:21:51:31 | call to Referer : string | email.go:58:52:58:65 | untrustedInput | Email content may contain $@. | email.go:51:21:51:31 | call to Referer | untrusted input |
| email.go:65:47:65:60 | untrustedInput | email.go:63:21:63:31 | call to Referer : string | email.go:65:47:65:60 | untrustedInput | Email content may contain $@. | email.go:63:21:63:31 | call to Referer | untrusted input |
| email.go:79:47:79:60 | untrustedInput | email.go:73:21:73:31 | call to Referer : string | email.go:79:47:79:60 | untrustedInput | Email content may contain $@. | email.go:73:21:73:31 | call to Referer | untrusted input |
| email.go:94:37:94:50 | untrustedInput | email.go:87:21:87:31 | call to Referer : string | email.go:94:37:94:50 | untrustedInput | Email content may contain $@. | email.go:87:21:87:31 | call to Referer | untrusted input |
| email.go:96:48:96:61 | untrustedInput | email.go:87:21:87:31 | call to Referer : string | email.go:96:48:96:61 | untrustedInput | Email content may contain $@. | email.go:87:21:87:31 | call to Referer | untrusted input |
1 change: 1 addition & 0 deletions ql/test/experimental/CWE-640/EmailInjection.qlref
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
experimental/CWE-640/EmailInjection.ql
Loading