How to Avoid Common Security Pitfalls in Go

How to Avoid Common Security Pitfalls in Go

Go has been one of the most popular languages for building microservices. Its simplicity and less overhead have proven to favor its rise in the community. Today, developers are using it to build web services for internet-scale. Even though it's a great language, you still have to follow certain best practices to ensure your service is secure and trustworthy.

In this blog, I am going to cover some common security pitfalls while building a Go application, namely:

  • Cross-site Scripting(XSS)
  • SQL injection
  • Cross-site Request Forgery(CSRF)
  • Common Vulnerabilities and Exposures(CVE)

Cross-site Scripting(XSS)

In the Cross-Site Scripting (XSS) attack, an attacker injects a malicious script into the app to modify its behavior. It's one of the most common attacks performed on websites.

Suppose you have an API endpoint that accepts a query parameter. You use that query parameter to build the HTML response page. An attacker can replace the query parameter with a javascript, and when you return the HTML response, a JavaScript command is executed on the user's browser. Let's with a code example.


package main

import (
    "log"
    "net/http"
    "text/template"
)

func helloHandler(w http.ResponseWriter, r *http.Request) {
    name := r.URL.Query().Get("name")
    w.Header().Set("Content-Type", "text/html")
    w.WriteHeader(http.StatusOK)
    t, _ := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`)
    t.ExecuteTemplate(w, "T", name)
}
func main() {
    http.HandleFunc("/hello", helloHandler)
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Now you can run your server using go run main.go. Go and visit http://localhost:8080/hello?name=umesh

Screenshot 2020-11-17 at 10.27.19 PM.png

Instead of passing a text as a name parameter, we will now be passing a JavaScript command, and it will get executed in the browser.

Screenshot 2020-11-17 at 10.28.34 PM.png

So, here’s how you can prevent it: simply replace text/template with html/template. html/template produces escaped HTML output in contrasts to text/template.


package main

import (
    "html/template"
    "log"
    "net/http"
)

func helloHandler(w http.ResponseWriter, r *http.Request) {
    name := r.URL.Query().Get("name")
    w.Header().Set("Content-Type", "text/html")
    w.WriteHeader(http.StatusOK)
    t, _ := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`)
    t.ExecuteTemplate(w, "T", name)
}
func main() {
    http.HandleFunc("/hello", helloHandler)
    log.Fatal(http.ListenAndServe(":8080", nil))
}

SQL Injection

Go provides you with an inbuilt package to perform DB queries. You can use database/sql to perform various database queries.

So suppose you are taking some user input and using it directly to perform database operations.


package main

import (
    "database/sql"
)
...

func someDBOperations() {
    id := userInput("id")
    _, err := db.Exec("SELECT *  from stories WHERE user_id =" + id)
    if err != nil {
        panic(err)
    }
}
...

As in the above example, if an attacker sends you a SQL input as id('select * from users') instead of the usual value, and you execute the query, you will query the users present in the table and return the answer to the malicious user instead of the expected results.

To avoid this, you should validate the user input. So you can perform simple validations on the user input before passing it on to the DB. After that, you should always use parameterized variables or use prepared statements.


package main

import (
    "database/sql"
)
...

func someDBOperations() {
    id := userInput("id")
    _, err := db.Exec("SELECT *  from stories WHERE user_id = ?", id)
    if err != nil {
        panic(err)
    }
}

By changing the db.Exec in the above example to using query params instead of using concatenation. We have avoided possible SQL injection attacks.

Cross-site Request Forgery(CSRF)

Cross-site Request Forgery happens when the web server does not verify whether the user authorizes the request; it only checks whether the request was created from the user's browser. The website assumes that the user generates every request that originates from the user’s browser.

This assumption leads to attacks such as CSRF because many other websites also run their code on the user's browser, and the attacker can capture the session to make requests on the user's behalf.

How can you prevent this from happening?

A webserver should generate a CSRF token unique to a session every time a user visits their website and store it inside the cookie on the user's browser. Whenever the user submits a form, it should also provide the CSRF token along with a cookie.

When a user submits the form with the cookie, the server verifies if the CSRF token in the cookie and form is identical. Make sure you are using the same-site cookies so that you will be able to restrict the cookie’s access to the same-site context. These steps will prevent the attacker from making any false request on behalf of your user.

You can use gorilla/csrf . It provides an easy way to implement CSRF protection, and you can use it to protect your website from any of these attacks. Here’s an example where you add a CSRF middleware on top of your HTTP handler and set the CSRF token in the header while returning the response.


package main

import (
    "encoding/json"
    "log"
    "net/http"

    "github.com/gorilla/csrf"
    "github.com/gorilla/mux"
)

type user struct {
    name string
}

func GetUserByID(w http.ResponseWriter, r *http.Request) {
    data := &user{
        name: "Someone",
    }

    w.Header().Set("X-CSRF-Token", csrf.Token(r))

    resp, err := json.Marshal(data)
    if err != nil {
        w.WriteHeader(http.StatusInternalServerError)
        w.Write([]byte("something wen wrong"))
        return
    }

    w.WriteHeader(http.StatusOK)
    w.Write(resp)
}

func main() {
    router := mux.NewRouter()
    csrfMiddleware := csrf.Protect([]byte("925d21d89fc208f62cad90b017cbc35b2dab62e06105c1268e04c9f6714cad94ba9babf715eac60597a67d2a28b1abe3d20a072bf02bd6f04655e95263780fce"))

    router.Use(csrfMiddleware)
    router.HandleFunc("/user/{id}", GetUserByID).Methods("GET")

    log.Fatal(http.ListenAndServe(":8000", router))
}

If a user submits a form or submits a request, the middleware can immediately check if the request is made by the user or the attacker. Depending on the CSRF token, it refuses the requests.

Common Vulnerabilities and Exposures(CVE)

Common Vulnerabilities and Exposures (CVE) is a catalog of publicly known security vulnerabilities and exposures.

Every software or tool ever created has introduced vulnerabilities in their code, and Go is no exception. Now we will take a look at how to find these vulnerabilities and plug them by keeping your library updated.

We will use WhiteSource Vulnerability Database to explore CVE. It's free and has a database of all the open-source CVE. It looks something like this.

image.png

You can search through the CVE using an ID, or an open-source project. As we are interested in Go, we will search for it.

image.png

Once you have searched for it, you will find multiple results with Go in their name, but we are interested in this one.

Screenshot 2020-11-17 at 11.20.02 PM.png

After clicking on it, it will take you to the detail page where you can look at the Go vulnerabilities reported over the year. If you look at the graphs, you will notice they denote the severity of the CVE reported and their reporting time. In the case of Go the frequency is less, but it still has many CVE reported since its launch.

image.png

Now suppose you want to know about a specific CVE, you can use the search functionality to find out more about it. Let search about one of the very recent security vulnerability CVE-2019-16276.

image.png

You can click on the link and explore more about the vulnerability. You can check the severity, and explore more details about the CVE.

image.png

Go team resolved these issues and adds a patch to fix it as soon as they get reported. You have to upgrade your Go version to avoid these vulnerabilities. But sometimes it might take time to deliver the patch depending upon the criticality, so if it's something that is going to impact your app, you can look for a temporary fix.

Conclusion

Go is an excellent language, and most of the time, it provides pretty strong security out of the box. But no code is immune to mistakes, so you should check it as much as you can and use security tools at your disposal.

If you liked this post, please share it with others so that it can help them as well. You can tag me on Twitter @imumesh18. You can subscribe to my newsletter to read more from me.

Did you find this article valuable?

Support Umesh Yadav by becoming a sponsor. Any amount is appreciated!