Padrões de Segurança em Perl
Diretrizes abrangentes de segurança para aplicações Perl, cobrindo validação de entrada, prevenção de injeção e práticas de codificação segura.
Quando Ativar
- Manipulando entrada de usuário em aplicações Perl
- Construindo aplicações web em Perl (CGI, Mojolicious, Dancer2, Catalyst)
- Revisando código Perl em busca de vulnerabilidades de segurança
- Realizando operações de arquivo com caminhos fornecidos pelo usuário
- Executando comandos do sistema a partir do Perl
- Escrevendo consultas de banco de dados DBI
Como Funciona
Comece com limites de entrada cientes de taint, depois vá para fora: valide e descontamine entradas, mantenha o sistema de arquivos e a execução de processos restritos, e use consultas DBI parametrizadas em todos os lugares. Os exemplos abaixo mostram os padrões seguros que este skill espera que você aplique antes de enviar código Perl que toca em entrada do usuário, shell ou rede.
Modo Taint
O modo taint do Perl (-T) rastreia dados de fontes externas e impede que sejam usados em operações inseguras sem validação explícita.
Habilitando o Modo Taint
#!/usr/bin/perl -T
use v5.36;
# Contaminado: qualquer coisa vinda de fora do programa
my $input = $ARGV[0]; # Contaminado
my $env_path = $ENV{PATH}; # Contaminado
my $form = <STDIN>; # Contaminado
my $query = $ENV{QUERY_STRING}; # Contaminado
# Sanitize PATH no início (obrigatório no modo taint)
$ENV{PATH} = '/usr/local/bin:/usr/bin:/bin';
delete @ENV{qw(IFS CDPATH ENV BASH_ENV)};
Padrão de Descontaminação
use v5.36;
# Bom: Valide e descontamine com uma regex específica
sub descontaminar_usuario($input) {
if ($input =~ /^([a-zA-Z0-9_]{3,30})$/) {
return $1; # $1 está descontaminado
}
die "Nome de usuário inválido: deve ter 3-30 caracteres alfanuméricos\n";
}
# Bom: Valide e descontamine um caminho de arquivo
sub descontaminar_arquivo($input) {
if ($input =~ m{^([a-zA-Z0-9._-]+)$}) {
return $1;
}
die "Nome de arquivo inválido: contém caracteres inseguros\n";
}
# Ruim: Descontaminação excessivamente permissiva (anula o propósito)
sub mau_descontaminar($input) {
$input =~ /^(.*)$/s;
return $1; # Aceita QUALQUER COISA — inútil
}
Validação de Entrada
Allowlist em vez de Blocklist
use v5.36;
# Bom: Allowlist — defina exatamente o que é permitido
sub validar_campo_ordenacao($campo) {
my %permitidos = map { $_ => 1 } qw(name email created_at updated_at);
die "Campo de ordenação inválido: $campo\n" unless $permitidos{$campo};
return $campo;
}
# Bom: Valide com padrões específicos
sub validar_email($email) {
if ($email =~ /^([a-zA-Z0-9._%+-]+\@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})$/) {
return $1;
}
die "Endereço de email inválido\n";
}
sub validar_inteiro($input) {
if ($input =~ /^(-?\d{1,10})$/) {
return $1 + 0; # Coagir para número
}
die "Inteiro inválido\n";
}
# Ruim: Blocklist — sempre incompleta
sub mau_validar($input) {
die "Inválido" if $input =~ /[<>"';&|]/; # Perde ataques codificados
return $input;
}
Restrições de Comprimento
use v5.36;
sub validar_comentario($texto) {
die "Comentário é obrigatório\n" unless length($texto) > 0;
die "Comentário excede 10000 caracteres\n" if length($texto) > 10_000;
return $texto;
}
Expressões Regulares Seguras
Prevenção de ReDoS
O catastrophic backtracking ocorre com quantificadores aninhados em padrões sobrepostos.
use v5.36;
# Ruim: Vulnerável a ReDoS (backtracking exponencial)
my $regex_ruim = qr/^(a+)+$/; # Quantificadores aninhados
my $regex_ruim2 = qr/^([a-zA-Z]+)*$/; # Quantificadores aninhados em classe
my $regex_ruim3 = qr/^(.*?,){10,}$/; # Combinação repetida greedy/lazy
# Bom: Reescreva sem aninhamento
my $regex_bom = qr/^a+$/; # Quantificador único
my $regex_bom2 = qr/^[a-zA-Z]+$/; # Quantificador único em classe
# Bom: Use quantificadores possessivos ou grupos atômicos para evitar backtracking
my $regex_seguro = qr/^[a-zA-Z]++$/; # Possessivo (5.10+)
my $regex_seguro2 = qr/^(?>a+)$/; # Grupo atômico
# Bom: Imponha timeout em padrões não confiáveis
use POSIX qw(alarm);
sub correspondencia_segura($string, $pattern, $timeout = 2) {
my $correspondeu;
eval {
local $SIG{ALRM} = sub { die "Tempo limite da regex excedido\n" };
alarm($timeout);
$correspondeu = $string =~ $pattern;
alarm(0);
};
alarm(0);
die $@ if $@;
return $correspondeu;
}
Operações Seguras com Arquivos
Open com Três Argumentos
use v5.36;
# Bom: Open com três argumentos, filehandle lexical, verifique retorno
sub ler_arquivo($caminho) {
open my $fh, '<:encoding(UTF-8)', $caminho
or die "Não foi possível abrir '$caminho': $!\n";
local $/;
my $conteudo = <$fh>;
close $fh;
return $conteudo;
}
# Ruim: Open com dois argumentos com dados do usuário (injeção de comandos)
sub mau_ler($caminho) {
open my $fh, $caminho; # Se $caminho = "|rm -rf /", executa comando!
open my $fh, "< $caminho"; # Injeção de metacaractere de shell
}
Prevenção de TOCTOU e Path Traversal
use v5.36;
use Fcntl qw(:DEFAULT :flock);
use File::Spec;
use Cwd qw(realpath);
# Criação atômica de arquivo
sub criar_arquivo_seguro($caminho) {
sysopen(my $fh, $caminho, O_WRONLY | O_CREAT | O_EXCL, 0600)
or die "Não foi possível criar '$caminho': $!\n";
return $fh;
}
# Valida que o caminho permanece dentro do diretório permitido
sub caminho_seguro($diretorio_base, $caminho_usuario) {
my $real = realpath(File::Spec->catfile($diretorio_base, $caminho_usuario))
// die "O caminho não existe\n";
my $base_real = realpath($diretorio_base)
// die "O diretório base não existe\n";
die "Path traversal bloqueado\n" unless $real =~ /^\Q$base_real\E(?:\/|\z)/;
return $real;
}
Use File::Temp para arquivos temporários (tempfile(UNLINK => 1)) e flock(LOCK_EX) para evitar condições de corrida.
Execução Segura de Processos
system e exec na Forma de Lista
use v5.36;
# Bom: Forma de lista — sem interpolação de shell
sub executar_comando(@cmd) {
system(@cmd) == 0
or die "Comando falhou: @cmd\n";
}
executar_comando('grep', '-r', $padrao_usuario, '/var/log/app/');
# Bom: Capture a saída com segurança usando IPC::Run3
use IPC::Run3;
sub capturar_saida(@cmd) {
my ($stdout, $stderr);
run3(\@cmd, \undef, \$stdout, \$stderr);
if ($?) {
die "Comando falhou (código $?): $stderr\n";
}
return $stdout;
}
# Ruim: Forma de string — injeção de shell!
sub mau_buscar($pattern) {
system("grep -r '$pattern' /var/log/app/"); # Se $pattern = "'; rm -rf / #"
}
# Ruim: Backticks com interpolação
my $saida = `ls $diretorio_usuario`; # Risco de injeção de shell
Use também Capture::Tiny para capturar stdout/stderr de comandos externos com segurança.
Prevenção de Injeção SQL
Placeholders DBI
use v5.36;
use DBI;
my $dbh = DBI->connect($dsn, $user, $pass, {
RaiseError => 1,
PrintError => 0,
AutoCommit => 1,
});
# Bom: Consultas parametrizadas — sempre use placeholders
sub encontrar_usuario($dbh, $email) {
my $sth = $dbh->prepare('SELECT * FROM users WHERE email = ?');
$sth->execute($email);
return $sth->fetchrow_hashref;
}
sub buscar_usuarios($dbh, $nome, $status) {
my $sth = $dbh->prepare(
'SELECT * FROM users WHERE name LIKE ? AND status = ? ORDER BY name'
);
$sth->execute("%$nome%", $status);
return $sth->fetchall_arrayref({});
}
# Ruim: Interpolação de string no SQL (vulnerabilidade SQLi!)
sub mau_encontrar($dbh, $email) {
my $sth = $dbh->prepare("SELECT * FROM users WHERE email = '$email'");
# Se $email = "' OR 1=1 --", retorna todos os usuários
$sth->execute;
return $sth->fetchrow_hashref;
}
Allowlists para Colunas Dinâmicas
use v5.36;
# Bom: Valide nomes de colunas contra uma allowlist
sub ordenar_por($dbh, $coluna, $direcao) {
my %colunas_permitidas = map { $_ => 1 } qw(name email created_at);
my %direcoes_permitidas = map { $_ => 1 } qw(ASC DESC);
die "Coluna inválida: $coluna\n" unless $colunas_permitidas{$coluna};
die "Direção inválida: $direcao\n" unless $direcoes_permitidas{uc $direcao};
my $sth = $dbh->prepare("SELECT * FROM users ORDER BY $coluna $direcao");
$sth->execute;
return $sth->fetchall_arrayref({});
}
# Ruim: Interpolar coluna escolhida pelo usuário diretamente
sub mau_ordenar($dbh, $coluna) {
$dbh->prepare("SELECT * FROM users ORDER BY $coluna"); # SQLi!
}
DBIx::Class (Segurança com ORM)
use v5.36;
# DBIx::Class gera consultas parametrizadas seguras
my @usuarios = $schema->resultset('User')->search({
status => 'active',
email => { -like => '%@example.com' },
}, {
order_by => { -asc => 'name' },
rows => 50,
});
Segurança Web
Prevenção de XSS
use v5.36;
use HTML::Entities qw(encode_entities);
use URI::Escape qw(uri_escape_utf8);
# Bom: Codifique a saída para contexto HTML
sub html_seguro($entrada_usuario) {
return encode_entities($entrada_usuario);
}
# Bom: Codifique para contexto de URL
sub parametro_url_seguro($valor) {
return uri_escape_utf8($valor);
}
# Bom: Codifique para contexto JSON
use JSON::MaybeXS qw(encode_json);
sub json_seguro($dados) {
return encode_json($dados); # Lida com escaping
}
# Escapamento automático em templates (Mojolicious)
# <%= $entrada_usuario %> — auto-escapado (seguro)
# <%== $html_cru %> — saída crua (perigoso, use apenas para conteúdo confiável)
# Escapamento automático em templates (Template Toolkit)
# [% entrada_usuario | html %] — codificação HTML explícita
# Ruim: Saída crua em HTML
sub html_ruim($input) {
print "<div>$input</div>"; # XSS se $input contiver <script>
}
Proteção CSRF
use v5.36;
use Crypt::URandom qw(urandom);
use MIME::Base64 qw(encode_base64url);
sub gerar_token_csrf() {
return encode_base64url(urandom(32));
}
Use comparação de tempo constante ao verificar tokens. A maioria dos frameworks web (Mojolicious, Dancer2, Catalyst) fornece proteção CSRF embutida — prefira essas soluções em vez de implementações manuais.
Segurança de Sessão e Cabeçalhos
use v5.36;
# Mojolicious sessão + cabeçalhos
$app->secrets(['segredo-longo-aletatório-rotacionado-regularmente']);
$app->sessions->secure(1); # Apenas 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');
});
Codificação de Saída
Sempre codifique a saída para seu contexto: HTML::Entities::encode_entities() para HTML, URI::Escape::uri_escape_utf8() para URLs, JSON::MaybeXS::encode_json() para JSON.
Segurança de Módulos CPAN
- Fixar versões no cpanfile:
requires 'DBI', '== 1.643'; - Preferir módulos mantidos: Verifique MetaCPAN por lançamentos recentes
- Minimizar dependências: Cada dependência é uma superfície de ataque
Ferramentas de Segurança
Políticas de Segurança do perlcritic
# .perlcriticrc — configuração focada em segurança
severity = 3
theme = security + core
# Exigir open com três argumentos
[InputOutput::RequireThreeArgOpen]
severity = 5
# Exigir chamadas de sistema verificadas
[InputOutput::RequireCheckedSyscalls]
functions = :builtins
severity = 4
# Proibir eval com string
[BuiltinFunctions::ProhibitStringyEval]
severity = 5
# Proibir operadores backtick
[InputOutput::ProhibitBacktickOperators]
severity = 4
# Exigir verificação de taint em CGI
[Modules::RequireTaintChecking]
severity = 5
# Proibir open com dois argumentos
[InputOutput::ProhibitTwoArgOpen]
severity = 5
# Proibir filehandles sem palavra
[InputOutput::ProhibitBarewordFileHandles]
severity = 5
Executando perlcritic
# Verificar um arquivo
perlcritic --severity 3 --theme security lib/MyApp/Handler.pm
# Verificar projeto inteiro
perlcritic --severity 3 --theme security lib/
# Integração CI
perlcritic --severity 4 --theme security --quiet lib/ || exit 1
Lista de Verificação Rápida de Segurança
| Verificação | O que Validar |
|---|---|
| Modo taint | Flag -T em scripts CGI/web |
| Validação de entrada | Padrões allowlist, limites de comprimento |
| Operações com arquivos | Open com três argumentos, verificação de path traversal |
| Execução de processos | Forma de lista do system, sem interpolação de shell |
| Consultas SQL | Placeholders DBI, nunca interpolar |
| Saída HTML | encode_entities(), auto-escapamento de template |
| Tokens CSRF | Gerados, verificados em requisições que alteram estado |
| Configuração de sessão | Cookies Secure, HttpOnly, SameSite |
| Cabeçalhos HTTP | CSP, X-Frame-Options, HSTS |
| Dependências | Versões fixadas, módulos auditados |
| Segurança de regex | Sem quantificadores aninhados, padrões ancorados |
| Mensagens de erro | Nenhum stack trace ou caminho vazado para usuários |
Anti-Padrões
# 1. Open com dois argumentos com dados do usuário (injeção de comandos)
open my $fh, $entrada_usuario; # Vulnerabilidade CRÍTICA
# 2. system na forma de string (injeção de shell)
system("convert $arquivo_usuario output.png"); # Vulnerabilidade CRÍTICA
# 3. Interpolação de string SQL
$dbh->do("DELETE FROM users WHERE id = $id"); # SQLi
# 4. eval com entrada do usuário (injeção de código)
eval $codigo_usuario; # Execução remota de código
# 5. Confiar em $ENV sem sanitizar
my $caminho = $ENV{UPLOAD_DIR}; # Pode ser manipulado
system("ls $caminho"); # Dupla vulnerabilidade
# 6. Desativar taint sem validação
($input) = $input =~ /(.*)/s; # Descontaminação preguiçosa — anula propósito
# 7. Dados crus do usuário em HTML
print "<div>Bem-vindo, $username!</div>"; # XSS
# 8. Redirecionamentos não validados
print $cgi->redirect($url_usuario); # Redirecionamento aberto
Lembre-se: A flexibilidade do Perl é poderosa, mas requer disciplina. Use o modo taint para código voltado para web, valide todas as entradas com allowlists, use placeholders DBI para todas as consultas e codifique toda a saída para seu contexto. Defesa em profundidade — nunca confie em uma única camada.