16 Feb 2023 · Software Engineering

    Vulnerability Scanning in Go With Govulncheck

    9 min read
    Contents

    Go recently introduced an official vulnerability scanner that promises to best every other third-party tool out there: govulncheck. This is a tool that understands which modules and standard library functions your Go project is using and gives warnings about known vulnerabilities that can affect the application.

    In this article, I want to explore how it works and how we can combine it with CI/CD to secure our Go projects.

    The state of Go’s security

    As of September 2022, the state of security scanning in Go was disappointing. All we had were a few third-party, ruleset-based scanners like gosec or Snyk code checker. What was missing was a tool with access to a Go-specific known vulnerability database.

    Enter govulnchecker

    The Go security team introduced govulncheck in September 2022. Govulncheck is an open-source command line utility that can analyze code and give warnings about known issues in Go modules or its standard library. Behind the scenes, govulncheck grabs its data from the Go vulnerability database, which is maintained and curated by the Go security team.

    The architecture diagram of how data reaches the vulnerability database. The security team takes data from Go maintainer reports, their internal security fixes, and other vulnerability databases such as NVD or GHSA. The data is curated by the Go Security Team and added to the Go database.
    The Go Security Team curates vulnerability reports.

    Compared to other security tools, govulncheck has a few important advantages:

    • Smart: the tool warns you only if you actually use vulnerable code. This means it’s a lot less noisy than the likes of npm audit, which only scans the package manifest.
    • Comprehensive: the database feeds from multiple sources, including internal reports, package maintainer submissions, the National Vulnerability Database (NVD), and the GitHub Advisory Database.
    • Official: the Go developer team maintains the tool. And it will eventually find its way into the Go distribution itself.
    • Curated: the database is curated by the Go security team, implying a higher level of supervision.

    Getting started with govulncheck

    Getting started with govulncheck is as straightforward as installing the CLI with go install and running it in the project folder:

    $ go install golang.org/x/vuln/cmd/govulncheck@latest
    $ govulncheck ./...

    Check your PATH if you get a “command not found” error. Go installs binaries in $GOPATH/bin. So, you may need to update your environment:

    $ export PATH=$PATH:$HOME/go/bin
    # or
    $ export PATH=$PATH:$GOPATH/bin

    Govulncheck is still experimental, so it has only a few options. We can supply -test to scan test files and -json for JSON output, and that’s it.

    To see govulncheck in action, let’s fork and clone the semaphoreci-demo/semaphore-demo-go repository. As you can see, I’m using Go version 1.19 here:

    $ go version
    go version go1.19 linux/amd64
    
    $ govulncheck ./...
    govulncheck is an experimental tool. Share feedback at https://go.dev/s/govulncheck-feedback.
    
    Scanning for dependencies with known vulnerabilities...
    Found 3 known vulnerabilities.
    
    Vulnerability #1: GO-2022-1144
      An attacker can cause excessive memory growth in a Go server
      accepting HTTP/2 requests. HTTP/2 server connections contain a
      cache of HTTP header keys sent by the client. While the total
      number of entries in this cache is capped, an attacker sending
      very large keys can cause the server to allocate approximately
      64 MiB per open connection.
    
      Call stacks in your code:
          main.go:76:28: github.com/semaphoreci-demos/semaphore-demo-go.main calls net/http.ListenAndServe
    
      Found in: net/http@go1.19
      Fixed in: net/http@go1.19.4
      More info: https://pkg.go.dev/vuln/GO-2022-1144
    
    Vulnerability #2: GO-2022-1039
      Programs which compile regular expressions from untrusted
      sources may be vulnerable to memory exhaustion or denial of
      service. The parsed regexp representation is linear in the size
      of the input, but in some cases the constant factor can be as
      high as 40,000, making relatively small regexps consume much
      larger amounts of memory. After fix, each regexp being parsed is
      limited to a 256 MB memory footprint. Regular expressions whose
      representation would use more space than that are rejected.
      Normal use of regular expressions is unaffected.
    
      Call stacks in your code:
          github.com/semaphoreci-demos/semaphore-demo-go.init calls github.com/lib/pq.init, which eventually calls regexp/syntax.Parse
    
      Found in: regexp/syntax@go1.19
      Fixed in: regexp/syntax@go1.19.2
      More info: https://pkg.go.dev/vuln/GO-2022-1039
    
    Vulnerability #3: GO-2022-0969
      HTTP/2 server connections can hang forever waiting for a clean
      shutdown that was preempted by a fatal error. This condition can
      be exploited by a malicious client to cause a denial of service.
    
      Call stacks in your code:
          main.go:76:28: github.com/semaphoreci-demos/semaphore-demo-go.main calls net/http.ListenAndServe
    
      Found in: net/http@go1.19
      Fixed in: net/http@go1.19.1
      More info: https://pkg.go.dev/vuln/GO-2022-0969
    
    === Informational ===
    
    The vulnerabilities below are in packages that you import, but your code
    doesn't appear to call any vulnerable functions. You may not need to take any
    action. See https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck
    for details.
    
    Vulnerability #1: GO-2022-1143
      On Windows, restricted files can be accessed via os.DirFS and
      http.Dir. The os.DirFS function and http.Dir type provide access
      to a tree of files rooted at a given directory. These functions
      permit access to Windows device files under that root. For
      example, os.DirFS("C:/tmp").Open("COM1") opens the COM1 device.
      Both os.DirFS and http.Dir only provide read-only filesystem
      access. In addition, on Windows, an os.DirFS for the directory
      (the root of the current drive) can permit a maliciously crafted
      path to escape from the drive and access any path on the system.
      With fix applied, the behavior of os.DirFS("") has changed.
      Previously, an empty root was treated equivalently to "/", so
      os.DirFS("").Open("tmp") would open the path "/tmp". This now
      returns an error.
      Found in: net/http@go1.19
      Fixed in: net/http@go1.19.4
      More info: https://pkg.go.dev/vuln/GO-2022-1143
    
    Vulnerability #2: GO-2022-1095
      Due to unsanitized NUL values, attackers may be able to
      maliciously set environment variables on Windows. In
      syscall.StartProcess and os/exec.Cmd, invalid environment
      variable values containing NUL values are not properly checked
      for. A malicious environment variable value can exploit this
      behavior to set a value for a different environment variable.
      For example, the environment variable string "A=B\x00C=D" sets
      the variables "A=B" and "C=D".
      Found in: os/exec@go1.19
      Fixed in: os/exec@go1.19.3
      More info: https://pkg.go.dev/vuln/GO-2022-1095
    
    Vulnerability #3: GO-2022-0988
      JoinPath and URL.JoinPath do not remove ../ path elements
      appended to a relative path. For example,
      JoinPath("https://go.dev", "../go") returns the URL
      "https://go.dev/../go", despite the JoinPath documentation
      stating that ../ path elements are removed from the result.
      Found in: net/url@go1.19
      Fixed in: net/url@go1.19.1
      More info: https://pkg.go.dev/vuln/GO-2022-0988
    

    Govulncheck reveals that I have three security issues in the project; all of them can be resolved by switching to a newer Go version, as the affected packages belong to the Go standard library. The tool can also warn us about other problems in imported modules. These are “informational” messages because we’re not using the affected functions, so we should be safe.

    Govulncheck quirks

    Govulncheck is an experimental tool, so finding a few quirks shouldn’t be too surprising. During the creation of this tutorial we found two issues.

    The first one appeared in projects using C extensions. Importing C code can make govulncheck fail:

    $ govulncheck ./...
    
    Scanning for dependencies with known vulnerabilities...
    govulncheck: Packages contain errors:
    /usr/local/go/src/runtime/cgo/cgo.go:33:8: could not import C (no metadata for C)
    /usr/local/go/src/os/user/cgo_listgroups_unix.go:19:8: could not import C (no metadata for C)
    /usr/local/go/src/net/cgo_linux.go:12:8: could not import C (no metadata for C)
    

    The workaround we found is to disable the cgo package with: export CGO_ENABLED=0.

    The second problem we encountered was high memory usage. According to this issue: govulncheck ate all available memory and got killed; govulncheck builds a call graph in memory that uses up a lot of it in big projects. So, before adding govulncheck to your project, do some trial runs to see how much memory it needs. If you experience this problem, you can use a system with more memory (e.g. choosing a bigger CI machine) or run govulncheck on smaller subsets of the project’s code.

    Using govulncheck in the CI pipeline

    The main benefit of having a CLI tool like govulncheck is that we can integrate it with our continuous integration pipeline. Implementing the integration lets everyone on the team know when a security issue emerges in the project, reducing the chance of releasing vulnerable software.

    Adding govulncheck to the CI pipeline is very straightforward. The demo repository I cloned earlier already has a started pipeline, so I only need to add one job at the end.

    We can add govulncheck into our CI pipeline by creating a new job. In this section, we’ll use the example pipeline in the semaphoreci-demo/semaphore-demo-go we forked earlier. The project already has a starter pipeline that looks like this:

    A screenshot a CI pipeline containing a build job, a format check job (static checker) and a test block.
    An example CI pipeline for a Go project. The pipeline features static, unit and integration tests.

    The job to create has the commands shown below. Adjust the sem-version command as needed. We’ll choose Go 1.19 to verify that the job fails.

    checkout
    sem-version go 1.19
    go install golang.org/x/vuln/cmd/govulncheck@latest
    govulncheck ./...
    

    After the changes, the pipeline will look like this:

    A screenshot of a Semaphore CI pipeline. A new job was added at the end called 'govulncheck'.
    Adding a govulncheck test to a CI pipeline

    Running the pipeline should throw an error since we’ve not yet fixed the security issues.

    A screenshot of a pipeline run. The last job we added failed because the code had vulnerabilities.
    The job fails when govulncheck detects vulnerabilities in the project. This prevents the pipeline from moving forward, preventing the release of unsafe software.

    Checking the job log reveals the same error that we encountered the first time.

    A screenshot of the failed job log showing the error output produced by govulncheck
    Clicking on the job reveals the problems that were found by govulncheck.

    In this example, we can fix the problem by changing sem-version go 1.19 to sem-version go 1.19.4 in all the jobs in the CI pipeline.

    Conclusion

    We dare not live without automated security scanning, especially in a system-level language like Go. Govulncheck may not have the catchiest of names and, being in the experimental stage, it has its share of troubles, but there is no doubt that the Go security team made a giant leap forward with its release.

    If this tool looks useful to you, check out the official VS Code extension, which lets you run security checks right in the IDE.

    Thanks for reading!

    Leave a Reply

    Your email address will not be published. Required fields are marked *

    Avatar
    Writen by:
    I picked up most of my skills during the years I worked at IBM. Was a DBA, developer, and cloud engineer for a time. After that, I went into freelancing, where I found the passion for writing. Now, I'm a full-time writer at Semaphore.
    Avatar
    Reviewed by:
    I picked up most of my soft/hardware troubleshooting skills in the US Army. A decade of Java development drove me to operations, scaling infrastructure to cope with the thundering herd. Engineering coach and CTO of Teleclinic.