Modèles de Sécurité Perl
Guide complet de sécurité pour applications Perl : validation d’entrée, prévention d’injection et bonnes pratiques de codage sécurisé.
Quand Activer
- Traitement d’entrées utilisateur dans des applications Perl
- Développement d’applications web Perl (CGI, Mojolicious, Dancer2, Catalyst)
- Revue de code Perl pour détecter 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 DBI vers une base de données
Fonctionnement
Commencez par les frontières d’entrée sensibles au taint, puis élargissez : validez et désinfectez 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 attend 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) trace 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é
# Désinfecter PATH tôt (obligatoire en mode taint)
$ENV{PATH} = '/usr/local/bin:/usr/bin:/bin';
delete @ENV{qw(IFS CDPATH ENV BASH_ENV)};
Modèle de Désinfection (Untainting)
use v5.36;
# Bon : Valider et désinfecter avec une regex spécifique
sub untaint_username($input) {
if ($input =~ /^([a-zA-Z0-9_]{3,30})$/) {
return $1; # $1 n’est plus tainté
}
die "Nom d’utilisateur invalide : 3-30 caractères alphanumériques\n";
}
# Bon : Valider et désinfecter un chemin de fichier
sub untaint_filename($input) {
if ($input =~ m{^([a-zA-Z0-9._-]+)$}) {
return $1;
}
die "Nom de fichier invalide : caractères non autorisés\n";
}
# Mauvais : Désinfection trop permissive (contre-productive)
sub bad_untaint($input) {
$input =~ /^(.*)$/s;
return $1; # Accepte N’IMPORTE QUOI — inutile
}
Validation d’Entrée
Liste Blanche Plutôt que Liste Noire
use v5.36;
# Bon : Liste blanche — définir précisément 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 motifs spécifiques
sub validate_email($email) {
if ($email =~ /^([a-zA-Z0-9._%+-]+\@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})$/) {
return $1;
}
die "Adresse email invalide\n";
}
sub validate_integer($input) {
if ($input =~ /^(-?\d{1,10})$/) {
return $1 + 0; # Convertir en nombre
}
die "Entier invalide\n";
}
# Mauvais : Liste noire — toujours incomplète
sub bad_validate($input) {
die "Invalide" if $input =~ /[<>"';&|]/; # Rate les attaques encodées
return $input;
}
Contraintes de Longueur
use v5.36;
sub validate_comment($text) {
die "Commentaire requis\n" unless length($text) > 0;
die "Commentaire dépasse 10 000 caractères\n" if length($text) > 10_000;
return $text;
}
Expressions Régulières Sûres
Prévention ReDoS
Le backtracking catastrophique survient avec des quantificateurs imbriqués sur des motifs se chevauchant.
use v5.36;
# Mauvais : Vulnérable à 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 glouton/paresseux 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 : Forcer 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 regex 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 arguments, 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 arguments avec 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 Parcours 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 "Parcours de chemin bloqué\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 éviter les conditions de concurrence.
Exécution Sécurisée de Processus
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 "Échec de la commande : @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 "Échec de la commande (code $?) : $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 manière sûre depuis des commandes externes.
Prévention d’Injection 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 dans SQL (vulnérabilité SQLi !)
sub bad_find($dbh, $email) {
my $sth = $dbh->prepare("SELECT * FROM users WHERE email = '$email'");
# Si $email = "' OR 1=1 --", retourne tous les utilisateurs
$sth->execute;
return $sth->fetchrow_hashref;
}
Listes Blanches pour 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 (Sécurité ORM)
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 dans du 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) offrent une protection CSRF intégrée — préférez-les aux solutions maison.
Sécurité des Sessions et En-têtes
use v5.36;
# Session et en-têtes Mojolicious
$app->secrets(['secret-aléatoire-long-roté-régulièrement']);
$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 selon son contexte : HTML::Entities::encode_entities() pour le HTML, URI::Escape::uri_escape_utf8() pour les URLs, JSON::MaybeXS::encode_json() pour le JSON.
Sécurité des Modules CPAN
- Épingler les versions dans cpanfile :
requires 'DBI', '== 1.643'; - Préférer les modules maintenus : Vérifier MetaCPAN pour les versions récentes
- Minimiser les dépendances : Chaque dépendance est une surface d’attaque
Outillage de Sécurité
Politiques perlcritic
# .perlcriticrc — configuration orientée sécurité
severity = 3
theme = security + core
# Exiger l’ouverture à trois arguments
[InputOutput::RequireThreeArgOpen]
severity = 5
# Exiger les 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 de taint dans CGI
[Modules::RequireTaintChecking]
severity = 5
# Interdire l’ouverture à deux arguments
[InputOutput::ProhibitTwoArgOpen]
severity = 5
# Interdire les handles de fichier sous forme de 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 tout le 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 | À valider |
|---|---|
| Mode taint | Drapeau -T sur les scripts CGI/web |
| Validation d’entrée | Motifs de liste blanche, limites de longueur |
| Opérations fichiers | Ouverture à trois arguments, vérification de parcours de chemin |
| Exécution de processus | Forme liste de system, pas d’interpolation shell |
| Requêtes SQL | Placeholders DBI, jamais d’interpolation |
| Sortie HTML | encode_entities(), échappement automatique des templates |
| Jetons CSRF | Générés et 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 données utilisateur (injection de commande)
open my $fh, $user_input; # Vulnérabilité CRITIQUE
# 2. system sous forme de 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 le désinfecter
my $path = $ENV{UPLOAD_DIR}; # Peut être manipulé
system("ls $path"); # Double vulnérabilité
# 6. Désactiver le taint sans validation
($input) = $input =~ /(.*)/s; # Désinfection paresseuse — contre-productive
# 7. Données utilisateur brutes dans HTML
print "<div>Welcome, $username!</div>"; # XSS
# 8. Redirections non validées
print $cgi->redirect($user_url); # Redirection ouverte
Souvenez-vous : La flexibilité de Perl est puissante mais exige de la discipline. Utilisez le mode taint pour le code web, validez toutes les entrées avec des listes blanches, utilisez les placeholders DBI pour chaque requête et encodez toute sortie selon son contexte. Défense en profondeur — ne reposez jamais sur une seule couche.