Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Go: New File System Access Sinks #14064

Merged
merged 22 commits into from
Oct 11, 2023
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
category: minorAnalysis
---
* Support has been added for file system access sinks in the following libraries: [net/http](https://pkg.go.dev/net/http), [Afero](https://github.com/spf13/afero), [beego](https://pkg.go.dev/github.com/astaxie/beego), [Echo](https://pkg.go.dev/github.com/labstack/echo), [Fiber](https://github.com/kataras/iris), [Gin](https://pkg.go.dev/github.com/gin-gonic/gin), [Iris](https://github.com/kataras/iris).
3 changes: 3 additions & 0 deletions go/ql/lib/go.qll
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import semmle.go.dataflow.GlobalValueNumbering
import semmle.go.dataflow.SSA
import semmle.go.dataflow.TaintTracking
import semmle.go.dataflow.TaintTracking2
import semmle.go.frameworks.Afero
import semmle.go.frameworks.Beego
import semmle.go.frameworks.BeegoOrm
import semmle.go.frameworks.Chi
Expand All @@ -38,11 +39,13 @@ import semmle.go.frameworks.Echo
import semmle.go.frameworks.ElazarlGoproxy
import semmle.go.frameworks.Email
import semmle.go.frameworks.Encoding
import semmle.go.frameworks.Fiber
import semmle.go.frameworks.Gin
import semmle.go.frameworks.Glog
import semmle.go.frameworks.GoMicro
import semmle.go.frameworks.GoRestfulHttp
import semmle.go.frameworks.Gqlgen
import semmle.go.frameworks.Iris
import semmle.go.frameworks.K8sIoApimachineryPkgRuntime
import semmle.go.frameworks.K8sIoApiCoreV1
import semmle.go.frameworks.K8sIoClientGo
Expand Down
114 changes: 114 additions & 0 deletions go/ql/lib/semmle/go/frameworks/Afero.qll
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/**
* Provides classes for working with sinks and taint propagators
* from the `github.com/spf13/afero` package.
*/

import go

/**
* Provide File system access sinks of [afero](https://github.com/spf13/afero) framework
*/
module Afero {
/**
* Gets all versions of `github.com/spf13/afero`
*/
string aferoPackage() { result = package("github.com/spf13/afero", "") }

/**
* The File system access sinks of [afero](https://github.com/spf13/afero) framework methods
*/
class AferoSystemAccess extends FileSystemAccess::Range, DataFlow::CallNode {
AferoSystemAccess() {
exists(Method m |
m.hasQualifiedName(aferoPackage(), "HttpFs",
["Create", "Open", "OpenFile", "Remove", "RemoveAll"]) and
this = m.getACall()
or
m.hasQualifiedName(aferoPackage(), "RegexpFs",
["Create", "Open", "OpenFile", "Remove", "RemoveAll", "Mkdir", "MkdirAll"]) and
this = m.getACall()
or
m.hasQualifiedName(aferoPackage(), "ReadOnlyFs",
["Create", "Open", "OpenFile", "ReadDir", "ReadlinkIfPossible", "Mkdir", "MkdirAll"]) and
this = m.getACall()
or
m.hasQualifiedName(aferoPackage(), "OsFs",
[
"Create", "Open", "OpenFile", "ReadlinkIfPossible", "Remove", "RemoveAll", "Mkdir",
"MkdirAll"
]) and
this = m.getACall()
or
m.hasQualifiedName(aferoPackage(), "MemMapFs",
["Create", "Open", "OpenFile", "Remove", "RemoveAll", "Mkdir", "MkdirAll"]) and
this = m.getACall()
)
}

override DataFlow::Node getAPathArgument() { result = this.getArgument(0) }
}

/**
* The File system access sinks of [afero](https://github.com/spf13/afero) framework utility functions
*
* Afero Type is basically is an wrapper around utility functions which make them like a method, look at [here](https://github.com/spf13/afero/blob/cf95922e71986c0116204b6eeb3b345a01ffd842/ioutil.go#L61)
*
* The Types that are not vulnerable: `afero.BasePathFs` and `afero.IOFS`
*/
class AferoUtilityFunctionSystemAccess extends FileSystemAccess::Range, DataFlow::CallNode {
int pathArg;

AferoUtilityFunctionSystemAccess() {
// utility functions
exists(Function f |
f.hasQualifiedName(aferoPackage(),
["WriteReader", "SafeWriteReader", "WriteFile", "ReadFile", "ReadDir"]) and
this = f.getACall() and
pathArg = 1 and
not aferoSanitizer(this.getArgument(0))
)
or
exists(Method m |
m.hasQualifiedName(aferoPackage(), "Afero",
["WriteReader", "SafeWriteReader", "WriteFile", "ReadFile", "ReadDir"]) and
this = m.getACall() and
pathArg = 0 and
not aferoSanitizer(this.getReceiver())
)
}

override DataFlow::Node getAPathArgument() { result = this.getArgument(pathArg) }
}

/**
* Holds if the Afero utility function has a first argument of a safe type like `NewBasePathFs`.
*
* e.g.
* ```
* basePathFs := afero.NewBasePathFs(osFS, "tmp")
* afero.ReadFile(basePathFs, filepath)
* ```
*/
predicate aferoSanitizer(DataFlow::Node n) {
exists(Function f |
f.hasQualifiedName(aferoPackage(), ["NewBasePathFs", "NewIOFS"]) and
TaintTracking::localTaint(f.getACall(), n)
)
}

/**
* Holds if there is a dataflow node from n1 to n2 when initializing the Afero instance
*
* A helper for `aferoSanitizer` for when the Afero instance is initialized with one of the safe FS types like IOFS
*
* e.g.`n2 := &afero.Afero{Fs: afero.NewBasePathFs(osFS, "./")}` n1 is `afero.NewBasePathFs(osFS, "./")`
*/
class AdditionalTaintStep extends TaintTracking::AdditionalTaintStep {
override predicate step(DataFlow::Node n1, DataFlow::Node n2) {
exists(StructLit st | st.getType().hasQualifiedName(aferoPackage(), "Afero") |
n1.asExpr() = st.getAnElement().(KeyValueExpr).getAChildExpr() and
n2.asExpr() = st
)
}
}
}
26 changes: 18 additions & 8 deletions go/ql/lib/semmle/go/frameworks/Beego.qll
Original file line number Diff line number Diff line change
Expand Up @@ -278,21 +278,31 @@ module Beego {
}
}

/**
* The File system access sinks
*/
private class FsOperations extends FileSystemAccess::Range, DataFlow::CallNode {
int pathArg;

FsOperations() {
this.getTarget().hasQualifiedName(packagePath(), "Walk")
this.getTarget().hasQualifiedName(packagePath(), "Walk") and pathArg = 1
or
exists(Method m | this = m.getACall() |
m.hasQualifiedName(packagePath(), "FileSystem", "Open") or
m.hasQualifiedName(packagePath(), "Controller", "SaveToFile")
m.hasQualifiedName(packagePath(), "FileSystem", "Open") and pathArg = 0
or
m.hasQualifiedName(packagePath(), "Controller", "SaveToFile") and pathArg = 1
or
m.hasQualifiedName(contextPackagePath(), "BeegoOutput", "Download") and
pathArg = 0
or
// SaveToFileWithBuffer only available in v2
m.hasQualifiedName("github.com/beego/beego/v2/server/web", "Controller",
"SaveToFileWithBuffer") and
pathArg = 1
)
}

override DataFlow::Node getAPathArgument() {
this.getTarget().getName() = ["Walk", "SaveToFile"] and result = this.getArgument(1)
or
this.getTarget().getName() = "Open" and result = this.getArgument(0)
}
override DataFlow::Node getAPathArgument() { result = this.getArgument(pathArg) }
}

private class RedirectMethods extends Http::Redirect::Range, DataFlow::CallNode {
Expand Down
14 changes: 14 additions & 0 deletions go/ql/lib/semmle/go/frameworks/Echo.qll
Original file line number Diff line number Diff line change
Expand Up @@ -98,4 +98,18 @@ private module Echo {

override Http::ResponseWriter getResponseWriter() { none() }
}

/**
* The File system access sinks
*/
class FsOperations extends FileSystemAccess::Range, DataFlow::CallNode {
FsOperations() {
exists(Method m |
m.hasQualifiedName(packagePath(), "Context", ["Attachment", "File"]) and
this = m.getACall()
)
}

override DataFlow::Node getAPathArgument() { result = this.getArgument(0) }
}
}
38 changes: 38 additions & 0 deletions go/ql/lib/semmle/go/frameworks/Fiber.qll
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* Provides classes for working the `github.com/gofiber/fiber` package.
*/

import go

private module Gin {
am0o0 marked this conversation as resolved.
Show resolved Hide resolved
/** Gets the package name `github.com/gofiber/fiber`. */
string packagePath() { result = package("github.com/gofiber/fiber", "") }

/** Gets the v2 module path `github.com/gofiber/fiber/v2` */
string v2modulePath() { result = "github.com/gofiber/fiber/v2" }

/**
* The File system access sinks
*/
class FsOperations extends FileSystemAccess::Range, DataFlow::CallNode {
int pathArg;

FsOperations() {
exists(Method m |
(
m.hasQualifiedName(packagePath(), "Ctx", ["SendFile", "Download"]) and
pathArg = 0
or
m.hasQualifiedName(packagePath(), "Ctx", "SaveFile") and
pathArg = 1
or
m.hasQualifiedName(v2modulePath(), "Ctx", "SaveFileToStorage") and
pathArg = 1
) and
this = m.getACall()
)
}

override DataFlow::Node getAPathArgument() { result = this.getArgument(pathArg) }
}
}
22 changes: 22 additions & 0 deletions go/ql/lib/semmle/go/frameworks/Gin.qll
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,26 @@ private module Gin {
)
}
}

/**
* The File system access sinks
*/
class FsOperations extends FileSystemAccess::Range, DataFlow::CallNode {
int pathArg;

FsOperations() {
exists(Method m |
(
m.hasQualifiedName(packagePath(), "Context", ["File", "FileAttachment"]) and
pathArg = 0
or
m.hasQualifiedName(packagePath(), "Context", "SaveUploadedFile") and
pathArg = 1
) and
this = m.getACall()
)
}

override DataFlow::Node getAPathArgument() { result = this.getArgument(pathArg) }
}
}
49 changes: 49 additions & 0 deletions go/ql/lib/semmle/go/frameworks/Iris.qll
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* Provides classes for working the `github.com/kataras/iris` package.
*/

import go

private module Iris {
/** Gets the v1 module path `github.com/kataras/iris`. */
string v1modulePath() { result = "github.com/kataras/iris" }

/** Gets the v12 module path `github.com/kataras/iris/v12` */
string v12modulePath() { result = "github.com/kataras/iris/v12" }

/** Gets the path for the context package of all versions of beego. */
string contextPackagePath() {
result = v12contextPackagePath()
or
result = v1contextPackagePath()
}

/** Gets the path for the context package of beego v12. */
string v12contextPackagePath() { result = v12modulePath() + "/context" }

/** Gets the path for the context package of beego v1. */
string v1contextPackagePath() { result = v1modulePath() + "/server/web/context" }

/**
* The File system access sinks
*/
class FsOperations extends FileSystemAccess::Range, DataFlow::CallNode {
int pathArg;

FsOperations() {
exists(Method m |
(
m.hasQualifiedName(contextPackagePath(), "Context",
["SendFile", "ServeFile", "SendFileWithRate", "ServeFileWithRate", "UploadFormFiles"]) and
pathArg = 0
or
m.hasQualifiedName(v12contextPackagePath(), "Context", "SaveFormFile") and
pathArg = 1
) and
this = m.getACall()
)
}

override DataFlow::Node getAPathArgument() { result = this.getArgument(pathArg) }
}
}
14 changes: 14 additions & 0 deletions go/ql/lib/semmle/go/frameworks/stdlib/NetHttp.qll
Original file line number Diff line number Diff line change
Expand Up @@ -288,4 +288,18 @@ module NetHttp {

override predicate guardedBy(DataFlow::Node check) { check = handlerReg.getArgument(0) }
}

/**
* The File system access sinks
*/
class HttpServeFile extends FileSystemAccess::Range, DataFlow::CallNode {
HttpServeFile() {
exists(Function f |
f.hasQualifiedName("net/http", "ServeFile") and
this = f.getACall()
)
}

override DataFlow::Node getAPathArgument() { result = this.getArgument(2) }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
testFailures
failures
34 changes: 34 additions & 0 deletions go/ql/test/library-tests/semmle/go/frameworks/Afero/Query.ql
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import go
import TestUtilities.InlineExpectationsTest

module FileSystemAccessTest implements TestSig {
string getARelevantTag() { result = ["FileSystemAccess", "succ", "pred"] }

predicate hasActualResult(Location location, string element, string tag, string value) {
exists(FileSystemAccess fsa |
fsa.hasLocationInfo(location.getFile().getAbsolutePath(), location.getStartLine(),
location.getStartColumn(), location.getEndLine(), location.getEndColumn()) and
element = fsa.getAPathArgument().toString() and
value = fsa.getAPathArgument().toString() and
tag = "FileSystemAccess"
)
or
exists(DataFlow::Node succ, DataFlow::Node pred |
any(Afero::AdditionalTaintStep adts).step(pred, succ)
|
succ.hasLocationInfo(location.getFile().getAbsolutePath(), location.getStartLine(),
location.getStartColumn(), location.getEndLine(), location.getEndColumn()) and
element = succ.toString() and
value = succ.asExpr().(StructLit).getType().getName() and
tag = "succ"
or
pred.hasLocationInfo(location.getFile().getAbsolutePath(), location.getStartLine(),
location.getStartColumn(), location.getEndLine(), location.getEndColumn()) and
element = pred.toString() and
value = pred.toString() and
tag = "pred"
)
}
}

import MakeTest<FileSystemAccessTest>
Loading
Loading