Skip to main content

Go-Sicherheitsleitfaden: 8 Best Practices für Go-Entwickler

Artikel von

Gerred Dillon

snyk web-based tools example

9. Februar 2021

0 Min. Lesezeit

In dieser Ausgabe unserer Leitfadenreihe stellen wir acht Best Practices für die Go-Sicherheit vor. Die Go-Sprache bietet zahlreiche integrierte Funktionen, die im Vergleich zu älteren und hardwarenäheren Sprachen wie C eine sicherere Entwicklung fördern – etwa die automatische Speicherbereinigung und stark typisierte Zeiger.

Diese Funktionen helfen Entwicklern, Fehler zu vermeiden, die zu Exploits führen können, indem sie ihnen die Verantwortung für die manuelle Speicherverwaltung abnehmen. Dennoch sollten Programmierer einige Best Practices für die Sicherheit kennen. Dieser von Eric Smalling und Gerred Dillon verfasste Leitfaden behandelt mit Unterstützung von Dan Enman (Senior Software Engineer bei Snyk) einige der häufigsten Themen.

Laden Sie hier den Spickzettel herunter!

  1. Go Modules verwenden

  2. Abhängigkeiten auf CVEs prüfen

  3. Die Standard-Kryptopakete von Go verwenden

  4. Mit html/template XSS-Angriffe vermeiden

  5. Subshells

  6. unsafe und cgo vermeiden

  7. Reflection sparsam einsetzen

  8. Die Angriffsfläche von Containern minimieren

1. Go Modules verwenden

Das System Go Modules ist seit Version 1.11 das offizielle Abhängigkeitsverwaltungssystem. Die älteren Systeme Vendor und Dep wurden inzwischen eingestellt. Mit Go Modules lassen sich Abhängigkeitsversionen festschreiben, einschließlich transitiver Module. Außerdem schützt die Prüfsummendatenbank go.sum vor unerwarteten Änderungen an Modulen.

Initialisieren Sie Ihr Projekt zunächst, indem Sie im obersten Verzeichnis den Befehl go mod init [namespace/project-name] ausführen.

$ go mod init mycorp.com/myapp

Dadurch wird im aktuellen Verzeichnis eine Datei namens go.mod erstellt. Sie enthält den Projektnamen und die Version von Go, die Sie verwenden. Wenn Ihr Quellcode Paketimporte enthält, aktualisiert der Befehl go build (oder test, install usw.) die Datei go.mod mit den verwendeten Modulen und ihren Versionen. Mit go get können Sie außerdem Ihre Abhängigkeiten aktualisieren und dabei bestimmte Versionen auswählen. Auch dadurch wird go.mod aktualisiert.

Beispiel für eine Datei go.mod:

module mycorp.com/myapp
go 1.15
require (
  github.com/containerd/console v1.0.1
  rsc.io/quote/v3 v3.1.0
)

Beachten Sie, dass auch eine Datei namens go.sum erstellt wurde. Diese Datei enthält eine Liste der Hashes aller verwendeten Module. Go nutzt sie, um zu überprüfen, ob bei jedem Build dieselben Binärdateien verwendet werden. Sowohl die Dateien go.mod als auch go.sum sollten Sie zusammen mit Ihrem Anwendungscode in die Versionsverwaltung aufnehmen.

Das Tutorial Using Go Modules im offiziellen Go Blog ist eine hervorragende Ressource, um mehr über Go Modules zu erfahren – etwa darüber, wie Sie Versionen transitiver Abhängigkeiten festschreiben, ungenutzte Abhängigkeiten bereinigen und vieles mehr.

2. Abhängigkeiten auf CVEs prüfen

Wie bei den meisten Projekten übersteigt der Code in den Modulen, von denen Ihre Anwendung abhängt, häufig den Umfang des Anwendungscodes selbst. Diese externen Abhängigkeiten sind ein häufiger Angriffsvektor für Sicherheitslücken. Mit Tools wie Snyk – unterstützt durch unsere umfangreiche Vulnerability Database – können Sie Abhängigkeitsgraphen auf bekannte Sicherheitslücken prüfen, Upgrade-Empfehlungen zur Behebung gefundener Probleme erhalten und Ihre Projekte kontinuierlich auf neu entdeckte Sicherheitslücken überwachen lassen.

Wenn Sie beispielsweise snyk test für eine Go-Anwendung ausführen, analysiert das Tool Ihre Module und meldet bekannte CVEs sowie Informationen zu verfügbaren behobenen Versionen, auf die Sie aktualisieren können. Außerdem können die webbasierten Tools von Snyk Ihre GitHub-Repositories direkt und kontinuierlich überwachen. Sie werden über neu entdeckte Sicherheitslücken benachrichtigt – selbst wenn Sie Ihren Code nicht geändert und keinen CI-Build ausgeführt haben.

Terminal, in dem Snyk eine Go-Anwendung testet und drei Schwachstellen in Abhängigkeiten mit Schweregraden und Behebungsvorschlägen meldet.
Snyk-Test an einem Beispiel für eine Go-Anwendung
Snyk-Web-Dashboard mit einem Problem hoher Schwere aufgrund einer fehlerhaften Signaturprüfung, Exploit-Details und Vulnerability-Filtern.
Beispiel für webbasierte Tools von Snyk

3. Standard-Kryptopakete von Go statt Paketen von Drittanbietern verwenden

Die Kryptopakete der Go-Standardbibliothek werden von Sicherheitsforschern gründlich geprüft. Da sie jedoch nicht alle Anwendungsfälle abdecken, könnten Sie versucht sein, Pakete von Drittanbietern zu verwenden.

So wie Sie keine eigenen kryptografischen Algorithmen entwickeln sollten, sollten Sie auch bei kryptografischen Bibliotheken von Drittanbietern sehr vorsichtig sein: Sie werden möglicherweise nicht mit derselben Sorgfalt geprüft. Achten Sie darauf, woher Ihr Code stammt.

4. Mit html/template XSS-Angriffe vermeiden

Ungefilterte Zeichenfolgen, die über io.WriteString() oder das Paket text/template an einen Webclient zurückgegeben werden, können Ihre Benutzer Cross-Site-Scripting-Angriffen (XSS) aussetzen. Denn HTML-Tags in den zurückgegebenen Zeichenfolgen werden ohne Kodierung im Ausgabestream gerendert. Außerdem kann ein falsch definierter Antwortheader Content-Type: plain/text gesendet werden, wenn er nicht ausdrücklich festgelegt ist.

Mit dem Paket html/template lassen sich zurückgegebene Inhalte ganz einfach automatisch für das Web kodieren, statt diese Kodierung in Ihrer Anwendungslogik manuell sicherstellen zu müssen. Die Dokumentation zu OWASP/GO-SCP enthält ein hervorragendes Kapitel mit Beispielen zu diesem Thema.

5. Subshells

In Go ermöglicht ein subshell im Wesentlichen den direkten Zugriff auf die Shell Ihres Systems. Seine Verwendung ist in der Regel auf Anwendungen beschränkt, die als Kommandozeilentools dienen. Bevorzugen Sie nach Möglichkeit Lösungen, die mit passenden Modulen direkt in Go implementiert sind.

Falls Sie doch ein Subshell benötigen, sollten Sie alle externen Daten, die Sie ihm übergeben, und auch die zurückgegebenen Daten bereinigen. So stellen Sie sicher, dass Ihre Anwendung keine unnötigen Details über das zugrunde liegende System preisgibt. Gehen Sie dabei ebenso sorgfältig vor wie beim Schutz vor Angriffen auf gerenderte Templates (siehe oben, Abschnitt 4) oder vor SQL-Befehlsinjektionen. Bedenken Sie auch, dass ein Aufruf eines externen Prozesses innerhalb eines Anwendungsthreads weitere Nebenwirkungen haben kann, die sich nicht über Ihren Go-Code kontrollieren lassen – etwa Änderungen am Dateisystem, Aufrufe externer Abhängigkeiten oder Änderungen an den Sicherheitsvorgaben, durch die solche Aufrufe blockiert werden können. Beispiele hierfür sind Einschränkungen durch den Betrieb in einem Container oder durch Tools wie AppArmor und SELinux.

6. unsafe und cgo mit Vorsicht verwenden

Ähnlich wie C unterstützt Go Zeigervariablen. Dabei gelten jedoch strenge Regeln zur Typsicherheit, die Entwickler vor unbeabsichtigten oder sogar böswilligen Nebenwirkungen schützen. In C können Sie jederzeit einen untypisierten Zeiger vom Typ void* definieren. Um etwas Vergleichbares in Go zu tun und die Einschränkungen der Typsicherheit zu umgehen, verwenden Sie das passend benannte Standardpaket unsafe. Von der Verwendung von unsafe wird in der Go-Dokumentation generell abgeraten, da es direkten Speicherzugriff ermöglicht. In Verbindung mit Benutzerdaten können Angreifer dadurch möglicherweise die Speichersicherheit von Go aushebeln.

Ähnlich bedenklich ist die Verwendung von cgo, einem leistungsstarken Befehl, mit dem sich beliebige C-Bibliotheken in Ihre Go-Anwendung integrieren lassen. Wie jedes mächtige Werkzeug muss auch cgo mit äußerster Vorsicht eingesetzt werden. Sie verlassen sich dabei auf eine vollständig externe Abhängigkeit, die in einer unsicheren Sprache geschrieben wurde und deren Implementierung korrekt sein muss. Bei Fehlern oder schädlichen Routinen im externen Code schützt Sie die Speichersicherheit von Go nicht. Sie können cgo deaktivieren, indem Sie beim Build einfach CGO_ENABLED=0 festlegen. Wenn Sie cgo nicht ausdrücklich benötigen, ist das in der Regel eine sichere Wahl, da die meisten modernen Go-Bibliotheken in reinem Go geschrieben sind.

7. Reflection

Go ist eine stark typisierte Sprache, in der die Typen von Variablen eine wichtige Rolle spielen. Manchmal müssen Sie zur Laufzeit in Ihrem Code Typ- oder Wertinformationen einer Variablen abfragen. Go bietet dafür das Paket `reflect`. Damit können Sie Typ und Wert einer Variablen beliebigen Typs ermitteln und bearbeiten, zum Beispiel feststellen, ob eine Variable einen bestimmten Typ hat oder bestimmte Eigenschaften beziehungsweise Funktionen enthält.

Reflection kann zwar nützlich sein, erhöht aber auch das Risiko von Laufzeitfehlern bei der Typisierung Ihres Go-Codes. Wenn Sie versuchen, eine reflektierte Variable auf unzulässige Weise zu ändern – etwa einen nicht setzbaren Wert in einer Struktur festzulegen –, löst Ihr Code einen Panic aus. Außerdem kann es schwierig sein, den Codefluss sowie die verschiedenen reflektierten Typ- und Wertarten zu überblicken. Bei der Arbeit mit reflektierten Typen oder Werten müssen Sie unter Umständen Typzusicherungen verwenden. Das kann den Code unübersichtlich machen und zu Laufzeitfehlern führen.

Reflection ist ein leistungsstarkes Werkzeug, sollte angesichts des Typ- und Interfacesystems von Go jedoch nur selten zum Einsatz kommen, da es leicht zu unerwarteten Problemen führen kann.

8. Die Angriffsfläche von Containern minimieren

Viele Go-Anwendungen kommen ohne externe Abhängigkeiten aus und sind für den Betrieb in Containern ausgelegt. Daher sollten wir das verfügbare Dateisystem mit einigen Methoden zum Erstellen von Images möglichst klein halten. Eine der einfachsten Möglichkeiten ist eine mehrstufige Dockerfile: Dabei wird die Anwendung in einer Build-Phase erstellt und anschließend für das Deployment ein scratch-Basisimage verwendet.

Sehen Sie sich das folgende Dockerfile-Beispiel an:

  1| FROM golang:1.15 as build
  2| 
  3| COPY . .
  4|
  5| ENV GOPATH=""
  6| ENV CGO_ENABLED=0
  7| ENV GOOS=linux
  8| ENV GOARCH=amd64
  9| RUN go build -trimpath -v -a -o myapp -ldflags="-w -s"
 10| RUN chmod +x go-goof
 11| 
 12| RUN useradd -u 12345 moby
 13| 
 14| FROM scratch
 15| COPY --from=build /go/myapp /myapp
 16| COPY --from=build /etc/passwd /etc/passwd
 17| USER moby
 18| 
 19| ENTRYPOINT ["/myapp"]

Wenn Sie noch nicht mit Dockerfiles vertraut sind: Sie enthalten schrittweise Anweisungen, die nahezu jeder OCI-Image-Build zum Erstellen von Images verwenden kann. Die Dokumentation finden Sie hier. Dieses Beispiel zeigt eine mehrstufige Dockerfile mit zwei klar getrennten Phasen: einer Build-Phase und der abschließenden Phase für das Runtime-Image.

Phase 1, Zeilen 1–12: die Build-Phase

Ausgehend vom offiziellen Basisimage golang:1.15 legen wir in dieser Phase einige Umgebungsvariablen fest und erstellen unsere Go-Anwendung. Nach Abschluss dieser Phase wird ein temporäres Image mit der Bezeichnung build zwischengespeichert, auf das wir später zugreifen können.

Sie fragen sich wahrscheinlich, warum wir all diese Umgebungsvariablen und Argumente an den Build übergeben:

  • GOPATH=””: Diese Variable wird gelöscht (sie war

    im Basisimage golang:1.15 gesetzt), da sie bei der Verwendung von Go Modules nicht benötigt wird.

  • CGO_ENABLED=0: Deaktiviert cgo (siehe Abschnitt 6 oben).

  • GOOS=linux: Gibt ausdrücklich an, dass Go für das Linux-Betriebssystem bauen soll.

  • GOARCH=amd66: Gibt ausdrücklich an, dass Go für die amd64-Architektur (Intel) bauen soll.

  • -trimpath: Entfernt Dateisystempfade aus der Binärdatei.

  • ldflag -s: Lässt die Symboltabelle und Debugging-Informationen weg.

  • ldflag -w: Lässt die DWARF-Symboltabelle weg.

Mit diesen Einstellungen erstellen Sie eine möglichst kleine Binärdatei. Einige Einstellungen eignen sich jedoch nicht unbedingt für Ihre Anwendung. Wählen Sie daher die benötigten Optionen aus.

Phase 2, Zeilen 13–10: die Runtime-Image-Phase

In dieser Phase kopieren wir einfach die statische Binärdatei und die Datei /etc/passwd aus der Phase build in ein leeres scratch-Dateisystem. Außerdem legen wir die passenden Eigentumsrechte und den Befehl fest, der beim Start des Containers ausgeführt werden soll.

Um dieses Image zu erstellen, führen Sie im selben Verzeichnis wie die Dockerfile einfach Folgendes aus:

docker build -t [app image name]:[version tag] .

Hinweis: Der . am Ende der Build-Zeile ist wichtig. Er gibt dem Build-System an, wo es die Dockerfile und alle weiteren referenzierten Dateien findet.

Das resultierende Image enthält in seinem Dateisystem genau zwei Dateien: unsere Anwendung und die passwd-Datei mit unserem Benutzer moby. Der Name des Benutzers ist unwichtig – wir möchten lediglich nichts als Root im Container ausführen. Es gibt weder sh noch ps oder andere Dateien, die ein Angreifer ausnutzen könnte. Wenn Sie für den Betrieb Ihrer Anwendung weitere Dateien benötigen, müssen Sie diese natürlich hinzufügen oder zur Laufzeit einbinden.

Laden Sie hier den Go-Sicherheitsleitfaden herunter!

Starten Sie mit Capture the Flag

Erfahren Sie in unserem virtuellen On-Demand-Workshop für Einsteiger, wie Sie Capture-the-Flag-Herausforderungen lösen.