In this article
Inyección SQL en Golang: ejemplo
Si recién empiezas a desarrollar backends en Golang, debes conocer las convenciones de codificación segura. Un aspecto de la seguridad de las aplicaciones es entender qué significa la inyección SQL en Go y cómo corregir las vulnerabilidades de código relacionadas.
Usaremos el framework de aplicaciones web Gin para Golang y una biblioteca de SQLite, y expondremos rutas de servidor HTTP para interactuar con interfaces SQL de Golang. En este experimento, aprenderemos a ejecutar exploits que demuestran la inyección SQL en una base de datos SQLite y otras bases de datos.
Un programa SQL en Golang
Antes de profundizar en un ejemplo de inyección SQL en Golang, es fundamental configurar correctamente el entorno de desarrollo. Esto implica instalar los módulos de Go necesarios y configurar el proyecto para administrar las dependencias de forma eficiente.
Usaremos los paquetes github.com/mattn/go-sqlite3 y github.com/jmoiron/sqlx para la interfaz SQL.
Administrar dependencias con módulos de Go
Los módulos de Go se han convertido en la forma estándar de administrar las dependencias en proyectos de Go. Te permiten especificar las versiones de los paquetes de los que depende tu proyecto, lo que garantiza la coherencia en distintos entornos. Para usar módulos de Go, debes inicializar el proyecto con un archivo go.mod.
Ten en cuenta que Snyk ofrece una excelente compatibilidad con Golang mediante Snyk Code, la herramienta SAST, y Snyk Open Source, la herramienta SCA para dependencias de terceros. Siempre es recomendable analizar tu código de Go para detectar vulnerabilidades, ¡y Snyk se integra a la perfección!
Para empezar con nuestro ejemplo de SQL en Golang, ve al directorio del proyecto y ejecuta el siguiente comando:
go mod init your_project_nameEste comando crea un archivo go.mod en el directorio del proyecto para registrar las dependencias y sus versiones.
Instalar el paquete de interfaz SQL de Golang
El paquete github.com/mattn/go-sqlite3 es una biblioteca de Go que proporciona una interfaz para bases de datos SQLite. Es fundamental para ejecutar consultas SQL en bases de datos SQLite desde tus programas SQL de Golang y lo usaremos en nuestra aplicación de ejemplo en Go. Para instalar este paquete, usa el siguiente comando:
go get github.com/mattn/go-sqlite3
Este comando descarga el paquete y lo agrega al archivo go.mod. El paquete estará disponible en el proyecto para que puedas interactuar con bases de datos SQLite.
Del mismo modo, también instalaremos el paquete github.com/jmoiron/sqlx, una extensión del paquete estándar database/sql de Go. Ofrece funciones adicionales, como compatibilidad con consultas con nombre y análisis de estructuras, que pueden simplificar las interacciones con bases de datos en aplicaciones SQL de Golang. Para instalarlo, ejecuta:
go get github.com/jmoiron/sqlxLuego, necesitaremos estos paquetes de Go en el programa, de la siguiente manera:
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"
)Como puedes ver en las instrucciones de importación de módulos de Go anteriores, la aplicación de Golang será una API HTTP que expondrá rutas GET y POST para desarrollar la demostración de un programa SQL de Golang.
Interfaces SQL de Golang
En nuestra aplicación web de Golang, el programa interactúa con una base de datos SQL mediante una ruta HTTP. La ruta HTTP /cloudpawnery/image atiende solicitudes GET y recibe el parámetro de consulta tenantID, con el que busca en la base de datos SQLite todos los registros de archivos para los que se realizaron conversiones de imágenes.
La interfaz SQL de Golang se demuestra de forma sutil en la siguiente API de ruta HTTP GET, que devuelve una respuesta JSON con archivos:
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})
})Ahora enfoquémonos en la parte de la interfaz SQL de Golang del programa.
Usaremos el módulo de Go sqlx para abrir la base de datos SQLite y luego la función Queryx para enviar una consulta SELECT a la base de datos, con un filtro WHERE que usa el tenantID del parámetro de consulta de la solicitud 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()Ahora que tenemos una referencia a los registros de la base de datos mediante la variable rows, podemos recorrerla y asignar los resultados de la consulta SQL al arreglo vacío de tipo 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)
}Mientras haya resultados de la consulta SQL por devolver, entramos en el bucle (de ahí la llamada rows.Next() ). Luego preparamos una entrada File con var f File y usamos la consulta rows.StructScan() del módulo de Go sqlx para extraer la información de la consulta y asignarla a la variable f de tipo File.
Esta es nuestra API de ruta HTTP GET completa:
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})
})Pero… ¿qué pasa con la seguridad de las aplicaciones? ¿Qué podría salir mal con el código anterior? Pues bien, puede haber una inyección SQL. ¡Veamos más!
Inyección SQL en SQLite
La inyección SQL es una vulnerabilidad de seguridad crítica que permite a los atacantes interferir con las consultas SQL que una aplicación de Golang ejecuta en su base de datos. Incluso en 2024, la inyección SQL sigue siendo una preocupación importante y suele aparecer en las listas de riesgos y exploits de ciberseguridad. Al manipular los datos de entrada, los atacantes pueden ejecutar código SQL arbitrario y, potencialmente, obtener acceso no autorizado a datos confidenciales, modificar el contenido de la base de datos o incluso ejecutar operaciones administrativas. El impacto de la inyección SQL puede ser grave y provocar filtraciones y pérdida de datos, además de daños considerables a la reputación de las organizaciones.
Demostración de una vulnerabilidad de inyección SQL en una aplicación de Golang
Para ilustrar una vulnerabilidad de inyección SQL en una aplicación de Golang, ampliemos la aplicación anterior, en la que usamos el paquete sqlx como interfaz SQL de Golang.
Sabemos que la interfaz de la API HTTP recibe una solicitud GET y devuelve JSON. Recibe un identificador de tenant mediante el parámetro de consulta tenantID. La solicitud de un usuario (o un atacante) podría verse así:
GET http://localhost:6000/cloudpawnery/image?tenantID=3971533981712
Content-Type: application/jsonSi el tenantID es correcto (y tenemos autorización para acceder a estos datos), obtendríamos una respuesta como la siguiente:
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"
}
]
}Se ve muy bien.
Sin embargo, podríamos intentar aplicar ingeniería inversa a la consulta SQL que ejecuta el programa de Golang para proporcionar estos datos. Por ejemplo, probablemente consulta una tabla files y filtra los resultados de los archivos según un campo de identificador de tenant, como tenantID, tenant_ID o tenant_identifier.
De hecho, si revisamos el código de la consulta que escribimos para el parámetro de la ruta GET, veremos que es muy similar:
rows, err := db.Queryx("SELECT * FROM files WHERE tenant_id = '" + tenantID + "'")Ese código que acabamos de escribir en nuestro programa de Golang es vulnerable a la inyección SQL.
La raíz del problema está en el código que concatena cadenas para agregar la variable tenantID, que proviene de la entrada del usuario (el parámetro de consulta), directamente a la consulta SQL. Al seguir esta convención de codificación SQL insegura, introducimos una vulnerabilidad de inyección SQL en SQLite.
Con esto en mente, vemos lo fácil que sería manipular el parámetro de consulta tenantID para cambiar el significado de la consulta SQL original del programa de Golang. Podemos enviar la siguiente solicitud HTTP:
GET http://localhost:6000/cloudpawnery/image?tenantID=3971533981712' OR tenant_id='432423
Content-Type: application/jsonEl valor de tenantID ahora usa una comilla simple para “cerrar” la asignación del campo de la cláusula WHERE y crear una nueva expresión lógica con OR tenant_id=’<some-number>, que filtra otro registro de la base de datos sin incluir una comilla simple de cierre. ¿Por qué no incluye la comilla simple de cierre? Porque la propia consulta la agrega como parte del código de la consulta SQL.
Con esto en mente, al enviar la solicitud HTTP anterior se devolverán registros de ambos tenants:
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"
}
]
}Inyección SQL en Golang para la autenticación
Para ver un ejemplo más clásico de un escenario de inyección SQL en Golang que muestre a los desarrolladores qué significa la inyección SQL, revisa el siguiente código de Golang, que:
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)
}
}El fragmento de código de Golang anterior se basa en la popular técnica para omitir la autenticación mediante inyección SQL conocida como OR 1=1. Por supuesto, el código de Golang anterior tiene muchas otras vulnerabilidades de seguridad. Te recomiendo encarecidamente que aprendas más sobre ellas si no las identificaste al leerlo:
Uso de contraseñas en texto plano y hash inseguro.
Las consecuencias de las vulnerabilidades de inyección SQL en las aplicaciones pueden ser graves. Los atacantes pueden:
Acceder a datos confidenciales: obtener acceso no autorizado a datos de usuarios, como vimos con la inyección del identificador de tenant, y a otros registros, como la información financiera.
Modificar o eliminar datos: aunque solo mostramos cómo seleccionar más datos, alterar o eliminar datos fundamentales también supone un riesgo importante de inyección SQL y puede causar problemas de integridad de datos.
Comprometer la seguridad de las aplicaciones: usar la inyección SQL como punto de partida para lanzar otros tipos de ataques, ya que los hackers suelen encadenar distintas vulnerabilidades para lograr un mayor impacto en la seguridad.
Cómo corregir la inyección SQL
Ahora que establecimos que la inyección SQL es una vulnerabilidad de seguridad frecuente que puede tener consecuencias graves si no se aborda correctamente, aprendamos más sobre cómo corregirla.
Uso de sentencias preparadas y consultas parametrizadas
Una de las formas más eficaces de prevenir la inyección SQL es usar sentencias preparadas y consultas parametrizadas al construir las consultas SQL. Estas técnicas, habituales en muchas bibliotecas, lenguajes de programación y plataformas, garantizan que la entrada del usuario se trate como datos y no como código ejecutable, lo que impide que se ejecute código SQL malicioso.
En Golang, podemos seguir usando el paquete sqlx para facilitar el uso de sentencias preparadas (que no elegimos usar en la aplicación vulnerable anterior).
Este es un ejemplo de cómo implementar consultas SQL seguras con 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)
}En este código de Golang, el carácter de marcador de posición ? se usa para insertar de forma segura la variable username en la consulta SQL. Esto le indica al motor de base de datos que debe tratarla como un dato y no como parte de la consulta SQL. Así, la entrada se escapa correctamente y se previenen los ataques de inyección SQL en Golang.
Usar Snyk Code para detectar y corregir vulnerabilidades de inyección SQL
Seguir las prácticas recomendadas, como usar sentencias SQL preparadas, es fundamental. Además, las herramientas automatizadas pueden mejorar aún más la seguridad de tu código al detectar vulnerabilidades que podrías pasar por alto. Snyk Code es una herramienta potente que puede ayudarte a identificar y corregir vulnerabilidades de inyección SQL en tus aplicaciones de Golang.
Integrar Snyk en tu flujo de trabajo de desarrollo ofrece varias ventajas:
Análisis automatizado: Snyk Code analiza automáticamente el código fuente para detectar vulnerabilidades, incluida la inyección SQL, y te ofrece información práctica para solucionarlas.
Monitoreo continuo: Snyk monitorea continuamente tu base de código para detectar nuevas vulnerabilidades y garantizar que tus aplicaciones sigan siendo seguras con el tiempo.
Diseñado para desarrolladores: Snyk se integra sin problemas con IDE como VS Code y otros flujos de trabajo de desarrollo, lo que facilita que los desarrolladores incorporen la seguridad sin afectar su productividad.
Para aprovechar las potentes funciones de seguridad de Snyk y proteger tus aplicaciones contra vulnerabilidades de inyección SQL, crea hoy una cuenta gratuita de Snyk. Con Snyk, puedes garantizar que tus aplicaciones de Golang sean seguras y cumplan con los estándares del sector.
Te recomiendo los siguientes recursos para seguir aprendiendo y profundizar en las vulnerabilidades de inyección SQL:
La guía rápida sobre inyección SQL de Brian Vermeer, que te enseña 8 prácticas recomendadas para prevenir ataques de inyección SQL.
La guía rápida de seguridad de Go de Eric Smalling
Por último, Cómo entender las vulnerabilidades de inyección de comandos en Go
Explora el estado de la seguridad del código abierto
Conoce las tendencias y los enfoques actuales sobre el software de código abierto y la seguridad de la cadena de suministro.