Sécurité d'un site web en Go (Golang)

Temps de lecture ~6 minutes

Dans le monde Go il n’est pas très courant d’utiliser des frameworks web comme on pourrait le voir dans d’autres langages. Cela donne certes de la liberté, mais un avantage majeur des frameworks web est qu’ils imposent certaines bonnes pratiques en terme de sécurité, notamment pour les néophytes.

Si, comme moi, vous développez des sites ou API en Go sans framework, voyons ici quels éléments de sécurité vous devez garder à l’esprit.

CSRF

Les attaques de type Cross-site Requests Forgery (CSRF) concernent les parties du site protégées par mot de passe faisant l’usage de formulaires. Les utilisateurs authentifiés (via un cookie de session dans leur navigateur) risquent, s’ils se rendent sur un site malicieux, de poster des informations sur un formulaire protégé à leur insu. Pour éviter ce risque, il faut que chaque formulaire intègre un champ caché contenant un token CSRF grâce auquel le serveur pourra vérifier l’authenticité de la requête.

Utilisons Gorilla Toolkit pour cela. Commencez par intégrer le middleware CSRF. Vous pouvez le faire soit pour tout le site:

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))
}

Soit pour certaines pages seulement:

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)
}

Puis passez le token CSRF à votre template:

func protectedPage(w http.ResponseWriter, r *http.Request) {
    var tmplData = ContactTmplData{CSRFField: csrf.TemplateField(r)}
    tmpl.Execute(w, tmplData)
}

Et enfin intégrez le champs caché {{.CSRFField}} à votre template.

CORS

Les attaques de type Cross-origin Resource Sharing (CORS) consistent à envoyer des informations vers un site malicieux en se rendant sur un site sain. Pour éviter cela le site sain doit empêcher ses utilisateurs d’émettre des requêtes asynchrones (XHR) vers un autre site que lui. Bonne nouvelle : c’est le comportement par défaut appliqué par tous les navigateurs modernes ! Mauvaise nouvelle : cela peut créer un faux positif qui fait que si vous souhaitez, depuis vote page web, consommer une API se trouvant sur un autre domaine que le domaine d’origine, ou sur un port différent, les requêtes seront bloquées par le navigateur. C’est souvent un casse-tête pour les débutants en développement d’API.

Afin de mettre certains domaines sur liste blanche et ainsi éviter le problème ci-dessus, vous pouvez utiliser la bibliothèque github.com/rs/cors comme suit :

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

Passer le site en HTTPS est un élément de sécurité incontournable. Je considère ici que vous utilisez le serveur HTTP built-in de Go. Si ce n’est pas le cas (parce que vous utilisez par exemple Nginx ou Apache) vous pouvez sauter cette section.

Obtenir un A sur SSLLabs.com

Afin d’obtenir la meilleure note de sécurité sur SSLLabs (signe que le certificat est parfaitement configuré et ainsi éviter potentiellement les erreurs de sécurité sur certains clients web), il faut proscrire l’utilisation de SSL et utiliser TLS 1.0 comme version minimum. Pour cela on utilise la bibliothèque crypto/tls, et pour servir les requêtes on utilise 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)
}

Rediriger HTTP vers HTTPS

C’est une bonne pratique de forcer les requêtes HTTP vers HTTPS. Il suffit de les rediriger comme suit via une goroutine dédiée :

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)
}

Renouvellement des certificats Let’s Encrypt

Let’s Encrypt est aujourd’hui la façon la plus répandue de provisionner les certificats TLS (car gratuite) mais pas nécessairement la plus facile. Une fois Let’s Encrypt installé et les premiers certificats provisionnés, se pose la question du renouvellement des certificats avec Certbot. Puisque Certbot ne s’intègre pas automatiquement au server HTTP de Go, il est nécessaire d’utiliser la version standard de certbot (par exemple celle-là pour Ubuntu 18.04), puis de couper brièvement (quelques secondes) le serveur de production pendant le renouvellement des certificats (afin d’éviter un conflit sur les ports 80 ou 443). Cela peut se faire en modifiant la commande de renouvellement lancée par le cron Certbot (dans /etc/cron.d/certbot). Sur Ubuntu Certbot utilise aussi le timer systemd (en priorité par rapport au cron) donc modifier le fichier de configuration /lib/systemd/system/certbot.service est préférable :

[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

Une alternative est d’utiliser une bibliothèque Go dédiée au renouvellement des certificats au sein même de votre programme appelée x/crypto/acme/autocert. Personnellement j’aime moins cette option car, même si elle ne crée aucune interruption de service contrairement à ma solution ci-dessus, elle oblige votre code à être fortement couplé à un type de renouvellement de certificats particulier (ACME).

XSS

Les attaques de type Cross-site scripting (XSS) consistent à exécuter du code Javascript dans le navigateur d’un utilisateur se rendant sur votre site, alors que ça ne devrait pas être le cas. Par exemple dans le cas d’un forum, si un utilisateur poste un message contenant du Javascript, et que vous affichez ce message à tous les utilisateurs sans filtre, ces derniers verront tous ce script s’exécuter dans leur propre navigateur. Pour éviter cela il faut “échapper” les chaînes de caractères avant de les afficher à l’utilisateur afin de les rendre inoffensives.

Bonne nouvelle : lorsque vous utilisez les templates via la bibliothèque html/template, Go sécurise les chaînes de caractères automatiquement. Attention à ne pas utiliser text/template qui elle n’échappe pas les caractères !

Injections SQL

Les attaques de type injection SQL consistent pour un utilisateur malicieux à entrer du code SQL dans un formulaire du site. Au moment de l’enregistrement ou de l’affichage des informations en base de donnée, ce code malicieux peut faire des dégâts.

Encore une fois, bonne nouvelle : cette attaque se contourne naturellement en utilisant correctement les bibliothèques SQL classiques. Par exemple en utilisant database/sql les injections SQL sont automatiquement échappées lorsque vous utilisez les mots clés $ ou ?. Voici le cas d’une requête SQL pour PostgreSQL bien écrite :

db.Exec("INSERT INTO users(name, email) VALUES($1, $2)",
  "Julien Salinas", "julien@salinas.com")

Personnellement j’utilise l’ORM optimisé pour PostgreSQL github.com/go-pg/pg. Une requête bien écrite dans ce cas serait :

user := &User{
    name:       "Julien Salinas",
    email:      "julien@salinas.com",
}
err = db.Insert(user)

Directory Listing

Le directory listing est le fait de pouvoir afficher tout le contenu d’un répertoire statique sur le site. Cela peut révéler des documents que vous ne souhaitez pas afficher à l’utilisateur s’ils n’ont pas l’url exacte. Le directory listing est activé par défaut si vous utilisez la bibliothèque standard http.FileServer. J’explique dans cet article comment le neutraliser.

Conclusion

Voici un petit aperçu des éléments essentiels de sécurité pour votre site en Go.

Il peut être une bonne idée d’utiliser un outil permettant de paramétrer facilement différents éléments essentiels relatifs à la sécurité. Je trouve que github.com/unrolled/secure est une excellente bibliothèque à cet égard. Elle permet de facilement paramétrer les redirections HTTPS, gérer CORS, mais aussi le filtrage d’hôtes autorisés et pas mal de choses encore plus pointues.

J’espère que ces bases seront utiles à certains !

Also available in English

Rate Limiting d'API avec Traefik, Docker, Go, et la mise en cache

Limiter l'utilisation de l'API en fonction d'une règle avancée de limitation du débit n'est pas si facile. Pour y parvenir derrière l'API NLP Cloud, nous utilisons une combinaison de Docker, Traefik (comme proxy inverse) et la mise en cache locale dans un script Go. Si cela est fait correctement, vous pouvez améliorer considérablement les performances de votre limitation de débit et étrangler correctement les demandes d'API sans sacrifier la vitesse des demandes. Continuer de lire