Jump to content
thirty bees forum
  • 0

Contact form exploit concern


Jeffrey de Bruijn

Question

I would like to raise a concern about the contact form.

The default behavior of the contact form: customer reaches the contact form, submits a message and the form sends the customer a copy of the message at the provided email address.

The problem: this behavior cannot be changed in the backoffice and it can easily be exploited. A malicious individual, human or robot, can provide the email address of another person and enter the message to send to the provided address, effectively turning the mail server to an open relay. This causes the domain of the shop to be blacklisted as spam/malicious domain and hurts email deliverability/reputation.

Proposed solution: provide the shop owner the ability to disable "sending a copy of the message" to the customer so that the copy of the email is only sent to the domain of the shop owner.

Hotfix solution: provide an override anyone can apply to their shop

Unacceptable solution: utilizing a captcha, specifically google's recaptcha, is not a viable solution because it is not a service available in all countries and in Europe it may be required to be loaded as an opt-in non-strictly-necessary cookie (bots are not going to accept optional cookies).

 

Edit: hotfix

Here is the code of my override if anybody needs it. Save it as ContactController.php in override/controllers/front and then delete cache/class_index.php. With this override, the contact form will no longer send a copy of the email to the email provided by the visitor.

<?php

class ContactController extends ContactControllerCore {

    public function postProcess()
    {
        if (Tools::isSubmit('submitMessage')) {
            $extension = ['.txt', '.rtf', '.doc', '.docx', '.pdf', '.zip', '.png', '.jpeg', '.gif', '.jpg'];
            $fileAttachment = Tools::fileAttachment('fileUpload');
            $message = Tools::getValue('message'); // Html entities is not usefull, iscleanHtml check there is no bad html tags.
            if (!($from = Tools::convertEmailToIdn(trim(Tools::getValue('from')))) || !Validate::isEmail($from)) {
                $this->errors[] = Tools::displayError('Invalid email address.');
            } elseif (!$message) {
                $this->errors[] = Tools::displayError('The message cannot be blank.');
            } elseif (!Validate::isCleanHtml($message)) {
                $this->errors[] = Tools::displayError('Invalid message');
            } elseif (!($idContact = (int) Tools::getValue('id_contact')) || !(Validate::isLoadedObject($contact = new Contact($idContact, $this->context->language->id)))) {
                $this->errors[] = Tools::displayError('Please select a subject from the list provided. ');
            } elseif (!empty($fileAttachment['name']) && $fileAttachment['error'] != 0) {
                $this->errors[] = Tools::displayError('An error occurred during the file-upload process.');
            } elseif (!empty($fileAttachment['name']) && !in_array(mb_strtolower(substr($fileAttachment['name'], -4)), $extension) && !in_array(mb_strtolower(substr($fileAttachment['name'], -5)), $extension)) {
                $this->errors[] = Tools::displayError('Bad file extension');
            } else {
                $customer = $this->context->customer;
                if (!$customer->id) {
                    $customer->getByEmail($from);
                }

                $idOrder = (int) $this->getOrder();

                if (!((
                        ($idCustomerThread = (int) Tools::getValue('id_customer_thread'))
                        && (int) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue(
                            (new DbQuery())
                                ->select('ct.`id_customer_thread`')
                                ->from('customer_thread', 'ct')
                                ->where('ct.`id_customer_thread` = '.(int) $idCustomerThread)
                                ->where('ct.`id_shop` = '.(int) $this->context->shop->id)
                                ->where('ct.`token` = \''.pSQL(Tools::getValue('token')).'\'')
                        )
                    ) || (
                    $idCustomerThread = CustomerThread::getIdCustomerThreadByEmailAndIdOrder($from, $idOrder)
                    ))
                ) {
                    $fields = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS(
                        (new DbQuery())
                            ->select('ct.`id_customer_thread`, ct.`id_contact`, ct.`id_customer`, ct.`id_order`, ct.`id_product`, ct.`email`')
                            ->from('customer_thread', 'ct')
                            ->where('ct.`email` = \''.pSQL($from).'\'')
                            ->where('ct.`id_shop` = '.(int) $this->context->shop->id)
                            ->where('('.($customer->id ? 'id_customer = '.(int) $customer->id.' OR ' : '').' id_order = '.(int) $idOrder.')')
                    );
                    $score = 0;
                    foreach ($fields as $key => $row) {
                        $tmp = 0;
                        if ((int) $row['id_customer'] && $row['id_customer'] != $customer->id && $row['email'] != $from) {
                            continue;
                        }
                        if ($row['id_order'] != 0 && $idOrder != $row['id_order']) {
                            continue;
                        }
                        if ($row['email'] == $from) {
                            $tmp += 4;
                        }
                        if ($row['id_contact'] == $idContact) {
                            $tmp++;
                        }
                        if (Tools::getValue('id_product') != 0 && $row['id_product'] == Tools::getValue('id_product')) {
                            $tmp += 2;
                        }
                        if ($tmp >= 5 && $tmp >= $score) {
                            $score = $tmp;
                            $idCustomerThread = $row['id_customer_thread'];
                        }
                    }
                }
                $oldMessage = Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue(
                    (new DbQuery())
                        ->select('cm.`message`')
                        ->from('customer_message', 'cm')
                        ->leftJoin('customer_thread', 'cc', 'cm.`id_customer_thread` = cc.`id_customer_thread`')
                        ->where('cc.`id_customer_thread` = '.(int) $idCustomerThread)
                        ->where('cc.`id_shop` = '.(int) $this->context->shop->id)
                        ->orderBy('cm.`date_add` DESC')
                );
                if ($oldMessage == $message) {
                    $this->context->smarty->assign('alreadySent', 1);
                    $contact->email = '';
                    $contact->customer_service = 0;
                }

                if ($contact->customer_service) {
                    if ((int) $idCustomerThread) {
                        $ct = new CustomerThread($idCustomerThread);
                        $ct->status = 'open';
                        $ct->id_lang = (int) $this->context->language->id;
                        $ct->id_contact = (int) $idContact;
                        $ct->id_order = (int) $idOrder;
                        if ($idProduct = (int) Tools::getValue('id_product')) {
                            $ct->id_product = $idProduct;
                        }
                        $ct->update();
                    } else {
                        $ct = new CustomerThread();
                        if (isset($customer->id)) {
                            $ct->id_customer = (int) $customer->id;
                        }
                        $ct->id_shop = (int) $this->context->shop->id;
                        $ct->id_order = (int) $idOrder;
                        if ($idProduct = (int) Tools::getValue('id_product')) {
                            $ct->id_product = $idProduct;
                        }
                        $ct->id_contact = (int) $idContact;
                        $ct->id_lang = (int) $this->context->language->id;
                        $ct->email = $from;
                        $ct->status = 'open';
                        $ct->token = Tools::passwdGen(12);
                        $ct->add();
                    }

                    if ($ct->id) {
                        $cm = new CustomerMessage();
                        $cm->id_customer_thread = $ct->id;
                        $cm->message = $message;
                        if (isset($fileAttachment['rename']) && !empty($fileAttachment['rename']) && rename($fileAttachment['tmp_name'], _PS_UPLOAD_DIR_.basename($fileAttachment['rename']))) {
                            $cm->file_name = $fileAttachment['rename'];
                            @chmod(_PS_UPLOAD_DIR_.basename($fileAttachment['rename']), 0664);
                        }
                        $cm->ip_address = (int) ip2long(Tools::getRemoteAddr());
                        $length = ObjectModel::getDefinition('CustomerMessage', 'user_agent')['size'];
                        $cm->user_agent = substr($_SERVER['HTTP_USER_AGENT'], 0, $length);
                        if (!$cm->add()) {
                            $this->errors[] = Tools::displayError('An error occurred while sending the message.');
                        }
                    } else {
                        $this->errors[] = Tools::displayError('An error occurred while sending the message.');
                    }
                }

                if (!count($this->errors)) {
                    $varList = [
                        '{order_name}'    => '-',
                        '{attached_file}' => '-',
                        '{message}'       => Tools::nl2br(stripslashes($message)),
                        '{email}'         => $from,
                        '{product_name}'  => '',
                    ];

                    if (isset($fileAttachment['name'])) {
                        $varList['{attached_file}'] = $fileAttachment['name'];
                    }

                    $idProduct = (int) Tools::getValue('id_product');

                    if (isset($ct) && Validate::isLoadedObject($ct) && $ct->id_order) {
                        $order = new Order((int) $ct->id_order);
                        $varList['{order_name}'] = $order->getUniqReference();
                        $varList['{id_order}'] = (int) $order->id;
                    }

                    if ($idProduct) {
                        $product = new Product((int) $idProduct);
                        if (Validate::isLoadedObject($product) && isset($product->name[$this->context->language->id])) {
                            $varList['{product_name}'] = $product->name[$this->context->language->id];
                        }
                    }
                    
                    // Here disable sending copy of email to customer https://forum.thirtybees.com/topic/5433-contact-form-exploit-concern
                    if (!empty($contact->email)) {
                        if (!Mail::Send($this->context->language->id, 'contact', Mail::l('Message from contact form') . ' [no_sync]', $varList, $contact->email, $contact->name, null, null, $fileAttachment, null, _PS_MAIL_DIR_, false, null, null, $from)) {
                            $this->errors[] = Tools::displayError('An error occurred while sending the message.');
                        }
                    }
                }

                if (count($this->errors) > 1) {
                    array_unique($this->errors);
                } elseif (!count($this->errors)) {
                    $this->context->smarty->assign('confirmation', 1);
                }
            }
        }
    }
}

 

Edited by Jeffrey de Bruijn
Link to comment
Share on other sites

3 answers to this question

Recommended Posts

  • 0
On 3/16/2022 at 8:54 AM, datakick said:

Other solution is to edit email template and make it send static text only. As a confirmation that message was received.

Yes this is also an acceptable solution. In my experience, people do not trust that contact forms actually send any messages, some form of confirmation is better than none.

As a system administrator I think that reducing the volume of emails sent is generally a good idea. If hosting a shop on a shared hosting account, most hosts limit the emails sent on a hourly/daily basis to a very low amount, I have seen as low as 50 per hour. Large shops may run into these limitations and the above override can help them.

Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
×
×
  • Create New...