# Chapter 22: Security Best Practices *"In the battle between attackers and defenders, Perl arms you with both sword and shield—but wisdom determines how you wield them."* ## The Security Mindset Security isn't a feature you add; it's a discipline you practice. In 2025, with systems more interconnected than ever, a single vulnerability can cascade into catastrophic breaches. Perl, with its power and flexibility, can be either your greatest ally or your worst enemy in security—the difference lies in how you use it. ## Input Validation and Sanitization Never trust input. Ever. This is the first commandment of secure programming: ```perl #!/usr/bin/env perl use Modern::Perl '2023'; use feature 'signatures'; no warnings 'experimental::signatures'; package Security::Validator { use Moo; use Types::Standard qw(HashRef CodeRef); use Email::Valid; use Data::Validate::IP; use URI; use HTML::Entities; use Try::Tiny; has rules => ( is => 'ro', isa => HashRef[CodeRef], default => sub { {} }, ); sub BUILD ($self, $args) { # Define validation rules $self->rules->{email} = sub ($value) { return Email::Valid->address($value) ? undef : "Invalid email address"; }; $self->rules->{ip} = sub ($value) { return is_ip($value) ? undef : "Invalid IP address"; }; $self->rules->{url} = sub ($value) { my $uri = URI->new($value); return ($uri && $uri->scheme && $uri->scheme =~ /^https?$/) ? undef : "Invalid URL"; }; $self->rules->{alphanumeric} = sub ($value) { return $value =~ /^[a-zA-Z0-9]+$/ ? undef : "Must be alphanumeric"; }; $self->rules->{integer} = sub ($value) { return $value =~ /^-?\d+$/ ? undef : "Must be an integer"; }; $self->rules->{positive_integer} = sub ($value) { return ($value =~ /^\d+$/ && $value > 0) ? undef : "Must be a positive integer"; }; $self->rules->{safe_string} = sub ($value) { # No shell metacharacters return $value =~ /[\$\`\|\;\&\<\>\(\)\{\}\[\]\*\?\~\!]/ ? "Contains unsafe characters" : undef; }; $self->rules->{sql_safe} = sub ($value) { # Basic SQL injection prevention return $value =~ /(\-\-|\/\*|\*\/|xp_|sp_|';|union|select|insert|update|delete|drop)/i ? "Contains potentially dangerous SQL patterns" : undef; }; $self->rules->{path_safe} = sub ($value) { # Prevent directory traversal return $value =~ /\.\./ ? "Path traversal attempt detected" : undef; }; $self->rules->{xss_safe} = sub ($value) { # Check for potential XSS patterns return $value =~ /{$field}; my $field_rules = ref $rules->{$field} eq 'ARRAY' ? $rules->{$field} : [$rules->{$field}]; # Check required if (grep { $_ eq 'required' } @$field_rules) { if (!defined $value || $value eq '') { $errors{$field} = "$field is required"; next; } } next unless defined $value && $value ne ''; # Apply validation rules for my $rule (@$field_rules) { next if $rule eq 'required'; if (ref $rule eq 'CODE') { if (my $error = $rule->($value)) { $errors{$field} = $error; last; } } elsif (exists $self->rules->{$rule}) { if (my $error = $self->rules->{$rule}->($value)) { $errors{$field} = $error; last; } } elsif ($rule eq 'sanitize_html') { $value = encode_entities($value); } elsif ($rule eq 'trim') { $value =~ s/^\s+|\s+$//g; } elsif ($rule =~ /^max_length:(\d+)$/) { if (length($value) > $1) { $errors{$field} = "$field exceeds maximum length of $1"; last; } } elsif ($rule =~ /^min_length:(\d+)$/) { if (length($value) < $1) { $errors{$field} = "$field must be at least $1 characters"; last; } } } $clean{$field} = $value unless exists $errors{$field}; } return (\%clean, \%errors); } sub sanitize_filename ($self, $filename) { # Remove any path components $filename =~ s/.*[\/\\]//; # Remove dangerous characters $filename =~ s/[^\w\.\-]/_/g; # Remove multiple dots (prevent extension confusion) $filename =~ s/\.{2,}/\./g; # Limit length $filename = substr($filename, 0, 255) if length($filename) > 255; return $filename; } sub sanitize_command ($self, $cmd) { # Quote shell metacharacters $cmd =~ s/([;<>\*\|`&\$!#\(\)\[\]\{\}:'"])/\\$1/g; return $cmd; } } # Usage example my $validator = Security::Validator->new(); my $input = { email => 'user@example.com', username => 'john_doe', age => '25', website => 'https://example.com', comment => 'Hello', filepath => '../../../etc/passwd', }; my ($clean, $errors) = $validator->validate($input, { email => ['required', 'email'], username => ['required', 'alphanumeric', 'min_length:3', 'max_length:20'], age => ['required', 'positive_integer'], website => ['url'], comment => ['required', 'xss_safe', 'sanitize_html'], filepath => ['required', 'path_safe'], }); if (%$errors) { say "Validation errors:"; say " $_: $errors->{$_}" for keys %$errors; } else { say "All inputs valid!"; } ``` ## Secure Password Handling Never store passwords in plain text. Use proper hashing with salt: ```perl #!/usr/bin/env perl use Modern::Perl '2023'; use feature 'signatures'; no warnings 'experimental::signatures'; package Security::Password { use Moo; use Crypt::Eksblowfish::Bcrypt qw(bcrypt en_base64); use Crypt::URandom qw(urandom); use Bytes::Random::Secure; use MIME::Base64; use Types::Standard qw(Int); has cost => ( is => 'ro', isa => Int, default => 12, # Increase for more security ); has min_length => ( is => 'ro', isa => Int, default => 8, ); sub hash_password ($self, $password) { # Generate random salt my $salt = en_base64(urandom(16)); # Format for bcrypt my $settings = sprintf('$2a$%02d$%s', $self->cost, $salt); # Hash the password return bcrypt($password, $settings); } sub verify_password ($self, $password, $hash) { # Constant-time comparison to prevent timing attacks return bcrypt($password, $hash) eq $hash; } sub generate_secure_token ($self, $length = 32) { my $rng = Bytes::Random::Secure->new(NonBlocking => 1); return encode_base64($rng->bytes($length), ''); } sub check_password_strength ($self, $password) { my @errors; push @errors, "Password too short (minimum $self->{min_length} characters)" if length($password) < $self->min_length; push @errors, "Password must contain at least one uppercase letter" unless $password =~ /[A-Z]/; push @errors, "Password must contain at least one lowercase letter" unless $password =~ /[a-z]/; push @errors, "Password must contain at least one digit" unless $password =~ /\d/; push @errors, "Password must contain at least one special character" unless $password =~ /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/; # Check for common patterns push @errors, "Password contains sequential characters" if $password =~ /(?:abc|bcd|cde|def|efg|fgh|ghi|hij|ijk|jkl|klm|lmn|mno|nop|opq|pqr|qrs|rst|stu|tuv|uvw|vwx|wxy|xyz|012|123|234|345|456|567|678|789)/i; push @errors, "Password contains repeated characters" if $password =~ /(.)\1{2,}/; return @errors ? \@errors : undef; } sub generate_password ($self, $length = 16) { my @chars = ( 'A'..'Z', 'a'..'z', '0'..'9', qw(! @ # $ % ^ & * ( ) - _ = + [ ] { } ; : , . < > / ?) ); my $rng = Bytes::Random::Secure->new(NonBlocking => 1); my $password = ''; # Ensure at least one of each required type $password .= $chars[$rng->irand(26)]; # Uppercase $password .= $chars[$rng->irand(26) + 26]; # Lowercase $password .= $chars[$rng->irand(10) + 52]; # Digit $password .= $chars[$rng->irand(22) + 62]; # Special # Fill the rest randomly for (my $i = 4; $i < $length; $i++) { $password .= $chars[$rng->irand(@chars)]; } # Shuffle the password my @password_chars = split //, $password; for (my $i = @password_chars - 1; $i > 0; $i--) { my $j = $rng->irand($i + 1); @password_chars[$i, $j] = @password_chars[$j, $i]; } return join '', @password_chars; } } # Session management package Security::Session { use Moo; use Types::Standard qw(Str Int HashRef); use Digest::SHA qw(sha256_hex); use JSON::XS; use Crypt::JWT qw(encode_jwt decode_jwt); use Try::Tiny; has secret => ( is => 'ro', isa => Str, required => 1, ); has timeout => ( is => 'ro', isa => Int, default => 3600, # 1 hour ); has algorithm => ( is => 'ro', isa => Str, default => 'HS256', ); sub create_token ($self, $user_id, $data = {}) { my $now = time(); my $payload = { sub => $user_id, # Subject iat => $now, # Issued at exp => $now + $self->timeout, # Expiration nbf => $now, # Not before jti => sha256_hex($user_id . $now . rand()), # JWT ID data => $data, }; return encode_jwt( payload => $payload, alg => $self->algorithm, key => $self->secret, ); } sub verify_token ($self, $token) { try { my $payload = decode_jwt( token => $token, key => $self->secret, verify_exp => 1, verify_nbf => 1, ); return $payload; } catch { return undef; }; } sub refresh_token ($self, $token) { my $payload = $self->verify_token($token); return unless $payload; # Check if token is close to expiry (within 5 minutes) if ($payload->{exp} - time() < 300) { return $self->create_token($payload->{sub}, $payload->{data}); } return $token; } } # Example usage my $pw_manager = Security::Password->new(); # Generate secure password my $password = $pw_manager->generate_password(16); say "Generated password: $password"; # Check strength if (my $errors = $pw_manager->check_password_strength($password)) { say "Password strength issues: " . join(", ", @$errors); } else { say "Password is strong"; } # Hash password my $hash = $pw_manager->hash_password($password); say "Password hash: $hash"; # Verify password if ($pw_manager->verify_password($password, $hash)) { say "Password verified successfully"; } # Session management my $session = Security::Session->new( secret => $pw_manager->generate_secure_token(32), ); my $token = $session->create_token('user123', { role => 'admin' }); say "JWT Token: $token"; if (my $payload = $session->verify_token($token)) { say "Token valid for user: $payload->{sub}"; say "User role: $payload->{data}{role}"; } ``` ## SQL Injection Prevention Always use parameterized queries, never string concatenation: ```perl #!/usr/bin/env perl use Modern::Perl '2023'; use feature 'signatures'; no warnings 'experimental::signatures'; package Security::Database { use Moo; use DBI; use DBD::SQLite; use Types::Standard qw(InstanceOf HashRef); use SQL::Abstract::More; use Try::Tiny; has dbh => ( is => 'ro', isa => InstanceOf['DBI::db'], required => 1, ); has sql_abstract => ( is => 'lazy', default => sub { SQL::Abstract::More->new() }, ); # NEVER DO THIS - Example of vulnerable code sub vulnerable_query ($self, $username) { # DON'T DO THIS - Direct string interpolation my $query = "SELECT * FROM users WHERE username = '$username'"; # Attacker could pass: admin' OR '1'='1 # Resulting in: SELECT * FROM users WHERE username = 'admin' OR '1'='1' warn "NEVER use string interpolation in SQL!"; return; } # SECURE: Use placeholders sub secure_query ($self, $username) { my $sth = $self->dbh->prepare( "SELECT * FROM users WHERE username = ?" ); $sth->execute($username); return $sth->fetchrow_hashref; } # SECURE: Use SQL::Abstract for dynamic queries sub dynamic_search ($self, $criteria) { my ($sql, @bind) = $self->sql_abstract->select( 'users', ['id', 'username', 'email'], $criteria, {-order_by => 'created_at DESC'} ); my $sth = $self->dbh->prepare($sql); $sth->execute(@bind); return $sth->fetchall_arrayref({}); } # SECURE: Stored procedures sub call_procedure ($self, $proc_name, @params) { # Whitelist procedure names my %allowed_procs = map { $_ => 1 } qw( get_user_by_id update_last_login calculate_statistics ); die "Unauthorized procedure: $proc_name" unless $allowed_procs{$proc_name}; my $placeholders = join(',', ('?') x @params); my $sth = $self->dbh->prepare("CALL $proc_name($placeholders)"); $sth->execute(@params); return $sth->fetchall_arrayref({}); } # SECURE: Whitelist-based table/column validation sub safe_dynamic_query ($self, $table, $columns, $where) { # Whitelist tables and columns my %allowed_tables = map { $_ => 1 } qw(users posts comments); my %allowed_columns = map { $_ => 1 } qw( id username email title content created_at ); die "Invalid table: $table" unless $allowed_tables{$table}; for my $col (@$columns) { die "Invalid column: $col" unless $allowed_columns{$col}; } my ($sql, @bind) = $self->sql_abstract->select( $table, $columns, $where ); my $sth = $self->dbh->prepare($sql); $sth->execute(@bind); return $sth->fetchall_arrayref({}); } # SECURE: Transaction with rollback on error sub secure_transaction ($self, $operations) { my $result; try { $self->dbh->begin_work; for my $op (@$operations) { my ($sql, @params) = @$op; $self->dbh->do($sql, undef, @params); } $self->dbh->commit; $result = { success => 1 }; } catch { $self->dbh->rollback; $result = { success => 0, error => $_ }; }; return $result; } } # Prepared statement cache for performance package Security::PreparedStatements { use Moo; use Types::Standard qw(InstanceOf HashRef); has dbh => ( is => 'ro', isa => InstanceOf['DBI::db'], required => 1, ); has statements => ( is => 'ro', isa => HashRef, default => sub { {} }, ); sub prepare ($self, $name, $sql) { $self->statements->{$name} ||= $self->dbh->prepare($sql); return $self->statements->{$name}; } sub execute ($self, $name, @params) { my $sth = $self->statements->{$name} or die "Unknown statement: $name"; $sth->execute(@params); return $sth; } sub DEMOLISH ($self) { # Clean up prepared statements $_->finish for values %{$self->statements}; } } ``` ## Command Injection Prevention Never pass user input directly to system commands: ```perl #!/usr/bin/env perl use Modern::Perl '2023'; use feature 'signatures'; no warnings 'experimental::signatures'; package Security::Command { use Moo; use IPC::Run3; use File::Which; use Types::Standard qw(HashRef ArrayRef); has allowed_commands => ( is => 'ro', isa => HashRef, default => sub {{ ls => '/bin/ls', grep => '/bin/grep', find => '/usr/bin/find', tar => '/bin/tar', }}, ); # NEVER DO THIS sub dangerous_exec ($self, $user_input) { # DON'T DO THIS - Direct interpolation # system("ls $user_input"); # Attacker could pass: "; rm -rf /" warn "NEVER interpolate user input into commands!"; return; } # SECURE: Use array form of system() sub safe_exec ($self, $command, @args) { # Validate command against whitelist my $cmd_path = $self->allowed_commands->{$command} or die "Command not allowed: $command"; die "Command not found: $cmd_path" unless -x $cmd_path; # Validate arguments for my $arg (@args) { die "Invalid character in argument" if $arg =~ /[\0\n\r]/; # Null bytes and newlines } # Use array form - no shell interpretation my $exit_code = system($cmd_path, @args); return $exit_code == 0; } # SECURE: Use IPC::Run3 for better control sub safe_capture ($self, $command, $args, $input = undef) { my $cmd_path = $self->allowed_commands->{$command} or die "Command not allowed: $command"; my ($stdout, $stderr); run3( [$cmd_path, @$args], \$input, \$stdout, \$stderr, ); return { stdout => $stdout, stderr => $stderr, exit_code => $? >> 8, }; } # SECURE: Taint mode checking sub check_tainted ($self, $value) { # In taint mode, this would detect tainted data return eval { local $@; kill 0, $value; 0; } || 1; } # SECURE: Safe file operations sub safe_file_operation ($self, $operation, $filename) { # Sanitize filename die "Invalid filename" if $filename =~ /\.\./; # No directory traversal die "Invalid filename" if $filename =~ /^\//; # No absolute paths die "Invalid filename" if $filename =~ /[\0]/; # No null bytes # Restrict to safe directory my $safe_dir = '/tmp/safe_uploads'; my $full_path = "$safe_dir/$filename"; # Validate operation my %allowed_ops = map { $_ => 1 } qw(read write delete); die "Invalid operation" unless $allowed_ops{$operation}; if ($operation eq 'read') { open my $fh, '<', $full_path or die "Cannot read: $!"; local $/; my $content = <$fh>; close $fh; return $content; } elsif ($operation eq 'write') { # Additional checks for write operations die "File too large" if -s $full_path > 10_000_000; # 10MB limit } return 1; } } # Safe templating to prevent injection package Security::Template { use Moo; use Template; use HTML::Entities; has tt => ( is => 'lazy', default => sub { Template->new({ # Disable dangerous operations EVAL_PERL => 0, LOAD_PERL => 0, LOAD_PLUGINS => 0, LOAD_TEMPLATES => 0, ABSOLUTE => 0, RELATIVE => 0, # Enable auto-escaping FILTERS => { html => sub { encode_entities($_[0]) }, js => sub { my $text = shift; $text =~ s/(['\\])/\\$1/g; $text =~ s/\n/\\n/g; $text =~ s/\r/\\r/g; return $text; }, }, }); }, ); sub render_safe ($self, $template, $vars) { # Sanitize all variables by default my $safe_vars = $self->sanitize_vars($vars); my $output; $self->tt->process(\$template, $safe_vars, \$output) or die $self->tt->error; return $output; } sub sanitize_vars ($self, $vars) { my $safe = {}; for my $key (keys %$vars) { my $value = $vars->{$key}; if (ref $value eq 'ARRAY') { $safe->{$key} = [map { encode_entities($_) } @$value]; } elsif (ref $value eq 'HASH') { $safe->{$key} = $self->sanitize_vars($value); } else { $safe->{$key} = encode_entities($value // ''); } } return $safe; } } ``` ## Cryptography and Secure Communication Implement proper encryption for sensitive data: ```perl #!/usr/bin/env perl use Modern::Perl '2023'; use feature 'signatures'; no warnings 'experimental::signatures'; package Security::Crypto { use Moo; use Crypt::CBC; use Crypt::Rijndael; # AES use Crypt::Random qw(makerandom_octet); use MIME::Base64; use Digest::SHA qw(sha256_hex); use Types::Standard qw(Str); has key => ( is => 'ro', isa => Str, required => 1, ); has cipher => ( is => 'lazy', ); sub _build_cipher ($self) { # Derive a proper key from the provided key my $derived_key = substr(sha256_hex($self->key), 0, 32); return Crypt::CBC->new( -key => $derived_key, -cipher => 'Rijndael', -header => 'salt', -pbkdf => 'pbkdf2', -iterations => 10000, ); } sub encrypt ($self, $plaintext) { my $ciphertext = $self->cipher->encrypt($plaintext); return encode_base64($ciphertext, ''); } sub decrypt ($self, $ciphertext) { my $decoded = decode_base64($ciphertext); return $self->cipher->decrypt($decoded); } sub generate_key ($self, $length = 32) { return makerandom_octet(Length => $length, Strength => 1); } sub secure_compare ($self, $a, $b) { # Constant-time comparison to prevent timing attacks return 0 unless defined $a && defined $b; return 0 unless length($a) == length($b); my $result = 0; for (my $i = 0; $i < length($a); $i++) { $result |= ord(substr($a, $i, 1)) ^ ord(substr($b, $i, 1)); } return $result == 0; } } # TLS/SSL configuration package Security::TLS { use Moo; use IO::Socket::SSL; use Mozilla::CA; # Mozilla's CA bundle sub create_client ($self, $host, $port) { return IO::Socket::SSL->new( PeerHost => $host, PeerPort => $port, SSL_verify_mode => SSL_VERIFY_PEER, SSL_ca_file => Mozilla::CA::SSL_ca_file(), SSL_version => '!SSLv2:!SSLv3:!TLSv1:!TLSv1_1', # TLS 1.2+ SSL_cipher_list => 'HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4', ) or die "SSL connection failed: $SSL_ERROR"; } sub create_server ($self, $port, $cert_file, $key_file) { return IO::Socket::SSL->new( LocalPort => $port, Listen => 10, SSL_cert_file => $cert_file, SSL_key_file => $key_file, SSL_verify_mode => SSL_VERIFY_PEER, SSL_version => '!SSLv2:!SSLv3:!TLSv1:!TLSv1_1', SSL_cipher_list => 'HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4', SSL_honor_cipher_order => 1, ) or die "SSL server creation failed: $SSL_ERROR"; } } ``` ## File Upload Security Validate and sanitize file uploads carefully: ```perl #!/usr/bin/env perl use Modern::Perl '2023'; use feature 'signatures'; no warnings 'experimental::signatures'; package Security::FileUpload { use Moo; use File::Type; use File::Temp; use Image::Size; use Archive::Zip; use Types::Standard qw(HashRef Int Str); has allowed_types => ( is => 'ro', isa => HashRef, default => sub {{ 'image/jpeg' => [qw(jpg jpeg)], 'image/png' => [qw(png)], 'image/gif' => [qw(gif)], 'application/pdf' => [qw(pdf)], 'text/plain' => [qw(txt)], }}, ); has max_size => ( is => 'ro', isa => Int, default => 5_000_000, # 5MB ); has upload_dir => ( is => 'ro', isa => Str, default => '/var/uploads', ); sub validate_upload ($self, $file_handle, $claimed_name, $claimed_type) { my @errors; # Check file size my $size = -s $file_handle; push @errors, "File too large (max: $self->{max_size} bytes)" if $size > $self->max_size; # Verify actual file type my $ft = File::Type->new(); my $actual_type = $ft->checktype_filehandle($file_handle); unless ($self->allowed_types->{$actual_type}) { push @errors, "File type not allowed: $actual_type"; } # Check for type mismatch if ($claimed_type && $claimed_type ne $actual_type) { push @errors, "File type mismatch (claimed: $claimed_type, actual: $actual_type)"; } # Validate filename if ($claimed_name =~ /\.\./ || $claimed_name =~ /[\/\\]/) { push @errors, "Invalid filename"; } # Check extension my ($ext) = $claimed_name =~ /\.([^.]+)$/; if ($ext) { my $allowed_exts = $self->allowed_types->{$actual_type} || []; unless (grep { lc($ext) eq $_ } @$allowed_exts) { push @errors, "Invalid file extension for type $actual_type"; } } # Additional checks for images if ($actual_type =~ /^image\//) { seek($file_handle, 0, 0); my ($width, $height) = Image::Size::imgsize($file_handle); unless ($width && $height) { push @errors, "Invalid image file"; } # Check for suspicious dimensions (possible zip bombs) if ($width * $height > 100_000_000) { # 100 megapixels push @errors, "Image dimensions too large"; } } # Check for embedded executables in archives if ($actual_type =~ /zip/) { push @errors, "ZIP files not allowed" unless $self->allowed_types->{'application/zip'}; my $zip = Archive::Zip->new(); $zip->readFromFileHandle($file_handle); for my $member ($zip->members()) { if ($member->fileName() =~ /\.(exe|dll|sh|bat|cmd|com|scr)$/i) { push @errors, "Archive contains executable files"; last; } } } return @errors ? \@errors : undef; } sub save_upload ($self, $file_handle, $original_name) { # Generate safe filename my $safe_name = $self->generate_safe_filename($original_name); # Create temporary file first my $temp = File::Temp->new( DIR => $self->upload_dir, SUFFIX => '.tmp', UNLINK => 0, ); # Copy content seek($file_handle, 0, 0); my $buffer; while (read($file_handle, $buffer, 8192)) { print $temp $buffer; } close $temp; # Move to final location my $final_path = "$self->{upload_dir}/$safe_name"; rename($temp->filename, $final_path) or die "Failed to save upload: $!"; # Set restrictive permissions chmod(0644, $final_path); return $final_path; } sub generate_safe_filename ($self, $original) { # Extract extension my ($ext) = $original =~ /\.([^.]+)$/; $ext = lc($ext // 'dat'); # Generate unique name my $timestamp = time(); my $random = int(rand(10000)); my $hash = substr(sha256_hex($original . $timestamp . $random), 0, 12); return "${timestamp}_${hash}.${ext}"; } } ``` ## Security Auditing and Logging Track security events for forensics and compliance: ```perl #!/usr/bin/env perl use Modern::Perl '2023'; use feature 'signatures'; no warnings 'experimental::signatures'; package Security::Audit { use Moo; use Log::Log4perl qw(:easy); use JSON::XS; use Time::HiRes qw(time); use Sys::Hostname; use Types::Standard qw(Str); has log_file => ( is => 'ro', isa => Str, default => '/var/log/security_audit.log', ); has hostname => ( is => 'lazy', default => sub { hostname() }, ); sub BUILD ($self, $args) { # Configure Log4perl my $conf = qq{ log4perl.rootLogger = INFO, File, Syslog log4perl.appender.File = Log::Log4perl::Appender::File log4perl.appender.File.filename = $self->{log_file} log4perl.appender.File.mode = append log4perl.appender.File.layout = Log::Log4perl::Layout::PatternLayout log4perl.appender.File.layout.ConversionPattern = %d{ISO8601} [%p] %m%n log4perl.appender.Syslog = Log::Log4perl::Appender::Syslog log4perl.appender.Syslog.ident = security_audit log4perl.appender.Syslog.facility = local0 log4perl.appender.Syslog.layout = Log::Log4perl::Layout::SimpleLayout }; Log::Log4perl->init(\$conf); } sub log_event ($self, $event_type, $details) { my $event = { timestamp => time(), hostname => $self->hostname, event_type => $event_type, details => $details, pid => $$, }; my $json = encode_json($event); # Log based on severity given ($event_type) { when (/^(auth_failure|intrusion|breach)/) { ERROR($json); } when (/^(auth_success|access_granted)/) { INFO($json); } when (/^(suspicious|anomaly)/) { WARN($json); } default { DEBUG($json); } } return $event; } sub log_authentication ($self, $username, $success, $ip_address, $details = {}) { $self->log_event( $success ? 'auth_success' : 'auth_failure', { username => $username, ip_address => $ip_address, %$details, } ); } sub log_access ($self, $user, $resource, $action, $allowed) { $self->log_event( $allowed ? 'access_granted' : 'access_denied', { user => $user, resource => $resource, action => $action, } ); } sub log_suspicious_activity ($self, $type, $details) { $self->log_event('suspicious', { type => $type, %$details, }); } sub detect_brute_force ($self, $username, $ip_address, $window = 300) { # Track failed attempts (would use Redis/Memcached in production) state %attempts; my $key = "$username:$ip_address"; my $now = time(); # Clean old attempts $attempts{$key} = [grep { $now - $_ < $window } @{$attempts{$key} // []}]; # Add current attempt push @{$attempts{$key}}, $now; # Check threshold if (@{$attempts{$key}} > 5) { $self->log_event('intrusion', { type => 'brute_force', username => $username, ip_address => $ip_address, attempts => scalar(@{$attempts{$key}}), window => $window, }); return 1; } return 0; } } ``` ## Best Practices Summary 1. **Input Validation** - Never trust user input - Validate on the server side - Use whitelisting over blacklisting - Sanitize for the specific context (HTML, SQL, Shell) 2. **Authentication & Authorization** - Use strong password hashing (bcrypt, scrypt, Argon2) - Implement proper session management - Use JWT tokens with expiration - Enforce principle of least privilege 3. **Data Protection** - Encrypt sensitive data at rest - Use TLS for data in transit - Implement key rotation - Secure key storage 4. **Secure Coding** - Use parameterized queries - Avoid system() with user input - Implement proper error handling - Don't expose sensitive information in errors 5. **Monitoring & Auditing** - Log security events - Detect anomalies and patterns - Regular security audits - Keep dependencies updated ## Security Checklist ```perl #!/usr/bin/env perl use Modern::Perl '2023'; # Security Checklist Module package Security::Checklist { use Moo; sub check_application ($self) { my @checks = ( { name => 'Input Validation', check => sub { $self->check_input_validation() } }, { name => 'SQL Injection Prevention', check => sub { $self->check_sql_injection() } }, { name => 'XSS Prevention', check => sub { $self->check_xss() } }, { name => 'CSRF Protection', check => sub { $self->check_csrf() } }, { name => 'Authentication', check => sub { $self->check_authentication() } }, { name => 'Authorization', check => sub { $self->check_authorization() } }, { name => 'Session Management', check => sub { $self->check_sessions() } }, { name => 'Cryptography', check => sub { $self->check_crypto() } }, { name => 'Error Handling', check => sub { $self->check_error_handling() } }, { name => 'Logging', check => sub { $self->check_logging() } }, { name => 'File Uploads', check => sub { $self->check_file_uploads() } }, { name => 'Dependencies', check => sub { $self->check_dependencies() } }, { name => 'Configuration', check => sub { $self->check_configuration() } }, { name => 'Headers', check => sub { $self->check_security_headers() } }, ); my $passed = 0; my $total = @checks; say "=" x 50; say "Security Checklist"; say "=" x 50; for my $check (@checks) { my $result = $check->{check}->(); my $status = $result ? '✓' : '✗'; my $color = $result ? "\e[32m" : "\e[31m"; # Green or Red printf "%s%-30s %s%s\e[0m\n", $color, $check->{name}, $status, $result ? '' : ' - NEEDS ATTENTION'; $passed++ if $result; } say "=" x 50; printf "Score: %d/%d (%.1f%%)\n", $passed, $total, ($passed/$total)*100; return $passed == $total; } # Individual check methods would be implemented here sub check_input_validation { 1 } # Placeholder sub check_sql_injection { 1 } sub check_xss { 1 } sub check_csrf { 1 } sub check_authentication { 1 } sub check_authorization { 1 } sub check_sessions { 1 } sub check_crypto { 1 } sub check_error_handling { 1 } sub check_logging { 1 } sub check_file_uploads { 1 } sub check_dependencies { 1 } sub check_configuration { 1 } sub check_security_headers { 1 } } my $checker = Security::Checklist->new(); $checker->check_application(); ``` ## Summary Security is not a destination but a journey. Every line of code you write is either strengthening or weakening your application's defenses. Perl provides powerful tools for security, but they must be used correctly. Remember: validate everything, trust nothing, encrypt sensitive data, log security events, and stay updated on the latest threats and patches. In 2025's threat landscape, where attacks are automated and adversaries are sophisticated, your Perl applications must be built with security as a foundation, not an afterthought. The techniques and patterns in this chapter aren't just best practices—they're essential practices for any system that handles real data in the real world.