-
Notifications
You must be signed in to change notification settings - Fork 2k
Expand file tree
/
Copy pathRevel.qll
More file actions
205 lines (179 loc) · 7.96 KB
/
Revel.qll
File metadata and controls
205 lines (179 loc) · 7.96 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
/**
* Provides classes for working with remote flow sources from the `github.com/revel/revel` package.
*/
import go
private import semmle.go.security.OpenUrlRedirectCustomizations
/** Provides classes and methods modeling the Revel web framework. */
module Revel {
/** Gets the package name `github.com/revel/revel`. */
string packagePath() {
result = package(["github.com/revel", "github.com/robfig"] + "/revel", "")
}
private class ParamsFixedSanitizer extends TaintTracking::DefaultTaintSanitizer,
DataFlow::FieldReadNode
{
ParamsFixedSanitizer() {
exists(Field f |
this.readsField(_, f) and
f.hasQualifiedName(packagePath(), "Params", "Fixed")
)
}
}
private string contentTypeFromFilename(DataFlow::Node filename) {
if filename.getStringValue().regexpMatch("(?i).*\\.html?")
then result = "text/html"
else result = "application/octet-stream"
// Actually Revel can figure out a variety of other content-types, but none of our analyses care to
// distinguish ones other than text/html.
}
/**
* `revel.Controller` methods which set the response content-type to and designate a result in one operation.
*
* Note these don't actually generate the response, they return a struct which is then returned by the controller
* method, but it is very likely if a string is being rendered that it will end up sent to the user.
*
* The `Render` and `RenderTemplate` methods are handled by `TemplateRender` below.
*
* The `RenderError` method can actually return HTML content, but again only via an HTML template if one exists;
* we assume it falls back to return plain text as this implies there is probably not an injection opportunity
* but there is an information leakage issue.
*
* The `RenderBinary` method can also return a variety of content-types based on the file extension passed.
* We look particularly for html file extensions, since these are the only ones we currently have special rules
* for (in particular, detecting XSS vulnerabilities).
*/
private class ControllerRenderMethods extends Http::ResponseBody::Range {
string contentType;
ControllerRenderMethods() {
exists(Method m, string methodName, DataFlow::CallNode methodCall |
m.hasQualifiedName(packagePath(), "Controller", methodName) and
methodCall = m.getACall()
|
exists(int exposedArgument |
this = methodCall.getArgument(exposedArgument) and
(
methodName = "RenderBinary" and
contentType = contentTypeFromFilename(methodCall.getArgument(1)) and
exposedArgument = 0
or
methodName = "RenderError" and contentType = "text/plain" and exposedArgument = 0
or
methodName = "RenderHTML" and contentType = "text/html" and exposedArgument = 0
or
methodName = "RenderJSON" and contentType = "application/json" and exposedArgument = 0
or
methodName = "RenderJSONP" and
contentType = "application/javascript" and
exposedArgument = 1
or
methodName = "RenderXML" and contentType = "text/xml" and exposedArgument = 0
)
)
or
methodName = "RenderText" and
contentType = "text/plain" and
this = methodCall.getASyntacticArgument()
)
}
override Http::ResponseWriter getResponseWriter() { none() }
override string getAContentType() { result = contentType }
}
/**
* A read in a Revel template that uses Revel's `raw` function.
*/
class RawTemplateRead extends HtmlTemplate::TemplateRead {
RawTemplateRead() { parent.getBody().regexpMatch("(?s)raw\\s.*") }
}
/**
* A write to a template argument field that is read raw inside of a template.
*/
private class RawTemplateArgument extends Http::TemplateResponseBody::Range {
RawTemplateRead read;
RawTemplateArgument() {
exists(TemplateRender render, VariableWithFields var |
render.getRenderedFile() = read.getFile() and
// if var is a.b.c, any rhs of a write to a, a.b, or a.b.cb
this = var.getParent*().getAWrite().getRhs()
|
var.getParent*() = render.getArgumentVariable() and
(
var = read.getReadVariable(render.getArgumentVariable())
or
// if no write or use of that variable exists, no VariableWithFields will be generated
// so we try to find a parent VariableWithFields
// this isn't covered by the 'getParent*' above because no match would be found at all
// for var
not exists(read.getReadVariable(render.getArgumentVariable())) and
exists(string fieldName | fieldName = read.getFieldName() |
var.getQualifiedName() =
render.getArgumentVariable().getQualifiedName() +
["." + fieldName.substring(0, fieldName.indexOf(".")), ""]
)
)
or
// a revel controller.Render(arg) will set controller.ViewArgs["arg"] = arg
exists(Variable arg | arg.getARead() = render.(ControllerRender).getASyntacticArgument() |
var.getBaseVariable() = arg and
var.getQualifiedName() = read.getFieldName()
)
)
}
override string getAContentType() { result = "text/html" }
override Http::ResponseWriter getResponseWriter() { none() }
override HtmlTemplate::TemplateRead getRead() { result = read }
}
/**
* A render of a template.
*/
abstract class TemplateRender extends TemplateInstantiation::Range {
/** Gets the name of the file that is rendered. */
abstract File getRenderedFile();
/** Gets the variable passed as an argument to the template. */
abstract VariableWithFields getArgumentVariable();
override DataFlow::Node getADataArgument() { result = this.getArgumentVariable().getAUse() }
}
private IR::EvalInstruction skipImplicitFieldReads(IR::Instruction insn) {
result = insn or
result = skipImplicitFieldReads(insn.(IR::ImplicitFieldReadInstruction).getBase())
}
/** A call to `Controller.Render`. */
private class ControllerRender extends TemplateRender, DataFlow::MethodCallNode {
ControllerRender() { this.getTarget().hasQualifiedName(packagePath(), "Controller", "Render") }
override DataFlow::Node getTemplateArgument() { none() }
override File getRenderedFile() {
exists(Type controllerType, string controllerRe, string handlerRe, string pathRe |
controllerType = skipImplicitFieldReads(this.getReceiver().asInstruction()).getResultType() and
controllerRe = "\\Q" + controllerType.getName() + "\\E" and
handlerRe = "\\Q" + this.getRoot().(FuncDef).getName() + "\\E" and
// find a file named '/views/<controller>/<handler>(.<template type>).html
pathRe = "/views/" + controllerRe + "/" + handlerRe + "(\\..*)?\\.html?"
|
result.getAbsolutePath().regexpMatch("(?i).*" + pathRe)
)
}
override VariableWithFields getArgumentVariable() {
exists(VariableWithFields base | base.getAUse().getASuccessor*() = this.getReceiver() |
result.getParent() = base and
result.getField().getName() = "ViewArgs"
)
}
}
/** A call to `Controller.RenderTemplate`. */
private class ControllerRenderTemplate extends TemplateRender, DataFlow::MethodCallNode {
ControllerRenderTemplate() {
this.getTarget().hasQualifiedName(packagePath(), "Controller", "RenderTemplate")
}
override DataFlow::Node getTemplateArgument() { result = this.getArgument(0) }
override File getRenderedFile() {
exists(string pathRe | pathRe = "\\Q" + this.getTemplateArgument().getStringValue() + "\\E" |
result.getAbsolutePath().regexpMatch(".*/" + pathRe)
)
}
override VariableWithFields getArgumentVariable() {
exists(VariableWithFields base | base.getAUse().getASuccessor*() = this.getReceiver() |
result.getParent() = base and
result.getField().getName() = "ViewArgs"
)
}
}
}