In this article
SQL-Injection in Golang anhand eines Beispiels
Wenn Sie gerade erst mit der Backend-Entwicklung in Golang beginnen, sollten Sie sich mit sicheren Programmierkonventionen vertraut machen. Dazu gehört auch, zu verstehen, was SQL-Injection in Go bedeutet und wie Sie Sicherheitslücken durch SQL-Injection im Code beheben.
Wir verwenden das Webanwendungs-Framework Gin für Golang und eine SQLite-Bibliothek und stellen HTTP-Server-Routen bereit, über die wir mit Golang-SQL-Schnittstellen interagieren können. In diesem Experiment lernen wir, Exploits auszuführen, die SQL-Injection in einer SQLite-Datenbank und anderen Datenbanken demonstrieren.
Ein Golang-SQL-Programm
Bevor wir uns mit einem Golang-SQL-Injection-Beispiel befassen, müssen wir zunächst eine geeignete Entwicklungsumgebung einrichten. Dazu installieren wir die erforderlichen Go-Module und konfigurieren das Projekt so, dass Abhängigkeiten effizient verwaltet werden.
Für die SQL-Schnittstelle verwenden wir die Pakete github.com/mattn/go-sqlite3 und github.com/jmoiron/sqlx.
Abhängigkeiten mit Go-Modulen verwalten
Go-Module haben sich als Standard für die Verwaltung von Abhängigkeiten in Go-Projekten etabliert. Sie ermöglichen es Ihnen, die Versionen der Pakete anzugeben, von denen Ihr Projekt abhängt, und sorgen so für Konsistenz in verschiedenen Umgebungen. Um Go-Module zu verwenden, müssen Sie Ihr Projekt mit einer go.mod-Datei initialisieren.
Hinweis: Snyk unterstützt Golang hervorragend – mit Snyk Code, dem SAST-Tool, und Snyk Open Source, dem SCA-Tool für Abhängigkeiten von Drittanbietern. Am besten überprüfen Sie Ihren Go-Code immer auf Sicherheitslücken. Snyk lässt sich ganz einfach in Ihren Workflow integrieren!
Um mit unserem Golang-SQL-Beispiel zu beginnen, wechseln Sie in Ihr Projektverzeichnis und führen Sie den folgenden Befehl aus:
go mod init your_project_nameDieser Befehl erstellt im Projektverzeichnis eine go.mod-Datei, in der die Abhängigkeiten und ihre Versionen erfasst werden.
Das Golang-SQL-Schnittstellenpaket installieren
Das Paket github.com/mattn/go-sqlite3 ist eine Go-Bibliothek, die eine Schnittstelle zu SQLite-Datenbanken bereitstellt. Es ist unverzichtbar, um in Ihren Golang-SQL-Programmen SQL-Abfragen an SQLite-Datenbanken auszuführen. Wir verwenden es auch in unserer Go-Beispiel-App. Installieren Sie das Paket mit folgendem Befehl:
go get github.com/mattn/go-sqlite3
Dieser Befehl lädt das Paket herunter und fügt es Ihrer go.mod-Datei hinzu. Anschließend steht das Paket in Ihrem Projekt zur Verfügung, sodass Sie mit SQLite-Datenbanken interagieren können.
Ebenso installieren wir das Paket github.com/jmoiron/sqlx, eine Erweiterung des standardmäßigen Go-Pakets database/sql. Es bietet zusätzliche Funktionen wie Unterstützung für benannte Abfragen und das Einlesen von Daten in Strukturen, die die Interaktion mit Datenbanken in Golang-SQL-Anwendungen vereinfachen können. Installieren Sie das Paket mit folgendem Befehl:
go get github.com/jmoiron/sqlxAnschließend binden wir diese Go-Pakete wie folgt in das Programm ein:
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"os/exec"
"time"
"log/slog"
// Gin web framework
"github.com/gin-gonic/gin"
// SQLite database driver and query builder
_ "github.com/mattn/go-sqlite3"
"github.com/jmoiron/sqlx"
)Wie Sie an den obigen Anweisungen zum Importieren von Go-Modulen sehen, handelt es sich bei der Golang-Anwendung um eine HTTP-API, die GET- und POST-Routen bereitstellt. Darauf bauen wir unsere Demonstration eines Golang-SQL-Programms auf.
Golang-SQL-Schnittstellen
In unserer Golang-Webanwendung greift das Programm über eine HTTP-Route auf eine SQL-Datenbank zu. Die HTTP-Route unter /cloudpawnery/image verarbeitet GET-Anfragen und nimmt den Abfrageparameter tenantID entgegen. Damit sucht sie in der SQLite-Datenbank nach allen Dateieinträgen, für die Bildkonvertierungen durchgeführt wurden.
Die Golang-SQL-Schnittstelle wird anschaulich durch die folgende GET-HTTP-API-Route demonstriert, die eine JSON-Antwort mit Dateien zurückgibt:
router.GET("/cloudpawnery/image", func(c *gin.Context) {
// get tenantID from query parameter
tenantID := c.Query("tenantID")
// pseudo-code :
for loop-over-db-rows
if err != nil {
// handle errors
}
// populate an array of files:
files = append(files, f)
}
// return files as an object in the JSON response
c.JSON(http.StatusOK, gin.H{"files": files})
})Sehen wir uns nun den Teil des Programms an, der die Golang-SQL-Schnittstelle verwendet.
Mit dem Go-Modul sqlx öffnen wir die SQLite-Datenbank und verwenden anschließend die Funktion Queryx, um eine SELECT-Abfrage mit einem WHERE-Filter an die Datenbank zu senden. Der Filter nutzt die tenantID aus dem Abfrageparameter der HTTP-GET-Anfrage.
db, err := sqlx.Open("sqlite3", "./mydb.db")
defer db.Close()
if err != nil {
slog.Error("Failed to open database", "error", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to open database"})
return
}
rows, err := db.Queryx("SELECT * FROM files WHERE tenant_id = '" + tenantID + "'")
if err != nil {
slog.Error("Failed to query database", "error", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to query database"})
return
}
defer rows.Close()Nachdem wir über die Variable rows einen Verweis auf die Datenbankeinträge erhalten haben, können wir sie durchlaufen und die Ergebnisse der SQL-Abfrage dem leeren Array files vom Typ File zuweisen.
var files []File
for rows.Next() {
var f File
err := rows.StructScan(&f)
if err != nil {
slog.Error("Failed to scan database row", "error", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to scan database row"})
return
}
files = append(files, f)
}Solange SQL-Abfrageergebnisse zurückgegeben werden, durchlaufen wir die Schleife – daher der Aufruf rows.Next() . Anschließend bereiten wir mit var f File einen einzelnen File-Eintrag vor und verwenden die Abfrage rows.StructScan() aus dem Go-Modul sqlx, um die Informationen aus der Abfrage auszulesen und der Variablen f vom Typ File zuzuweisen.
Hier sehen Sie unsere vollständige HTTP-GET-API-Route:
router.GET("/cloudpawnery/image", func(c *gin.Context) {
// get tenantID from query parameter
tenantID := c.Query("tenantID")
db, err := sqlx.Open("sqlite3", "./mydb.db")
defer db.Close()
if err != nil {
slog.Error("Failed to open database", "error", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to open database"})
return
}
rows, err := db.Queryx("SELECT * FROM files WHERE tenant_id = '" + tenantID + "'")
if err != nil {
slog.Error("Failed to query database", "error", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to query database"})
return
}
defer rows.Close()
var files []File
for rows.Next() {
var f File
err := rows.StructScan(&f)
if err != nil {
slog.Error("Failed to scan database row", "error", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to scan database row"})
return
}
files = append(files, f)
}
c.JSON(http.StatusOK, gin.H{"files": files})
})Doch was ist mit der Anwendungssicherheit? Was kann beim obigen Code schiefgehen? SQL-Injection. Sehen wir uns das genauer an!
SQL-Injection in SQLite
SQL-Injection ist eine kritische Sicherheitslücke, über die Angreifer die SQL-Abfragen manipulieren können, die eine Golang-Anwendung an ihre Datenbank sendet. Auch 2024 zählt SQL-Injection weiterhin zu den größten Risiken und taucht regelmäßig auf Listen zu Cyberangriffen und Sicherheitsrisiken auf. Durch manipulierte Eingabedaten können Angreifer beliebigen SQL-Code ausführen. So können sie sich möglicherweise unbefugten Zugriff auf sensible Daten verschaffen, Datenbankinhalte ändern oder sogar administrative Vorgänge ausführen. Die Folgen einer SQL-Injection können schwerwiegend sein und zu Datenschutzverletzungen, Datenverlust und erheblichen Reputationsschäden für Unternehmen führen.
Eine SQL-Injection-Sicherheitslücke in einer Golang-Anwendung demonstrieren
Um eine SQL-Injection-Sicherheitslücke in einer Golang-Anwendung zu veranschaulichen, erweitern wir die obige Anwendung, in der wir das Paket sqlx als Golang-SQL-Schnittstelle verwenden.
Wir wissen, dass die HTTP-API-Schnittstelle eine GET-Anfrage ist, die JSON zurückgibt. Sie empfängt eine Mandantenkennung über den Abfrageparameter tenantID. Eine Anfrage von einem Benutzer oder Angreifer könnte beispielsweise so aussehen:
GET http://localhost:6000/cloudpawnery/image?tenantID=3971533981712
Content-Type: application/jsonWenn die tenantID korrekt ist und wir zum Zugriff auf diese Daten berechtigt sind, erhalten wir folgende Antwort:
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Sun, 01 Dec 2024 12:37:05 GMT
Content-Length: 248
Connection: close
{
"files": [
{
"ID": 1,
"Filename": "john-smith-profile.jpg",
"Signature": "",
"TenantID": "3971533981712",
"CreatedAt": "0001-01-01T00:00:00Z"
}
]
}Sieht gut aus.
Wir könnten jedoch versuchen, die SQL-Abfrage zurückzuentwickeln, die das Golang-Programm ausführt, um diese Daten bereitzustellen. Es fragt wahrscheinlich eine Tabelle files ab und filtert die Dateiergebnisse vermutlich anhand eines Felds für die Mandantenkennung, etwa tenantID, tenant_ID oder tenant_identifier.
Tatsächlich stellen wir bei einer Prüfung des Abfragecodes für den GET-Routenparameter fest, dass er dem sehr ähnlich ist:
rows, err := db.Queryx("SELECT * FROM files WHERE tenant_id = '" + tenantID + "'")Genau hier liegt der anfällige SQL-Injection-Code, den wir in unserem Golang-Programm geschrieben haben.
Die Ursache des Problems liegt in unserem Code, der die Variable tenantID aus der Benutzereingabe (dem Abfrageparameter) durch Zeichenfolgenverkettung an die SQL-Abfrage anhängt. Durch diese unsichere SQL-Programmierkonvention entsteht eine SQL-Injection-Sicherheitslücke in SQLite.
Mit diesem Wissen sehen wir, wie einfach sich der Abfrageparameter tenantID manipulieren lässt, um die Bedeutung der ursprünglichen SQL-Abfrage im Golang-Programm zu ändern. Wir können folgende HTTP-Anfrage senden:
GET http://localhost:6000/cloudpawnery/image?tenantID=3971533981712' OR tenant_id='432423
Content-Type: application/jsonDer Wert von tenantID enthält jetzt ein einfaches Anführungszeichen, um die WHERE-Klausel-Zuweisung zu „schließen“ und mit OR tenant_id=’<some-number> einen neuen logischen Ausdruck zu erstellen. Dieser filtert nach einem anderen Eintrag in der Datenbank und benötigt elegant kein schließendes einfaches Anführungszeichen. Warum nicht? Weil die Abfrage dieses im SQL-Abfragecode selbst hinzufügt.
Mit diesem Wissen führt das Senden der obigen HTTP-Anfrage dazu, dass Einträge für beide Mandanten zurückgegeben werden:
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Sun, 01 Dec 2024 12:37:05 GMT
Content-Length: 248
Connection: close
{
"files": [
{
"ID": 1,
"Filename": "john-smith-profile.jpg",
"Signature": "",
"TenantID": "3971533981712",
"CreatedAt": "0001-01-01T00:00:00Z"
},
{
"ID": 2,
"Filename": "john-smith-profile.jpg",
"Signature": "",
"TenantID": "432423",
"CreatedAt": "0001-01-01T00:00:00Z"
}
]
}Golang-SQL-Injection zur Umgehung der Authentifizierung
Als klassischeres Beispiel für ein Golang-SQL-Injection-Szenario, das Entwicklerinnen und Entwicklern die Bedeutung von SQL-Injection veranschaulicht, sehen Sie sich den folgenden Golang-Code an:
package main
import (
"database/sql"
"fmt"
"log"
_ "github.com/mattn/go-sqlite3"
)
func main() {
db, err := sql.Open("sqlite3", "./example.db")
if err != nil {
log.Fatal(err)
}
defer db.Close()
username := "admin' OR '1'='1"
query := fmt.Sprintf("SELECT * FROM users WHERE username = '%s'", username)
rows, err := db.Query(query)
if err != nil {
log.Fatal(err)
}
defer rows.Close()
for rows.Next() {
var id int
var name string
if err := rows.Scan(&id, &name); err != nil {
log.Fatal(err)
}
fmt.Printf("User: %d, %s\n", id, name)
}
}Das obige Golang-Code-Snippet baut auf der bekannten Umgehung der Authentifizierung durch SQL-Injection mit OR 1=1 auf. Der obige Golang-Code weist natürlich noch viele weitere Sicherheitslücken auf. Ich empfehle Ihnen dringend, sich näher damit zu befassen, falls Sie diese beim Lesen nicht erkannt haben:
Verwendung von Klartextpasswörtern und unsicheren Hashes.
Die Folgen von SQL-Injection-Sicherheitslücken in Anwendungen können gravierend sein. Angreifer können:
Auf sensible Daten zugreifen: Unbefugten Zugriff auf Benutzerdaten erlangen, wie wir es bei der Injektion der Mandantenkennung gesehen haben, und auf weitere Einträge wie Finanzinformationen.
Daten ändern oder löschen: Wir haben zwar nur gezeigt, wie sich mehr Daten auswählen lassen. Das Ändern oder Entfernen wichtiger Daten ist jedoch ebenfalls ein erhebliches SQL-Injection-Risiko und kann die Datenintegrität beeinträchtigen.
Die Anwendungssicherheit kompromittieren: SQL-Injection als Ausgangspunkt für weitere Angriffe nutzen, da Hacker oft verschiedene Sicherheitslücken miteinander verknüpfen, um größere Schäden anzurichten.
SQL-Injection beheben
Wir haben nun festgestellt, dass SQL-Injection eine weitverbreitete Sicherheitslücke ist, die schwerwiegende Folgen haben kann, wenn sie nicht angemessen behoben wird. Sehen wir uns nun an, wie Sie SQL-Injection verhindern können.
Prepared Statements und parametrisierte Abfragen verwenden
Eine der effektivsten Methoden, SQL-Injection zu verhindern, besteht darin, Prepared Statements und parametrisierte Abfragen beim Erstellen von SQL-Abfragen zu verwenden. Diese in vielen Bibliotheken, Programmiersprachen und Plattformen verbreiteten Techniken stellen sicher, dass Benutzereingaben als Daten und nicht als ausführbarer Code behandelt werden. So wird verhindert, dass schädlicher SQL-Code ausgeführt wird.
In Golang können wir weiterhin das Paket sqlx verwenden, um Prepared Statements zu nutzen, auf die wir in der anfälligen Anwendung oben noch verzichtet haben.
Hier sehen Sie ein Beispiel für die Implementierung sicherer SQL-Abfragen mit sqlx:
package main
import (
"fmt"
"log"
"github.com/jmoiron/sqlx"
_ "github.com/mattn/go-sqlite3"
)
type User struct {
ID int `db:"id"`
Name string `db:"name"`
}
func main() {
db, err := sqlx.Connect("sqlite3", "example.db")
if err != nil {
log.Fatalln(err)
}
var user User
username := "john_doe"
query := "SELECT id, name FROM users WHERE name = ?"
err = db.Get(&user, query, username)
if err != nil {
log.Fatalln(err)
}
fmt.Printf("User ID: %d, Name: %s\n", user.ID, user.Name)
}In diesem Golang-Code dient das Platzhalterzeichen ? dazu, die Variable username sicher in die SQL-Abfrage einzufügen. Damit teilen wir der Datenbank-Engine mit, dass es sich um einen zu verarbeitenden Wert und nicht um einen Teil der SQL-Abfrage handelt. Die Eingabe wird korrekt maskiert und SQL-Injection-Angriffe in Golang werden verhindert.
SQL-Injection-Sicherheitslücken mit Snyk Code erkennen und beheben
Best Practices wie SQL-Prepared-Statements zu befolgen, ist unverzichtbar. Automatisierte Tools können die Sicherheit Ihres Codes zusätzlich verbessern, indem sie Sicherheitslücken aufdecken, die Ihnen möglicherweise entgehen. Snyk Code ist ein leistungsstarkes Tool, mit dem Sie SQL-Injection-Sicherheitslücken in Ihren Golang-Anwendungen erkennen und beheben können.
Die Integration von Snyk in Ihren Entwicklungsworkflow bietet mehrere Vorteile:
Automatisierte Scans: Snyk Code scannt Ihren Quellcode automatisch auf Sicherheitslücken wie SQL-Injection und liefert Ihnen konkrete Handlungsempfehlungen.
Kontinuierliche Überwachung: Snyk überwacht Ihre Codebasis fortlaufend auf neue Sicherheitslücken und sorgt so dafür, dass Ihre Anwendungen langfristig sicher bleiben.
Entwicklerfreundlich: Snyk lässt sich nahtlos in IDEs wie VS Code und andere Entwicklungsworkflows integrieren. So können Entwicklerinnen und Entwickler Sicherheit ganz einfach in ihren Arbeitsablauf einbinden, ohne ihre Produktivität zu beeinträchtigen.
Um die leistungsstarken Sicherheitsfunktionen von Snyk zu nutzen und Ihre Anwendungen vor SQL-Injection-Sicherheitslücken zu schützen, registrieren Sie sich noch heute kostenlos bei Snyk. Mit Snyk stellen Sie sicher, dass Ihre Golang-Anwendungen sicher sind und Branchenstandards erfüllen.
Wenn Sie sich eingehender mit SQL-Injection-Sicherheitslücken befassen möchten, empfehle ich Ihnen folgende weiterführende Ressourcen:
Brian Vermeers SQL-Injection-Cheat-Sheet mit acht Best Practices zum Verhindern von SQL-Injection-Angriffen.
Eric Smallings Go-Security-Cheat-Sheet
Und schließlich SQL-Injection-Sicherheitslücken in Go verstehen
Entdecken Sie den Stand der Open-Source-Sicherheit
Erfahren Sie mehr über aktuelle Trends und Ansätze für Open-Source-Software und Supply-Chain-Sicherheit.