#!/usr/bin/perl -T # -T для taint mode (додатковий захист від небезпечних даних) use strict; use warnings; use CGI qw(:standard -utf8); use HTML::Entities qw(encode_entities); use Digest::SHA qw(sha256_hex); # Для CSRF-токена use Fcntl qw(:flock SEEK_END); # Для простої сесії у файлі use utf8; # Шляхи (змінюйте на ваші, поза веб-коренем!) my $SESSION_FILE = '/var/tmp/myapp_session.dat'; # Для сесій (CSRF) # Дозволені методи exit_with_error('Недозволений метод') if request_method() ne 'POST' && request_method() ne 'GET'; # Генерація/перевірка CSRF-токена (проста сесія на файлі) sub get_session_token { my $session_id = cookie('session_id') // sha256_hex(time() . rand() . $$); # Новий ID, якщо немає my $token = sha256_hex($session_id . time() . rand()); # Зберегти в файл (простий, але thread-unsafe; в production — Redis) open(my $fh, '>>', $SESSION_FILE) or die "Не можу відкрити сесію: $!"; flock($fh, LOCK_EX) or die "Не можу заблокувати: $!"; print $fh "$session_id:$token\n"; close($fh); # Повернути токен і встановити куку (Secure, HttpOnly, SameSite) my $cookie = cookie(-name => 'session_id', -value => $session_id, -secure => 1, -httponly => 1, -samesite => 'Strict'); header(-cookie => $cookie); return $token; } sub validate_csrf { my $user_token = param('csrf_token') // ''; return 0 if !$user_token; # Прочитати сесію open(my $fh, '<', $SESSION_FILE) or return 0; while (<$fh>) { chomp; my ($sid, $stored_token) = split /:/; return 1 if cookie('session_id') eq $sid && $user_token eq $stored_token; } close($fh); return 0; } # Показ форми (GET) if (request_method() eq 'GET') { my $csrf_token = get_session_token(); print header(-charset => 'UTF-8', -type => 'text/html'); print < Форма вводу

Введіть дані



HTML exit; } # Обробка форми (POST) exit_with_error('Недійсний CSRF-токен') unless validate_csrf(); # Отримання та валідація даних (taint mode допомагає) my $name = param('name') // ''; my $age = param('age') // ''; # Тріммінг \( name =~ s/^\s+|\s+ \)//g; \( age =~ s/^\s+|\s+ \)//g; # Валідація тексту: тільки букви, пробіли, довжина 1–50 exit_with_error('Недійсне ім’я: тільки букви та пробіли, 1–50 символів') unless \( name =~ /^[A-Za-zА-Яа-яЁёІіЇї Ґґ\s]{1,50} \)/; # Валідація числа: integer 1–120 exit_with_error('Недійсний вік: ціле число від 1 до 120') unless \( age =~ /^\d+ \)/ && $age >= 1 && $age <= 120; # Екранування для XSS my $safe_name = encode_entities($name); my $safe_age = encode_entities($age); # На всяк випадок, хоч число # Заголовки безпеки print header( -charset => 'UTF-8', -type => 'text/html', 'Content-Security-Policy' => "default-src 'self'; script-src 'none'; object-src 'none';", 'X-XSS-Protection' => '1; mode=block', 'X-Frame-Options' => 'DENY', 'X-Content-Type-Options' => 'nosniff', ); # Виведення HTML з даними print < Результати

Ваші дані

Ім'я: $safe_name

Вік: $safe_age

Повернутися до форми HTML # Функція для помилок (не показувати деталі користувачу) sub exit_with_error { my $msg = shift; warn "Помилка: $msg"; # Лог в STDERR (Apache log) print header(-status => 400, -charset => 'UTF-8', -type => 'text/html'); print "

Помилка вводу

Будь ласка, перевірте дані та спробуйте знову.

"; exit; }