Web frameworks are not so frequent in the Go world compared to other languages. It gives you more freedom but web frameworks are also a great way to force people to implement basic security practices, especially beginners.
If you’re developing Go websites or APIs without frameworks, like me, here are some security points you should keep in mind.
CSRF
Cross-site Requests Forgery (CSRF) attacks target the password protected pages of your website which are using forms. Authenticated users (via a session cookie in their browser) might post information to a protected form without knowing it if they’re visiting a malicious website. In order to avoid this, every forms should have a hidden field containing a CSRF token that the server will use to check the authenticity of the request.
Let’s use Gorilla Toolkit in this regard. First integrate the CSRF middleware. You can either do it for the whole website:
package main
import (
"net/http"
"github.com/gorilla/csrf"
"github.com/gorilla/mux"
)
func main() {
r := mux.NewRouter()
http.ListenAndServe(":8000",
csrf.Protect([]byte("32-byte-long-auth-key"))(r))
}
Or for some pages only:
package main
import (
"net/http"
"github.com/gorilla/csrf"
"github.com/gorilla/mux"
)
func main() {
r := mux.NewRouter()
csrfMiddleware := csrf.Protect([]byte("32-byte-long-auth-key"))
protectedPageRouter := r.PathPrefix("/protected-page").Subrouter()
protectedPageRouter.Use(csrfMiddleware)
protectedPageRouter.HandleFunc("", protectedPage).Methods("POST")
http.ListenAndServe("8080", r)
}
Pass the CSRF token to your template then:
func protectedPage(w http.ResponseWriter, r *http.Request) {
var tmplData = ContactTmplData{CSRFField: csrf.TemplateField(r)}
tmpl.Execute(w, tmplData)
}
Last of all put the hidden field {{.CSRFField}}
into your template.
CORS
Cross-origin Resource Sharing (CORS) attacks consist in sending information to a malicious website from a clean website. In order to mitigate this, the clean website should prevent users from sending asynchronous requests (XHR) to another website. Good news: this behavior is implemented by default in every browsers! Bad news: it can lead to false positives so, if you want to consume an API located on another domain or another port from your web page, requests will be blocked by the browser. It’s often a tricky behavior for the API rookies…
You should whitelist some domains in order to avoid the above problem. You can do this using the github.com/rs/cors
:
package main
import (
"net/http"
"github.com/rs/cors"
)
func main() {
c := cors.New(cors.Options{
AllowedOrigins: []string{"http://my-whitelisted-domain"},
})
handler = c.Handler(handler)
http.ListenAndServe(":8080", handler)
}
HTTPS
Switching your website to HTTPS is an essential security point. Here I’m assuming that you’re using the Go built-in server. If not (maybe because you’re using Nginx or Apache), you can skip this section.
Get a A on SSLLabs.com
In order to get the best security grade on SSLLabs (meaning the certificate is perfectly configured and thus avoid security warnings on some web clients), you should disable SSL and only use TLS 1.0 and above. That’s why I’m using the crypto/tls library. In order to serve HTTPS requests we’re using http.ListenAndServeTLS
:
package main
import (
"crypto/tls"
"net/http"
)
func main() {
config := &tls.Config{MinVersion: tls.VersionTLS10}
server := &http.Server{Addr: ":443", Handler: r, TLSConfig: config}
server.ListenAndServeTLS(tlsCertPath, tlsKeyPath)
}
Redirect HTTP to HTTPS
It’s good practice to force HTTP requests to HTTPS. You should use a dedicated goroutine:
package main
import (
"crypto/tls"
"net/http"
)
// httpsRedirect redirects http requests to https
func httpsRedirect(w http.ResponseWriter, r *http.Request) {
http.Redirect(
w, r,
"https://"+r.Host+r.URL.String(),
http.StatusMovedPermanently,
)
}
func main() {
go http.ListenAndServe(":80", http.HandlerFunc(httpsRedirect))
config := &tls.Config{MinVersion: tls.VersionTLS10}
server := &http.Server{Addr: ":443", Handler: r, TLSConfig: config}
server.ListenAndServeTLS(tlsCertPath, tlsKeyPath)
}
Let’s Encrypt’s certificates renewal
Let’s Encrypt has become the most common way of provisioning TLS certs (because it’s free of course) but not necessarily the easiest. Once Let’s Encrypt has been installed and the first certs have been provisioned, the question is how to renew them automatically with Certbot. Certbot does not integrate to the Go HTTP server so it’s necessary to use the Certbot’s standard version (this one for Ubuntu 18.04 for example), and then briefly (a couple of seconds) turn off the production server during the certificates renewal (in order to avoid conflicts on the 80 and 443 ports). It can be done by modifying the renewal command launched by Certbot’s cron (in /etc/cron.d/certbot
). On Ubuntu Certbot also uses the systemd timer (as a first choice rather than cron) so it’s better to modify the /lib/systemd/system/certbot.service
config file:
[Unit]
Description=Certbot
Documentation=file:///usr/share/doc/python-certbot-doc/html/index.html
Documentation=https://letsencrypt.readthedocs.io/en/latest/
[Service]
Type=oneshot
# Proper command to stop server before renewal and restart server afterwards
ExecStart=/usr/bin/certbot -q renew --pre-hook "command to stop go server" --post-hook "command to start go server"
PrivateTmp=true
One interesting alternative could be to use a Go lib dedicated to the certs renewal inside your program called x/crypto/acme/autocert. Personally I’m not sure I like this solution because - even if it does not create downtime contrary to my solution - it means your code it strongly coupled to a specific type of certs renewal (ACME).
XSS
Cross-site scripting (XSS) attacks consist in executing some Javascript code in the user browser when he’s visiting your website, while it should not be happening. For example in case of a forum, if a user is posting a message containing some Javascript and you’re displaying this message to all users without any filter, the script will execute on all user browsers. In order to mitigate this, strings should be “escaped” before being displayed in order to be harmless.
Good news: when you’re using templates via the html/template
lib, Go ensures strings are automatically escaped. Be careful though: text/template
does not escape strings!
SQL Injections
SQL Injection attacks consist in posting malicious data containing SQL in an HTML form. When inserting data into database or retrieving data, this malicious code can cause damage.
Once again: good news is this attack is easily mitigated if you’re correctly using the standard SQL libs. For example if you’re using database/sql
SQL injections are automatically escaped when you’re using the $
or ?
keywords. Here’s an SQL request properly written for PostgreSQL:
db.Exec("INSERT INTO users(name, email) VALUES($1, $2)",
"Julien Salinas", "julien@salinas.com")
Personally I’m using the PostgreSQL optimized ORM github.com/go-pg/pg
. A properly formed request in that case would be:
user := &User{
name: "Julien Salinas",
email: "julien@salinas.com",
}
err = db.Insert(user)
Directory Listing
Directory listing is the fact that you can display all the files from a given directory on the website. It might disclose documents to users that you don’t want to show them if they don’t have the exact url. Directory listing is enabled by default if you’re using the standard lib http.FileServer
. I’m explaining in this article how to disable this behavior.
Conclusion
Here’s a short insight about the major security points for your Go website.
It might be a good idea to use a tool giving you the ability to easily set various security related essential elements. In my opinion github.com/unrolled/secure is a great lib in this regard. It allows you to easily set up HTTPS redirects, handle CORS, but also filter authorized hosts and many other complex things.
I hope these basic tips helped some of you!