Security

In this chapter, we’ll discuss a few ways to improve the security of our Go server. Most are quick and easy to implement. In reality a large production environment will require a robust security setup with a Web Application Firewall (WAF) and other tools, but here we will only discuss improvements for our Go code.

Keep Go Up to Date

One of the most important things we can do to ensure the security of our Go environment is to keep our Go version up to date.

The golang-announce mailing list will announce (in a message prefixed with [security]) when a minor revision will be released with security fixes. For example, Go version 1.11.3 was released in December 2018 with a few security fixes, including a fix in go get -u for a remote execution bug.

See more about Go’s security policy here.

CSRF

Cross-Site Request Forgery, or CSRF, is “an attack that forces an end user to execute unwanted actions on a web application in which they’re currently authenticated.”11

Imagine a user is logged into a bank’s website and receives a link from someone with malicious intent. The HTML of the malicious page redirects the user to the bank’s website and the money transfer form is pre-filled with the malicious user’s account and routing information. The malicious HTML might look like this:

CSRF example
 1 <!DOCTYPE html>
 2 <html>
 3   <head>
 4     <meta charset="UTF-8" />
 5     <title>CSRF</title>
 6   </head>
 7   <body onload="document.forms[0].submit()">
 8     <form action="http://insecurebank.com/transfer" onsubmit="" method="POST">
 9     <input type="hidden" name="account" value="5555555555" />
10     <input type="hidden" name="routing" value="555555555" />
11     <input type="hidden" name="action" value="save" />
12     </form>
13   </body>
14 </html>

This attack might work if the bank is not protected against CSRF attacks.

To prevent CSRF attacks in our application, we must generate a unique per-user CSRF token and embed it in our HTML forms as a hidden field. The token is generated per-session, and must be sufficiently unique such that an attacker cannot guess it. If our web server doesn’t receive a CSRF token in a POST, or the token received doesn’t match the token from the user’s session, the request should be denied.

Luckily there are packages such as nosurf and gorilla/csrf which make it easy to protect our web application against CSRF attacks.

HTTP Strict Transport Security (HSTS)

HTTP Strict Transport Security helps protect websites against protocol downgrade attacks and cookie hijacking.12

Setting the Strict-Transport-Security on our web server will allow us to tell clients (web browsers) to only access our site over HTTPS, and never HTTP. A max-age expiration in seconds must be set with the header. In an example Go web server it could look like this, with a wrapper handler:

HSTS example
 1 package main
 2 
 3 import (
 4 	"flag"
 5 	"fmt"
 6 	"log"
 7 	"net/http"
 8 )
 9 
10 var (
11 	addr = flag.String("http", ":8000", "HTTP listen address")
12 )
13 
14 func headerWrap(h http.HandlerFunc) http.HandlerFunc {
15 	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
16 		w.Header().Add("Strict-Transport-Security", "max-age=31536000; includeSubDo\
17 mains")
18 		h.ServeHTTP(w, r)
19 	})
20 }
21 
22 func homeHandler(w http.ResponseWriter, r *http.Request) {
23 	fmt.Fprint(w, "Hello, world")
24 }
25 
26 func main() {
27 	flag.Parse()
28 	http.HandleFunc("/", headerWrap(homeHandler))
29 
30 	log.Printf("Running on %s ...", *addr)
31 	log.Fatal(http.ListenAndServe(*addr, nil))
32 }

If we start the server and then run curl -i localhost:8000 in another terminal, we can see that the header is set:

HSTS curl
$ curl -i localhost:8000
HTTP/1.1 200 OK
Strict-Transport-Security: max-age=31536000; includeSubDomains
Date: Thu, 20 Dec 2018 12:35:52 GMT
Content-Length: 12
Content-Type: text/plain; charset=utf-8

Hello, world%

We would wrap all of our handlers with headerWrap to ensure they all have the proper headers set.

Another option would be to set the header in the web server configuration, such as in /etc/nginx/sites-available/[config].

Content Security Policy (CSP)

A Content-Security-Policy header helps prevent attacks such as cross-site scripting (XSS). The CSP header value allows us to specify the origins of our content.

For example, to ensure that all content comes from the site’s own origin, the following header can be set in the response:

1 Content-Security-Policy: default-src 'self'

SQL Injection

SQL injection is a common website vulnerability in which an attacker enters a query into a text input box in order to either retrieve more data than is meant to be returned, or to run arbitrary commands on the database. Any backend code which creates SQL strings directly from HTML forms is vulnerable. For example, if we have code in our backend for a form on our website that looks like this:

1 username := r.FormValue("username")
2 query := "SELECT * from users WHERE username='" + username + "';"

then an attacker could put the following malicious query into the HTML form that passes username to the backend:

1 ' OR 1=1; '

and we would end up with a query that looks like this:

1 SELECT * from users WHERE username='' OR 1=1; '';

This is problematic because now the query is selecting everything from the users table rather than scoping the query to a single username. This is a simplified example; there are many ways a query could be vulnerable, and many SQL injection queries besides our ' OR 1=1; ' example. And attackers will often use tools which try lots of different known SQL injections on webpages to see if the results they get back are anomalous and showing them that a page is vulnerable.

The way we can protect against SQL injection is by using what’s called “prepared statements.” Prepared statements are precompiled SQL statements which then insert values into the correct positions in the query. Go’s database/sql package does this for us automatically:

1 query := "SELECT * from users WHERE username=?"
2 username := r.FormValue("username")
3 rows, err := db.Query(query, username)
4 if err != nil {
5     log.Fatal(err)
6 }
7 defer rows.Close()
8 ...

See Also