Encodera

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.

Оглавление

Формы, проверка и безопасность

OpenCart мало что делает за программиста: нет ни запросов с параметрами, ни автоматического экранирования в шаблонах, ни проверки форм. Каждое значение из формы нужно проверить, защитить перед запросом к базе и экранировать при выводе — вручную. В этом уроке в модуль добавляется форма заявки на звонок, и каждый шаг разобран по очереди.

Форма

Добавьте форму в шаблон модуля, а её адрес — в index() контроллера витрины:

// extension/acme_hello/catalog/controller/module/acme_hello.php, в index()
$data['send'] = $this->url->link('extension/acme_hello/module/acme_hello.send', 'language=' . $this->config->get('config_language'));
{# extension/acme_hello/catalog/view/template/module/acme_hello.twig #}
<form id="form-callback" action="{{ send }}" method="post" data-oc-toggle="ajax">
  <input type="text" name="name" id="input-name" placeholder="{{ entry_name }}" class="form-control mb-2">
  <div id="error-name" class="invalid-feedback"></div>
  <input type="tel" name="phone" id="input-phone" placeholder="{{ entry_phone }}" class="form-control mb-2">
  <div id="error-phone" class="invalid-feedback"></div>
  <button type="submit" class="btn btn-primary">{{ button_submit }}</button>
</form>

Теперь на главной странице в карточке модуля два поля и кнопка.

Проверка

public function send(): void
{
    $this->load->language('extension/acme_hello/module/acme_hello');
 
    $json = [];
 
    $name = trim($this->request->post['name'] ?? '');
    $phone = preg_replace('/\D/', '', $this->request->post['phone'] ?? '');
 
    if (mb_strlen($name) < 2 || mb_strlen($name) > 64) {
        $json['error']['name'] = $this->language->get('error_name');
    }
 
    if (strlen($phone) < 10) {
        $json['error']['phone'] = $this->language->get('error_phone');
    }
 
    if (!$json) {
        $this->load->model('extension/acme_hello/module/acme_hello');
 
        $this->model_extension_acme_hello_module_acme_hello->addCallback($name, $phone);
 
        $json['success'] = $this->language->get('text_success');
    }
 
    $this->response->addHeader('Content-Type: application/json');
    $this->response->setOutput(json_encode($json));
}

Отправьте форму пустой — под обоими полями появятся ошибки. С правильными значениями появится сообщение об успехе.

SQL-инъекция

SQL-инъекция — это когда значение из формы становится частью запроса и меняет его смысл. Модель защищает каждое значение:

// extension/acme_hello/catalog/model/module/acme_hello.php
namespace Opencart\Catalog\Model\Extension\AcmeHello\Module;
 
class AcmeHello extends \Opencart\System\Engine\Model
{
    public function addCallback(string $name, string $phone): int
    {
        $this->db->query(
            "INSERT INTO `" . DB_PREFIX . "acme_callback` SET
             `name` = '" . $this->db->escape($name) . "',
             `phone` = '" . $this->db->escape($phone) . "',
             `customer_id` = '" . (int) $this->customer->getId() . "',
             `date_added` = NOW()"
        );
 
        return $this->db->getLastId();
    }
}

Строка появится в таблице oc_acme_callback и в списке на странице настроек модуля. Имя столбца для сортировки нельзя защитить через escape() — его сверяют с заранее заданным списком разрешённых имён.

XSS

XSS — это когда текст, введённый одним посетителем, выполняется как скрипт в браузере другого. Строки из $this->request движок уже пропустил через htmlspecialchars(), и они хранятся экранированными. Данные из других источников — файл импорта, JSON от внешней службы — не экранированы, поэтому их экранируют при выводе: {{ name|escape }}.

Подделка запроса

В панели каждый запрос защищён user_token. На витрине такой защиты нет: чужой сайт может отправить эту форму от имени посетителя. Для важной формы добавьте собственный токен:

// в index()
$this->session->data['acme_hello_token'] ??= bin2hex(random_bytes(16));
 
$data['token'] = $this->session->data['acme_hello_token'];
// в send(), перед остальными проверками
if (!hash_equals($this->session->data['acme_hello_token'] ?? '', (string) ($this->request->post['token'] ?? ''))) {
    $json['error'] = $this->language->get('error_token');
}

Шаблон отправляет его в скрытом поле с именем token.

Частая ошибка

Значение экранируют дважды: запрос уже заменил & на &amp;, затем шаблон ещё раз применяет |escape — и на странице стоит Tom &amp; Anna. Экранируйте значение один раз: строки из запроса уже экранированы, данные из других источников экранируются при выводе.

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.