Patrones de Seguridad en Perl
Guía completa de seguridad para aplicaciones Perl que cubre validación de entrada, prevención de inyección y prácticas de codificación segura.
Cuándo Activarlo
- Al manejar entrada de usuario en aplicaciones Perl
- Al construir aplicaciones web Perl (CGI, Mojolicious, Dancer2, Catalyst)
- Al revisar código Perl en busca de vulnerabilidades de seguridad
- Al realizar operaciones de archivos con rutas proporcionadas por el usuario
- Al ejecutar comandos del sistema desde Perl
- Al escribir consultas a bases de datos con DBI
Cómo Funciona
Comience con los límites de entrada conscientes del taint, luego avance hacia afuera: valide y descontamine entradas, mantenga el sistema de archivos y la ejecución de procesos bajo control, y use consultas DBI parametrizadas en todas partes. Los ejemplos a continuación muestran los valores seguros predeterminados que esta habilidad espera que aplique antes de enviar código Perl que toque entrada de usuario, el shell o la red.
Modo Taint
El modo taint de Perl (-T) rastrea datos provenientes de fuentes externas y evita que se utilicen en operaciones inseguras sin validación explícita.
Habilitar el Modo Taint
#!/usr/bin/perl -T
use v5.36;
# Contaminado: cualquier cosa externa al programa
my $input = $ARGV[0]; # Contaminado
my $env_path = $ENV{PATH}; # Contaminado
my $form = <STDIN>; # Contaminado
my $query = $ENV{QUERY_STRING}; # Contaminado
# Sanitizar PATH al inicio (requerido en modo taint)
$ENV{PATH} = '/usr/local/bin:/usr/bin:/bin';
delete @ENV{qw(IFS CDPATH ENV BASH_ENV)};
Patrón de Descontaminación
use v5.36;
# Bueno: Validar y descontaminar con una expresión regular específica
sub descontaminar_usuario($input) {
if ($input =~ /^([a-zA-Z0-9_]{3,30})$/) {
return $1; # $1 está descontaminado
}
die "Nombre de usuario inválido: debe tener entre 3 y 30 caracteres alfanuméricos\n";
}
# Bueno: Validar y descontaminar una ruta de archivo
sub descontaminar_nombre_archivo($input) {
if ($input =~ m{^([a-zA-Z0-9._-]+)$}) {
return $1;
}
die "Nombre de archivo inválido: contiene caracteres no seguros\n";
}
# Malo: Descontaminación demasiado permisiva (anula el propósito)
sub mal_descontaminar($input) {
$input =~ /^(.*)$/s;
return $1; # Acepta CUALQUIER COSA — inútil
}
Validación de Entrada
Lista Blanca sobre Lista Negra
use v5.36;
# Bueno: Lista blanca — defina exactamente lo permitido
sub validar_campo_orden($field) {
my %permitidos = map { $_ => 1 } qw(name email created_at updated_at);
die "Campo de orden inválido: $field\n" a menos que $permitidos{$field};
return $field;
}
# Bueno: Validar con patrones específicos
sub validar_email($email) {
if ($email =~ /^([a-zA-Z0-9._%+-]+\@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})$/) {
return $1;
}
die "Dirección de email inválida\n";
}
sub validar_entero($input) {
if ($input =~ /^(-?\d{1,10})$/) {
return $1 + 0; # Convertir a número
}
die "Entero inválido\n";
}
# Malo: Lista negra — siempre incompleta
sub mal_validar($input) {
die "Inválido" si $input =~ /[<>"';&|]/; # Omite ataques codificados
return $input;
}
Restricciones de Longitud
use v5.36;
sub validar_comentario($text) {
die "El comentario es requerido\n" a menos que length($text) > 0;
die "El comentario supera los 10000 caracteres\n" si length($text) > 10_000;
return $text;
}
Expresiones Regulares Seguras
Prevención de ReDoS
El retroceso catastrófico ocurre con cuantificadores anidados en patrones superpuestos.
use v5.36;
# Malo: Vulnerable a ReDoS (retroceso exponencial)
my $mal_re = qr/^(a+)+$/; # Cuantificadores anidados
my $mal_re2 = qr/^([a-zA-Z]+)*$/; # Cuantificadores anidados en clase
my $mal_re3 = qr/^(.*?,){10,}$/; # Combinación repetitiva greedy/lazy
# Bueno: Reescribir sin anidamiento
my $bien_re = qr/^a+$/; # Cuantificador simple
my $bien_re2 = qr/^[a-zA-Z]+$/; # Cuantificador simple en clase
# Bueno: Usar cuantificadores posesivos o grupos atómicos para prevenir retroceso
my $seguro_re = qr/^[a-zA-Z]++$/; # Posesivo (5.10+)
my $seguro_re2 = qr/^(?>a+)$/; # Grupo atómico
# Bueno: Forzar timeout en patrones no confiables
use POSIX qw(alarm);
sub coincidencia_segura($string, $pattern, $timeout = 2) {
my $coincidio;
eval {
local $SIG{ALRM} = sub { die "Timeout de expresión regular\n" };
alarm($timeout);
$coincidio = $string =~ $pattern;
alarm(0);
};
alarm(0);
die $@ si $@;
return $coincidio;
}
Operaciones Seguras de Archivos
Open de Tres Argumentos
use v5.36;
# Bueno: Open de tres argumentos, manejador de archivo léxico, verificar retorno
sub leer_archivo($path) {
open my $fh, '<:encoding(UTF-8)', $path
or die "No se puede abrir '$path': $!\n";
local $/;
my $contenido = <$fh>;
close $fh;
return $contenido;
}
# Malo: Open de dos argumentos con datos del usuario (inyección de comandos)
sub mal_leer($path) {
open my $fh, $path; # Si $path = "|rm -rf /", ejecuta un comando!
open my $fh, "< $path"; # Inyección de metacaracteres del shell
}
Prevención de TOCTOU y Path Traversal
use v5.36;
use Fcntl qw(:DEFAULT :flock);
use File::Spec;
use Cwd qw(realpath);
# Creación atómica de archivos
sub crear_archivo_seguro($path) {
sysopen(my $fh, $path, O_WRONLY | O_CREAT | O_EXCL, 0600)
or die "No se puede crear '$path': $!\n";
return $fh;
}
# Validar que la ruta permanezca dentro del directorio permitido
sub ruta_segura($dir_base, $ruta_usuario) {
my $real = realpath(File::Spec->catfile($dir_base, $ruta_usuario))
// die "La ruta no existe\n";
my $base_real = realpath($dir_base)
// die "El directorio base no existe\n";
die "Path traversal bloqueado\n" a menos que $real =~ /^\Q$base_real\E(?:\/|\z)/;
return $real;
}
Use File::Temp para archivos temporales (tempfile(UNLINK => 1)) y flock(LOCK_EX) para prevenir condiciones de carrera.
Ejecución Segura de Procesos
Forma Lista de system y exec
use v5.36;
# Bueno: Forma lista — sin interpolación del shell
sub ejecutar_comando(@cmd) {
system(@cmd) == 0
or die "El comando falló: @cmd\n";
}
ejecutar_comando('grep', '-r', $patron_usuario, '/var/log/app/');
# Bueno: Capturar salida de forma segura con IPC::Run3
use IPC::Run3;
sub capturar_salida(@cmd) {
my ($stdout, $stderr);
run3(\@cmd, \undef, \$stdout, \$stderr);
si ($?) {
die "El comando falló (código $?): $stderr\n";
}
return $stdout;
}
# Malo: Forma cadena — ¡inyección de shell!
sub mala_busqueda($pattern) {
system("grep -r '$pattern' /var/log/app/"); # Si $pattern = "'; rm -rf / #"
}
# Malo: Backticks con interpolación
my $output = `ls $dir_usuario`; # Riesgo de inyección de shell
También use Capture::Tiny para capturar stdout/stderr de comandos externos de forma segura.
Prevención de Inyección SQL
Placeholders de DBI
use v5.36;
use DBI;
my $dbh = DBI->connect($dsn, $user, $pass, {
RaiseError => 1,
PrintError => 0,
AutoCommit => 1,
});
# Bueno: Consultas parametrizadas — use siempre placeholders
sub buscar_usuario($dbh, $email) {
my $sth = $dbh->prepare('SELECT * FROM users WHERE email = ?');
$sth->execute($email);
return $sth->fetchrow_hashref;
}
sub buscar_usuarios($dbh, $name, $status) {
my $sth = $dbh->prepare(
'SELECT * FROM users WHERE name LIKE ? AND status = ? ORDER BY name'
);
$sth->execute("%$name%", $status);
return $sth->fetchall_arrayref({});
}
# Malo: Interpolación de cadenas en SQL (¡vulnerabilidad SQLi!)
sub mal_buscar($dbh, $email) {
my $sth = $dbh->prepare("SELECT * FROM users WHERE email = '$email'");
# Si $email = "' OR 1=1 --", devuelve todos los usuarios
$sth->execute;
return $sth->fetchrow_hashref;
}
Listas Blancas de Columnas Dinámicas
use v5.36;
# Bueno: Validar nombres de columna contra una lista blanca
sub ordenar_por($dbh, $column, $direction) {
my %cols_permitidas = map { $_ => 1 } qw(name email created_at);
my %dirs_permitidas = map { $_ => 1 } qw(ASC DESC);
die "Columna inválida: $column\n" a menos que $cols_permitidas{$column};
die "Dirección inválida: $direction\n" a menos que $dirs_permitidas{uc $direction};
my $sth = $dbh->prepare("SELECT * FROM users ORDER BY $column $direction");
$sth->execute;
return $sth->fetchall_arrayref({});
}
# Malo: Interpolar directamente columna elegida por el usuario
sub mal_orden($dbh, $column) {
$dbh->prepare("SELECT * FROM users ORDER BY $column"); # SQLi!
}
DBIx::Class (Seguridad con ORM)
use v5.36;
# DBIx::Class genera consultas parametrizadas seguras
my @usuarios = $schema->resultset('User')->search({
status => 'active',
email => { -like => '%@example.com' },
}, {
order_by => { -asc => 'name' },
rows => 50,
});
Seguridad Web
Prevención de XSS
use v5.36;
use HTML::Entities qw(encode_entities);
use URI::Escape qw(uri_escape_utf8);
# Bueno: Codificar salida para contexto HTML
sub html_seguro($input_usuario) {
return encode_entities($input_usuario);
}
# Bueno: Codificar para contexto URL
sub parametro_url_seguro($value) {
return uri_escape_utf8($value);
}
# Bueno: Codificar para contexto JSON
use JSON::MaybeXS qw(encode_json);
sub json_seguro($data) {
return encode_json($data); # Maneja el escape
}
# Auto-escaped en plantillas (Mojolicious)
# <%= $input_usuario %> — auto-escaped (seguro)
# <%== $html_crudo %> — salida cruda (peligroso, usar solo para contenido confiable)
# Auto-escaped en plantillas (Template Toolkit)
# [% input_usuario | html %] — codificación HTML explícita
# Malo: Salida cruda en HTML
sub mal_html($input) {
print "<div>$input</div>"; # XSS si $input contiene <script>
}
Protección CSRF
use v5.36;
use Crypt::URandom qw(urandom);
use MIME::Base64 qw(encode_base64url);
sub generar_token_csrf() {
return encode_base64url(urandom(32));
}
Use comparación en tiempo constante al verificar tokens. La mayoría de los frameworks web (Mojolicious, Dancer2, Catalyst) proporcionan protección CSRF integrada — prefiera esas antes que soluciones artesanales.
Seguridad de Sesión y Cabeceras
use v5.36;
# Sesión y cabeceras en Mojolicious
$app->secrets(['secreto-largo-aleatorio-rotado-regularmente']);
$app->sessions->secure(1); # Solo HTTPS
$app->sessions->samesite('Lax');
$app->hook(after_dispatch => sub ($c) {
$c->res->headers->header('X-Content-Type-Options' => 'nosniff');
$c->res->headers->header('X-Frame-Options' => 'DENY');
$c->res->headers->header('Content-Security-Policy' => "default-src 'self'");
$c->res->headers->header('Strict-Transport-Security' => 'max-age=31536000; includeSubDomains');
});
Codificación de Salida
Codifique siempre la salida según su contexto: HTML::Entities::encode_entities() para HTML, URI::Escape::uri_escape_utf8() para URLs, JSON::MaybeXS::encode_json() para JSON.
Seguridad de Módulos CPAN
- Fije versiones en cpanfile:
requires 'DBI', '== 1.643'; - Prefiera módulos mantenidos: Verifique MetaCPAN para lanzamientos recientes
- Minimice dependencias: Cada dependencia es una superficie de ataque
Herramientas de Seguridad
Políticas de Seguridad de perlcritic
# .perlcriticrc — configuración enfocada en seguridad
severity = 3
theme = security + core
# Requerir open de tres argumentos
[InputOutput::RequireThreeArgOpen]
severity = 5
# Requerir llamadas al sistema verificadas
[InputOutput::RequireCheckedSyscalls]
functions = :builtins
severity = 4
# Prohibir eval de cadenas
[BuiltinFunctions::ProhibitStringyEval]
severity = 5
# Prohibir operadores de backtick
[InputOutput::ProhibitBacktickOperators]
severity = 4
# Requerir verificación de taint en CGI
[Modules::RequireTaintChecking]
severity = 5
# Prohibir open de dos argumentos
[InputOutput::ProhibitTwoArgOpen]
severity = 5
# Prohibir manejadores de archivo en palabras sueltas
[InputOutput::ProhibitBarewordFileHandles]
severity = 5
Ejecutar perlcritic
# Verificar un archivo
perlcritic --severity 3 --theme security lib/MyApp/Handler.pm
# Verificar todo el proyecto
perlcritic --severity 3 --theme security lib/
# Integración con CI
perlcritic --severity 4 --theme security --quiet lib/ || exit 1
Lista de Verificación Rápida de Seguridad
| Verificación | Qué Verificar |
|---|---|
| Modo taint | Bandera -T en scripts CGI/web |
| Validación de entrada | Patrones de lista blanca, límites de longitud |
| Operaciones de archivos | Open de tres argumentos, comprobaciones de path traversal |
| Ejecución de procesos | Forma lista de system, sin interpolación del shell |
| Consultas SQL | Placeholders de DBI, nunca interpolar |
| Salida HTML | encode_entities(), auto-escaped en plantillas |
| Tokens CSRF | Generados y verificados en solicitudes que cambian estado |
| Configuración de sesión | Cookies Secure, HttpOnly, SameSite |
| Cabeceras HTTP | CSP, X-Frame-Options, HSTS |
| Dependencias | Versiones fijadas, módulos auditados |
| Seguridad de expresiones regulares | Sin cuantificadores anidados, patrones anclados |
| Mensajes de error | Sin rastreos de pila ni rutas filtradas a usuarios |
Anti-Patrones
# 1. Open de dos argumentos con datos del usuario (inyección de comandos)
open my $fh, $input_usuario; # Vulnerabilidad CRÍTICA
# 2. system en forma cadena (inyección de shell)
system("convert $archivo_usuario output.png"); # Vulnerabilidad CRÍTICA
# 3. Interpolación de cadenas SQL
$dbh->do("DELETE FROM users WHERE id = $id"); # SQLi
# 4. eval con entrada del usuario (inyección de código)
eval $codigo_usuario; # Ejecución remota de código
# 5. Confiar en $ENV sin sanitizar
my $path = $ENV{UPLOAD_DIR}; # Podría ser manipulado
system("ls $path"); # Doble vulnerabilidad
# 6. Deshabilitar taint sin validación
($input) = $input =~ /(.*)/s; # Descontaminación perezosa — anula el propósito
# 7. Datos de usuario en bruto en HTML
print "<div>Bienvenido, $username!</div>"; # XSS
# 8. Redirecciones no validadas
print $cgi->redirect($url_usuario); # Redirección abierta
Recuerde: La flexibilidad de Perl es poderosa pero requiere disciplina. Use el modo taint para código expuesto a la web, valide toda entrada con listas blancas, use placeholders de DBI para cada consulta, y codifique toda salida según su contexto. Defensa en profundidad — nunca confíe en una sola capa.