Patrones de Seguridad para Perl
Guía integral 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 de base de datos con DBI
Cómo funciona
Comienza con los límites de entrada sensibles a taint, luego avanza hacia afuera: valida y descontamina entradas, mantén el sistema de archivos y la ejecución de procesos bajo control, y usa consultas DBI parametrizadas en todas partes. Los ejemplos a continuación muestran los valores seguros predeterminados que este skill espera que apliques antes de publicar código Perl que toque entrada de usuario, el shell o la red.
Modo Taint
El modo taint de Perl (-T) rastrea los datos de fuentes externas y evita que se utilicen en operaciones inseguras sin validación explícita.
Habilitando el Modo Taint
#!/usr/bin/perl -T
use v5.36;
# Contaminado: cualquier cosa de fuera del programa
my $input = $ARGV[0]; # Contaminado
my $env_path = $ENV{PATH}; # Contaminado
my $form = <STDIN>; # Contaminado
my $query = $ENV{QUERY_STRING}; # Contaminado
# Sanear PATH desde el principio (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;
# Bien: 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";
}
# Bien: Validar y descontaminar una ruta de archivo
sub descontaminar_archivo($input) {
if ($input =~ m{^([a-zA-Z0-9._-]+)$}) {
return $1;
}
die "Nombre de archivo inválido: contiene caracteres inseguros\n";
}
# Mal: Descontaminación demasiado permisiva (anula el propósito)
sub mala_descontaminacion($input) {
$input =~ /^(.*)$/s;
return $1; # Acepta CUALQUIER COSA — inútil
}
Validación de Entrada
Lista Blanca sobre Lista Negra
use v5.36;
# Bien: Lista blanca — define exactamente lo que está permitido
sub validar_campo_orden($field) {
my %permitido = map { $_ => 1 } qw(name email created_at updated_at);
die "Campo de orden inválido: $field\n" unless $permitido{$field};
return $field;
}
# Bien: 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 correo inválida\n";
}
sub validar_entero($input) {
if ($input =~ /^(-?\d{1,10})$/) {
return $1 + 0; # Forzar a número
}
die "Entero inválido\n";
}
# Mal: Lista negra — siempre incompleta
sub mala_validacion($input) {
die "Inválido" if $input =~ /[<>"';&|]/; # Omite ataques codificados
return $input;
}
Restricciones de Longitud
use v5.36;
sub validar_comentario($text) {
die "Se requiere comentario\n" unless length($text) > 0;
die "El comentario excede los 10000 caracteres\n" if 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;
# Mal: Vulnerable a ReDoS (retroceso exponencial)
my $re_mal = qr/^(a+)+$/; # Cuantificadores anidados
my $re_mal2 = qr/^([a-zA-Z]+)*$/; # Cuantificadores anidados en clase
my $re_mal3 = qr/^(.*?,){10,}$/; # Combinación repetida greedy/lazy
# Bien: Reescribir sin anidamiento
my $re_bien = qr/^a+$/; # Cuantificador único
my $re_bien2 = qr/^[a-zA-Z]+$/; # Cuantificador único en clase
# Bien: Usar cuantificadores posesivos o grupos atómicos para evitar retroceso
my $re_seguro = qr/^[a-zA-Z]++$/; # Posesivo (5.10+)
my $re_seguro2 = qr/^(?>a+)$/; # Grupo atómico
# Bien: Forzar tiempo de espera en patrones no confiables
use POSIX qw(alarm);
sub coincidencia_segura($string, $pattern, $timeout = 2) {
my $coincidio;
eval {
local $SIG{ALRM} = sub { die "Tiempo de espera de regex agotado\n" };
alarm($timeout);
$coincidio = $string =~ $pattern;
alarm(0);
};
alarm(0);
die $@ if $@;
return $coincidio;
}
Operaciones Seguras de Archivos
Open de Tres Argumentos
use v5.36;
# Bien: 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 $content = <$fh>;
close $fh;
return $content;
}
# Mal: Open de dos argumentos con datos de usuario (inyección de comandos)
sub mala_lectura($path) {
open my $fh, $path; # Si $path = "|rm -rf /", ¡ejecuta comando!
open my $fh, "< $path"; # Inyección de metacaracteres de 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($base_dir, $user_path) {
my $real = realpath(File::Spec->catfile($base_dir, $user_path))
// die "La ruta no existe\n";
my $base_real = realpath($base_dir)
// die "El directorio base no existe\n";
die "Path traversal bloqueado\n" unless $real =~ /^\Q$base_real\E(?:\/|\z)/;
return $real;
}
Usa File::Temp para archivos temporales (tempfile(UNLINK => 1)) y flock(LOCK_EX) para evitar condiciones de carrera.
Ejecución Segura de Procesos
system y exec en Forma de Lista
use v5.36;
# Bien: Forma de lista — sin interpolación de shell
sub ejecutar_comando(@cmd) {
system(@cmd) == 0
or die "Comando falló: @cmd\n";
}
ejecutar_comando('grep', '-r', $patron_usuario, '/var/log/app/');
# Bien: Capturar salida de forma segura con IPC::Run3
use IPC::Run3;
sub capturar_salida(@cmd) {
my ($stdout, $stderr);
run3(\@cmd, \undef, \$stdout, \$stderr);
if ($?) {
die "Comando falló (salida $?): $stderr\n";
}
return $stdout;
}
# Mal: Forma de cadena — ¡inyección de shell!
sub busqueda_mala($pattern) {
system("grep -r '$pattern' /var/log/app/"); # Si $pattern = "'; rm -rf / #"
}
# Mal: Backticks con interpolación
my $output = `ls $directorio_usuario`; # Riesgo de inyección de shell
También usa 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,
});
# Bien: Consultas parametrizadas — usa 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({});
}
# Mal: Interpolación de cadenas en SQL (¡vulnerabilidad SQLi!)
sub mala_busqueda($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;
# Bien: Validar nombres de columna contra una lista blanca
sub order_by($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" unless $cols_permitidas{$column};
die "Dirección inválida: $direction\n" unless $dirs_permitidas{uc $direction};
my $sth = $dbh->prepare("SELECT * FROM users ORDER BY $column $direction");
$sth->execute;
return $sth->fetchall_arrayref({});
}
# Mal: Interpolar directamente la columna elegida por el usuario
sub mal_order($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 @users = $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);
# Bien: Codificar salida para contexto HTML
sub html_seguro($user_input) {
return encode_entities($user_input);
}
# Bien: Codificar para contexto URL
sub parametro_url_seguro($value) {
return uri_escape_utf8($value);
}
# Bien: Codificar para contexto JSON
use JSON::MaybeXS qw(encode_json);
sub json_seguro($data) {
return encode_json($data); # Maneja el escape
}
# Auto-escape de plantillas (Mojolicious)
# <%= $user_input %> — auto-escaped (seguro)
# <%== $raw_html %> — salida sin escape (peligroso, usar solo para contenido confiable)
# Auto-escape de plantillas (Template Toolkit)
# [% user_input | html %] — codificación HTML explícita
# Mal: Salida sin procesar en HTML
sub html_malo($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));
}
Utiliza comparación de tiempo constante al verificar tokens. La mayoría de los frameworks web (Mojolicious, Dancer2, Catalyst) proporcionan protección CSRF incorporada — prefiere esas sobre soluciones hechas a mano.
Seguridad de Sesiones y Cabeceras
use v5.36;
# Mojolicious sesión + cabeceras
$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
Siempre codifica 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
- Fija versiones en cpanfile:
requires 'DBI', '== 1.643'; - Prefiere módulos mantenidos: Revisa MetaCPAN para lanzamientos recientes
- Minimiza 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 sin comillas
[InputOutput::ProhibitBarewordFileHandles]
severity = 5
Ejecutando 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 CI
perlcritic --severity 4 --theme security --quiet lib/ || exit 1
Lista de Verificación Rápida de Seguridad
| Verificación | Qué Comprobar |
|---|---|
| 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 | system en forma de lista, sin interpolación de shell |
| Consultas SQL | Placeholders de DBI, nunca interpolar |
| Salida HTML | encode_entities(), auto-escape de plantillas |
| Tokens CSRF | Generados, verificados en solicitudes que cambian el estado |
| Configuración de sesión | Cookies Secure, HttpOnly, SameSite |
| Cabeceras HTTP | CSP, X-Frame-Options, HSTS |
| Dependencias | Versiones fijas, módulos auditados |
| Seguridad de regex | Sin cuantificadores anidados, patrones anclados |
| Mensajes de error | Sin rastros de pila ni rutas filtradas a los usuarios |
Anti-Patrones
# 1. Open de dos argumentos con datos de usuario (inyección de comandos)
open my $fh, $user_input; # Vulnerabilidad CRÍTICA
# 2. system en forma de cadena (inyección de shell)
system("convert $user_file output.png"); # Vulnerabilidad CRÍTICA
# 3. Interpolación de cadenas en SQL
$dbh->do("DELETE FROM users WHERE id = $id"); # SQLi
# 4. eval con entrada de usuario (inyección de código)
eval $user_code; # Ejecución remota de código
# 5. Confiar en $ENV sin sanear
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 sin procesar en HTML
print "<div>Welcome, $username!</div>"; # XSS
# 8. Redirecciones no validadas
print $cgi->redirect($url_usuario); # Redirección abierta
Recuerda: La flexibilidad de Perl es poderosa pero requiere disciplina. Usa el modo taint para código orientado a la web, valida toda la entrada con listas blancas, usa placeholders de DBI para cada consulta y codifica toda la salida según su contexto. Defensa en profundidad — nunca confíes en una sola capa.