l#!/usr/bin/perl -T # -T (taint mode) — один з найважливіших механізмів безпеки в Perl CGI # Увімкнення taint mode змушує Perl позначати ВСІ дані, отримані ззовні (GET/POST, cookies, env) # як "забруднені" (tainted). Такі дані не можна використовувати в: # • викликах system(), exec(), open() з shell # • SQL-запитах (якщо не валідувати) # • шляхах до файлів # Це значно зменшує ризик command injection та інших атак через невалідовані дані. use strict; # Забороняє використання змінних без оголошення (my/our/state) # Допомагає уникнути помилок типу "використав $naem замість $name" use warnings; # Виводить попередження про потенційно небезпечний або неоднозначний код # Наприклад: використання неініціалізованих змінних, зайві лапки тощо use CGI qw(:standard -utf8); # :standard — імпортує найпоширеніші функції CGI (param, header, cookie, start_html тощо) # -utf8 — автоматично декодує параметри форми в UTF-8 (дуже важливо для української мови) use HTML::Entities qw(encode_entities); # Основний інструмент захисту від XSS. # Перетворює < > & " ' на < > & " ' # Використовується для всіх даних, які потрапляють у HTML-вивід use Digest::SHA qw(sha256_hex); # Криптостійкий хеш SHA-256 — використовується для генерації: # • ідентифікаторів сесії # • CSRF-токенів # Не використовуйте rand() + time() без хешу — це передбачувано use Fcntl qw(:flock); # LOCK_EX, LOCK_SH — блокування файлів для безпечного одночасного доступу # Без flock при великій кількості запитів можливі race conditions у файлі сесій use utf8; # Дозволяє писати українські літери безпосередньо в коді (коментарі, рядки) # ============================================================================= # КОНФІГУРАЦІЯ — ці значення обов’язково потрібно змінити під реальний сервер # ============================================================================= my $SESSION_FILE = '/var/tmp/myapp_cgi_sessions.dat'; # Шлях до файлу сесій. Вимоги: # 1. поза document root (щоб не можна було прочитати через веб) # 2. права доступу 0600 або 0640, власник — користувач, під яким працює CGI # 3. найкраще — tmpfs або ramdisk для швидкості та безпеки # У production → Redis, Memcached або DB замість файлу! # ============================================================================= # ФУНКЦІЇ ДЛЯ СЕСІЇ ТА CSRF ЗАХИСТУ # ============================================================================= sub get_session_id { # Повертає або створює унікальний ідентифікатор сесії # Зберігається в захищеній куці (Secure + HttpOnly + SameSite=Strict) my $sid = cookie('MYAPP_SESSION'); unless ($sid) { # Генеруємо новий ідентифікатор тільки якщо куки немає # Додаємо ентропію: час + випадкове число + PID + TAI + IP клієнта $sid = 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 не має доступу → захист від XSS -samesite => 'Strict' # Блокує майже всі cross-site запити # Lax теж добре працює, Strict — найсуворіший )); } return $sid; } sub generate_csrf_token { my ($sid) = @_; # Генеруємо одноразовий (або обмежено-часовий) токен # Сіль + час + випадкове значення + фіксована константа # Це робить токен непередбачуваним навіть при відомому sid return sha256_hex($sid . time() . rand() . 'csrf_salt_very_long_2026'); } sub store_csrf_token { my ($sid, $token) = @_; # Записуємо токен у файл (простий приклад) # У реальному проєкті краще: # • Redis з TTL (наприклад 30 хвилин) # • Memcached # • База даних (окремий рядок на сесію) 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 defined $user_token && length $user_token > 0 && $sid; open my $fh, '<', $SESSION_FILE or return 0; while (my $line = <$fh>) { chomp $line; my ($stored_sid, $stored_token) = split /:/, $line, 2; if (defined $stored_sid && defined $stored_token && $stored_sid eq $sid && $stored_token eq $user_token) { close $fh; return 1; } } 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); # Безпекові заголовки — блокують багато типів атак print header( -charset => 'UTF-8', 'Content-Security-Policy' => join('; ', "default-src 'self'", # тільки ресурси з того ж домену "script-src 'none'", # блокуємо всі скрипти (можна дозволити 'self') "object-src 'none'", # блокуємо плагіни (flash тощо) "base-uri 'self'", # блокуємо base href атаки "form-action 'self'" # форми тільки на цей домен ), 'X-Content-Type-Options' => 'nosniff', # блокує MIME-type sniffing 'X-Frame-Options' => 'DENY', # блокує clickjacking 'X-XSS-Protection' => '1; mode=block' # активує вбудований фільтр браузера ); print <<'HTML'; Захищена форма (CGI Perl)

Приклад форми з CSRF та XSS захистом







HTML exit; } # ── POST: обробка тільки після перевірки CSRF ─────────────────────────────── if (request_method() ne 'POST') { print header(-status => '405 Method Not Allowed', -charset => 'UTF-8'); print "

Помилка

Дозволені тільки методи GET та POST.

"; exit; } my $submitted_token = param('csrf_token') // ''; unless (validate_csrf_token($session_id, $submitted_token)) { print header(-status => '403 Forbidden', -charset => 'UTF-8'); print "

Помилка безпеки (403)

"; print "

Недійсний або відсутній CSRF-токен. Це може бути спроба атаки.

"; print "

Повернутися до форми

"; exit; } # ── Отримання та базова обробка даних ─────────────────────────────────────── my $name = param('name') // ''; my $message = param('message') // ''; # Видаляємо зайві пробіли на початку та в кінці s/^\s+|\s+$//g for ($name, $message); # Валідація довжини та формату (whitelist) my @errors; push @errors, "Ім'я повинно бути від 2 до 60 символів" if length($name) < 2 || length($name) > 60; push @errors, "Повідомлення повинно бути не коротшим за 5 символів" if length($message) < 5; # Додаткова перевірка імені (тільки дозволені символи) push @errors, "Ім'я містить недозволені символи" unless \( name =~ /^[A-Za-zА-Яа-яІіЇїЄєҐґ\s'-]+ \)/; if (@errors) { print header(-charset => 'UTF-8'); print "

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

Спробувати ще раз

"; exit; } # ── Захист від XSS: екрануємо ВСЕ, що потрапляє у вивід ──────────────────── my $safe_name = encode_entities($name, '<>&"\''); my $safe_message = encode_entities($message, '<>&"\''); # encode_entities з другим аргументом екранує саме ці символи # Це найбезпечніший варіант для HTML-контексту # ── Успішна відповідь ────────────────────────────────────────────────────── print header( -charset => 'UTF-8', 'Content-Security-Policy' => "default-src 'self'; script-src 'none';" ); print <<'HTML'; Дані успішно отримано

Дякуємо!

Ім'я: $safe_name

Повідомлення:

$safe_message
  

Повернутися до форми

HTML