Skip to main content

Command-Injection-Schwachstellen in Go verstehen

Artikel von
feature insights context

14. November 2024

0 Min. Lesezeit

Go-Entwickler müssen in verschiedenen Situationen möglicherweise Systembefehle verwenden, etwa zur Bildbearbeitung, wenn sie Bilder verarbeiten oder in der Größe ändern müssen, oder um Ressourcen zu verwalten sowie Metriken oder Protokolle zu erfassen.

Vielleicht entwickeln Sie auch ein neues System in Go, das mit bestehenden Legacy-Systemen kommunizieren muss. Diese Schnittstelle basiert auf der Ausführung von Systembefehlen und der Verarbeitung ihrer Ausgaben.

In beiden Fällen ist es unerlässlich, beim Starten von Systembefehlen sichere Programmierkonventionen einzuhalten, da andernfalls eine als Command Injection bekannte Sicherheitslücke entstehen kann.

Was ist Command Injection?

Command Injection ist eine Sicherheitslücke, bei der eine Anwendung nicht vertrauenswürdige, vom Benutzer bereitgestellte Daten (etwa Eingaben aus einem Webformular) an eine System-Shell übergibt. Dadurch kann ein Angreifer beliebige Befehle auf dem Host-Betriebssystem mit den Rechten des Anwendungskontos ausführen.

In Go umfasst Command Injection häufig die Verwendung des Pakets os/exec, um Systembefehle zu starten.

Betrachten Sie das folgende Go-Codebeispiel:

func handler(req *http.Request) {
  cmdName := req.URL.Query()["cmd"][0]
  cmd := exec.Command(cmdName)
  cmd.Run()
}

In diesem Codeausschnitt wird cmdName direkt aus den Anfrageparametern extrahiert und zum Erstellen eines Befehls verwendet, der auf dem Server ausgeführt wird. Dies ist ein klassisches Beispiel für eine Command-Injection-Schwachstelle: Ein Angreifer kann den Wert des Abfrageparameters cmd manipulieren, um beliebige Befehle auszuführen und möglicherweise den Server zu kompromittieren.

Ein Angreifer könnte eine Anfrage mit einem schädlichen Befehl erstellen, zum Beispiel:

http://example.com/execute?cmd=rm%20-rf%20/

Warum ist Command Injection gefährlich? Sie ist gefährlich, weil Angreifer damit beliebige Befehle auf dem Server ausführen können. Das kann schwerwiegende Folgen haben, darunter:

  • Datenschutzverletzung: Angreifer können auf sensible Daten auf dem Server zugreifen. Stellen Sie sich vor, sie könnten Konfigurationsdateien wie config.toml und andere Dateien mit Ressourcen und Zugangsdaten einsehen.

  • Kompromittierung des Systems: Angreifer können die Kontrolle über den Server erlangen und ihn weiter ausnutzen. Häufig breiten sie sich dabei lateral im kompromittierten System aus und suchen nach weiteren Servern, die sie kompromittieren könnten.

  • Dienstunterbrechung: Angreifer können Befehle ausführen, die Dienste stören und Ausfallzeiten verursachen. Dies kann unmittelbar zu finanziellen Verlusten und Geschäftsrisiken führen, etwa durch einen Denial-of-Service-Angriff.

Die Folgen eines erfolgreichen Command-Injection-Angriffs können verheerend sein: Betroffen sind nicht nur das kompromittierte System, sondern auch der Ruf des Unternehmens und das Vertrauen der Kunden.

Unsichere Prozessausführung und Command Injection in Go

Go-Entwickler bevorzugen bekanntlich Einfachheit und Leistung und entscheiden sich möglicherweise dafür, Systembefehle einzubinden, um die Funktionen solcher Tools zu nutzen. So können sie sich auf robuste Anwendungen konzentrieren, ohne das Rad neu erfinden zu müssen. Diese Integration bringt jedoch eigene Herausforderungen mit sich, insbesondere in puncto Sicherheit.

Betrachten wir zur Veranschaulichung ein realistischeres Szenario: eine Go-Anwendung, die Bilddateien mit einem Befehlszeilentool wie convert verarbeitet.

Wir sehen uns eine Go-Anwendung an, die Anfragen zum Ändern der Bildgröße verarbeitet. Die Anwendung verwendet das Gin-Webframework, um einen POST-Endpunkt /cloudpawnery/image zu definieren, der anhand von Benutzereingaben Bilder skaliert. Dieser Endpunkt akzeptiert Abfrageparameter wie tenantID, fileID und fileSize. Der Parameter fileSize ist optional und hat standardmäßig den Wert "200", wenn er nicht angegeben wird.

Der folgende Codeausschnitt zeigt eine unsichere Implementierung in Go.

func main() {
    // Create a Gin router
    router := gin.Default()

    // Define a POST endpoint
    router.POST("/cloudpawnery/image", func(c *gin.Context) {
        tenantID := c.Query("tenantID")
        fileID := c.Query("fileID")
        fileSize := c.Query("fileSize")

        if fileSize == "" {
            fileSize = "200"
        }

        // Validate tenantID and fileID
        if tenantID == "" || fileID == "" {
            c.JSON(http.StatusBadRequest, gin.H{"error": "Missing tenantID or fileID"})
            return
        }

        // Call the download and resize function
        err := downloadAndResize(c, tenantID, fileID, fileSize)
        if err != nil {
            c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
            return
        }

        // Return a success response
        c.JSON(http.StatusOK, gin.H{"message": "File downloaded and resized successfully"})
    })

    // Start the HTTP server
    router.Run(":7000")
}

func downloadAndResize(ctx *gin.Context, tenantID, fileID, fileSize string) error {
    slog.Info("Processing request", "tenantID", tenantID, "fileID", fileID)

    convertCmd := fmt.Sprintf("convert %s -resize %sx%s %s", targetFilename, fileSize, fileSize, targetFilename)
    slog.Info("Running command", "command", convertCmd)

    _, err := exec.CommandContext(ctx, "sh", "-c", convertCmd).CombinedOutput()
    if err != nil {
        slog.Error("Error resizing image", "error", err)
        return fmt.Errorf("%w: %v", ErrImageResize, err)
    }

    slog.Info("Downloaded and resized image", "filename", targetFilename)
    return nil
}

Die Funktion downloadAndResize erstellt einen Befehlsstring, um das Bild mit convert zu skalieren, und führt ihn mit exec.CommandContext aus.

Wie erstellt die Funktion downloadAndResize einen Befehlsstring? Sie übernimmt dafür die Benutzereingabe fileSize. Dieser String wird anschließend ausgeführt, wodurch ein Angreifer möglicherweise schädliche Befehle einschleusen kann. Um dieses Risiko zu mindern, sollten Go-Entwickler alle Benutzereingaben validieren und bereinigen, parametrisierte Befehle verwenden und sichere Verfahren für die Befehlsausführung einsetzen.

Command-Injection-Schwachstellen entschärfen

In der Go-Entwicklung ist es – wie in jeder anderen Programmiersprache – entscheidend, Ihren Code vor Command-Injection-Schwachstellen zu schützen. Command Injection liegt vor, wenn ein Angreifer beliebige Befehle auf dem Host ausführen kann. Dies kann zu unbefugtem Zugriff, Datenschutzverletzungen und anderen schwerwiegenden Sicherheitsproblemen führen.

Sehen wir uns einige Best Practices an, um diese Risiken zu mindern.

Eingaben validieren und bereinigen

Ein grundlegender Schritt zur Vermeidung von Command Injection ist die sorgfältige Validierung und Bereinigung von Eingaben. Im gezeigten Beispiel erstellt die Funktion downloadAndResize einen Befehlsstring aus Benutzereingaben wie fileSize. Werden diese Eingaben nicht ordnungsgemäß validiert, könnte ein Angreifer schädliche Befehle einschleusen.

So können Sie die Eingabevalidierung verbessern:

  1. Eingaben per Allowlist zulassen: Legen Sie fest, welche Werte für Eingaben wie fileSize zulässig sind. Erlauben Sie beispielsweise nur Zahlen innerhalb eines angemessenen Bereichs.

  2. Eingaben bereinigen: Entfernen oder maskieren Sie potenziell gefährliche Zeichen in Benutzereingaben. Dazu gehören Shell-Metazeichen wie ;, |, & usw. Am besten codieren Sie den Befehl fest und lassen nicht zu, dass Benutzereingaben ihn steuern.

  3. Strenge Typen verwenden: Wandeln Sie Eingaben so früh wie möglich in den erwarteten Datentyp um. Konvertieren Sie beispielsweise fileSize in eine Ganzzahl und überprüfen Sie, ob der Wert im zulässigen Bereich liegt.

Hier sehen Sie ein Beispiel für die Umsetzung dieser Maßnahmen:

func sanitizeInput(input string) (int, error) {
    size, err := strconv.Atoi(input)
    if err != nil || size <= 0 || size > 1000 {
        return 0, fmt.Errorf("invalid input")
    }
    return size, nil
}

func downloadAndResize(ctx *gin.Context, tenantID, fileID, fileSize string) error {
    size, err := sanitizeInput(fileSize)
    if err != nil {
        return fmt.Errorf("invalid file size: %w", err)
    }

    convertCmd := fmt.Sprintf("convert %s -resize %dx%d %s", targetFilename, size, size, targetFilename)
    // ...
}

 Trotzdem gibt es noch deutlich bessere Möglichkeiten, Go-Code vor Command Injection zu schützen. Lesen Sie weiter!

Sichere APIs oder Bibliotheken statt Systembefehlen verwenden

Eine weitere wirksame Strategie besteht darin, Systembefehle nach Möglichkeit nicht direkt auszuführen. Verwenden Sie stattdessen sichere APIs oder Bibliotheken, die die erforderliche Funktionalität bereitstellen, ohne Ihre Anwendung den Risiken von Command Injection auszusetzen.

Wenn Ihre Anwendung beispielsweise Bilder bearbeiten muss, sollten Sie eine Go-Bibliothek wie github.com/disintegration/imaging verwenden, statt einen externen Befehl wie convert aus der ImageMagick-Softwarebibliothek aufzurufen. So bleibt die Funktionalität in der typsicheren Go-Umgebung gekapselt und die Angriffsfläche wird verkleinert.

import (
    "github.com/disintegration/imaging"
    // ...
)

func resizeImage(inputPath, outputPath string, width, height int) error {
    img, err := imaging.Open(inputPath)
    if err != nil {
        return fmt.Errorf("failed to open image: %w", err)
    }

    resizedImg := imaging.Resize(img, width, height, imaging.Lanczos)
    err = imaging.Save(resizedImg, outputPath)
    if err != nil {
        return fmt.Errorf("failed to save image: %w", err)
    }

    return nil
}

Mit Bibliotheken wie imaging in Go müssen Sie keine Shell-Befehle erstellen und ausführen. Dadurch mindern Sie das Risiko von Command Injection. Bei manchen Bibliotheken und Sprachökosystemen kann ein Drittanbieterpaket jedoch lediglich ein einfacher Wrapper für die Befehlsausführung sein. Prüfen Sie deshalb unbedingt den Code auf derartige sicherheitskritische Operationen.

Die anfällige Funktion downloadAndResize so überarbeiten, dass sie vor Injection schützt

Im vorherigen Beispiel haben wir eine potenziell anfällige Go-Anwendung gezeigt, die die Funktion exec.CommandContext zur Ausführung von Shell-Befehlen verwendet. Wie bereits erwähnt, kann dieser Ansatz zu Command-Injection-Schwachstellen führen.

Versuchen wir, die Funktion downloadAndResize so zu überarbeiten, dass Benutzereingaben nicht zur Ausführung beliebiger Befehle führen.

Eine wirksame Methode, Command Injection zu verhindern, besteht darin, Shell-Befehlsstrings nicht direkt aus Benutzereingaben zu erstellen. Verwenden Sie stattdessen die Funktion exec.Command mit separaten Argumenten. So können Sie Benutzereingaben sicher als Parameter übergeben, ohne die Shell aufzurufen oder Benutzern die Kontrolle über den eigentlichen Befehl zu geben.

Hier ist eine überarbeitete Version der Funktion downloadAndResize, die die Command-Injection-Schwachstelle behebt:

func downloadAndResize(ctx *gin.Context, tenantID, fileID, fileSize string) error {
    slog.Info("Processing request", "tenantID", tenantID, "fileID", fileID)

    // Define the command and its arguments separately
    args := []string{targetFilename, "-resize", fmt.Sprintf("%sx%s", fileSize, fileSize), targetFilename}
    cmd := exec.CommandContext(ctx, "convert", args...)

    slog.Info("Running command", "command", cmd.String())

    // Execute the command
    _, err := cmd.CombinedOutput()
    if err != nil {
        slog.Error("Error resizing image", "error", err)
        return fmt.Errorf("%w: %v", ErrImageResize, err)
    }

    slog.Info("Downloaded and resized image", "filename", targetFilename)
    return nil
}

Bei dieser Überarbeitung haben wir den Befehl von seinen Argumenten getrennt. Mit exec.CommandContext und separaten Argumenten müssen wir keinen Shell-Befehlsstring erstellen. Dadurch werden Benutzereingaben als Daten statt als ausführbarer Code behandelt, was das Risiko von Command Injection deutlich senkt.

Außerdem benötigen wir keinen Aufruf der Shell mehr. Der überarbeitete Code ruft die Shell (sh -c) nicht auf, die häufig als Angriffsvektor für Command Injection dient. Stattdessen wird das Dienstprogramm convert direkt mit den angegebenen Argumenten aufgerufen.

Mit Snyk die Codesicherheit verbessern

Snyk Code ist ein leistungsstarkes Tool für die statische Analyse, das Entwicklern dabei hilft, Schwachstellen in ihrer Codebasis zu erkennen und zu beheben. Es lässt sich nahtlos in Ihre IDE und Ihren Entwicklungsworkflow integrieren und liefert in Echtzeit Hinweise auf potenzielle Sicherheitsprobleme.

So hilft Snyk dabei, Command-Injection-Schwachstellen in Go zu erkennen

Im gezeigten Go-Beispiel erstellt die Funktion downloadAndResize mit einer Benutzereingabe einen Shell-Befehl:

convertCmd := fmt.Sprintf("convert %s -resize %sx%s %s", targetFilename, fileSize, fileSize, targetFilename)

_, err := exec.CommandContext(ctx, "sh", "-c", convertCmd).CombinedOutput()

Dieser Code ist anfällig für Command Injection, da er Benutzereingaben direkt in den Befehlsstring einfügt.

Was wäre, wenn in Ihrem Team Entwickler wären, die Command-Injection-Schwachstellen nicht kennen?

Könnten Sie den dateiübergreifenden Aufrufpfad der statischen Analyse in einem Code-Review problemlos nachvollziehen, um diese Command-Injection-Schwachstelle zu finden?

Hier kommt Snyk ins Spiel.

Sehen Sie, wie einfach Anwendungssicherheit wird: Die Snyk Code extension hebt anfälligen Code direkt im VS-Code-Editor in Echtzeit hervor:

Snyk Code zeigt Go-Code mit einer Command-Injection-Sicherheitslücke und ein korrigiertes Beispiel für exec.Command.

Snyk kann solche Schwachstellen erkennen, indem es Ihren Go-Code scannt und Stellen kennzeichnet, an denen Benutzereingaben unsicher in Shell-Befehlen verwendet werden. Snyk zeigt Ihnen echte Commits aus anderen Open-Source-Projekten, mit denen genau diese Schwachstelle behoben wurde. So erhalten Sie mehrere Codebeispiele, die zeigen, wie eine gute Lösung aussieht.

Wenn Sie zusätzlich auf die Registerkarte ISSUE OVERVIEW klicken, erhalten Sie weitere Details und Best Practices zur Vermeidung von Command Injection. Dort finden Sie ausführliche Einblicke und Empfehlungen zur Minderung dieser Risiken – direkt in der IDE-Ansicht, während Sie programmieren. So können Sie die vorgeschlagenen Maßnahmen umsetzen, ohne kostspielig den Kontext wechseln zu müssen:

Understanding Command Injection vulnerabilities in Go

Wenn Sie mehr darüber erfahren möchten, wie Sie Ihren Code vor solchen Schwachstellen schützen können, installieren Sie die IDE-Erweiterung Snyk Code und verbinden Sie Ihre Git-Projekte, um Sicherheitsprobleme in Ihren Go-Anwendungen zu erkennen und zu beheben. Das geht ganz einfach und kostenlos: Melden Sie sich an, um Ihren Code auf Schwachstellen zu scannen.

Kostenloses Tool zur Codeprüfung

Sichern Sie Ihren Code, bevor Sie Ihren nächsten Commit erstellen.

Weiterlesen

blog feature snyk container custom base recommendations
Blog

Evo Agentic AppSec: Sicherheit, die mit der KI-Entwicklung Schritt hält

KI-gestützte Entwicklung überholt herkömmliche Security-Workflows. Snyk Agentic AppSec setzt Security-Agenten ein, die Schwachstellen kontinuierlich finden, beheben und überprüfen – während Menschen die Maßstäbe festlegen.

blog feature ai violet
Blog

VulnBump: Können KI-Agenten anfällige Abhängigkeiten beheben, ohne den Build zu beschädigen?

VulnBump testet KI-Agenten anhand von 307 realen Dependency-Upgrades. Eine Lösung zählt nur, wenn die Sicherheitslücke behoben ist und Build sowie Tests weiterhin erfolgreich sind.

Blog

Frontier-Modelle fanden die Schwachstellen. Nur der Angreifer fand die Angriffsketten.

Die statische Analyse fand die Schwachstellen, doch erst Live-Angriffstests bewiesen, wie sie sich zu Angriffen verketten lassen. Ein Vergleich von Evo COS, Claude Security und Claude Code Security.