Saltar al contenido principal

SAST, análisis estático de código

SAST (Static Application Security Testing) es analizar el código fuente buscando patrones peligrosos sin llegar a ejecutarlo. Es el equivalente a un corrector ortográfico, pero para fallos de seguridad.

Su gran ventaja es que funciona desde el primer momento: no necesitas desplegar nada, ni una base de datos, ni un entorno. Con el código basta, así que puede correr en cada push y darte respuesta en segundos.

Cómo funciona por dentro

Un analizador estático no lee el código como texto. Lo convierte en un árbol sintáctico y busca formas, no cadenas.

Por eso una regla que busque eval($X) encuentra las tres:

eval(entrada);
eval( entrada );
eval(
entrada
);

Los más avanzados hacen además taint tracking: siguen el rastro de un dato desde donde entra (una petición HTTP) hasta donde se usa (una consulta SQL). Si el camino no pasa por ninguna validación, saltan.

Laboratorio: analizar el código

Usamos Semgrep, que es rápido, no necesita compilar y trae miles de reglas de serie:

cd devsecops-demo

docker run --rm -v "$PWD:/src" semgrep/semgrep:latest \
semgrep --config=auto /src

Tarda unos dieciséis segundos y no requiere cuenta. Los 9 hallazgos reales:

Dockerfile:22 [ERROR] missing-user
infra/main.tf:26 [WARNING] s3-public-read-bucket
infra/main.tf:52 [WARNING] aws-db-instance-no-logging
infra/main.tf:58 [WARNING] rds-insecure-password-storage-in-source-code
infra/main.tf:59 [WARNING] rds-public-access
src/config.js:18 [ERROR] detected-aws-access-key-id-value
src/index.js:5 [INFO] express-check-csurf-middleware-usage
src/routes.js:34 [WARNING] eval-detected
src/routes.js:34 [ERROR] code-string-concat

Dos observaciones antes de seguir.

Primera: Semgrep encuentra la clave de AWS que gitleaks no vio. En el capítulo anterior gitleaks detectó dos secretos, pero el identificador AKIA... de la línea 18 se le escapó. Aquí sí aparece. No es que una herramienta sea mejor: es que miran de forma distinta, y por eso se usan las dos.

Segunda: analiza también el Dockerfile y el Terraform. Semgrep no es solo para código de aplicación. Ya te está avisando de que el contenedor corre como root y de que el bucket es público — cosas que veremos a fondo en los capítulos 5 y 7.

El hallazgo estrella

src/routes.js:34 eval-detected
34┆ const resultado = eval(req.query.expr);

eval() ejecuta como código lo que le pases. Y aquí le estamos pasando, literalmente, un parámetro de la URL. Levanta la aplicación y compruébalo:

npm install && npm start
curl "http://localhost:3000/calcular?expr=7*6"
{"resultado":42}

Ha ejecutado 7*6 en tu servidor. Cambia esa expresión por algo menos amable y tienes ejecución remota de código. Esto no es un aviso teórico.

Lo que las reglas gratuitas no ven

Aquí viene la parte interesante, y es una lección más valiosa que cualquier hallazgo.

En src/routes.js hay una inyección SQL de manual:

const { rows } = await pool.query(
'SELECT * FROM usuarios WHERE id = ' + req.params.id
);

Semgrep no la detecta. Ni con --config=auto, ni con p/javascript, ni con p/security-audit. El motivo es que el taint tracking para librerías de base de datos forma parte de las reglas de pago.

Es un recordatorio útil: pasar el escáner sin hallazgos no significa que el código sea seguro. Significa que no ha saltado ninguna de las reglas que tienes activadas.

La buena noticia es que escribir la regla cuesta doce líneas.

Escribir tu propia regla

# .semgrep.yml
rules:
- id: sql-por-concatenacion
message: >-
Consulta SQL construida concatenando texto. Si alguno de los trozos viene
del usuario, esto es una inyección SQL. Usa consultas parametrizadas.
languages: [javascript, typescript]
severity: ERROR
patterns:
- pattern-either:
- pattern: $CLIENT.query("..." + $X, ...)
- pattern: $CLIENT.query('...' + $X, ...)
- pattern: $CLIENT.query(`...${$X}...`, ...)

La sintaxis es el propio lenguaje con comodines: $CLIENT y $X capturan cualquier cosa, "..." es cualquier cadena y ... cualquier lista de argumentos. No hay que aprender un lenguaje nuevo.

docker run --rm -v "$PWD:/src" semgrep/semgrep:latest \
semgrep --config=/src/.semgrep.yml /src
❯❯❱ sql-por-concatenacion
22┆ const { rows } = await pool.query(
23┆ 'SELECT * FROM usuarios WHERE id = ' + req.params.id

Cazada. Y ahora cualquiera que escriba esa forma en el repositorio la tendrá marcada automáticamente.

La forma correcta, para comparar:

const { rows } = await pool.query(
'SELECT * FROM usuarios WHERE id = $1',
[req.params.id]
);

El parámetro viaja aparte de la consulta, así que el motor nunca lo interpreta como SQL.

Los falsos positivos son el problema real

Un SAST mal calibrado produce cientos de avisos, el equipo deja de mirarlos y en la práctica te has quedado sin control. Tres cosas que funcionan:

  1. Empieza solo por ERROR. Los WARNING e INFO como informe, sin bloquear.
  2. Silencia con contexto. Un // nosemgrep: regla-concreta en la línea, indicando la regla exacta y por qué. Nunca el fichero entero.
  3. Analiza solo lo que cambia en los pull requests (--baseline-commit). Nadie va a arreglar de golpe la deuda de cinco años, pero sí evita añadir más.

SAST en el pipeline

# .github/workflows/sast.yml
name: SAST
on: [push, pull_request]

jobs:
semgrep:
runs-on: ubuntu-latest
container:
image: semgrep/semgrep
steps:
- uses: actions/checkout@v4
- run: semgrep --config=auto --config=.semgrep.yml --sarif -o semgrep.sarif
- uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: semgrep.sarif

El formato SARIF es el estándar para resultados de análisis. Subirlo hace que los hallazgos aparezcan en la pestaña Security del repositorio y como comentarios en línea dentro del pull request, que es donde la gente los va a leer.

¿Y SonarQube?

Semgrep y SonarQube resuelven cosas parecidas con filosofías distintas:

SemgrepSonarQube
EnfoqueSeguridad, reglas propias fácilesCalidad global: bugs, cobertura, deuda
InstalaciónUn contenedorUn servidor y una base de datos
VelocidadSegundosMinutos
HistóricoNo de serieSí, con tendencias y quality gates

Para empezar en DevSecOps, Semgrep. SonarQube gana cuando quieres un panel de calidad para toda la organización, más allá de la seguridad.

Resumen

  • SAST analiza el código sin ejecutarlo: rápido, barato y en cada push
  • Semgrep encuentra eval(), secretos y hasta problemas del Dockerfile y del Terraform
  • Ninguna herramienta lo ve todo: la inyección SQL se le escapa a las reglas gratuitas
  • Escribir una regla propia cuesta doce líneas y es la diferencia entre detectar y no detectar
  • Los falsos positivos matan el control: empieza por lo crítico y analiza solo lo que cambia
  • Sube los resultados en SARIF para que aparezcan donde el equipo los lee

Recursos adicionales


💡 Recuerda: un escaneo sin hallazgos no dice que tu código sea seguro. Dice que no has activado la regla que lo habría encontrado.

En el próximo capítulo dejamos el código y pasamos al artefacto: la imagen de contenedor.