diff --git a/ql/src/go.qll b/ql/src/go.qll index 85f99b3c774..393858594c0 100644 --- a/ql/src/go.qll +++ b/ql/src/go.qll @@ -34,6 +34,7 @@ import semmle.go.frameworks.BeegoOrm import semmle.go.frameworks.Chi import semmle.go.frameworks.Couchbase import semmle.go.frameworks.Echo +import semmle.go.frameworks.ElazarlGoproxy import semmle.go.frameworks.Email import semmle.go.frameworks.Encoding import semmle.go.frameworks.EvanphxJsonPatch diff --git a/ql/src/semmle/go/frameworks/ElazarlGoproxy.qll b/ql/src/semmle/go/frameworks/ElazarlGoproxy.qll new file mode 100644 index 00000000000..f6023ba12c7 --- /dev/null +++ b/ql/src/semmle/go/frameworks/ElazarlGoproxy.qll @@ -0,0 +1,135 @@ +/** + * Provides classes for working with concepts relating to the [github.com/elazarl/goproxy](https://pkg.go.dev/github.com/elazarl/goproxy) package. + */ + +import go + +/** + * Provides classes for working with concepts relating to the [github.com/elazarl/goproxy](https://pkg.go.dev/github.com/elazarl/goproxy) package. + */ +module ElazarlGoproxy { + /** Gets the package name. */ + bindingset[result] + string packagePath() { result = package("github.com/elazarl/goproxy", "") } + + private class NewResponse extends HTTP::HeaderWrite::Range, DataFlow::CallNode { + NewResponse() { this.getTarget().hasQualifiedName(packagePath(), "NewResponse") } + + override string getHeaderName() { this.definesHeader(result, _) } + + override string getHeaderValue() { this.definesHeader(_, result) } + + override DataFlow::Node getName() { none() } + + override DataFlow::Node getValue() { result = this.getArgument([1, 2]) } + + override predicate definesHeader(string header, string value) { + header = "status" and value = this.getArgument(2).getIntValue().toString() + or + header = "content-type" and value = this.getArgument(1).getStringValue() + } + + override HTTP::ResponseWriter getResponseWriter() { none() } + } + + /** A body argument to a `NewResponse` call. */ + private class NewResponseBody extends HTTP::ResponseBody::Range { + NewResponse call; + + NewResponseBody() { this = call.getArgument(3) } + + override DataFlow::Node getAContentTypeNode() { result = call.getArgument(1) } + + override HTTP::ResponseWriter getResponseWriter() { none() } + } + + private class TextResponse extends HTTP::HeaderWrite::Range, DataFlow::CallNode { + TextResponse() { this.getTarget().hasQualifiedName(packagePath(), "TextResponse") } + + override string getHeaderName() { this.definesHeader(result, _) } + + override string getHeaderValue() { this.definesHeader(_, result) } + + override DataFlow::Node getName() { none() } + + override DataFlow::Node getValue() { none() } + + override predicate definesHeader(string header, string value) { + header = "status" and value = "200" + or + header = "content-type" and value = "text/plain" + } + + override HTTP::ResponseWriter getResponseWriter() { none() } + } + + /** A body argument to a `TextResponse` call. */ + private class TextResponseBody extends HTTP::ResponseBody::Range, TextResponse { + TextResponse call; + + TextResponseBody() { this = call.getArgument(2) } + + override DataFlow::Node getAContentTypeNode() { result = call.getArgument(1) } + + override HTTP::ResponseWriter getResponseWriter() { none() } + } + + /** A handler attached to a goproxy proxy type. */ + private class ProxyHandler extends HTTP::RequestHandler::Range { + DataFlow::MethodCallNode handlerReg; + + ProxyHandler() { + handlerReg + .getTarget() + .hasQualifiedName(packagePath(), "ReqProxyConds", ["Do", "DoFunc", "HandleConnect"]) and + this = handlerReg.getArgument(0) + } + + override predicate guardedBy(DataFlow::Node check) { + // note OnResponse is not modeled, as that server responses are not currently considered untrusted input + exists(DataFlow::MethodCallNode onreqcall | + onreqcall.getTarget().hasQualifiedName(packagePath(), "ProxyHttpServer", "OnRequest") + | + handlerReg.getReceiver() = onreqcall.getASuccessor*() and + check = onreqcall.getArgument(0) + ) + } + } + + private class UserControlledRequestData extends UntrustedFlowSource::Range { + UserControlledRequestData() { + exists(DataFlow::FieldReadNode frn | this = frn | + // liberally consider ProxyCtx.UserData to be untrusted; it's a data field set by a request handler + frn.getField().hasQualifiedName(packagePath(), "ProxyCtx", "UserData") + ) + or + exists(DataFlow::MethodCallNode call | this = call | + call.getTarget().hasQualifiedName(packagePath(), "ProxyCtx", "Charset") + ) + } + } + + private class ProxyLog extends LoggerCall::Range, DataFlow::MethodCallNode { + ProxyLog() { this.getTarget().hasQualifiedName(packagePath(), "ProxyCtx", ["Logf", "Warnf"]) } + + override DataFlow::Node getAMessageComponent() { result = this.getAnArgument() } + } + + private class MethodModels extends TaintTracking::FunctionModel, Method { + FunctionInput inp; + FunctionOutput outp; + + MethodModels() { + // Methods: + // signature: func CertStorage.Fetch(hostname string, gen func() (*tls.Certificate, error)) (*tls.Certificate, error) + // + // `hostname` excluded because if the cert storage or generator function themselves have not + // been tainted, `hostname` would be unlikely to fetch user-controlled data + this.hasQualifiedName(packagePath(), "CertStorage", "Fetch") and + (inp.isReceiver() or inp.isParameter(1)) and + outp.isResult(0) + } + + override predicate hasTaintFlow(FunctionInput i, FunctionOutput o) { i = inp and o = outp } + } +} diff --git a/ql/test/library-tests/semmle/go/frameworks/ElazarlGoproxy/go.mod b/ql/test/library-tests/semmle/go/frameworks/ElazarlGoproxy/go.mod new file mode 100644 index 00000000000..21f5e109021 --- /dev/null +++ b/ql/test/library-tests/semmle/go/frameworks/ElazarlGoproxy/go.mod @@ -0,0 +1,8 @@ +module main + +go 1.14 + +require ( + github.com/elazarl/goproxy v0.0.0-20201021153353-00ad82a08272 + github.com/github/depstubber v0.0.0-20201214172518-12c3da4b7c9d // indirect +) diff --git a/ql/test/library-tests/semmle/go/frameworks/ElazarlGoproxy/main.go b/ql/test/library-tests/semmle/go/frameworks/ElazarlGoproxy/main.go new file mode 100644 index 00000000000..5c5f104e1b2 --- /dev/null +++ b/ql/test/library-tests/semmle/go/frameworks/ElazarlGoproxy/main.go @@ -0,0 +1,27 @@ +//go:generate depstubber -vendor github.com/elazarl/goproxy ProxyCtx NewResponse,TextResponse,AlwaysReject,ContentTypeText,ContentTypeHtml,ReqHostMatches + +package main + +import ( + "fmt" + "github.com/elazarl/goproxy" + "net/http" +) + +func handler(r *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) { + data := ctx.UserData // $untrustedflowsource=selection of UserData + + // note no content type result here because we don't seem to extract the value of `ContentTypeHtml` + return r, goproxy.NewResponse(r, goproxy.ContentTypeHtml, http.StatusForbidden, fmt.Sprintf("Bad request: %v", data)) // $headerwrite=status:403 +} + +func handler1(r *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) { + ctx.Logf("test") // $logger="test" + ctx.Warnf("test1") // $logger="test1" + + return r, goproxy.TextResponse(r, "Hello!") // $headerwrite=status:200 $headerwrite=content-type:text/plain +} + +func main() { + +} diff --git a/ql/test/library-tests/semmle/go/frameworks/ElazarlGoproxy/test.expected b/ql/test/library-tests/semmle/go/frameworks/ElazarlGoproxy/test.expected new file mode 100644 index 00000000000..e69de29bb2d diff --git a/ql/test/library-tests/semmle/go/frameworks/ElazarlGoproxy/test.ql b/ql/test/library-tests/semmle/go/frameworks/ElazarlGoproxy/test.ql new file mode 100644 index 00000000000..cf7ff09fb3f --- /dev/null +++ b/ql/test/library-tests/semmle/go/frameworks/ElazarlGoproxy/test.ql @@ -0,0 +1,46 @@ +import go +import TestUtilities.InlineExpectationsTest + +class UntrustedFlowSourceTest extends InlineExpectationsTest { + UntrustedFlowSourceTest() { this = "untrustedflowsource" } + + override string getARelevantTag() { result = "untrustedflowsource" } + + override predicate hasActualResult(string file, int line, string element, string tag, string value) { + tag = "untrustedflowsource" and + value = element and + exists(UntrustedFlowSource src | value = src.toString() | + src.hasLocationInfo(file, line, _, _, _) + ) + } +} + +class HeaderWriteTest extends InlineExpectationsTest { + HeaderWriteTest() { this = "headerwrite" } + + override string getARelevantTag() { result = "headerwrite" } + + override predicate hasActualResult(string file, int line, string element, string tag, string value) { + tag = "headerwrite" and + exists(HTTP::HeaderWrite hw, string name, string val | element = hw.toString() | + hw.definesHeader(name, val) and + value = name + ":" + val and + hw.hasLocationInfo(file, line, _, _, _) + ) + } +} + +class LoggerTest extends InlineExpectationsTest { + LoggerTest() { this = "LoggerTest" } + + override string getARelevantTag() { result = "logger" } + + override predicate hasActualResult(string file, int line, string element, string tag, string value) { + exists(LoggerCall log | + log.hasLocationInfo(file, line, _, _, _) and + element = log.toString() and + value = log.getAMessageComponent().toString() and + tag = "logger" + ) + } +} diff --git a/ql/test/library-tests/semmle/go/frameworks/ElazarlGoproxy/vendor/github.com/elazarl/goproxy/.gitignore b/ql/test/library-tests/semmle/go/frameworks/ElazarlGoproxy/vendor/github.com/elazarl/goproxy/.gitignore new file mode 100644 index 00000000000..1005f6f1ecd --- /dev/null +++ b/ql/test/library-tests/semmle/go/frameworks/ElazarlGoproxy/vendor/github.com/elazarl/goproxy/.gitignore @@ -0,0 +1,2 @@ +bin +*.swp diff --git a/ql/test/library-tests/semmle/go/frameworks/ElazarlGoproxy/vendor/github.com/elazarl/goproxy/LICENSE b/ql/test/library-tests/semmle/go/frameworks/ElazarlGoproxy/vendor/github.com/elazarl/goproxy/LICENSE new file mode 100644 index 00000000000..2067e567c9f --- /dev/null +++ b/ql/test/library-tests/semmle/go/frameworks/ElazarlGoproxy/vendor/github.com/elazarl/goproxy/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2012 Elazar Leibovich. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Elazar Leibovich. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/ql/test/library-tests/semmle/go/frameworks/ElazarlGoproxy/vendor/github.com/elazarl/goproxy/stub.go b/ql/test/library-tests/semmle/go/frameworks/ElazarlGoproxy/vendor/github.com/elazarl/goproxy/stub.go new file mode 100644 index 00000000000..3d4882efc8b --- /dev/null +++ b/ql/test/library-tests/semmle/go/frameworks/ElazarlGoproxy/vendor/github.com/elazarl/goproxy/stub.go @@ -0,0 +1,159 @@ +// Code generated by depstubber. DO NOT EDIT. +// This is a simple stub for github.com/elazarl/goproxy, strictly for use in testing. + +// See the LICENSE file for information about the licensing of the original library. +// Source: github.com/elazarl/goproxy (exports: ProxyCtx; functions: NewResponse,TextResponse,AlwaysReject,ContentTypeText,ContentTypeHtml,ReqHostMatches) + +// Package goproxy is a stub of github.com/elazarl/goproxy, generated by depstubber. +package goproxy + +import ( + tls "crypto/tls" + net "net" + http "net/http" + regexp "regexp" +) + +var AlwaysReject FuncHttpsHandler = nil + +type CertStorage interface { + Fetch(_ string, _ func() (*tls.Certificate, error)) (*tls.Certificate, error) +} + +type ConnectAction struct { + Action ConnectActionLiteral + Hijack func(*http.Request, net.Conn, *ProxyCtx) + TLSConfig func(string, *ProxyCtx) (*tls.Config, error) +} + +type ConnectActionLiteral int + +var ContentTypeHtml string = "" + +var ContentTypeText string = "" + +type FuncHttpsHandler func(string, *ProxyCtx) (*ConnectAction, string) + +func (_ FuncHttpsHandler) HandleConnect(_ string, _ *ProxyCtx) (*ConnectAction, string) { + return nil, "" +} + +type HttpsHandler interface { + HandleConnect(_ string, _ *ProxyCtx) (*ConnectAction, string) +} + +type Logger interface { + Printf(_ string, _ ...interface{}) +} + +func NewResponse(_ *http.Request, _ string, _ int, _ string) *http.Response { + return nil +} + +type ProxyConds struct{} + +func (_ *ProxyConds) Do(_ RespHandler) {} + +func (_ *ProxyConds) DoFunc(_ func(*http.Response, *ProxyCtx) *http.Response) {} + +type ProxyCtx struct { + Req *http.Request + Resp *http.Response + RoundTripper RoundTripper + Error error + UserData interface{} + Session int64 + Proxy *ProxyHttpServer +} + +func (_ *ProxyCtx) Charset() string { + return "" +} + +func (_ *ProxyCtx) Logf(_ string, _ ...interface{}) {} + +func (_ *ProxyCtx) RoundTrip(_ *http.Request) (*http.Response, error) { + return nil, nil +} + +func (_ *ProxyCtx) Warnf(_ string, _ ...interface{}) {} + +type ProxyHttpServer struct { + KeepDestinationHeaders bool + Verbose bool + Logger Logger + NonproxyHandler http.Handler + Tr *http.Transport + ConnectDial func(string, string) (net.Conn, error) + CertStore CertStorage + KeepHeader bool +} + +func (_ *ProxyHttpServer) NewConnectDialToProxy(_ string) func(string, string) (net.Conn, error) { + return nil +} + +func (_ *ProxyHttpServer) NewConnectDialToProxyWithHandler(_ string, _ func(*http.Request)) func(string, string) (net.Conn, error) { + return nil +} + +func (_ *ProxyHttpServer) OnRequest(_ ...ReqCondition) *ReqProxyConds { + return nil +} + +func (_ *ProxyHttpServer) OnResponse(_ ...RespCondition) *ProxyConds { + return nil +} + +func (_ *ProxyHttpServer) ServeHTTP(_ http.ResponseWriter, _ *http.Request) {} + +type ReqCondition interface { + HandleReq(_ *http.Request, _ *ProxyCtx) bool + HandleResp(_ *http.Response, _ *ProxyCtx) bool +} + +type ReqConditionFunc func(*http.Request, *ProxyCtx) bool + +func (_ ReqConditionFunc) HandleReq(_ *http.Request, _ *ProxyCtx) bool { + return false +} + +func (_ ReqConditionFunc) HandleResp(_ *http.Response, _ *ProxyCtx) bool { + return false +} + +type ReqHandler interface { + Handle(_ *http.Request, _ *ProxyCtx) (*http.Request, *http.Response) +} + +func ReqHostMatches(_ ...*regexp.Regexp) ReqConditionFunc { + return nil +} + +type ReqProxyConds struct{} + +func (_ *ReqProxyConds) Do(_ ReqHandler) {} + +func (_ *ReqProxyConds) DoFunc(_ func(*http.Request, *ProxyCtx) (*http.Request, *http.Response)) {} + +func (_ *ReqProxyConds) HandleConnect(_ HttpsHandler) {} + +func (_ *ReqProxyConds) HandleConnectFunc(_ func(string, *ProxyCtx) (*ConnectAction, string)) {} + +func (_ *ReqProxyConds) HijackConnect(_ func(*http.Request, net.Conn, *ProxyCtx)) {} + +type RespCondition interface { + HandleResp(_ *http.Response, _ *ProxyCtx) bool +} + +type RespHandler interface { + Handle(_ *http.Response, _ *ProxyCtx) *http.Response +} + +type RoundTripper interface { + RoundTrip(_ *http.Request, _ *ProxyCtx) (*http.Response, error) +} + +func TextResponse(_ *http.Request, _ string) *http.Response { + return nil +} diff --git a/ql/test/library-tests/semmle/go/frameworks/ElazarlGoproxy/vendor/modules.txt b/ql/test/library-tests/semmle/go/frameworks/ElazarlGoproxy/vendor/modules.txt new file mode 100644 index 00000000000..bf027718a74 --- /dev/null +++ b/ql/test/library-tests/semmle/go/frameworks/ElazarlGoproxy/vendor/modules.txt @@ -0,0 +1,5 @@ +# github.com/elazarl/goproxy v0.0.0-20201021153353-00ad82a08272 +## explicit +github.com/elazarl/goproxy +# github.com/github/depstubber v0.0.0-20201214172518-12c3da4b7c9d +## explicit