Perl-Sicherheitsmuster
Umfassende Sicherheitsrichtlinien für Perl-Anwendungen, die Eingabevalidierung, Injektionsprävention und sichere Programmierpraktiken abdecken.
Wann zu aktivieren
- Verarbeitung von Benutzereingaben in Perl-Anwendungen
- Erstellung von Perl-Webanwendungen (CGI, Mojolicious, Dancer2, Catalyst)
- Überprüfung von Perl-Code auf Sicherheitslücken
- Durchführung von Dateioperationen mit benutzerbereitgestellten Pfaden
- Ausführung von Systembefehlen aus Perl heraus
- Schreiben von DBI-Datenbankabfragen
Funktionsweise
Beginnen Sie mit taint-bewussten Eingabegrenzen und arbeiten Sie sich dann nach außen vor: Validieren und enttainten Sie Eingaben, halten Sie Dateisystem- und Prozessausführungen eingeschränkt und verwenden Sie durchgängig parametrisierte DBI-Abfragen. Die untenstehenden Beispiele zeigen die sicheren Standardeinstellungen, die diese Fertigkeit vor dem Ausliefern von Perl-Code erwartet, der Benutzereingaben, die Shell oder das Netzwerk berührt.
Taint-Modus
Perls Taint-Modus (-T) verfolgt Daten aus externen Quellen und verhindert deren Verwendung in unsicheren Operationen ohne explizite Validierung.
Aktivieren des Taint-Modus
#!/usr/bin/perl -T
use v5.36;
# Getaint: alles, was von außerhalb des Programms kommt
my $input = $ARGV[0]; # Getaint
my $env_path = $ENV{PATH}; # Getaint
my $form = <STDIN>; # Getaint
my $query = $ENV{QUERY_STRING}; # Getaint
# Säubern Sie PATH frühzeitig (im Taint-Modus erforderlich)
$ENV{PATH} = '/usr/local/bin:/usr/bin:/bin';
delete @ENV{qw(IFS CDPATH ENV BASH_ENV)};
Enttaintungs-Muster
use v5.36;
# Gut: Validieren und enttainten mit einem spezifischen Regex
sub untaint_username($input) {
if ($input =~ /^([a-zA-Z0-9_]{3,30})$/) {
return $1; # $1 ist enttaintet
}
die "Ungültiger Benutzername: muss 3-30 alphanumerische Zeichen sein\n";
}
# Gut: Einen Dateipfad validieren und enttainten
sub untaint_filename($input) {
if ($input =~ m{^([a-zA-Z0-9._-]+)$}) {
return $1;
}
die "Ungültiger Dateiname: enthält unsichere Zeichen\n";
}
# Schlecht: Zu nachgiebiges Enttainten (macht den Zweck zunichte)
sub bad_untaint($input) {
$input =~ /^(.*)$/s;
return $1; # Akzeptiert ALLES — sinnlos
}
Eingabevalidierung
Erlaubnisliste statt Sperrliste
use v5.36;
# Gut: Erlaubnisliste — genau definieren, was erlaubt ist
sub validate_sort_field($field) {
my %allowed = map { $_ => 1 } qw(name email created_at updated_at);
die "Ungültiges Sortierfeld: $field\n" unless $allowed{$field};
return $field;
}
# Gut: Mit spezifischen Mustern validieren
sub validate_email($email) {
if ($email =~ /^([a-zA-Z0-9._%+-]+\@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})$/) {
return $1;
}
die "Ungültige E-Mail-Adresse\n";
}
sub validate_integer($input) {
if ($input =~ /^(-?\d{1,10})$/) {
return $1 + 0; # In Zahl umwandeln
}
die "Ungültige Ganzzahl\n";
}
# Schlecht: Sperrliste — immer unvollständig
sub bad_validate($input) {
die "Ungültig" if $input =~ /[<>"';&|]/; # Übersieht codierte Angriffe
return $input;
}
Längenbeschränkungen
use v5.36;
sub validate_comment($text) {
die "Kommentar ist erforderlich\n" unless length($text) > 0;
die "Kommentar überschreitet 10000 Zeichen\n" if length($text) > 10_000;
return $text;
}
Sichere reguläre Ausdrücke
ReDoS-Prävention
Katastrophales Backtracking tritt bei verschachtelten Quantifizierern auf sich überlappenden Mustern auf.
use v5.36;
# Schlecht: Angreifbar durch ReDoS (exponentielles Backtracking)
my $bad_re = qr/^(a+)+$/; # Verschachtelte Quantifizierer
my $bad_re2 = qr/^([a-zA-Z]+)*$/; # Verschachtelte Quantifizierer auf Klasse
my $bad_re3 = qr/^(.*?,){10,}$/; # Wiederholte greedy/lazy Kombination
# Gut: Ohne Verschachtelung umschreiben
my $good_re = qr/^a+$/; # Einzelner Quantifizierer
my $good_re2 = qr/^[a-zA-Z]+$/; # Einzelner Quantifizierer auf Klasse
# Gut: Possessive Quantifizierer oder Atomgruppen verwenden, um Backtracking zu verhindern
my $safe_re = qr/^[a-zA-Z]++$/; # Possessiv (5.10+)
my $safe_re2 = qr/^(?>a+)$/; # Atomgruppe
# Gut: Timeout bei nicht vertrauenswürdigen Mustern erzwingen
use POSIX qw(alarm);
sub safe_match($string, $pattern, $timeout = 2) {
my $matched;
eval {
local $SIG{ALRM} = sub { die "Regex-Timeout\n" };
alarm($timeout);
$matched = $string =~ $pattern;
alarm(0);
};
alarm(0);
die $@ if $@;
return $matched;
}
Sichere Dateioperationen
Drei-Argument-Open
use v5.36;
# Gut: Drei-Argument-Open, lexikalischer Datei-Handle, Rückgabewert prüfen
sub read_file($path) {
open my $fh, '<:encoding(UTF-8)', $path
or die "Kann '$path' nicht öffnen: $!\n";
local $/;
my $content = <$fh>;
close $fh;
return $content;
}
# Schlecht: Zwei-Argument-Open mit Benutzerdaten (Befehlsinjektion)
sub bad_read($path) {
open my $fh, $path; # Wenn $path = "|rm -rf /", führt das einen Befehl aus!
open my $fh, "< $path"; # Shell-Metazeichen-Injektion
}
TOCTOU-Prävention und Pfad-Traversal
use v5.36;
use Fcntl qw(:DEFAULT :flock);
use File::Spec;
use Cwd qw(realpath);
# Atomare Dateierstellung
sub create_file_safe($path) {
sysopen(my $fh, $path, O_WRONLY | O_CREAT | O_EXCL, 0600)
or die "Kann '$path' nicht erstellen: $!\n";
return $fh;
}
# Validieren, dass der Pfad innerhalb des erlaubten Verzeichnisses bleibt
sub safe_path($base_dir, $user_path) {
my $real = realpath(File::Spec->catfile($base_dir, $user_path))
// die "Pfad existiert nicht\n";
my $base_real = realpath($base_dir)
// die "Basisverzeichnis existiert nicht\n";
die "Pfad-Traversal blockiert\n" unless $real =~ /^\Q$base_real\E(?:\/|\z)/;
return $real;
}
Verwenden Sie File::Temp für temporäre Dateien (tempfile(UNLINK => 1)) und flock(LOCK_EX), um Wettlaufsituationen zu verhindern.
Sichere Prozessausführung
Listenform von system und exec
use v5.36;
# Gut: Listenform — keine Shell-Interpolation
sub run_command(@cmd) {
system(@cmd) == 0
or die "Befehl fehlgeschlagen: @cmd\n";
}
run_command('grep', '-r', $user_pattern, '/var/log/app/');
# Gut: Ausgabe sicher mit IPC::Run3 erfassen
use IPC::Run3;
sub capture_output(@cmd) {
my ($stdout, $stderr);
run3(\@cmd, \undef, \$stdout, \$stderr);
if ($?) {
die "Befehl fehlgeschlagen (Exit $?): $stderr\n";
}
return $stdout;
}
# Schlecht: Zeichenkettenform — Shell-Injektion!
sub bad_search($pattern) {
system("grep -r '$pattern' /var/log/app/"); # Wenn $pattern = "'; rm -rf / #"
}
# Schlecht: Backticks mit Interpolation
my $output = `ls $user_dir`; # Risiko der Shell-Injektion
Verwenden Sie auch Capture::Tiny, um stdout/stderr von externen Befehlen sicher zu erfassen.
SQL-Injektions-Prävention
DBI-Platzhalter
use v5.36;
use DBI;
my $dbh = DBI->connect($dsn, $user, $pass, {
RaiseError => 1,
PrintError => 0,
AutoCommit => 1,
});
# Gut: Parametrisierte Abfragen — immer Platzhalter verwenden
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({});
}
# Schlecht: Zeichenketteninterpolation in SQL (SQLi-Sicherheitslücke!)
sub bad_find($dbh, $email) {
my $sth = $dbh->prepare("SELECT * FROM users WHERE email = '$email'");
# Wenn $email = "' OR 1=1 --", werden alle Benutzer zurückgegeben
$sth->execute;
return $sth->fetchrow_hashref;
}
Dynamische Spalten-Erlaubnislisten
use v5.36;
# Gut: Spaltennamen gegen eine Erlaubnisliste validieren
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 "Ungültige Spalte: $column\n" unless $allowed_cols{$column};
die "Ungültige Richtung: $direction\n" unless $allowed_dirs{uc $direction};
my $sth = $dbh->prepare("SELECT * FROM users ORDER BY $column $direction");
$sth->execute;
return $sth->fetchall_arrayref({});
}
# Schlecht: Direkte Interpolation einer vom Benutzer gewählten Spalte
sub bad_order($dbh, $column) {
$dbh->prepare("SELECT * FROM users ORDER BY $column"); # SQLi!
}
DBIx::Class (ORM-Sicherheit)
use v5.36;
# DBIx::Class generiert sichere parametrisierte Abfragen
my @users = $schema->resultset('User')->search({
status => 'active',
email => { -like => '%@example.com' },
}, {
order_by => { -asc => 'name' },
rows => 50,
});
Websicherheit
XSS-Prävention
use v5.36;
use HTML::Entities qw(encode_entities);
use URI::Escape qw(uri_escape_utf8);
# Gut: Ausgabe für HTML-Kontext kodieren
sub safe_html($user_input) {
return encode_entities($user_input);
}
# Gut: Für URL-Kontext kodieren
sub safe_url_param($value) {
return uri_escape_utf8($value);
}
# Gut: Für JSON-Kontext kodieren
use JSON::MaybeXS qw(encode_json);
sub safe_json($data) {
return encode_json($data); # Handhabt Escape-Sequenzen
}
# Template-Auto-Escaping (Mojolicious)
# <%= $user_input %> — automatisch escaped (sicher)
# <%== $raw_html %> — Rohausgabe (gefährlich, nur für vertrauenswürdige Inhalte verwenden)
# Template-Auto-Escaping (Template Toolkit)
# [% user_input | html %] — explizite HTML-Kodierung
# Schlecht: Rohausgabe in HTML
sub bad_html($input) {
print "<div>$input</div>"; # XSS, wenn $input <script> enthält
}
CSRF-Schutz
use v5.36;
use Crypt::URandom qw(urandom);
use MIME::Base64 qw(encode_base64url);
sub generate_csrf_token() {
return encode_base64url(urandom(32));
}
Verwenden Sie einen zeitkonstanten Vergleich bei der Überprüfung von Token. Die meisten Webframeworks (Mojolicious, Dancer2, Catalyst) bieten einen integrierten CSRF-Schutz — bevorzugen Sie diese gegenüber eigenentwickelten Lösungen.
Session- und Header-Sicherheit
use v5.36;
# Mojolicious Session- und Header
$app->secrets(['lang-zufallsgeheimnis-regelmässig-rotieren']);
$app->sessions->secure(1); # Nur 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');
});
Ausgabekodierung
Kodieren Sie die Ausgabe immer für ihren Kontext: HTML::Entities::encode_entities() für HTML, URI::Escape::uri_escape_utf8() für URLs, JSON::MaybeXS::encode_json() für JSON.
CPAN-Modul-Sicherheit
- Versionen festlegen in cpanfile:
requires 'DBI', '== 1.643'; - Gepflegte Module bevorzugen: Auf MetaCPAN nach aktuellen Veröffentlichungen suchen
- Abhängigkeiten minimieren: Jede Abhängigkeit ist eine Angriffsfläche
Sicherheitswerkzeuge
perlcritic-Sicherheitsrichtlinien
# .perlcriticrc — sicherheitsorientierte Konfiguration
severity = 3
theme = security + core
# Drei-Argument-Open erzwingen
[InputOutput::RequireThreeArgOpen]
severity = 5
# Geprüfte Systemaufrufe erzwingen
[InputOutput::RequireCheckedSyscalls]
functions = :builtins
severity = 4
# Zeichenketten-eval verbieten
[BuiltinFunctions::ProhibitStringyEval]
severity = 5
# Backtick-Operatoren verbieten
[InputOutput::ProhibitBacktickOperators]
severity = 4
# Taint-Überprüfung in CGI erzwingen
[Modules::RequireTaintChecking]
severity = 5
# Zwei-Argument-Open verbieten
[InputOutput::ProhibitTwoArgOpen]
severity = 5
# Bare-Word-Datei-Handles verbieten
[InputOutput::ProhibitBarewordFileHandles]
severity = 5
perlcritic ausführen
# Eine Datei überprüfen
perlcritic --severity 3 --theme security lib/MyApp/Handler.pm
# Gesamtes Projekt überprüfen
perlcritic --severity 3 --theme security lib/
# CI-Integration
perlcritic --severity 4 --theme security --quiet lib/ || exit 1
Kurze Sicherheits-Checkliste
| Prüfung | Was zu überprüfen ist |
|---|---|
| Taint-Modus | -T-Flag auf CGI/Web-Skripten |
| Eingabevalidierung | Erlaubnislisten-Muster, Längenbeschränkungen |
| Dateioperationen | Drei-Argument-Open, Pfad-Traversal-Prüfungen |
| Prozessausführung | Listenform system, keine Shell-Interpolation |
| SQL-Abfragen | DBI-Platzhalter, niemals interpolieren |
| HTML-Ausgabe | encode_entities(), Template-Auto-Escape |
| CSRF-Tokens | Generiert, bei zustandsändernden Anfragen verifiziert |
| Session-Konfiguration | Sichere, HttpOnly, SameSite-Cookies |
| HTTP-Header | CSP, X-Frame-Options, HSTS |
| Abhängigkeiten | Feste Versionen, geprüfte Module |
| Regex-Sicherheit | Keine verschachtelten Quantifizierer, verankerte Muster |
| Fehlermeldungen | Keine Stack-Traces oder Pfade, die an Benutzer weitergegeben werden |
Anti-Patterns
# 1. Zwei-Argument-Open mit Benutzerdaten (Befehlsinjektion)
open my $fh, $user_input; # KRITISCHE Sicherheitslücke
# 2. Zeichenkettenform von system (Shell-Injektion)
system("convert $user_file output.png"); # KRITISCHE Sicherheitslücke
# 3. SQL-Zeichenketteninterpolation
$dbh->do("DELETE FROM users WHERE id = $id"); # SQLi
# 4. eval mit Benutzereingabe (Code-Injektion)
eval $user_code; # Remote-Code-Ausführung
# 5. $ENV ohne Säuberung vertrauen
my $path = $ENV{UPLOAD_DIR}; # Könnte manipuliert werden
system("ls $path"); # Doppelte Sicherheitslücke
# 6. Taint ohne Validierung deaktivieren
($input) = $input =~ /(.*)/s; # Faules Enttainten — macht Zweck zunichte
# 7. Rohdaten von Benutzern in HTML
print "<div>Willkommen, $username!</div>"; # XSS
# 8. Unvalidierte Weiterleitungen
print $cgi->redirect($user_url); # Offene Weiterleitung
Denken Sie daran: Perls Flexibilität ist mächtig, erfordert aber Disziplin. Verwenden Sie den Taint-Modus für webzugänglichen Code, validieren Sie alle Eingaben mit Erlaubnislisten, nutzen Sie DBI-Platzhalter für jede Abfrage und kodieren Sie alle Ausgaben für ihren Kontext. Verteidigung in der Tiefe — niemals auf eine einzige Schicht verlassen.