#!/usr/bin/perl -T # -T (taint mode) — дуже важливий для CGI: Perl автоматично позначає всі дані ззовні як "небезпечні" # і змушує нас явно їх валідувати перед використанням у критичних операціях (файли, shell, SQL тощо) use strict; # Забороняє неоголошені змінні — зменшує помилки use warnings; # Попереджає про потенційно небезпечний код use CGI qw(:standard -utf8); # Основний модуль для роботи з CGI-запитами # -utf8 — автоматична підтримка UTF-8 у параметрах use HTML::Entities qw(encode_entities); # Для захисту від XSS — перетворює < > & " ' на сутності use Digest::SHA qw(sha256_hex); # Криптостійкий хеш для генерації CSRF-токенів use Fcntl qw(:flock); # Блокування файлів — потрібно для безпечної роботи з сесією use utf8; # Дозволяє використовувати українські символи в коді # ──────────────────────────────────────────────── # КОНФІГУРАЦІЯ — змініть ці значення під свій сервер # ──────────────────────────────────────────────── my $SESSION_FILE = '/var/tmp/myapp_cgi_sessions.dat'; # Файл для зберігання CSRF-токенів. Повинен бути поза веб-коренем! # У production краще використовувати Redis або Memcached замість файлу # ──────────────────────────────────────────────── # ФУНКЦІЇ ДЛЯ РОБОТИ З СЕСІЄЮ ТА CSRF # ──────────────────────────────────────────────── # Отримуємо або створюємо ідентифікатор сесії (зберігається в куці) sub get_session_id { my $sid = cookie('MYAPP_SESSION') || sha256_hex(time() . rand() . $$ . $^T . ($ENV{REMOTE_ADDR} || 'noip')); # Встановлюємо куку з максимальними параметрами безпеки print header(-cookie => cookie( -name => 'MYAPP_SESSION', -value => $sid, -path => '/', -secure => 1, # тільки HTTPS (вимкніть на localhost для тестів) -httponly => 1, # JavaScript не має доступу до куки -samesite => 'Strict' # Найсильніший захист від CSRF (блокує cross-site запити) )); return $sid; } # Генеруємо унікальний CSRF-токен для поточної сесії sub generate_csrf_token { my $sid = shift; # Додаємо сіль + час + випадкове значення — робить токен непередбачуваним return sha256_hex($sid . time() . rand() . 'csrf_salt_2026'); } # Зберігаємо токен у файлі (простий приклад — не thread-safe) sub store_csrf_token { my ($sid, $token) = @_; open my $fh, '>>', $SESSION_FILE or die "Не можу відкрити файл сесії: $!"; flock($fh, LOCK_EX) or die "Не можу заблокувати файл: $!"; # Блокуємо для запису print $fh "$sid:$token\n"; close $fh; } # Перевіряємо, чи токен, надісланий клієнтом, співпадає з нашим sub validate_csrf_token { my ($sid, $user_token) = @_; return 0 unless $user_token && $sid; open my $fh, '<', $SESSION_FILE or return 0; while (my $line = <$fh>) { chomp $line; my ($stored_sid, $stored_token) = split /:/, $line, 2; return 1 if $stored_sid eq $sid && $stored_token eq $user_token; } close $fh; return 0; } # ──────────────────────────────────────────────── # ГОЛОВНА ЛОГІКА СКРИПТУ # ──────────────────────────────────────────────── # Отримуємо ідентифікатор сесії (створюється автоматично при першому запиті) my $session_id = get_session_id(); # ── GET: показуємо форму з CSRF-токеном ── if (request_method() eq 'GET') { my $csrf_token = generate_csrf_token($session_id); store_csrf_token($session_id, $csrf_token); # Виводимо безпечні заголовки + HTML print header( -charset => 'UTF-8', 'Content-Security-Policy' => "default-src 'self'; script-src 'none';", 'X-Frame-Options' => 'DENY', 'X-XSS-Protection' => '1; mode=block' ); print <<'HTML';
Недійсний або відсутній CSRF-токен.
"; exit; } # ── Валідація та обробка даних ── my $name = param('name') // ''; my $message = param('message') // ''; # Видаляємо зайві пробіли s/^\s+|\s+$//g for ($name, $message); # Простий приклад валідації if (length($name) < 2 || length($name) > 60) { print header(-charset => 'UTF-8'); print "Ім'я повинно бути від 2 до 60 символів.
"; exit; } if (length($message) < 5) { print header(-charset => 'UTF-8'); print "Повідомлення занадто коротке.
"; exit; } # Захист від XSS — екрануємо ВСЕ, що виводимо my $safe_name = encode_entities($name); my $safe_message = encode_entities($message); # ── Успішна обробка ── print header( -charset => 'UTF-8', 'Content-Security-Policy' => "default-src 'self'; script-src 'none';" ); print <<'HTML';Ім'я: $safe_name
Повідомлення:
$safe_messageПовернутися до форми HTML