In this article
L’injection SQL en Golang : exemple
Si vous débutez dans le développement backend en Golang, vous devez connaître les conventions de codage sécurisé. Cela implique notamment de comprendre ce qu’est une injection SQL en Go et comment corriger les vulnérabilités d’injection SQL dans le code.
Nous allons utiliser le framework d’applications web Gin pour Golang, une bibliothèque SQLite et des routes de serveur HTTP pour interagir avec les interfaces SQL de Golang. Cette expérience nous permettra d’apprendre à exécuter des exploits illustrant les injections SQL dans une base de données SQLite et d’autres bases de données.
Un programme SQL en Golang
Avant d’aborder un exemple d’injection SQL en Golang, il est essentiel de configurer correctement l’environnement de développement. Pour cela, vous devez installer les modules Go nécessaires et configurer le projet afin de gérer efficacement ses dépendances.
Nous allons utiliser les packages github.com/mattn/go-sqlite3 et github.com/jmoiron/sqlx pour l’interface SQL.
Gérer les dépendances avec les modules Go
Les modules Go sont devenus la méthode standard de gestion des dépendances dans les projets Go. Ils vous permettent de spécifier les versions des packages dont dépend votre projet, pour assurer la cohérence entre différents environnements. Pour utiliser les modules Go, vous devez initialiser votre projet avec un fichier go.mod.
À noter : Snyk prend parfaitement en charge Golang avec Snyk Code, l’outil SAST, et Snyk Open Source, l’outil SCA pour les dépendances tierces. Il est toujours préférable d’analyser votre code Go à la recherche de vulnérabilités, et Snyk s’intègre parfaitement à votre processus !
Pour commencer notre exemple SQL en Golang, accédez au répertoire de votre projet et exécutez la commande suivante :
go mod init your_project_nameCette commande crée un fichier go.mod dans le répertoire de votre projet, qui répertorie les dépendances et leurs versions.
Installer le package d’interface SQL pour Golang
Le package github.com/mattn/go-sqlite3 est une bibliothèque Go qui fournit une interface pour les bases de données SQLite. Il est indispensable pour exécuter des requêtes SQL sur des bases de données SQLite dans vos programmes SQL en Golang. Nous l’utiliserons dans notre exemple d’application Go. Pour installer ce package, exécutez la commande suivante :
go get github.com/mattn/go-sqlite3
Cette commande télécharge le package et l’ajoute à votre fichier go.mod. Vous pourrez ensuite l’utiliser dans votre projet pour interagir avec des bases de données SQLite.
Nous allons également installer le package github.com/jmoiron/sqlx, une extension du package standard Go database/sql. Il offre des fonctionnalités supplémentaires, comme la prise en charge des requêtes nommées et l’analyse des structures, qui peuvent simplifier les interactions avec les bases de données dans les applications SQL en Golang. Pour installer ce package, exécutez :
go get github.com/jmoiron/sqlxNous devrons ensuite inclure ces packages Go dans le programme comme suit :
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"
)Comme vous pouvez le voir dans les instructions d’importation des modules Go ci-dessus, l’application Golang sera une API HTTP qui expose des routes GET et POST pour notre démonstration d’un programme SQL en Golang.
Interfaces SQL en Golang
Dans notre application web Golang, le programme communique avec une base de données SQL via une route HTTP. La route HTTP /cloudpawnery/image traite les requêtes GET et reçoit le paramètre de requête tenantID. Elle recherche alors dans la base de données SQLite tous les enregistrements de fichiers ayant fait l’objet de conversions d’images.
L’interface SQL de Golang est illustrée par l’API de route HTTP GET suivante, qui renvoie une réponse JSON contenant des fichiers :
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})
})Concentrons-nous maintenant sur la partie du programme qui concerne l’interface SQL de Golang.
Nous allons utiliser le module Go sqlx pour ouvrir la base de données SQLite, puis la fonction Queryx pour envoyer une requête SELECT à la base de données, avec un filtre WHERE utilisant le tenantID du paramètre de requête de la requête HTTP GET.
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()Maintenant que nous avons obtenu une référence aux enregistrements de la base de données via la variable rows, nous pouvons parcourir ces enregistrements et affecter les résultats de la requête SQL au tableau vide de type File files.
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)
}Tant qu’il reste des résultats de requête SQL à renvoyer, nous entrons dans la boucle (d’où l’appel rows.Next() ). Nous préparons ensuite une entrée File avec var f File et utilisons la requête rows.StructScan() du module Go sqlx pour extraire les informations de la requête et les affecter à la variable f de type File.
Voici l’intégralité de notre API de route HTTP GET :
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})
})Mais qu’en est-il de la sécurité des applications ? Qu’est-ce qui pourrait mal tourner avec le code ci-dessus ? Une injection SQL. Voyons cela de plus près !
Injection SQL dans SQLite
L’injection SQL est une vulnérabilité de sécurité critique qui permet aux attaquants de manipuler les requêtes SQL qu’une application Golang envoie à sa base de données. Même en 2024, l’injection SQL reste une préoccupation majeure et figure souvent dans les listes de risques et d’exploits en cybersécurité. En manipulant les données saisies, les attaquants peuvent exécuter du code SQL arbitraire et potentiellement accéder sans autorisation à des données sensibles, modifier le contenu de la base de données ou même exécuter des opérations d’administration. Les conséquences d’une injection SQL peuvent être graves : fuites ou pertes de données, et atteinte importante à la réputation des organisations.
Illustrer une vulnérabilité d’injection SQL dans une application Golang
Pour illustrer une vulnérabilité d’injection SQL dans une application Golang, partons de l’application ci-dessus, qui utilise le package sqlx comme interface SQL pour Golang.
Nous savons que l’interface de l’API HTTP est une requête GET qui renvoie du JSON. Elle reçoit un identifiant de locataire via le paramètre de requête tenantID. La requête d’un utilisateur (ou d’un attaquant) pourrait ressembler à ceci :
GET http://localhost:6000/cloudpawnery/image?tenantID=3971533981712
Content-Type: application/jsonSi le tenantID est correct (et que nous sommes autorisés à accéder à ces données), nous obtiendrons la réponse suivante :
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"
}
]
}Tout semble correct.
Cependant, nous pourrions tenter de déduire la requête SQL exécutée par le programme Golang pour fournir ces données. Par exemple, il interroge probablement une table files et filtre probablement les résultats selon un champ d’identifiant de locataire, tel que tenantID, tenant_ID ou tenant_identifier.
En examinant le code de la requête que nous avons écrite pour le paramètre de route GET, nous verrons qu’il correspond effectivement à cette description :
rows, err := db.Queryx("SELECT * FROM files WHERE tenant_id = '" + tenantID + "'")Voilà le code vulnérable à l’injection SQL que nous avons écrit dans notre programme Golang.
Le problème vient du code qui utilise la concaténation de chaînes pour ajouter la variable tenantID issue des données utilisateur (le paramètre de requête) directement à la requête SQL. En suivant cette convention de codage SQL non sécurisée, nous introduisons une vulnérabilité d’injection SQL dans SQLite.
Nous comprenons maintenant à quel point il serait facile de manipuler le paramètre de requête tenantID pour modifier le sens de la requête SQL d’origine dans le programme Golang. Nous pouvons envoyer la requête HTTP suivante :
GET http://localhost:6000/cloudpawnery/image?tenantID=3971533981712' OR tenant_id='432423
Content-Type: application/jsonLa valeur de tenantID utilise désormais une apostrophe pour « fermer » l’affectation du champ de la clause WHERE et créer une nouvelle expression logique avec OR tenant_id=’<some-number>, qui filtre un autre enregistrement de la base de données sans avoir besoin d’ajouter une apostrophe fermante. Pourquoi ? Parce que la requête SQL elle-même s’en charge dans le code.
En envoyant la requête HTTP ci-dessus, nous obtiendrons des enregistrements pour les deux locataires :
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"
}
]
}Injection SQL en Golang dans l’authentification
Pour découvrir un exemple plus classique d’injection SQL en Golang, qui illustre ce que signifie une injection SQL pour les développeurs, examinez le code Golang suivant :
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)
}
}L’extrait de code Golang ci-dessus s’appuie sur la méthode classique de contournement de l’authentification par injection SQL, connue sous le nom de OR 1=1. Bien sûr, le code ci-dessus présente de nombreuses autres vulnérabilités de sécurité. Je vous recommande vivement de vous renseigner si vous ne les avez pas repérées en le lisant :
Utilisation de mots de passe en texte brut et de hachages non sécurisés.
Les conséquences des vulnérabilités d’injection SQL dans les applications peuvent être graves. Les attaquants peuvent :
Accéder à des données sensibles : accéder sans autorisation à des données utilisateur, comme nous l’avons vu avec l’injection de l’identifiant de locataire, ainsi qu’à d’autres enregistrements, par exemple des informations financières.
Modifier ou supprimer des données : nous avons uniquement montré comment sélectionner davantage de données, mais la modification ou la suppression de données critiques représente également un risque important d’injection SQL et peut compromettre l’intégrité des données.
Compromettre la sécurité des applications : utiliser l’injection SQL comme point de départ pour lancer d’autres types d’attaques, car les pirates enchaînent souvent plusieurs vulnérabilités afin d’amplifier l’impact sur la sécurité.
Comment corriger une injection SQL
Nous avons établi que l’injection SQL est une vulnérabilité de sécurité fréquente, qui peut avoir de graves conséquences si elle n’est pas correctement prise en charge. Voyons maintenant comment corriger les injections SQL.
Utiliser des instructions préparées et des requêtes paramétrées
L’un des moyens les plus efficaces de prévenir les injections SQL consiste à utiliser des instructions préparées et des requêtes paramétrées lors de la construction des requêtes SQL. Couramment employées dans de nombreuses bibliothèques, ainsi que dans différents langages de programmation et plateformes, ces techniques garantissent que les données utilisateur sont traitées comme des données et non comme du code exécutable, empêchant ainsi l’exécution de code SQL malveillant.
En Golang, nous pouvons continuer à utiliser le package sqlx pour faciliter l’utilisation des instructions préparées, que nous n’avions pas choisies dans l’application vulnérable ci-dessus.
Voici un exemple d’implémentation de requêtes SQL sécurisées avec 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)
}Dans ce code Golang, le caractère générique ? permet d’insérer en toute sécurité la variable username dans la requête SQL. Il indique au moteur de base de données qu’il s’agit d’une donnée à traiter, et non d’une partie de la requête SQL. L’entrée est alors correctement échappée, ce qui prévient les attaques par injection SQL en Golang.
Détecter et corriger les vulnérabilités d’injection SQL avec Snyk Code
Il est essentiel de suivre les bonnes pratiques, comme l’utilisation d’instructions SQL préparées. Les outils automatisés peuvent renforcer davantage la sécurité de votre code en détectant les vulnérabilités qui pourraient vous échapper. Snyk Code est un outil puissant qui vous aide à repérer et à corriger les vulnérabilités d’injection SQL dans vos applications Golang.
L’intégration de Snyk à votre processus de développement offre plusieurs avantages :
Analyse automatisée : Snyk Code analyse automatiquement votre code source à la recherche de vulnérabilités, notamment d’injections SQL, et vous fournit des recommandations concrètes.
Surveillance continue : Snyk surveille en permanence votre base de code pour détecter les nouvelles vulnérabilités et garantir la sécurité de vos applications dans la durée.
Adapté aux développeurs : Snyk s’intègre parfaitement aux IDE comme VS Code et aux autres processus de développement. Les développeurs peuvent ainsi intégrer facilement la sécurité à leur flux de travail sans nuire à leur productivité.
Pour profiter des puissantes fonctionnalités de sécurité de Snyk et protéger vos applications contre les vulnérabilités d’injection SQL, créez gratuitement un compte Snyk dès aujourd’hui. Avec Snyk, vous pouvez vous assurer que vos applications Golang sont sécurisées et conformes aux normes du secteur.
Pour approfondir vos connaissances sur les vulnérabilités d’injection SQL, je vous recommande les ressources suivantes :
La antisèche sur l’injection SQL de Brian Vermeer, qui présente 8 bonnes pratiques pour prévenir les attaques par injection SQL.
La antisèche sur la sécurité de Go d’Eric Smalling
Enfin, Comprendre les vulnérabilités d’injection de commandes en Go
Découvrez l’état de la sécurité des logiciels open source
Découvrez les tendances et les approches actuelles en matière de logiciels open source et de sécurité de la chaîne d’approvisionnement.