Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

Forms, validation and security

OpenCart does little for the programmer: there are no parameterised queries, no automatic escaping in templates, no form validator. Every value from a form has to be checked, protected before the database query and escaped on output — by hand. This lesson adds a callback form to the module and walks through each step.

The form

Add the form to the module template and its address to index() of the storefront controller:

// extension/acme_hello/catalog/controller/module/acme_hello.php, in 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>

The home page now shows two fields and a button inside the module card.

Validation

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));
}

Send the form empty — errors appear under both fields. With correct values the success message appears.

SQL injection

SQL injection is when a value from a form becomes part of the query and changes its meaning. The model protects every value:

// 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();
    }
}

The row appears in the table oc_acme_callback and in the list on the settings page of the module. A column name for sorting cannot be protected with escape() — it is compared with a fixed list of allowed names.

XSS

XSS is when text entered by one visitor is executed as a script in the browser of another. Strings from $this->request are already passed through htmlspecialchars() by the engine and are stored escaped. Data from other sources — an import file, JSON from an external service — is not, so it is escaped on output: {{ name|escape }}.

Request forgery

In the admin panel every request is protected by user_token. The storefront has no such protection: another site can send this form on behalf of a visitor. For an important form add your own token:

// in index()
$this->session->data['acme_hello_token'] ??= bin2hex(random_bytes(16));
 
$data['token'] = $this->session->data['acme_hello_token'];
// in send(), before the other checks
if (!hash_equals($this->session->data['acme_hello_token'] ?? '', (string) ($this->request->post['token'] ?? ''))) {
    $json['error'] = $this->language->get('error_token');
}

The template sends it in a hidden field named token.

Common mistake

A value is escaped twice: the request already replaced & with &amp;, then the template applies |escape again — and the page shows Tom &amp; Anna. Escape a value once: request strings are already escaped, data from other sources is escaped on output.

Thanks for reading!

Donate to the site — we work hard for you.