Modèles de Sécurité Perl
Directives de sécurité complètes pour les applications Perl, couvrant la validation des entrées, la prévention des injections et les pratiques de codage sécurisé.
Quand Activer
- Traitement des entrées utilisateur dans les applications Perl
- Construction d'applications web Perl (CGI, Mojolicious, Dancer2, Catalyst)
- Révision de code Perl pour des vulnérabilités de sécurité
- Opérations sur fichiers avec des chemins fournis par l'utilisateur
- Exécution de commandes système depuis Perl
- Écriture de requêtes de base de données DBI
Comment Ça Fonctionne
Commencez par les limites d'entrée sensibles au mode taint, puis élargissez : validez et décontaminez les entrées, maintenez le système de fichiers et l'exécution de processus sous contrôle, et utilisez des requêtes DBI paramétrées partout. Les exemples ci-dessous montrent les valeurs par défaut sécurisées que cette compétence s'attend à voir appliquées avant de déployer du code Perl qui touche aux entrées utilisateur, au shell ou au réseau.
Mode Taint
Le mode taint de Perl (-T) suit les données provenant de sources externes et empêche leur utilisation dans des opérations dangereuses sans validation explicite.
Activation du Mode Taint
#!/usr/bin/perl -T
use v5.36;
# Tainté : tout ce qui vient de l'extérieur du programme
my $input = $ARGV[0]; # Tainté
my $env_path = $ENV{PATH}; # Tainté
my $form = <STDIN>; # Tainté
my $query = $ENV{QUERY_STRING}; # Tainté
# Nettoyer PATH dès le début (obligatoire en mode taint)
$ENV{PATH} = '/usr/local/bin:/usr/bin:/bin';
delete @ENV{qw(IFS CDPATH ENV BASH_ENV)};
Modèle de Décontamination
use v5.36;
# Bon : Valider et décontaminer avec une expression régulière spécifique
sub untaint_username($input) {
if ($input =~ /^([a-zA-Z0-9_]{3,30})$/) {
return $1; # $1 est décontaminé
}
die "Nom d'utilisateur invalide : doit comporter 3 à 30 caractères alphanumériques\n";
}
# Bon : Valider et décontaminer un chemin de fichier
sub untaint_filename($input) {
if ($input =~ m{^([a-zA-Z0-9._-]+)$}) {
return $1;
}
die "Nom de fichier invalide : contient des caractères dangereux\n";
}
# Mauvais : Décontamination trop permissive (va à l'encontre du but)
sub bad_untaint($input) {
$input =~ /^(.*)$/s;
return $1; # Accepte N'IMPORTE QUOI — inutile
}
Validation des Entrées
Liste Blanche Plutôt que Liste Noire
use v5.36;
# Bon : Liste blanche — définir exactement ce qui est autorisé
sub validate_sort_field($field) {
my %allowed = map { $_ => 1 } qw(name email created_at updated_at);
die "Champ de tri invalide : $field\n" unless $allowed{$field};
return $field;
}
# Bon : Valider avec des modèles spécifiques
sub validate_email($email) {
if ($email =~ /^([a-zA-Z0-9._%+-]+\@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})$/) {
return $1;
}
die "Adresse e-mail invalide\n";
}
sub validate_integer($input) {
if ($input =~ /^(-?\d{1,10})$/) {
return $1 + 0; # Forcer en nombre
}
die "Entier invalide\n";
}
# Mauvais : Liste noire — toujours incomplète
sub bad_validate($input) {
die "Invalide" if $input =~ /[<>"';&|]/; # Manque les attaques encodées
return $input;
}
Contraintes de Longueur
use v5.36;
sub validate_comment($text) {
die "Le commentaire est obligatoire\n" unless length($text) > 0;
die "Le commentaire dépasse 10000 caractères\n" if length($text) > 10_000;
return $text;
}
Expressions Régulières Sûres
Prévention ReDoS
Le backtracking catastrophique se produit avec des quantificateurs imbriqués sur des motifs qui se chevauchent.
use v5.36;
# Mauvais : Vulnérable au ReDoS (backtracking exponentiel)
my $bad_re = qr/^(a+) + $/; # Quantificateurs imbriqués
my $bad_re2 = qr/^([a-zA-Z]+)*$/; # Quantificateurs imbriqués sur classe
my $bad_re3 = qr/^(.*?,){10,}$/; # Combinaison gourmande/paresseuse répétée
# Bon : Réécrire sans imbrication
my $good_re = qr/^a+$/; # Quantificateur unique
my $good_re2 = qr/^[a-zA-Z]+$/; # Quantificateur unique sur classe
# Bon : Utiliser des quantificateurs possessifs ou des groupes atomiques pour empêcher le backtracking
my $safe_re = qr/^[a-zA-Z]++$/; # Possessif (5.10+)
my $safe_re2 = qr/^(?>a+)$/; # Groupe atomique
# Bon : Imposer un délai d'attente sur les motifs non fiables
use POSIX qw(alarm);
sub safe_match($string, $pattern, $timeout = 2) {
my $matched;
eval {
local $SIG{ALRM} = sub { die "Délai d'attente de l'expression régulière dépassé\n" };
alarm($timeout);
$matched = $string =~ $pattern;
alarm(0);
};
alarm(0);
die $@ if $@;
return $matched;
}
Opérations Fichiers Sûres
Ouverture à Trois Arguments
use v5.36;
# Bon : Ouverture à trois args, handle lexical, vérifier le retour
sub read_file($path) {
open my $fh, '<:encoding(UTF-8)', $path
or die "Impossible d'ouvrir '$path' : $!\n";
local $/;
my $content = <$fh>;
close $fh;
return $content;
}
# Mauvais : Ouverture à deux args avec des données utilisateur (injection de commande)
sub bad_read($path) {
open my $fh, $path; # Si $path = "|rm -rf /", exécute une commande !
open my $fh, "< $path"; # Injection de métacaractères shell
}
Prévention TOCTOU et Traversée de Chemin
use v5.36;
use Fcntl qw(:DEFAULT :flock);
use File::Spec;
use Cwd qw(realpath);
# Création atomique de fichier
sub create_file_safe($path) {
sysopen(my $fh, $path, O_WRONLY | O_CREAT | O_EXCL, 0600)
or die "Impossible de créer '$path' : $!\n";
return $fh;
}
# Valider que le chemin reste dans le répertoire autorisé
sub safe_path($base_dir, $user_path) {
my $real = realpath(File::Spec->catfile($base_dir, $user_path))
// die "Le chemin n'existe pas\n";
my $base_real = realpath($base_dir)
// die "Le répertoire de base n'existe pas\n";
die "Traversée de chemin bloquée\n" unless $real =~ /^\Q$base_real\E(?:\/|\z)/;
return $real;
}
Utilisez File::Temp pour les fichiers temporaires (tempfile(UNLINK => 1)) et flock(LOCK_EX) pour prévenir les conditions de concurrence.
Exécution de Processus Sûre
Forme Liste de system et exec
use v5.36;
# Bon : Forme liste — pas d'interpolation shell
sub run_command(@cmd) {
system(@cmd) == 0
or die "La commande a échoué : @cmd\n";
}
run_command('grep', '-r', $user_pattern, '/var/log/app/');
# Bon : Capturer la sortie de manière sûre avec IPC::Run3
use IPC::Run3;
sub capture_output(@cmd) {
my ($stdout, $stderr);
run3(\@cmd, \undef, \$stdout, \$stderr);
if ($?) {
die "La commande a échoué (exit $?) : $stderr\n";
}
return $stdout;
}
# Mauvais : Forme chaîne — injection shell !
sub bad_search($pattern) {
system("grep -r '$pattern' /var/log/app/"); # Si $pattern = "'; rm -rf / #"
}
# Mauvais : Backticks avec interpolation
my $output = `ls $user_dir`; # Risque d'injection shell
Utilisez aussi Capture::Tiny pour capturer stdout/stderr de commandes externes de manière sûre.
Prévention des Injections SQL
Placeholders DBI
use v5.36;
use DBI;
my $dbh = DBI->connect($dsn, $user, $pass, {
RaiseError => 1,
PrintError => 0,
AutoCommit => 1,
});
# Bon : Requêtes paramétrées — toujours utiliser des placeholders
sub find_user($dbh, $email) {
my $sth = $dbh->prepare('SELECT * FROM users WHERE email = ?');
$sth->execute($email);
return $sth->fetchrow_hashref;
}
sub search_users($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({});
}
# Mauvais : Interpolation de chaîne en SQL (vulnérabilité SQLi !)
sub bad_find($dbh, $email) {
my $sth = $dbh->prepare("SELECT * FROM users WHERE email = '$email'");
# Si $email = "' OR 1=1 --", renvoie tous les utilisateurs
$sth->execute;
return $sth->fetchrow_hashref;
}
Listes Blanches de Colonnes Dynamiques
use v5.36;
# Bon : Valider les noms de colonnes contre une liste blanche
sub order_by($dbh, $column, $direction) {
my %allowed_cols = map { $_ => 1 } qw(name email created_at);
my %allowed_dirs = map { $_ => 1 } qw(ASC DESC);
die "Colonne invalide : $column\n" unless $allowed_cols{$column};
die "Direction invalide : $direction\n" unless $allowed_dirs{uc $direction};
my $sth = $dbh->prepare("SELECT * FROM users ORDER BY $column $direction");
$sth->execute;
return $sth->fetchall_arrayref({});
}
# Mauvais : Interpolation directe d'une colonne choisie par l'utilisateur
sub bad_order($dbh, $column) {
$dbh->prepare("SELECT * FROM users ORDER BY $column"); # SQLi !
}
DBIx::Class (ORM Sécurité)
use v5.36;
# DBIx::Class génère des requêtes paramétrées sûres
my @users = $schema->resultset('User')->search({
status => 'active',
email => { -like => '%@example.com' },
}, {
order_by => { -asc => 'name' },
rows => 50,
});
Sécurité Web
Prévention XSS
use v5.36;
use HTML::Entities qw(encode_entities);
use URI::Escape qw(uri_escape_utf8);
# Bon : Encoder la sortie pour le contexte HTML
sub safe_html($user_input) {
return encode_entities($user_input);
}
# Bon : Encoder pour le contexte URL
sub safe_url_param($value) {
return uri_escape_utf8($value);
}
# Bon : Encoder pour le contexte JSON
use JSON::MaybeXS qw(encode_json);
sub safe_json($data) {
return encode_json($data); # Gère l'échappement
}
# Échappement automatique des templates (Mojolicious)
# <%= $user_input %> — échappé automatiquement (sûr)
# <%== $raw_html %> — sortie brute (dangereux, utiliser uniquement pour du contenu de confiance)
# Échappement automatique des templates (Template Toolkit)
# [% user_input | html %] — encodage HTML explicite
# Mauvais : Sortie brute en HTML
sub bad_html($input) {
print "<div>$input</div>"; # XSS si $input contient <script>
}
Protection CSRF
use v5.36;
use Crypt::URandom qw(urandom);
use MIME::Base64 qw(encode_base64url);
sub generate_csrf_token() {
return encode_base64url(urandom(32));
}
Utilisez une comparaison en temps constant lors de la vérification des jetons. La plupart des frameworks web (Mojolicious, Dancer2, Catalyst) fournissent une protection CSRF intégrée — préférez ceux-ci aux solutions faites maison.
Sécurité des Sessions et des En-têtes
use v5.36;
# Mojolicious sessions + en-têtes
$app->secrets(['long-random-secret-rotated-regularly']);
$app->sessions->secure(1); # HTTPS uniquement
$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');
});
Encodage de Sortie
Encodez toujours la sortie pour son contexte : HTML::Entities::encode_entities() pour le HTML, URI::Escape::uri_escape_utf8() pour les URL, JSON::MaybeXS::encode_json() pour le JSON.
Sécurité des Modules CPAN
- Épingler les versions dans cpanfile :
requires 'DBI', '== 1.643'; - Privilégier les modules maintenus : Vérifier MetaCPAN pour les versions récentes
- Minimiser les dépendances : Chaque dépendance est une surface d'attaque
Outils de Sécurité
Politiques de Sécurité perlcritic
# .perlcriticrc — configuration axée sur la sécurité
severity = 3
theme = security + core
# Exiger l'ouverture à trois arguments
[InputOutput::RequireThreeArgOpen]
severity = 5
# Exiger des appels système vérifiés
[InputOutput::RequireCheckedSyscalls]
functions = :builtins
severity = 4
# Interdire l'évaluation de chaîne
[BuiltinFunctions::ProhibitStringyEval]
severity = 5
# Interdire les opérateurs backtick
[InputOutput::ProhibitBacktickOperators]
severity = 4
# Exiger la vérification taint dans CGI
[Modules::RequireTaintChecking]
severity = 5
# Interdire l'ouverture à deux arguments
[InputOutput::ProhibitTwoArgOpen]
severity = 5
# Interdire les handles de fichier en mots nus
[InputOutput::ProhibitBarewordFileHandles]
severity = 5
Exécution de perlcritic
# Vérifier un fichier
perlcritic --severity 3 --theme security lib/MyApp/Handler.pm
# Vérifier l'ensemble du projet
perlcritic --severity 3 --theme security lib/
# Intégration CI
perlcritic --severity 4 --theme security --quiet lib/ || exit 1
Liste de Vérification Rapide
| Vérification | Que Vérifier |
|---|---|
| Mode taint | Drapeau -T sur les scripts CGI/web |
| Validation des entrées | Modèles de liste blanche, limites de longueur |
| Opérations fichiers | Ouverture à trois arguments, vérification de traversée de chemin |
| Exécution de processus | Forme liste system, pas d'interpolation shell |
| Requêtes SQL | Placeholders DBI, ne jamais interpoler |
| Sortie HTML | encode_entities(), échappement automatique des templates |
| Jetons CSRF | Générés, vérifiés sur les requêtes modifiant l'état |
| Configuration session | Cookies Secure, HttpOnly, SameSite |
| En-têtes HTTP | CSP, X-Frame-Options, HSTS |
| Dépendances | Versions épinglées, modules audités |
| Sécurité des regex | Pas de quantificateurs imbriqués, motifs ancrés |
| Messages d'erreur | Aucune trace de pile ou chemin divulgué aux utilisateurs |
Anti-Patrons
# 1. Ouverture à deux arguments avec des données utilisateur (injection de commande)
open my $fh, $user_input; # Vulnérabilité CRITIQUE
# 2. system sous forme chaîne (injection shell)
system("convert $user_file output.png"); # Vulnérabilité CRITIQUE
# 3. Interpolation de chaîne SQL
$dbh->do("DELETE FROM users WHERE id = $id"); # SQLi
# 4. eval avec entrée utilisateur (injection de code)
eval $user_code; # Exécution de code à distance
# 5. Faire confiance à $ENV sans nettoyage
my $path = $ENV{UPLOAD_DIR}; # Pourrait être manipulé
system("ls $path"); # Double vulnérabilité
# 6. Désactiver le mode taint sans validation
($input) = $input =~ /(.*)/s; # Décontamination paresseuse — va à l'encontre du but
# 7. Données utilisateur brutes en HTML
print "<div>Welcome, $username!</div>"; # XSS
# 8. Redirections non validées
print $cgi->redirect($user_url); # Redirection ouverte
Rappel : La flexibilité de Perl est puissante mais nécessite de la discipline. Utilisez le mode taint pour le code exposé au web, validez toutes les entrées avec des listes blanches, utilisez des placeholders DBI pour chaque requête, et encodez toute sortie pour son contexte. Défense en profondeur — ne comptez jamais sur une seule couche.