Perl-Sicherheitsmuster
Umfassende Sicherheitsrichtlinien für Perl-Anwendungen zur Eingabevalidierung, Injection-Prävention und sicheren Codierungspraxis.
Wann aktivieren
- Verarbeitung von Benutzereingaben in Perl-Anwendungen
- Erstellung von Perl-Webanwendungen (CGI, Mojolicious, Dancer2, Catalyst)
- Überprüfung von Perl-Code auf Sicherheitslücken
- Dateioperationen mit benutzerdefinierten Pfaden
- Ausführung von Systembefehlen aus Perl
- Schreiben von DBI-Datenbankabfragen
Funktionsweise
Beginnen Sie mit Taint-bewussten Eingabegrenzen, dann erweitern Sie nach außen: Validieren und Enttainten von Eingaben, halten Sie Dateisystem- und Prozessausführung eingeschränkt, und verwenden Sie überall parametrisierte DBI-Abfragen. Die folgenden Beispiele zeigen die sicheren Standardeinstellungen, die dieses Skill 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.
Taint-Modus aktivieren
#!/usr/bin/perl -T
use v5.36;
# Tainting: alles von außerhalb des Programms
my $input = $ARGV[0]; # Tainting
my $env_path = $ENV{PATH}; # Tainting
my $form = <STDIN>; # Tainting
my $query = $ENV{QUERY_STRING}; # Tainting
# PATH frühzeitig bereinigen (erforderlich im Taint-Modus)
$ENV{PATH} = '/usr/local/bin:/usr/bin:/bin';
delete @ENV{qw(IFS CDPATH ENV BASH_ENV)};
Enttaint-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 enttaint
}
die "Ungültiger Benutzername: muss 3-30 alphanumerische Zeichen sein\n";
}
# Gut: Validieren und Enttainten eines Dateipfads
sub untaint_filename($input) {
if ($input =~ m{^([a-zA-Z0-9._-]+)$}) {
return $1;
}
die "Ungültiger Dateiname: enthält unsichere Zeichen\n";
}
# Schlecht: Zu permissives Enttainten (macht Zweck zunichte)
sub bad_untaint($input) {
$input =~ /^(.*)$/s;
return $1; # Akzeptiert ALLES – sinnlos
}
Eingabevalidierung
Positivliste statt Negativliste
use v5.36;
# Gut: Positivliste – 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: Validieren mit spezifischen Mustern
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: Negativliste – immer unvollständig
sub bad_validate($input) {
die "Ungültig" if $input =~ /[<>"';&|]/; # Übersieht kodierte 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 Quantoren auf sich überlappenden Mustern auf.
use v5.36;
# Schlecht: Anfällig für ReDoS (exponentielles Backtracking)
my $bad_re = qr/^(a+)+$/; # Verschachtelte Quantoren
my $bad_re2 = qr/^([a-zA-Z]+)*$/; # Verschachtelte Quantoren auf Klasse
my $bad_re3 = qr/^(.*?,){10,}$/; # Wiederholte Greedy/Lazy-Kombination
# Gut: Ohne Verschachtelung umschreiben
my $good_re = qr/^a+$/; # Einzelner Quantor
my $good_re2 = qr/^[a-zA-Z]+$/; # Einzelner Quantor auf Klasse
# Gut: Besitzergreifende Quantoren oder atomare Gruppen verwenden, um Backtracking zu verhindern
my $safe_re = qr/^[a-zA-Z]++$/; # Besitzergreifend (5.10+)
my $safe_re2 = qr/^(?>a+)$/; # Atomare Gruppe
# 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-Argumente-open
use v5.36;
# Gut: Drei-Argumente-open, lexikalischer Dateihandle, 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-Argumente-open mit Benutzerdaten (Befehlseinschleusung)
sub bad_read($path) {
open my $fh, $path; # Wenn $path = "|rm -rf /", wird Befehl ausgeführt!
open my $fh, "< $path"; # Shell-Metazeichen-Einschleusung
}
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 Pfad im erlaubten Verzeichnis 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 Race Conditions zu vermeiden.
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: Stringform – Shell-Einschleusung!
sub bad_search($pattern) {
system("grep -r '$pattern' /var/log/app/"); # Wenn $pattern = "'; rm -rf / #"
}
# Schlecht: Backticks mit Interpolation
my $output = `ls $user_dir`; # Shell-Einschleusungsrisiko
Verwenden Sie auch Capture::Tiny zur sicheren Erfassung von stdout/stderr aus externen Befehlen.
SQL-Injection-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: String-Interpolation in SQL (SQLi-Schwachstelle!)
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-Positivlisten
use v5.36;
# Gut: Spaltennamen gegen eine Positivliste 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 benutzergewählter 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
}
# Template Auto-Escaping (Mojolicious)
# <%= $user_input %> — automatisch escaped (sicher)
# <%== $raw_html %> — rohe Ausgabe (gefährlich, nur für vertrauenswürdigen Inhalt)
# Template Auto-Escaping (Template Toolkit)
# [% user_input | html %] — explizite HTML-Kodierung
# Schlecht: Rohe Ausgabe 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 beim Überprüfen von Token einen konstanten Zeitvergleich. Die meisten Web-Frameworks (Mojolicious, Dancer2, Catalyst) bieten integrierten CSRF-Schutz – bevorzugen Sie diese gegenüber Eigenbau-Lösungen.
Sitzungs- und Header-Sicherheit
use v5.36;
# Mojolicious Sitzung + Header
$app->secrets(['langes-zufälliges-geheimnis-regelmäßig-rotiert']);
$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-Modulsicherheit
- Versionen festlegen in der cpanfile:
requires 'DBI', '== 1.643'; - Gepflegte Module bevorzugen: Überprüfen Sie MetaCPAN auf aktuelle Veröffentlichungen
- Abhängigkeiten minimieren: Jede Abhängigkeit ist eine Angriffsfläche
Sicherheitstools
perlcritic-Sicherheitsrichtlinien
# .perlcriticrc — sicherheitsorientierte Konfiguration
severity = 3
theme = security + core
# Drei-Argumente-open erzwingen
[InputOutput::RequireThreeArgOpen]
severity = 5
# Geprüfte Systemaufrufe erzwingen
[InputOutput::RequireCheckedSyscalls]
functions = :builtins
severity = 4
# String-eval verbieten
[BuiltinFunctions::ProhibitStringyEval]
severity = 5
# Backtick-Operatoren verbieten
[InputOutput::ProhibitBacktickOperators]
severity = 4
# Taint-Prüfung in CGI erzwingen
[Modules::RequireTaintChecking]
severity = 5
# Zwei-Argumente-open verbieten
[InputOutput::ProhibitTwoArgOpen]
severity = 5
# Bareword-Dateihandles verbieten
[InputOutput::ProhibitBarewordFileHandles]
severity = 5
perlcritic ausführen
# Eine Datei prüfen
perlcritic --severity 3 --theme security lib/MyApp/Handler.pm
# Gesamtes Projekt prü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 bei CGI/Web-Skripten |
| Eingabevalidierung | Positivlisten-Muster, Längenbeschränkungen |
| Dateioperationen | Drei-Argumente-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 |
| Sitzungskonfiguration | Secure, HttpOnly, SameSite-Cookies |
| HTTP-Header | CSP, X-Frame-Options, HSTS |
| Abhängigkeiten | Festgelegte Versionen, geprüfte Module |
| Regex-Sicherheit | Keine verschachtelten Quantoren, verankerte Muster |
| Fehlermeldungen | Keine Stack-Traces oder Pfade an Benutzer weitergeben |
Anti-Patterns
# 1. Zwei-Argumente-open mit Benutzerdaten (Befehlseinschleusung)
open my $fh, $user_input; # KRITISCHE Schwachstelle
# 2. Stringform-system (Shell-Einschleusung)
system("convert $user_file output.png"); # KRITISCHE Schwachstelle
# 3. SQL-String-Interpolation
$dbh->do("DELETE FROM users WHERE id = $id"); # SQLi
# 4. eval mit Benutzereingabe (Code-Einschleusung)
eval $user_code; # Remote-Code-Ausführung
# 5. $ENV ohne Bereinigung vertrauen
my $path = $ENV{UPLOAD_DIR}; # Könnte manipuliert werden
system("ls $path"); # Doppelte Schwachstelle
# 6. Taint ohne Validierung deaktivieren
($input) = $input =~ /(.*)/s; # Faules Enttainten – macht Zweck zunichte
# 7. Rohe Benutzerdaten in HTML
print "<div>Willkommen, $username!</div>"; # XSS
# 8. Nicht validierte 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 Positivlisten, verwenden Sie DBI-Platzhalter für jede Abfrage und kodieren Sie alle Ausgaben für ihren Kontext. Defense in Depth – verlassen Sie sich nie auf eine einzelne Schicht.