#!/usr/bin/perl -T use strict; use warnings; use CGI qw(:standard -utf8); use CGI::Carp qw(fatalsToBrowser); # Показ помилок у браузері (тільки dev!) use DBI; use HTML::Entities qw(encode_entities); use Digest::SHA qw(sha256_hex); use Fcntl qw(:flock); use utf8; # Конфігурація (змінюйте!) my $DSN = 'DBI:mysql:database=myapp;host=localhost;charset=utf8mb4'; my $DBUSER = 'your_db_user'; my $DBPASS = 'your_strong_password'; my $SESSION_FILE = '/var/tmp/myapp_sessions.dat'; # Поза веб-коренем! # Підключення до БД (один раз) my $dbh = DBI->connect($DSN, $DBUSER, $DBPASS, { RaiseError => 1, AutoCommit => 1, mysql_enable_utf8mb4 => 1, PrintError => 0, }) or die "DB connect failed: $DBI::errstr"; # Функції для CSRF (простий приклад на файлі) sub generate_csrf_token { my $sid = cookie('session_id') // sha256_hex(time() . rand() . $$ . $^T); my $token = sha256_hex($sid . time() . rand()); open my $fh, '>>', $SESSION_FILE or die "Session file error: $!"; flock($fh, LOCK_EX) or die "Lock error: $!"; print $fh "$sid:$token\n"; close $fh; # Встановлюємо куку з максимальним захистом print header(-cookie => cookie( -name => 'session_id', -value => $sid, -secure => 1, # тільки HTTPS -httponly => 1, -samesite => 'Strict' )); return $token; } sub validate_csrf { my $user_token = param('csrf_token') // return 0; my $sid = cookie('session_id') // return 0; open my $fh, '<', $SESSION_FILE or return 0; while (<$fh>) { chomp; my ($stored_sid, $stored_token) = split /:/, $_, 2; return 1 if $stored_sid eq $sid && $stored_token eq $user_token; } close $fh; return 0; } # ──────────────────────────────────────────────── # Головна логіка # ──────────────────────────────────────────────── print header( -charset => 'UTF-8', 'Content-Security-Policy' => "default-src 'self'; script-src 'none'; object-src 'none';", 'X-Frame-Options' => 'DENY', 'X-XSS-Protection' => '1; mode=block', 'X-Content-Type-Options'=> 'nosniff' ); # Показ форми (GET) if (request_method() eq 'GET') { my $csrf_token = generate_csrf_token(); print <<'HTML'; Форма

Додати користувача




HTML exit; } # Обробка форми (тільки POST!) if (request_method() ne 'POST') { print "

Помилка

Використовуйте тільки POST.

"; exit; } # Перевірка CSRF unless (validate_csrf()) { print "

Помилка безпеки

Недійсний CSRF-токен. Спробуйте ще раз.

"; exit; } # Отримання та валідація даних my $name = param('name') // ''; my $age = param('age') // ''; my $email = param('email') // ''; # Тріммінг s/^\s+|\s+$//g for ($name, $age, $email); # Валідація (whitelist) my @errors; push @errors, "Ім'я: тільки букви, пробіли, апостроф, дефіс (1–60 символів)" unless \( name =~ /^[A-Za-zА-Яа-яІіЇїЄєҐґ\s'-]{1,60} \)/; push @errors, "Вік: ціле число 1–120" unless \( age =~ /^\d{1,3} \)/ && $age >= 1 && $age <= 120; push @errors, "Email: недійсний формат" unless \( email =~ /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,} \)/ && length($email) <= 100; if (@errors) { print "

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

"; print "

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

"; exit; } # ──────────────────────────────────────────────── # Збереження в БД — БЕЗПЕЧНО (prepared statement + placeholders) # ──────────────────────────────────────────────── eval { my $sth = $dbh->prepare(q{ INSERT INTO users (name, age, email, created_at) VALUES (?, ?, ?, NOW()) }); $sth->execute($name, $age + 0, $email) # +0 для явного перетворення в число or die "Execute failed: " . $sth->errstr; $sth->finish(); }; if ($@) { warn "DB error: $@"; # Лог в error_log print "

Помилка сервера

Не вдалося зберегти дані. Спробуйте пізніше.

"; exit; } # ──────────────────────────────────────────────── # Вивід результатів (з екрануванням) # ──────────────────────────────────────────────── my $safe_name = encode_entities($name); my $safe_age = encode_entities($age); my $safe_email = encode_entities($email); print < Успіх

Дані успішно збережено!

Ім'я: $safe_name

Вік: $safe_age

Email: $safe_email

Додати ще одного

HTML # Закриття з'єднання (необов'язково, але гарна практика) $dbh->disconnect;