Skip to main content

Cómo entender las vulnerabilidades de inyección de comandos en Go

Escrito por
feature insights context

14 de noviembre de 2024

0 minutos de lectura

Es posible que los desarrolladores de Go necesiten usar comandos del sistema en distintos escenarios, como la manipulación de imágenes, donde deben procesarlas o cambiar su tamaño, o ejecutar comandos del sistema para administrar recursos o recopilar métricas o registros.

En otras ocasiones, quizás estés creando un nuevo sistema en Go que debe conectarse con sistemas heredados existentes. Esta conexión depende de la ejecución de comandos del sistema y del procesamiento de sus resultados.

En cualquiera de los casos, es esencial seguir prácticas de codificación segura al iniciar comandos del sistema, ya que esto podría dar lugar a una vulnerabilidad de seguridad conocida como inyección de comandos.

¿Qué es la inyección de comandos?

La inyección de comandos es una vulnerabilidad de seguridad que ocurre cuando una aplicación envía datos no seguros proporcionados por el usuario (como los datos ingresados en un formulario web) a un shell del sistema. Esta debilidad permite que un atacante ejecute comandos arbitrarios en el sistema operativo del host con los mismos permisos que el usuario de la aplicación.

En Go, la inyección de comandos suele implicar el uso del paquete os/exec para iniciar comandos del sistema.

Considera el siguiente ejemplo de código en Go:

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

En este fragmento de código, cmdName se extrae directamente de los parámetros de consulta de la solicitud y se usa para construir un comando que se ejecuta en el servidor. Este es un ejemplo clásico de vulnerabilidad de inyección de comandos, ya que un atacante puede manipular el valor de la cadena de consulta cmd para ejecutar cualquier comando que elija y potencialmente comprometer el servidor.

Un atacante podría crear una solicitud con un comando malicioso, como:

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

¿Por qué es peligrosa la inyección de comandos? Es peligrosa porque permite que los atacantes ejecuten comandos arbitrarios en el servidor, lo que puede tener consecuencias graves, entre ellas:

  • Filtración de datos: los atacantes pueden acceder a datos confidenciales almacenados en el servidor. Imagina que acceden a archivos de configuración como config.toml y otros que enumeran recursos y credenciales.

  • Compromiso del sistema: los atacantes pueden tomar el control del servidor, lo que permite una mayor explotación. Esto suele manifestarse en movimientos laterales dentro de un sistema comprometido y acciones de reconocimiento de otros servidores que podrían comprometerse.

  • Interrupción del servicio: los atacantes pueden ejecutar comandos que interrumpan los servicios y provoquen tiempo de inactividad. Esto puede traducirse directamente en pérdidas financieras y riesgos para el negocio debido a un ataque de denegación de servicio.

El impacto de un ataque de inyección de comandos exitoso puede ser devastador: afecta no solo al sistema comprometido, sino también a la reputación de la organización y a la confianza de sus clientes.

Ejecución de procesos vulnerables e inyección de comandos en Go

Los desarrolladores de Go, conocidos por preferir la simplicidad y el rendimiento, podrían optar por integrar comandos del sistema para aprovechar las capacidades de estas herramientas. Este enfoque les permite concentrarse en crear aplicaciones sólidas sin reinventar la rueda. Sin embargo, esta integración presenta sus propios desafíos, sobre todo en materia de seguridad.

Para ilustrar un escenario más realista, considera una aplicación de Go que procesa archivos de imagen con una utilidad de línea de comandos como convert.

Veamos una aplicación de Go diseñada para procesar solicitudes de cambio de tamaño de imágenes. La aplicación usa el framework web Gin para definir un endpoint POST, /cloudpawnery/image, que cambia el tamaño de las imágenes según los datos ingresados por el usuario. Este endpoint acepta parámetros como tenantID, fileID y fileSize en la cadena de consulta. El parámetro fileSize es opcional y, si no se proporciona, su valor predeterminado es "200".

El siguiente fragmento de código muestra una implementación vulnerable en 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
}

La función downloadAndResize construye una cadena de comandos para cambiar el tamaño de la imagen con convert y la ejecuta mediante exec.CommandContext.

¿Cómo construye una cadena de comandos la función downloadAndResize? Toma esta entrada del valor fileSize proporcionado por el usuario. Luego, ejecuta esta cadena, lo que podría permitir que un atacante inyecte comandos maliciosos. Para mitigar este riesgo, los desarrolladores de Go deben validar y sanear todas las entradas del usuario, usar comandos parametrizados y aplicar prácticas de seguridad que permitan ejecutar comandos de forma segura.

Cómo mitigar las vulnerabilidades de inyección de comandos

En el desarrollo con Go, como en cualquier otro lenguaje, es fundamental asegurarse de que el código esté protegido contra vulnerabilidades de inyección de comandos. Estas se producen cuando un atacante puede ejecutar comandos arbitrarios en el host, lo que puede dar lugar a accesos no autorizados, filtraciones de datos y otros problemas de seguridad graves.

Veamos algunas prácticas recomendadas para mitigar estos riesgos.

Validación y saneamiento de entradas

Uno de los pasos fundamentales para prevenir la inyección de comandos es validar y sanear rigurosamente las entradas. En el ejemplo, la función downloadAndResize construye una cadena de comandos con datos proporcionados por el usuario, como fileSize. Si estas entradas no se validan correctamente, un atacante podría inyectar comandos maliciosos.

Así puedes mejorar la validación de entradas:

  1. Permitir solo valores de una lista: define un conjunto de valores aceptables para entradas como fileSize. Por ejemplo, permite únicamente valores numéricos dentro de un rango razonable.

  2. Sanear las entradas: elimina o escapa los caracteres potencialmente peligrosos de las entradas del usuario. Esto puede incluir metacaracteres de shell como ;, |, &, etc. Lo mejor es codificar el comando directamente y no permitir que las entradas del usuario lo controlen.

  3. Usar tipos estrictos: convierte las entradas al tipo de datos esperado lo antes posible. Por ejemplo, convierte fileSize en un entero y valida su rango.

Aquí tienes un ejemplo de cómo implementar estas prácticas:

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)
    // ...
}

 Aun así, podemos hacer mucho más para proteger el código de Go contra la inyección de comandos. ¡Sigue leyendo!

Usar API o bibliotecas seguras en lugar de comandos del sistema

Otra estrategia eficaz es evitar, siempre que sea posible, la ejecución directa de comandos del sistema. En su lugar, aprovecha API o bibliotecas seguras que ofrecen la funcionalidad necesaria sin exponer tu aplicación a riesgos de inyección de comandos.

Por ejemplo, si tu aplicación necesita manipular imágenes, considera usar una biblioteca de Go como github.com/disintegration/imaging en lugar de llamar a un comando externo como convert de la biblioteca de software ImageMagick. Este enfoque encapsula la funcionalidad en el entorno de Go, que es seguro en cuanto a tipos, y reduce la superficie de ataque.

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
}

Al usar bibliotecas como imaging en Go, eliminas la necesidad de construir y ejecutar comandos de shell, con lo que reduces el riesgo de inyección de comandos. Sin embargo, en algunas bibliotecas y ecosistemas de lenguajes, es posible que un paquete de terceros sea solo un contenedor sencillo para ejecutar comandos. Por eso, es imprescindible revisar el código en busca de este tipo de operaciones sensibles.

Refactoriza la función vulnerable downloadAndResize para prevenir la inyección

En el ejemplo anterior, mostramos una aplicación de Go potencialmente vulnerable que usa la función exec.CommandContext para ejecutar comandos de shell. Como señalamos, este enfoque puede dar lugar a vulnerabilidades de inyección de comandos.

Intentemos refactorizar la función downloadAndResize para asegurarnos de que las entradas del usuario no provoquen la ejecución de comandos arbitrarios.

Una forma eficaz de prevenir la inyección de comandos es evitar construir cadenas de comandos de shell directamente a partir de las entradas del usuario. En su lugar, podemos usar la función exec.Command con argumentos separados. Así, podemos pasar las entradas del usuario al comando de forma segura, sin invocar el shell ni permitir que los usuarios controlen el comando en sí.

Aquí tienes una versión refactorizada de la función downloadAndResize que resuelve la vulnerabilidad de inyección de comandos:

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
}

En esta refactorización, separamos el comando de sus argumentos. Al usar exec.CommandContext con argumentos separados, evitamos tener que construir una cadena de comandos de shell. Este método garantiza que las entradas del usuario se traten como datos y no como código ejecutable, lo que reduce significativamente el riesgo de inyección de comandos.

También eliminamos la necesidad de invocar el shell. El código refactorizado no invoca el shell (sh -c), que es una vía común para la inyección de comandos. En su lugar, llama directamente a la utilidad convert con los argumentos especificados.

Usar Snyk para proteger el código

Snyk Code es una potente herramienta de análisis estático que ayuda a los desarrolladores a identificar y corregir vulnerabilidades en sus bases de código. Se integra perfectamente con tu IDE y tu flujo de trabajo de desarrollo, y te ofrece comentarios en tiempo real sobre posibles problemas de seguridad.

Cómo puede ayudar Snyk a identificar vulnerabilidades de inyección de comandos en Go

En el ejemplo de Go, la función downloadAndResize construye un comando de shell con una entrada proporcionada por el usuario:

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

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

Este código es vulnerable a la inyección de comandos porque incorpora directamente las entradas del usuario en la cadena de comandos.

¿Qué pasaría si tu equipo tuviera desarrolladores que no conocieran las vulnerabilidades de inyección de comandos?

¿Podrías identificar fácilmente el flujo de llamadas de análisis estático entre archivos en una revisión de código para encontrar esta vulnerabilidad de inyección de comandos?

Aquí es donde entra Snyk.

Mira lo fácil que es mejorar la seguridad de las aplicaciones con la extensión de Snyk Code, que resalta en tiempo real el código vulnerable en el editor de VS Code:

Pantalla de Snyk Code que muestra código de Go con una vulnerabilidad de inyección de comandos y un ejemplo corregido de exec.Command.

Snyk puede ayudar a identificar estas vulnerabilidades al analizar tu código de Go y señalar los casos en los que las entradas del usuario se usan de forma insegura en comandos de shell. Snyk te muestra commits reales de otros proyectos de código abierto que mitigaron esta vulnerabilidad específica, para que tengas varias referencias de código que muestran cómo se ve una buena solución.

Además, si haces clic en la pestaña ISSUE OVERVIEW, encontrarás un análisis más detallado y prácticas recomendadas para prevenir la inyección de comandos. Esta pestaña ofrece información detallada y recomendaciones para mitigar estos riesgos. Está disponible directamente en la vista del IDE mientras programas, para que puedas aplicar estas medidas sin costosos cambios de contexto:

Understanding Command Injection vulnerabilities in Go

Para aprender más sobre cómo proteger tu código contra este tipo de vulnerabilidades, considera instalar la extensión de IDE Snyk Code y conectar tus proyectos de Git para identificar y corregir problemas de seguridad en tus aplicaciones de Go. Puedes hacerlo fácilmente y gratis si te registras para comenzar a analizar tu código en busca de vulnerabilidades.

Herramienta gratuita para revisar código

Protege tu código antes de tu próximo commit.