Mejores prácticas de seguridad con go-mssqldb

Este artículo trata sobre las prácticas de seguridad para aplicaciones Go que se conectan a SQL Server utilizando el go-mssqldb controlador. Aborda la prevención de inyección SQL, la gestión de credenciales, la configuración de cifrado y el principio del privilegio menor.

Impedir la inyección de CÓDIGO SQL

La inyección SQL es la vulnerabilidad más común en la seguridad de bases de datos. El go-mssqldb controlador proporciona consultas parametrizadas que separan el código SQL de los valores de datos. Utiliza siempre parámetros para la entrada suministrada por el usuario.

Uso de consultas con parámetros

Los parámetros se envían por separado del texto SQL, por lo que la entrada del usuario nunca puede interpretarse como código SQL:

// CORRECT: Parameters are sent separately from the SQL text.
rows, err := db.QueryContext(ctx,
    "SELECT * FROM Sales.vSalesPerson WHERE FirstName = @name AND CountryRegionName = @loc",
    sql.Named("name", userName),
    sql.Named("loc", userLocation))

Nunca concatenes la entrada del usuario en SQL

La concatenación de cadenas permite a los atacantes inyectar SQL arbitrario:

// WRONG: SQL injection vulnerability.
query := fmt.Sprintf("SELECT * FROM Sales.vSalesPerson WHERE FirstName = '%s'", userName)
rows, err := db.QueryContext(ctx, query) // If userName is "'; DROP TABLE HumanResources.Department;--" ...

Nombres dinámicos de tablas o columnas

No puedes usar consultas parametrizadas para nombres de tablas, nombres de columnas u otros identificadores. Si tu aplicación requiere identificadores dinámicos, valida los resultados frente a una lista de permisos:

// Validate against known-safe values.
var validColumns = map[string]bool{
    "FirstName": true, "CountryRegionName": true, "TerritoryName": true,
}

func queryByColumn(ctx context.Context, db *sql.DB, column, value string) (*sql.Rows, error) {
    if !validColumns[column] {
        return nil, fmt.Errorf("invalid column name: %q", column)
    }
    // Safe to use column directly because it was validated against the allowlist.
    query := fmt.Sprintf("SELECT * FROM Sales.vSalesPerson WHERE [%s] = @p1", column)
    return db.QueryContext(ctx, query, sql.Named("p1", value))
}

Precaución

Nunca coloques la entrada suministrada por el usuario directamente en identificadores SQL, incluso con escape de corchetes. Valida siempre los valores con respecto a una lista de permitidos.

Procedimientos almacenados e inyección SQL

Los procedimientos almacenados no impiden automáticamente la inyección SQL. Si el procedimiento utiliza EXECUTE o sp_executesql con cadenas concatenadas internamente, puede seguir siendo vulnerable. Utiliza llamadas parametrizadas:

// Parameters are passed safely to the procedure.
_, err := db.ExecContext(ctx, "dbo.SearchEmployees",
    sql.Named("searchTerm", userInput))

Gestionar los secretos de la cadena de conexión

Las cadenas de conexión pueden contener contraseñas, secretos del cliente y rutas de certificados. Nunca los almacenes en código fuente.

Uso de variables de entorno

Lee la cadena de conexión completa de una variable de entorno:

connString := os.Getenv("MSSQL_CONNECTION_STRING")
if connString == "" {
    log.Fatal("MSSQL_CONNECTION_STRING environment variable is required")
}
db, err := sql.Open("sqlserver", connString)

Construir cadenas de conexión a partir de secretos individuales

Ensambla la cadena de conexión a partir de variables de entorno separadas para cada componente:

import (
    "net/url"
    "os"
)

query := url.Values{}
query.Add("database", os.Getenv("DB_NAME"))
query.Add("encrypt", "true")

password := os.Getenv("DB_PASSWORD")

u := &url.URL{
    Scheme:   "sqlserver",
    User:     url.UserPassword(os.Getenv("DB_USER"), password),
    Host:     os.Getenv("DB_HOST"),
    RawQuery: query.Encode(),
}
db, err := sql.Open("sqlserver", u.String())

Elimina por completo las contraseñas

La cadena de conexión más segura no incluye ninguna contraseña. Utiliza la autenticación Microsoft Entra ID con identidad gestionada:

import _ "github.com/microsoft/go-mssqldb/azuread"

// No password, no secret. The managed identity handles authentication.
db, err := sql.Open("azuresql",
    "sqlserver://<server>.database.windows.net?database=AdventureWorks2025&fedauth=ActiveDirectoryDefault&encrypt=true&TrustServerCertificate=false")

Opciones de almacenamiento secreto

Medio ambiente Almacenamiento recomendado Notas
Desarrollo local Variables de entorno o archivo de secretos de usuario No subas .env archivos al control de versiones.
Azure App Service / Container Apps Configuración de la app o referencias de Key Vault Las referencias de Key Vault inyectan secretos como variables de entorno en tiempo de ejecución.
Kubernetes Kubernetes Secrets o Azure Key Vault con el controlador CSI Los secretos se montan como archivos o variables de entorno.
Canalizaciones de CI/CD Secretos de la canalización / Secretos de Acciones de GitHub Usa ActiveDirectoryServicePrincipal o ActiveDirectoryAzurePipelines para Azure SQL.

Importante

Añade .env, *.pfx, y *.pem a tu .gitignore archivo. Transferir secretos accidentalmente en un repositorio es una de las fuentes más comunes de filtraciones de credenciales.

Configuración del cifrado

Utiliza cifrado para todas las conexiones remotas

Configure encrypt=true para todas las conexiones remotas. El cifrado que solo exige el servidor es vulnerable a ataques de adversario en el medio. El cliente debe hacer cumplir el cifrado. Azure SQL permite el cifrado por defecto:

sqlserver://<user>:<password>@<server>:1433?database=AdventureWorks2025&encrypt=true

Utiliza TDS 8.0 para el cifrado más fuerte

TDS 8.0 (modo estricto) realiza el handshake TLS antes de intercambiar cualquier dato del protocolo TDS, evitando ataques de degradación de protocolo:

sqlserver://<user>:<password>@<server>:1433?database=AdventureWorks2025&encrypt=strict

TDS 8.0 requiere SQL Server 2022 o Azure SQL.

No use TrustServerCertificate en producción

La configuración TrustServerCertificate=true desactiva la validación del certificado, lo que permite ataques con adversario en medio:

// DEVELOPMENT ONLY. Never deploy to production.
connString := "sqlserver://<user>:<password>@<server>?database=AdventureWorks2025&TrustServerCertificate=true"

Para producción, configura una validación adecuada de certificados. Consulta Cifrado y certificados.

Establece una versión mínima de TLS

Aplicar TLS 1.2 o posterior para evitar ataques de degradación de protocolo.

sqlserver://<user>:<password>@<server>:1433?database=AdventureWorks2025&encrypt=true&tlsmin=1.2

Aplicación del principio de privilegios mínimos

Utiliza usuarios con alcance de base de datos

Crear usuarios de bases de datos contenidas con alcance a una única base de datos en lugar de iniciar sesión a nivel de servidor que acceden a múltiples bases de datos.

-- Create a contained database user (no server-level login needed).
CREATE USER [app_user] WITH PASSWORD = '<password>';

-- Grant only the permissions the application needs.
ALTER ROLE db_datareader ADD MEMBER [app_user];
ALTER ROLE db_datawriter ADD MEMBER [app_user];
GRANT EXECUTE ON SCHEMA::dbo TO [app_user];

Conexiones separadas de lectura y escritura

Si tu aplicación tiene rutas de lectura y escritura distintas, crea usuarios de base de datos separados con permisos apropiados.

// Read-only queries use a restricted user.
readDB, _ := sql.Open("sqlserver",
    "sqlserver://app_reader:password@<server>?database=AdventureWorks2025&ApplicationIntent=ReadOnly&encrypt=true")

// Write operations use a user with write permissions.
writeDB, _ := sql.Open("sqlserver",
    "sqlserver://app_writer:password@<server>?database=AdventureWorks2025&encrypt=true")

Evita usar sa o dbo en aplicaciones

El sa inicio de sesión y dbo el usuario tienen acceso sin restricciones. Si una aplicación conectada a sa tiene una vulnerabilidad de inyección SQL, el atacante obtiene control total sobre el servidor de base de datos.

Proteger las claves Always Encrypted

Cuando se utiliza Always Encrypted, la clave maestra de columna (CMK) protege las claves de cifrado de columna (CEK). Asegurar el CMK adecuadamente:

Proveedor de claves Procedimiento recomendado
Certificado local (PFX) Guarda el archivo PFX fuera del directorio de la aplicación. Establece permisos restrictivos para archivos. Usa una variable de entorno como contraseña.
Almacén de certificados de Windows Usa CurrentUser\My para cuentas de servicio. Establecer permisos de certificado usando certutil.
Azure Key Vault Usa la identidad gestionada para el acceso. Establezca las directivas de acceso de Key Vault siguiendo el principio de privilegio mínimo. Habilitar la eliminación temporal y la protección contra purgas.

Para la configuración del proveedor de claves, véase Siempre cifrado.

Registrar con seguridad

No registres cadenas de conexión ni credenciales

Las cadenas de conexión pueden contener contraseñas que acaban en archivos de registro:

// WRONG: Logs the password.
log.Printf("Connecting to %s", connString)

// CORRECT: Log only the server and database.
log.Printf("Connecting to server=%s database=%s", serverName, dbName)

Si debes registrar detalles de conexión para diagnóstico, redacta primero la cadena de conexión:

func redactSQLServerURL(raw string) string {
    u, err := url.Parse(raw)
    if err != nil {
        return "<invalid connection string>"
    }

    if u.User != nil {
        username := u.User.Username()
        if username != "" {
            u.User = url.UserPassword(username, "REDACTED")
        }
    }

    q := u.Query()
    for _, key := range []string{"password", "clientassertion", "systemtoken"} {
        if q.Has(key) {
            q.Set(key, "REDACTED")
        }
    }
    u.RawQuery = q.Encode()
    return u.String()
}

Registra el valor redactado solo cuando lo necesites para diagnósticos de corta duración. Prefiero registrar el nombre del servidor, el nombre de la base de datos y el modo de autenticación por separado.

No registres parámetros sensibles de consulta

Evita registrar valores de parámetros que contengan datos personales o sensibles:

// WRONG: Logs sensitive parameter values.
log.Printf("Query: SELECT * FROM HumanResources.Employee WHERE NationalIDNumber = %s", nationalIDNumber)

// CORRECT: Log the query structure without parameter values.
log.Printf("Querying HumanResources.Employee by NationalIDNumber")

Usa las banderas de registro con cuidado en producción

El parámetro del log controlador puede generar sentencias SQL y valores de parámetros:

Flag Riesgo ¿Es seguro para la producción?
1 (errores) Bajo
2 (mensajes) Bajo
4 (filas) Medio, expone datos No
8 (SQL) Medium, expone consultas Condicional
16 (parámetros) Alto, expone los valores de los parámetros No
32 (transacciones) Bajo
64 (depuración) Alto, expone detalles del protocolo No

Para producción, usar log=1 (solo errores) o log=3 (errores + mensajes).

Lista de verificación de seguridad

Area Recommendation
Inyección de código SQL Utiliza consultas parametrizadas para todas las entradas del usuario. Valida los identificadores dinámicos frente a una lista de permisos.
Credentials Utiliza la identidad gestionada de Microsoft Entra siempre que sea posible. Nunca incluyas contraseñas de forma fija en el código fuente.
Encryption Configure encrypt=true o encrypt=strict para todas las conexiones de producción. Establezca tlsmin=1.2.
Certificados No use TrustServerCertificate=true en producción. Validar los certificados del servidor.
Privilegio mínimo Crea usuarios específicos de bases de datos de aplicaciones con solo los permisos requeridos.
Secrets Almacenar cadenas de conexión en variables de entorno, Key Vault o almacenes secretos de plataforma.
Logging No registres cadenas de conexión, contraseñas ni valores de parámetros sensibles.
Control de código fuente Sumemos .env, *.pfx, y *.pem a .gitignore.