Skip to content
This repository was archived by the owner on Jan 5, 2023. It is now read-only.

Commit c61881a

Browse files
authored
Merge pull request #344 from smowton/smowton/feature/echo-models
Add models for the Echo framework
2 parents c905149 + 7b917f9 commit c61881a

File tree

20 files changed

+967
-32
lines changed

20 files changed

+967
-32
lines changed

change-notes/2020-09-17-echo.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
lgtm,codescanning
2+
* Added support for the Echo web framework

ql/src/go.qll

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import semmle.go.dataflow.GlobalValueNumbering
2727
import semmle.go.dataflow.SSA
2828
import semmle.go.dataflow.TaintTracking
2929
import semmle.go.frameworks.Chi
30+
import semmle.go.frameworks.Echo
3031
import semmle.go.frameworks.Email
3132
import semmle.go.frameworks.Encoding
3233
import semmle.go.frameworks.Gin

ql/src/semmle/go/Concepts.qll

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -509,6 +509,17 @@ module HTTP {
509509
abstract class Range extends DataFlow::Node {
510510
/** Gets the response writer associated with this header write, if any. */
511511
abstract ResponseWriter getResponseWriter();
512+
513+
/** Gets a content-type associated with this body. */
514+
string getAContentType() { result = getAContentTypeNode().getStringValue() }
515+
516+
/** Gets a dataflow node for a content-type associated with this body. */
517+
DataFlow::Node getAContentTypeNode() {
518+
exists(HTTP::HeaderWrite hw | hw = getResponseWriter().getAHeaderWrite() |
519+
hw.getName().getStringValue().toLowerCase() = "content-type" and
520+
result = hw.getValue()
521+
)
522+
}
512523
}
513524
}
514525

@@ -525,6 +536,12 @@ module HTTP {
525536

526537
/** Gets the response writer associated with this header write, if any. */
527538
ResponseWriter getResponseWriter() { result = self.getResponseWriter() }
539+
540+
/** Gets a content-type associated with this body. */
541+
string getAContentType() { result = self.getAContentType() }
542+
543+
/** Gets a dataflow node for a content-type associated with this body. */
544+
DataFlow::Node getAContentTypeNode() { result = self.getAContentTypeNode() }
528545
}
529546

530547
/** Provides a class for modeling new HTTP client request APIs. */
@@ -576,7 +593,7 @@ module HTTP {
576593
/** Gets the data-flow node representing the URL being redirected to. */
577594
abstract DataFlow::Node getUrl();
578595

579-
/** Gets the response writer that this redirect is sent on. */
596+
/** Gets the response writer that this redirect is sent on, if any. */
580597
abstract ResponseWriter getResponseWriter();
581598
}
582599

@@ -591,6 +608,12 @@ module HTTP {
591608

592609
override ResponseWriter getResponseWriter() { result = HeaderWrite.super.getResponseWriter() }
593610
}
611+
612+
/**
613+
* An HTTP request attribute that is generally not attacker-controllable for
614+
* open redirect exploits; for example, a form field submitted in a POST request.
615+
*/
616+
abstract class UnexploitableSource extends DataFlow::Node { }
594617
}
595618

596619
/**
@@ -607,7 +630,7 @@ module HTTP {
607630
/** Gets the data-flow node representing the URL being redirected to. */
608631
DataFlow::Node getUrl() { result = self.getUrl() }
609632

610-
/** Gets the response writer that this redirect is sent on. */
633+
/** Gets the response writer that this redirect is sent on, if any. */
611634
ResponseWriter getResponseWriter() { result = self.getResponseWriter() }
612635
}
613636
}

ql/src/semmle/go/dataflow/FunctionInputsAndOutputs.qll

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,20 @@ class FunctionInput extends TFunctionInput {
5151
abstract string toString();
5252
}
5353

54+
module FunctionInput {
55+
/** Gets a `FunctionInput` representing the `i`th parameter. */
56+
FunctionInput parameter(int i) { result.isParameter(i) }
57+
58+
/** Gets a `FunctionInput` representing the receiver. */
59+
FunctionInput receiver() { result.isReceiver() }
60+
61+
/** Gets a `FunctionInput` representing the result of a single-result function. */
62+
FunctionInput functionResult() { result.isResult() }
63+
64+
/** Gets a `FunctionInput` representing the `i`th result. */
65+
FunctionInput functionResult(int i) { result.isParameter(i) }
66+
}
67+
5468
/** A parameter position of a function, viewed as a source of input. */
5569
private class ParameterInput extends FunctionInput, TInParameter {
5670
int index;
@@ -172,6 +186,20 @@ class FunctionOutput extends TFunctionOutput {
172186
abstract string toString();
173187
}
174188

189+
module FunctionOutput {
190+
/** Gets a `FunctionOutput` representing the result of a single-result function. */
191+
FunctionOutput functionResult() { result.isResult() }
192+
193+
/** Gets a `FunctionOutput` representing the `i`th result. */
194+
FunctionOutput functionResult(int i) { result.isParameter(i) }
195+
196+
/** Gets a `FunctionOutput` representing the receiver after a function returns. */
197+
FunctionOutput receiver() { result.isReceiver() }
198+
199+
/** Gets a `FunctionOutput` representing the `i`th parameter after a function returns. */
200+
FunctionOutput parameter(int i) { result.isParameter(i) }
201+
}
202+
175203
/** A result position of a function, viewed as an output. */
176204
private class OutResult extends FunctionOutput, TOutResult {
177205
int index;

ql/src/semmle/go/frameworks/Echo.qll

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
/**
2+
* Provides classes for working with untrusted flow sources, taint propagators, and HTTP sinks
3+
* from the `github.com/labstack/echo` package.
4+
*/
5+
6+
import go
7+
8+
private module Echo {
9+
/** Gets an Echo package name. */
10+
bindingset[result]
11+
private string packagePath() { result = package("github.com/labstack/echo", "") }
12+
13+
/**
14+
* Data from a `Context` interface method, considered as a source of untrusted flow.
15+
*/
16+
private class EchoContextSource extends UntrustedFlowSource::Range {
17+
EchoContextSource() {
18+
exists(DataFlow::MethodCallNode call, string methodName |
19+
methodName =
20+
["Param", "ParamValues", "QueryParam", "QueryParams", "QueryString", "FormValue",
21+
"FormParams", "FormFile", "MultipartForm", "Cookie", "Cookies"] and
22+
call.getTarget().hasQualifiedName(packagePath(), "Context", methodName) and
23+
this = call.getResult(0)
24+
)
25+
}
26+
}
27+
28+
/**
29+
* Data from a `Context` interface method that is not generally exploitable for open-redirect attacks.
30+
*/
31+
private class EchoContextRedirectUnexploitableSource extends HTTP::Redirect::UnexploitableSource {
32+
EchoContextRedirectUnexploitableSource() {
33+
exists(DataFlow::MethodCallNode call, string methodName |
34+
methodName = ["FormValue", "FormParams", "FormFile", "MultipartForm", "Cookie", "Cookies"] and
35+
call.getTarget().hasQualifiedName(packagePath(), "Context", methodName) and
36+
this = call.getResult(0)
37+
)
38+
}
39+
}
40+
41+
/**
42+
* Models of `Context.Get/Set`. `Context` behaves like a map, with corresponding taint propagation.
43+
*/
44+
private class ContextMapModels extends TaintTracking::FunctionModel, Method {
45+
string methodName;
46+
FunctionInput input;
47+
FunctionOutput output;
48+
49+
ContextMapModels() {
50+
(
51+
methodName = "Get" and input.isReceiver() and output.isResult()
52+
or
53+
methodName = "Set" and input.isParameter(1) and output.isReceiver()
54+
) and
55+
this.hasQualifiedName(packagePath(), "Context", methodName)
56+
}
57+
58+
override predicate hasTaintFlow(FunctionInput inp, FunctionOutput outp) {
59+
inp = input and outp = output
60+
}
61+
}
62+
63+
/**
64+
* A call to a method on `Context` struct that unmarshals data into a target.
65+
*/
66+
private class EchoContextBinder extends UntrustedFlowSource::Range {
67+
EchoContextBinder() {
68+
exists(DataFlow::MethodCallNode call |
69+
call.getTarget().hasQualifiedName(packagePath(), "Context", "Bind")
70+
|
71+
this = FunctionOutput::parameter(0).getExitNode(call)
72+
)
73+
}
74+
}
75+
76+
/**
77+
* `echo.Context` methods which set the content-type to `text/html` and write a result in one operation.
78+
*/
79+
private class EchoHtmlOutputs extends HTTP::ResponseBody::Range {
80+
EchoHtmlOutputs() {
81+
exists(Method m | m.hasQualifiedName(packagePath(), "Context", ["HTML", "HTMLBlob"]) |
82+
this = m.getACall().getArgument(1)
83+
)
84+
}
85+
86+
override HTTP::ResponseWriter getResponseWriter() { none() }
87+
88+
override string getAContentType() { result = "text/html" }
89+
}
90+
91+
/**
92+
* `echo.Context` methods which take a content-type as a parameter.
93+
*/
94+
private class EchoParameterizedOutputs extends HTTP::ResponseBody::Range {
95+
DataFlow::CallNode callNode;
96+
97+
EchoParameterizedOutputs() {
98+
exists(Method m | m.hasQualifiedName(packagePath(), "Context", ["Blob", "Stream"]) |
99+
callNode = m.getACall() and this = callNode.getArgument(2)
100+
)
101+
}
102+
103+
override HTTP::ResponseWriter getResponseWriter() { none() }
104+
105+
override DataFlow::Node getAContentTypeNode() { result = callNode.getArgument(1) }
106+
}
107+
108+
/**
109+
* The `echo.Context.Redirect` method.
110+
*/
111+
private class EchoRedirectMethod extends HTTP::Redirect::Range, DataFlow::CallNode {
112+
EchoRedirectMethod() {
113+
exists(Method m | m.hasQualifiedName(packagePath(), "Context", "Redirect") |
114+
this = m.getACall()
115+
)
116+
}
117+
118+
override DataFlow::Node getUrl() { result = this.getArgument(1) }
119+
120+
override HTTP::ResponseWriter getResponseWriter() { none() }
121+
}
122+
}

ql/src/semmle/go/frameworks/Gin.qll

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ private module Gin {
128128
methodName = "ShouldBindYAML"
129129
)
130130
|
131-
this = any(FunctionOutput output | output.isParameter(0)).getExitNode(call)
131+
this = FunctionOutput::parameter(0).getExitNode(call)
132132
)
133133
)
134134
}

ql/src/semmle/go/frameworks/HTTP.qll

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ private module GoRestfulHttp {
3737
.getTarget()
3838
.hasQualifiedName(package("github.com/emicklei/go-restful", ""), "Request", "ReadEntity")
3939
|
40-
this = any(FunctionOutput output | output.isParameter(0)).getExitNode(call)
40+
this = FunctionOutput::parameter(0).getExitNode(call)
4141
)
4242
}
4343
}

ql/src/semmle/go/frameworks/stdlib/NetHttp.qll

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,25 @@ module NetHttp {
216216
}
217217
}
218218

219+
/** Fields and methods of `net/http.Request` that are not generally exploitable in an open-redirect attack. */
220+
private class RedirectUnexploitableRequestFields extends HTTP::Redirect::UnexploitableSource {
221+
RedirectUnexploitableRequestFields() {
222+
exists(Field f, string fieldName |
223+
f.hasQualifiedName("net/http", "Request", fieldName) and
224+
this = f.getARead()
225+
|
226+
fieldName = ["Body", "GetBody", "PostForm", "MultipartForm", "Header", "Trailer"]
227+
)
228+
or
229+
exists(Method m, string methName |
230+
m.hasQualifiedName("net/http", "Request", methName) and
231+
this = m.getACall()
232+
|
233+
methName = ["Cookie", "Cookies", "MultipartReader", "PostFormValue", "Referer", "UserAgent"]
234+
)
235+
}
236+
}
237+
219238
private class FunctionModels extends TaintTracking::FunctionModel {
220239
FunctionInput inp;
221240
FunctionOutput outp;

ql/src/semmle/go/security/OpenUrlRedirectCustomizations.qll

Lines changed: 1 addition & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -41,28 +41,7 @@ module OpenUrlRedirect {
4141
UntrustedFlowAsSource() {
4242
// exclude some fields and methods of URLs that are generally not attacker-controllable for
4343
// open redirect exploits
44-
not exists(Field f, string fieldName |
45-
f.hasQualifiedName("net/http", "Request", fieldName) and
46-
this = f.getARead()
47-
|
48-
fieldName = "Body" or
49-
fieldName = "GetBody" or
50-
fieldName = "PostForm" or
51-
fieldName = "MultipartForm" or
52-
fieldName = "Header" or
53-
fieldName = "Trailer"
54-
) and
55-
not exists(Method m, string methName |
56-
m.hasQualifiedName("net/http", "Request", methName) and
57-
this = m.getACall()
58-
|
59-
methName = "Cookie" or
60-
methName = "Cookies" or
61-
methName = "MultipartReader" or
62-
methName = "PostFormValues" or
63-
methName = "Referer" or
64-
methName = "UserAgent"
65-
)
44+
not this instanceof HTTP::Redirect::UnexploitableSource
6645
}
6746
}
6847

ql/src/semmle/go/security/ReflectedXssCustomizations.qll

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,7 @@ module ReflectedXss {
4747
* Holds if `body` specifies the response's content type to be HTML.
4848
*/
4949
private predicate htmlTypeSpecified(HTTP::ResponseBody body) {
50-
exists(HTTP::HeaderWrite hw, string tp | hw = body.getResponseWriter().getAHeaderWrite() |
51-
hw.definesHeader("content-type", tp) and tp.regexpMatch("(?i).*html.*")
52-
)
50+
body.getAContentType().regexpMatch("(?i).*html.*")
5351
}
5452

5553
/**
@@ -58,9 +56,7 @@ module ReflectedXss {
5856
private predicate nonHtmlContentType(HTTP::ResponseBody body) {
5957
not htmlTypeSpecified(body) and
6058
(
61-
exists(HTTP::HeaderWrite hw | hw = body.getResponseWriter().getAHeaderWrite() |
62-
hw.getName().getStringValue().toLowerCase() = "content-type"
63-
)
59+
exists(body.getAContentTypeNode())
6460
or
6561
exists(DataFlow::CallNode call | call.getTarget().hasQualifiedName("fmt", "Fprintf") |
6662
body = call.getAnArgument() and

0 commit comments

Comments
 (0)