Это три отдельные поломки, а не одна. Каждая лечится точечно, без переделки сайта: сначала снимаем нагрузку, потом закрываем форму, потом чиним заголовок From. Ниже порядок работ, код и чек-лист приёмки. Данные в примерах условные (вымышленный магазин), структура и правки реальные.
1' AND EXTRACTVALUE(...) в поле «Клиент»«Включить кэш» без диагностики часто не помогает: у OpenCart нагрузку дают разные источники. Порядок такой.
| Шаг | Что смотрим | Что делаем |
|---|---|---|
| 1 | Access-лог за сутки: топ IP и User-Agent, доля 404 и 301/302 | Отдельных ботов (не Google/Bing) режем в .htaccess; «I'm Under Attack» убираем, когда нагрузка спадёт: он отпугивает и покупателей, и поисковик |
| 2 | Какие URL бьют чаще всего: поиск, фильтры, sort=, limit=, page= до бесконечности | Закрываем перебор в robots.txt и отдаём 410/404 дёшево, без запуска всего OpenCart |
| 3 | Цепочки редиректов (http→https, www, слэш, SEO-URL): 2-3 прыжка на каждый запрос бота | Один прямой редирект на каноничный адрес, правило в начале .htaccess |
| 4 | Slow query log / EXPLAIN по самым частым запросам: SEO-URL, категории, фильтры, поиск | Индексы на поля поиска (seo_url.keyword, url_alias.keyword/query), отключаем лишние модули и OCMod/vQmod-правки с тяжёлыми запросами |
| 5 | Cron и внешние API: курсы валют, синхронизация, чужие виджеты | Реже или по cron вместо запуска на каждый визит |
# robots.txt: не пускаем ботов в бесконечные сочетания фильтров и сортировок User-agent: * Disallow: /*?*sort= Disallow: /*?*order= Disallow: /*?*limit= Disallow: /*?*filter= Disallow: /index.php?route=product/search Disallow: /index.php?route=checkout/ Disallow: /index.php?route=account/ # .htaccess: прожорливые краулеры и пустой User-Agent (список сверяем с вашим логом, не вслепую) RewriteCond %{HTTP_USER_AGENT} (AhrefsBot|SemrushBot|MJ12bot|DotBot|PetalBot|Bytespider|DataForSeoBot|^-?$) [NC] RewriteRule .* - [F,L]
Что можно проверить: график CPU-минут в панели ukraine.com.ua за 3 суток после правок и число запросов в access-логе до и после.
На скрине хостинга: письмо ушло с ящика на web798.default-host.net, а в заголовке From: стоял чужой адрес на rambler.ru. Хостинг такое режет: отправитель не принадлежит домену, это признак подмены. В OpenCart так бывает, когда в форме обратной связи в From подставляют адрес посетителя.
$mail->setFrom($this->request->post['email']); $mail->setSender($this->request->post['name']);
$mail->setFrom($this->config->get('config_email')); // ящик на вашем домене
$mail->setReplyTo($this->request->post['email']); // ответ уйдёт посетителю
$mail->setSender($this->config->get('config_name'));setFrom() берёт значение из $this->request (контакты, обратный звонок, «купить в 1 клик», сторонние модули).mail с корректным From, либо SMTP с ящиком домена.p=none на старте. Проверка тестовым письмом на Gmail: «Показать оригинал» → SPF и DKIM = PASS.На скрине заявка: «Клиент: 1' AND EXTRACTVALUE(1,CONCAT(0x7e,1)) AND '1'='1», телефон «1», комментарий «1». Это автоматический сканер: он проверяет, сломает ли кавычка запрос. Два следствия: нужно закрыть поток и нужно убедиться, что поле не попадает в SQL без экранирования (в модулях «быстрый заказ» это встречается).
| Слой | Мера | Эффект |
|---|---|---|
| Сервер | Валидация: имя без кавычек и SQL-символов, телефон по маске, длина комментария | Мусор отбивается до письма и до БД |
| БД | Все значения только через $this->db->escape(); проверка кода модуля на склейку SQL | Закрывает саму инъекцию |
| Бот | Скрытое поле-ловушка + Cloudflare Turnstile или reCAPTCHA | Режет основную массу скриптов без мучений для покупателя |
| Темп | Лимит: не больше 3 заявок с IP за 10 минут (таблица или сессия) | «300 за несколько минут» становятся невозможны |
| Почта | Письмо уходит только после прохождения всех проверок | Ящик больше не «обрушивается» |
// костяк проверки в контроллере формы / «быстрого заказа» if (!empty($this->request->post['website'])) { exit; } // honeypot: человек это поле не видит $name = trim($this->request->post['name'] ?? ''); $phone = preg_replace('/\D+/', '', $this->request->post['phone'] ?? ''); if (!preg_match('/^[\p{L}\s\-\.]{2,60}$/u', $name)) $error['name'] = 'Укажите имя'; if (strlen($phone) < 10 || strlen($phone) > 13) $error['phone'] = 'Проверьте номер'; if (too_many_from_ip($this->request->server['REMOTE_ADDR'], 3, 600)) { http_response_code(429); exit; } // только после этого: запись в БД через escape() и письмо
Работа считается сданной, когда каждая строка подтверждена фактом, а не словами.
0 из 9