Skip to main content

Guía de seguridad de Go: 8 prácticas recomendadas para desarrolladores de Go

Escrito por

Gerred Dillon

snyk web-based tools example

9 de febrero de 2021

0 minutos de lectura

En esta entrega de nuestra serie de guías, repasaremos ocho prácticas recomendadas de seguridad de Go para desarrolladores. El lenguaje Go incorpora muchas funciones integradas que promueven prácticas de desarrollo más seguras —en comparación con lenguajes más antiguos y de bajo nivel como C—, como la recolección de basura en memoria y los punteros con tipos estrictos.

Estas funciones ayudan a los desarrolladores a evitar errores que pueden dar lugar a exploits, ya que eliminan la responsabilidad de administrar la memoria por cuenta propia. Sin embargo, los programadores deben tener en cuenta otras prácticas recomendadas de seguridad. Esta guía, escrita por Eric Smalling y Gerred Dillon, con la ayuda de Dan Enman (ingeniero sénior de software en Snyk), aborda algunos de los temas más comunes.

Descarga la guía aquí.

  1. Usa Go Modules

  2. Analiza las dependencias para detectar CVE

  3. Usa los paquetes criptográficos estándar de Go

  4. Usa html/template para ayudar a evitar ataques XSS

  5. Uso de subshells

  6. Evita unsafe y cgo

  7. Usa reflection con moderación

  8. Reduce la superficie de ataque de los contenedores

1. Usa Go Modules

El sistema Go Modules es el sistema oficial de administración de dependencias desde la versión v1.11; los sistemas anteriores Vendor y Dep quedaron obsoletos. Go Modules permite fijar las versiones de las dependencias, incluidos los módulos transitivos, y también protege contra modificaciones inesperadas de los módulos mediante la base de datos de sumas de comprobación go.sum.

Primero, inicializa el proyecto ejecutando go mod init [namespace/project-name] en el directorio de nivel superior.

$ go mod init mycorp.com/myapp

Esto creará en el directorio actual un archivo llamado go.mod, que contiene el nombre del proyecto y la versión de Go que estás usando. Si tu código fuente incluye importaciones de paquetes, basta con ejecutar go build (o test, install, etc.) para actualizar el archivo go.mod con los módulos utilizados, incluidas sus versiones. También puedes usar go get para actualizar tus dependencias, incluso a versiones específicas; esto también actualizará go.mod.

Ejemplo de archivo go.mod:

module mycorp.com/myapp
go 1.15
require (
  github.com/containerd/console v1.0.1
  rsc.io/quote/v3 v3.1.0
)

Observa que también se creó un archivo llamado go.sum. Este archivo contiene una lista de hashes para cada módulo utilizado, que Go aprovecha para validar que se usen los mismos binarios en cada compilación. Debes incluir los archivos go.mod y go.sum en el control de versiones junto con el código de la aplicación.

El tutorial Using Go Modules, publicado en el blog oficial de Go, es un excelente recurso para aprender más sobre Go Modules, por ejemplo, cómo fijar las versiones de dependencias transitivas, eliminar las dependencias que no se usan y mucho más.

2. Analiza las dependencias para detectar CVE

Como ocurre con la mayoría de los proyectos, el código de los módulos de los que depende tu aplicación suele superar en cantidad al de la propia aplicación, y estas dependencias externas son una vía común para introducir vulnerabilidades de seguridad. Herramientas como Snyk —con el respaldo de nuestra extensa Base de datos de vulnerabilidades— pueden analizar estos grafos de dependencias para detectar vulnerabilidades conocidas, recomendar actualizaciones que corrijan los problemas encontrados e incluso monitorear continuamente tus proyectos para detectar vulnerabilidades nuevas en el futuro.

Por ejemplo, al ejecutar snyk test en una aplicación Go, se analizan los módulos y se informa sobre cualquier CVE conocida, además de proporcionar información sobre las versiones corregidas a las que puedes actualizar. Además, las herramientas web de Snyk pueden monitorear tus repositorios de GitHub de forma directa y continua, y alertarte sobre las vulnerabilidades que se detecten en el futuro, incluso si no has cambiado el código ni ejecutado una compilación de CI.

Terminal que muestra a Snyk analizando una aplicación Go y reportando tres vulnerabilidades en las dependencias, con sus niveles de gravedad y correcciones.
Ejemplo de Snyk Test en una aplicación de Go
Panel web de Snyk que muestra un problema de verificación incorrecta de firma de alta gravedad, con detalles del exploit y filtros de vulnerabilidades.
Ejemplo de herramientas web de Snyk

3. Usa los paquetes criptográficos estándar de Go en lugar de los de terceros

Los paquetes criptográficos de la biblioteca estándar de Go han sido auditados exhaustivamente por investigadores de seguridad, pero, como no abarcan todos los casos, podrías sentir la tentación de usar paquetes de terceros.

Al igual que no deberías crear tus propios algoritmos criptográficos, debes tener mucho cuidado con las bibliotecas criptográficas de terceros, ya que quizá no hayan sido auditadas con el mismo nivel de rigor. Investiga su procedencia.

4. Usa html/template para ayudar a evitar ataques XSS

Las cadenas sin filtrar que se envían a un cliente web mediante io.WriteString() o el paquete text/template pueden exponer a tus usuarios a ataques de secuencias de comandos entre sitios (XSS). Esto se debe a que las etiquetas HTML de las cadenas devueltas se renderizan en el flujo de salida sin codificación y también podrían enviarse con un encabezado de respuesta Content-Type: plain/text definido incorrectamente, si no se especifica de forma explícita.

Usar el paquete html/template es una forma sencilla de codificar automáticamente para la web el contenido devuelto, en lugar de intentar asegurarte de hacerlo manualmente en la lógica de tu aplicación. La documentación de OWASP/GO-SCP incluye un excelente capítulo y un ejemplo detallado sobre este tema.

5. Uso de subshells

En Go, un subshell básicamente proporciona acceso directo al shell del sistema y su uso suele limitarse a aplicaciones de tipo herramienta de línea de comandos. Siempre que sea posible, opta por soluciones implementadas de forma nativa en código Go mediante los módulos adecuados.

Si necesitas usar un subshell, asegúrate de sanear todos los datos externos que puedan pasarse a él, así como los datos que devuelve, para evitar que tu aplicación exponga detalles innecesarios del sistema subyacente. Esta precaución es similar a la que tomarías ante ataques a plantillas renderizadas (consulta el punto 4 anterior) o inyecciones de comandos SQL. También considera que ejecutar un proceso externo como parte de un hilo de solicitud de la aplicación podría tener otros efectos secundarios que no puedes controlar desde tu código Go, como cambios en el sistema de archivos, llamadas a dependencias externas o cambios en las condiciones de seguridad que podrían bloquear esas llamadas; por ejemplo, las limitaciones de ejecutar el programa en un contenedor o con herramientas como AppArmor, SELinux, etc.

6. Ten cuidado con unsafe y cgo

Al igual que el lenguaje C, Go admite el uso de variables de tipo puntero; sin embargo, lo hace con una estricta seguridad de tipos para proteger a los desarrolladores de efectos secundarios no deseados o incluso maliciosos. En C siempre puedes definir el puntero void*, que no tiene asignado ningún tipo; para hacer algo similar en Go, puedes usar el paquete estándar unsafe, cuyo nombre describe bien cómo rompe las restricciones de seguridad de tipos. Por lo general, la documentación de Go desaconseja usar unsafe, ya que permite el acceso directo a la memoria y, combinado con datos de usuario, podría permitir que los atacantes vulneren la seguridad de memoria de Go.

El uso de cgo también requiere precaución. Este potente comando permite integrar bibliotecas C arbitrarias en tu aplicación Go. Como cualquier herramienta poderosa, cgo debe usarse con extrema precaución, porque confías en que una dependencia completamente externa, escrita en un lenguaje inseguro, esté implementada correctamente. La protección de memoria de Go no podrá ayudarte si en ese código externo hay errores o rutinas maliciosas. Puedes desactivar cgo simplemente configurando CGO_ENABLED=0 durante la compilación; suele ser una opción segura si no lo necesitas explícitamente, ya que la mayoría de las bibliotecas modernas de Go están escritas exclusivamente en Go.

7. Reflection

Go es un lenguaje con tipos estrictos, lo que significa que los tipos de las variables son importantes. A veces necesitas obtener en tiempo de ejecución información sobre el tipo o el valor de una variable dentro del código. Go ofrece el paquete `reflect`, que permite consultar y manipular el tipo y el valor de una variable de cualquier tipo; por ejemplo, para saber si una variable es de cierto tipo o contiene determinadas propiedades o funciones.

Aunque reflection puede ser útil, también aumenta el riesgo de errores de tipos en tiempo de ejecución en tu código Go. Si intentas modificar una variable reflejada de una forma no permitida (por ejemplo, asignar a un campo de una estructura un valor que no se puede modificar), el código generará un panic. También puede ser difícil comprender bien el flujo del código y los distintos tipos de tipos y valores que se reflejan. Por último, al trabajar con tipos o valores reflejados, es posible que tengas que hacer aserciones de tipos, lo que puede confundir al leer el código y provocar errores en tiempo de ejecución.

Reflection puede ser una herramienta poderosa, pero, dada la forma en que funcionan los tipos y las interfaces de Go, se debe usar con poca frecuencia, ya que puede causar problemas inesperados con facilidad.

8. Reduce la superficie de ataque de los contenedores

Muchas aplicaciones Go no tienen dependencias externas y están diseñadas para ejecutarse en contenedores, por lo que deberíamos reducir el sistema de archivos disponible para ellas mediante un par de técnicas de creación de imágenes. Una de las formas más sencillas de hacerlo es usar un Dockerfile multietapa: compilamos la aplicación en una etapa de compilación y luego usamos una imagen base scratch para la imagen del artefacto de implementación.

Revisa el siguiente ejemplo de Dockerfile:

  1| FROM golang:1.15 as build
  2| 
  3| COPY . .
  4|
  5| ENV GOPATH=""
  6| ENV CGO_ENABLED=0
  7| ENV GOOS=linux
  8| ENV GOARCH=amd64
  9| RUN go build -trimpath -v -a -o myapp -ldflags="-w -s"
 10| RUN chmod +x go-goof
 11| 
 12| RUN useradd -u 12345 moby
 13| 
 14| FROM scratch
 15| COPY --from=build /go/myapp /myapp
 16| COPY --from=build /etc/passwd /etc/passwd
 17| USER moby
 18| 
 19| ENTRYPOINT ["/myapp"]

Si no conoces los Dockerfiles, son instrucciones paso a paso que se pueden usar para crear prácticamente cualquier imagen OCI; están documentados aquí. Este es un Dockerfile multietapa con dos etapas distintas: una etapa de compilación y la etapa de la imagen final de ejecución.

Etapa 1, líneas 1–12: la etapa de compilación

Partimos de la imagen base oficial golang:1.15; en esta etapa, configuramos algunas variables de entorno y compilamos nuestra aplicación Go. Cuando termina esta etapa, se guarda en caché una imagen temporal con la etiqueta build, a la que podremos hacer referencia más adelante.

Probablemente te preguntes para qué sirven todas las variables de entorno y los argumentos que pasamos a la compilación:

  • GOPATH=””: borramos esta variable (que se

    configuró en la imagen base golang:1.15), ya que no se necesita al usar Go Modules.

  • CGO_ENABLED=0: desactiva cgo (consulta la sección 6).

  • GOOS=linux: indica explícitamente a Go que compile para el sistema operativo Linux.

  • GOARCH=amd66: indica explícitamente a Go que compile para la arquitectura amd64 (Intel).

  • -trimpath: elimina la información de las rutas del sistema de archivos del binario.

  • ldflag -s: omite la tabla de símbolos y la información de depuración.

  • ldflag -w: omite la tabla de símbolos DWARF.

Este conjunto de opciones permite compilar, prácticamente, el archivo binario más pequeño posible, pero algunas quizá no sean adecuadas para tu aplicación. Elige las que necesites.

Etapa 2, líneas 13–10: la etapa de la imagen de ejecución

En esta etapa, simplemente copiamos los archivos binarios estáticos y /etc/passwd de la etapa build a un sistema de archivos scratch vacío, especificamos el propietario adecuado y definimos el comando que se ejecutará al iniciar el contenedor.

Para crear esta imagen, ejecutamos lo siguiente desde el mismo directorio que el Dockerfile:

docker build -t [app image name]:[version tag] .

Nota: el . al final de esa línea de compilación es importante, ya que indica al sistema de compilación dónde encontrar el Dockerfile y cualquier otro archivo al que haga referencia.

La imagen resultante tendrá un sistema de archivos que contiene exactamente dos archivos: nuestra aplicación y el archivo passwd con el usuario moby (el nombre del usuario no es importante; simplemente no queremos ejecutar nada como root dentro del contenedor). No habrá ningún sh, ps ni otros archivos que un atacante pueda aprovechar. Por supuesto, si necesitas otros archivos para que tu aplicación funcione, tendrás que incluirlos o montarlos durante la ejecución.

Descarga aquí la guía de seguridad de Go.

Comienza con Capture the Flag

Aprende a resolver desafíos de Capture the Flag con nuestro taller virtual 101 a pedido.