|
| 1 | +/** |
| 2 | + * Provides classes for working with untrusted flow sources from the `github.com/revel/revel` package. |
| 3 | + */ |
| 4 | + |
| 5 | +import go |
| 6 | +private import semmle.go.security.OpenUrlRedirectCustomizations |
| 7 | + |
| 8 | +module Revel { |
| 9 | + /** Gets the package name. */ |
| 10 | + bindingset[result] |
| 11 | + string packagePath() { result = package(["github.com/revel", "github.com/robfig"], "revel") } |
| 12 | + |
| 13 | + private class ControllerParams extends UntrustedFlowSource::Range, DataFlow::FieldReadNode { |
| 14 | + ControllerParams() { |
| 15 | + exists(Field f | |
| 16 | + this.readsField(_, f) and |
| 17 | + f.hasQualifiedName(packagePath(), "Controller", "Params") |
| 18 | + ) |
| 19 | + } |
| 20 | + } |
| 21 | + |
| 22 | + private class ParamsFixedSanitizer extends TaintTracking::DefaultTaintSanitizer, |
| 23 | + DataFlow::FieldReadNode { |
| 24 | + ParamsFixedSanitizer() { |
| 25 | + exists(Field f | |
| 26 | + this.readsField(_, f) and |
| 27 | + f.hasQualifiedName(packagePath(), "Params", "Fixed") |
| 28 | + ) |
| 29 | + } |
| 30 | + } |
| 31 | + |
| 32 | + private class ParamsBind extends TaintTracking::FunctionModel, Method { |
| 33 | + ParamsBind() { this.hasQualifiedName(packagePath(), "Params", ["Bind", "BindJSON"]) } |
| 34 | + |
| 35 | + override predicate hasTaintFlow(FunctionInput inp, FunctionOutput outp) { |
| 36 | + inp.isReceiver() and outp.isParameter(0) |
| 37 | + } |
| 38 | + } |
| 39 | + |
| 40 | + private class RouteMatchParams extends UntrustedFlowSource::Range, DataFlow::FieldReadNode { |
| 41 | + RouteMatchParams() { |
| 42 | + exists(Field f | |
| 43 | + this.readsField(_, f) and |
| 44 | + f.hasQualifiedName(packagePath(), "RouteMatch", "Params") |
| 45 | + ) |
| 46 | + } |
| 47 | + } |
| 48 | + |
| 49 | + /** An access to an HTTP request field whose value may be controlled by an untrusted user. */ |
| 50 | + private class UserControlledRequestField extends UntrustedFlowSource::Range, |
| 51 | + DataFlow::FieldReadNode { |
| 52 | + UserControlledRequestField() { |
| 53 | + exists(string fieldName | |
| 54 | + this.getField().hasQualifiedName(packagePath(), "Request", fieldName) |
| 55 | + | |
| 56 | + fieldName in ["Header", "ContentType", "AcceptLanguages", "Locale", "URL", "Form", |
| 57 | + "MultipartForm"] |
| 58 | + ) |
| 59 | + } |
| 60 | + } |
| 61 | + |
| 62 | + private class UserControlledRequestMethod extends UntrustedFlowSource::Range, |
| 63 | + DataFlow::MethodCallNode { |
| 64 | + UserControlledRequestMethod() { |
| 65 | + this |
| 66 | + .getTarget() |
| 67 | + .hasQualifiedName(packagePath(), "Request", |
| 68 | + ["FormValue", "PostFormValue", "GetQuery", "GetForm", "GetMultipartForm", "GetBody", |
| 69 | + "Cookie", "GetHttpHeader", "GetRequestURI", "MultipartReader", "Referer", |
| 70 | + "UserAgent"]) |
| 71 | + } |
| 72 | + } |
| 73 | + |
| 74 | + private class ServerCookieGetValue extends TaintTracking::FunctionModel, Method { |
| 75 | + ServerCookieGetValue() { this.hasQualifiedName(packagePath(), "ServerCookie", "GetValue") } |
| 76 | + |
| 77 | + override predicate hasTaintFlow(FunctionInput inp, FunctionOutput outp) { |
| 78 | + inp.isReceiver() and outp.isResult() |
| 79 | + } |
| 80 | + } |
| 81 | + |
| 82 | + private class ServerMultipartFormGetFiles extends TaintTracking::FunctionModel, Method { |
| 83 | + ServerMultipartFormGetFiles() { |
| 84 | + this.hasQualifiedName(packagePath(), "ServerMultipartForm", ["GetFiles", "GetValues"]) |
| 85 | + } |
| 86 | + |
| 87 | + override predicate hasTaintFlow(FunctionInput inp, FunctionOutput outp) { |
| 88 | + inp.isReceiver() and outp.isResult() |
| 89 | + } |
| 90 | + } |
| 91 | + |
| 92 | + private string contentTypeFromFilename(DataFlow::Node filename) { |
| 93 | + if filename.getStringValue().toLowerCase().matches(["%.htm", "%.html"]) |
| 94 | + then result = "text/html" |
| 95 | + else result = "application/octet-stream" |
| 96 | + // Actually Revel can figure out a variety of other content-types, but none of our analyses care to |
| 97 | + // distinguish ones other than text/html. |
| 98 | + } |
| 99 | + |
| 100 | + /** |
| 101 | + * `revel.Controller` methods which set the response content-type to and designate a result in one operation. |
| 102 | + * |
| 103 | + * Note these don't actually generate the response, they return a struct which is then returned by the controller |
| 104 | + * method, but it is very likely if a string is being rendered that it will end up sent to the user. |
| 105 | + * |
| 106 | + * The `Render` and `RenderTemplate` methods are excluded for now because both execute HTML templates, and deciding |
| 107 | + * whether a particular value is exposed unescaped or not requires parsing the template. |
| 108 | + * |
| 109 | + * The `RenderError` method can actually return HTML content, but again only via an HTML template if one exists; |
| 110 | + * we assume it falls back to return plain text as this implies there is probably not an injection opportunity |
| 111 | + * but there is an information leakage issue. |
| 112 | + * |
| 113 | + * The `RenderBinary` method can also return a variety of content-types based on the file extension passed. |
| 114 | + * We look particularly for html file extensions, since these are the only ones we currently have special rules |
| 115 | + * for (in particular, detecting XSS vulnerabilities). |
| 116 | + */ |
| 117 | + private class ControllerRenderMethods extends HTTP::ResponseBody::Range { |
| 118 | + string contentType; |
| 119 | + |
| 120 | + ControllerRenderMethods() { |
| 121 | + exists(Method m, string methodName, DataFlow::CallNode methodCall | |
| 122 | + m.hasQualifiedName(packagePath(), "Controller", methodName) and |
| 123 | + methodCall = m.getACall() |
| 124 | + | |
| 125 | + exists(int exposedArgument | |
| 126 | + this = methodCall.getArgument(exposedArgument) and |
| 127 | + ( |
| 128 | + methodName = "RenderBinary" and |
| 129 | + contentType = contentTypeFromFilename(methodCall.getArgument(1)) and |
| 130 | + exposedArgument = 0 |
| 131 | + or |
| 132 | + methodName = "RenderError" and contentType = "text/plain" and exposedArgument = 0 |
| 133 | + or |
| 134 | + methodName = "RenderHTML" and contentType = "text/html" and exposedArgument = 0 |
| 135 | + or |
| 136 | + methodName = "RenderJSON" and contentType = "application/json" and exposedArgument = 0 |
| 137 | + or |
| 138 | + methodName = "RenderJSONP" and |
| 139 | + contentType = "application/javascript" and |
| 140 | + exposedArgument = 1 |
| 141 | + or |
| 142 | + methodName = "RenderXML" and contentType = "text/xml" and exposedArgument = 0 |
| 143 | + ) |
| 144 | + ) |
| 145 | + or |
| 146 | + methodName = "RenderText" and |
| 147 | + contentType = "text/plain" and |
| 148 | + this = methodCall.getAnArgument() |
| 149 | + ) |
| 150 | + } |
| 151 | + |
| 152 | + override HTTP::ResponseWriter getResponseWriter() { none() } |
| 153 | + |
| 154 | + override string getAContentType() { result = contentType } |
| 155 | + } |
| 156 | + |
| 157 | + /** |
| 158 | + * The `revel.Controller.RenderFileName` method, which instructs Revel to open a file and return its contents. |
| 159 | + * We extend FileSystemAccess rather than HTTP::ResponseBody as this will usually mean exposing a user-controlled |
| 160 | + * file rather than the actual contents being user-controlled. |
| 161 | + */ |
| 162 | + private class RenderFileNameCall extends FileSystemAccess::Range, DataFlow::CallNode { |
| 163 | + RenderFileNameCall() { |
| 164 | + this = |
| 165 | + any(Method m | m.hasQualifiedName(packagePath(), "Controller", "RenderFileName")).getACall() |
| 166 | + } |
| 167 | + |
| 168 | + override DataFlow::Node getAPathArgument() { result = getArgument(0) } |
| 169 | + } |
| 170 | + |
| 171 | + /** |
| 172 | + * The `revel.Controller.Redirect` method. |
| 173 | + * |
| 174 | + * It is currently assumed that a tainted `value` in `Redirect(url, value)`, which calls `Sprintf(url, value)` |
| 175 | + * internally, cannot lead to an open redirect vulnerability. |
| 176 | + */ |
| 177 | + private class ControllerRedirectMethod extends HTTP::Redirect::Range, DataFlow::CallNode { |
| 178 | + ControllerRedirectMethod() { |
| 179 | + exists(Method m | m.hasQualifiedName(packagePath(), "Controller", "Redirect") | |
| 180 | + this = m.getACall() |
| 181 | + ) |
| 182 | + } |
| 183 | + |
| 184 | + override DataFlow::Node getUrl() { result = this.getArgument(0) } |
| 185 | + |
| 186 | + override HTTP::ResponseWriter getResponseWriter() { none() } |
| 187 | + } |
| 188 | + |
| 189 | + /** |
| 190 | + * The getter and setter methods of `revel.RevelHeader`. |
| 191 | + * |
| 192 | + * Note we currently don't implement `HeaderWrite` and related concepts, as they are currently only used |
| 193 | + * to track content-type, and directly setting headers does not seem to be the usual way to set the response |
| 194 | + * content-type for this framework. If and when the `HeaderWrite` concept has a more abstract idea of the |
| 195 | + * relationship between header-writes and HTTP responses than looking for a particular `http.ResponseWriter` |
| 196 | + * instance connecting the two, then we may implement it here for completeness. |
| 197 | + */ |
| 198 | + private class RevelHeaderMethods extends TaintTracking::FunctionModel { |
| 199 | + FunctionInput input; |
| 200 | + FunctionOutput output; |
| 201 | + string name; |
| 202 | + |
| 203 | + RevelHeaderMethods() { |
| 204 | + this.(Method).hasQualifiedName(packagePath(), "RevelHeader", name) and |
| 205 | + ( |
| 206 | + name = ["Add", "Set"] and input.isParameter([0, 1]) and output.isReceiver() |
| 207 | + or |
| 208 | + name = ["Get", "GetAll"] and input.isReceiver() and output.isResult() |
| 209 | + or |
| 210 | + name = "SetCookie" and input.isParameter(0) and output.isReceiver() |
| 211 | + ) |
| 212 | + } |
| 213 | + |
| 214 | + override predicate hasTaintFlow(FunctionInput inp, FunctionOutput outp) { |
| 215 | + inp = input and outp = output |
| 216 | + } |
| 217 | + } |
| 218 | +} |
0 commit comments