If your Magento store suddenly emails you a Payment Transaction Failed Reminder for a guest, with a total of 0.0000 and a billing address that literally reads {{var postcode}}, nothing is wrong with your payment gateway. You are looking at the side effect of an automated exploit attempt, and the broken address is the tell.
- Recognise the email and understand what actually produced it
- Find out, in five commands, whether the attacker got anything
- Block the request patterns at the web server, before PHP runs
- Clean the poisoned log files that the attack feeds on
The email you received
This is the message people paste into search engines, so here it is in full. Names and addresses are redacted; everything else is verbatim.
Subject: Payment Transaction Failed Reminder
Payment Transaction Failed
Reason
The transaction has been declined
Checkout Type
onepage
Customer:
Guest <test@example.com>
Items
Total:
EUR 0.0000
Billing Address:
A B
{{var postcode}}{{var postcode}}
Andorra
T: 0
Payment Method:
Date & Time:
24 Sep 2026, 06:57:41If your store is localised, the reason line arrives translated โ for example La transazione รจ stata rifiutata in Italian, or Die Transaktion wurde abgelehnt in German. The rest of the layout is identical, and so is the meaning.
Four details identify it as a probe rather than a customer:
{{var postcode}}printed raw, usually twice, instead of a post code- A total of
0.0000in your base currency - A guest with an implausible e-mail, often
@example.comor a domain ending in.invalid - A random, rarely-ordered-from country โ Andorra shows up constantly โ and a phone of
0
They arrive in bursts: dozens within two or three minutes, then silence for hours.
What actually produced it
Magento sends this notification when a checkout reaches the payment step and fails. An attacker driving the checkout from a script, rather than a browser, trips the same notification โ so the email is collateral, not the attack itself.
The address is mangled for the same reason. Magento stores a per-country address format containing placeholders such as {{var postcode}}, and fills them from the address data. When the address never came from a real form, the placeholders are rendered with nothing to substitute, and the template text reaches your inbox unchanged.
The request patterns behind it
Across stores we monitor, the same automated kit produces these emails while trying several published Magento vulnerabilities in sequence. In the access log the bursts look like this โ the parameter names matter, the payloads are deliberately omitted here:
POST /paypal/transparent/response/?<a PHP open tag and payload>โ the goal is not to pay. Magento logs unknown store codes and malformed input intovar/log/system.log, so this request writes attacker-controlled text into a file on your server.POST /graphql?styles[first]=<path to that log file>&styles[generatorClass]=โฆโ this is the inclusion step, the one that turns written text into executed code. It is CVE-2026-75650, fixed by Adobe'sVULN-39341hotfix.POST /graphql?type=2&text={{block class=โฆ}}โ template-directive injection, aimed at Magento's template engine through a different door.POST /customer/address_file/uploadfollowed byPUT /rest/V1/guest-carts/<id>/orderโ the CVE-2025-54236 ("SessionReaper") chain, which is what creates the guest cart and fires the email.
The same four patterns are tried against every store the kit finds, from many rotating IP addresses. In five days of logs on one small group of stores we counted more than fifteen distinct source addresses.
200 response to one of the GraphQL requests does not mean the attacker succeeded. GraphQL answers 200 even when the operation fails. The proof of a breach is a file on disk, never an HTTP status code. Step 1: Find out whether anything actually landed
Run these on the server, from the Magento root. They are read-only.
First, is the hotfix in place? The answer must be 2:
grep -c "Rejects non-string input" \
vendor/magento/module-email/Model/AbstractTemplate.phpSecond, did anything new appear where uploads land? The payloads in circulation write a small .txt marker next to a .php file, so an unexpected pair with a random name is the thing to look for:
find pub/media -maxdepth 2 -newermt "-7 days" \
\( -name "*.php*" -o -name "*.phar" \) -printThird, is your log file poisoned? Any hit here means the first stage of the chain worked, even if the second did not:
grep -c "<?php" var/log/system.log
grep -rl "<?php" var/report | wc -lFourth, did the web server user write anything outside the directories it should own?
find . -user www-data -newermt "-7 days" -type f \
| grep -vE "/var/(log|report|cache|page_cache|session|tmp)/|/pub/(static|media)/"Fifth, is anything running or scheduled that should not be?
crontab -l -u www-data
ps -eo user,pid,etimes,args | grep -v grep | awk '$1=="www-data"'Archive the poisoned files before you delete them: copy var/log/system.log, the matching var/report entries and the relevant access-log lines into a dated folder. If you later need to prove what happened, truncating first destroys the only record you have.
Step 2: Apply the hotfix, and re-check it after every composer run
Adobe publishes the VULN-39341 patch set as a zip that downloads without repository credentials. It contains one .patch per version line; pick the one that applies cleanly with patch -p1 --dry-run rather than trusting the version table, because neighbouring patch levels often apply with fuzz.
It touches PHP files under vendor/ only โ no frontend templates โ so you need to regenerate generated/, not redeploy static content.
composer update or composer require can silently restore the unpatched files. Make the grep -c above part of your deploy routine, and re-run it before you put the site back in front of visitors. Step 3: Block the patterns at the web server
The hotfix closes the inclusion step. Blocking the requests stops them reaching PHP at all, which also keeps them out of your logs. None of these parameters appear in legitimate Magento traffic:
# Reject the styles[...] parameter in any form, encoded or not
if ($request_uri ~* "styles(\[|%5[bB])") { return 403; }
# Template directives in a query string are always an attack
if ($request_uri ~* "(\{\{|%7[bB]%7[bB])\s*(block|var|config|trans|template|layout)") { return 403; }
# PHP open tags and decode calls in a query string
if ($request_uri ~* "(<\?|%3[cC]%3[fF])") { return 403; }
if ($request_uri ~* "base64_decode") { return 403; }
# Endpoints no store needs unless it actually uses them
location ~* "^/(?:index\.php/)?(?:[a-z]{2}(?:_[a-z]{2})?/)?customer/address_file/upload" { return 410; }
location ^~ /media/customer_address/ { return 410; }
location ~* "/paypal/transparent/response" { return 410; }Two things that cost us time, and will cost you the same:
Your blocks will look like they are not working. Magento's nginx.conf.sample contains error_page 404 403 = /errors/404.php;, which rewrites every 403 into a 404 โ and routes it through PHP on the way. A rule that logs 404 instead of 403 is not a broken rule. Returning 410 sidesteps the remap entirely and skips PHP.
Do not reach for return 444. It closes the connection without a response, and if you run a reverse proxy in front of the store, the proxy turns that silence into a 502 for the visitor โ which looks exactly like an outage.
Before you enable the last three lines, confirm those endpoints are genuinely unused. customer/address_file/upload only matters if you have file-type attributes on customer addresses, which almost nobody does. paypal/transparent/response belongs to PayPal Payflow Pro, Advanced and Link โ check each one:
PayPal Express Checkout and Braintree use different routes and are unaffected.
Step 4: Clean the poisoned logs
This step gets skipped, and it matters. The first stage of the attack writes PHP into var/log/system.log. Patching stops the inclusion, but the loaded file stays on disk โ ready for the next bypass someone finds.
Once you have archived a copy, truncate rather than delete, so ownership and permissions survive:
: > var/log/system.logThen remove the var/report entries that contain payloads, and make sure log rotation actually exists for var/log/*.log. Use copytruncate: Magento keeps its log files open and will not reopen them after a rename.
While you are in there, check the size of var/log/debug.log. On one store we found 7.6 GB of a single repeated error, growing about a gigabyte a day on a disk that was already filling up.
Step 5: Make repeat offenders expensive
The IP addresses rotate, and a single burst is a hundred-plus requests in ninety seconds โ the shape of traffic an automatic ban handles well. A tool such as fail2ban, watching the same patterns in your access log and banning the source for a day, takes the noise out of your logs and the load off your PHP workers. Configure it to read the log that holds the real client IP: behind a reverse proxy, your application server only ever sees the proxy.
Should you turn the notification off?
You can โ the setting lives under Sales > Checkout > Payment Failed Emails โ but we would not.
Three separate intrusion attempts across the stores we look after were noticed because somebody read one of these emails and thought it looked odd. Until you have alerting on the request patterns themselves, this ugly little message is your only smoke detector. Once fail2ban is banning and your rules are rejecting, the emails stop on their own, because the checkout is never reached.
Checklist
- โ The hotfix marker check returns
2 - โ No unexpected
.php,.php8or.pharfiles underpub/media - โ
var/log/system.logcontains no<?php - โ Nothing written by the web user outside
var/,pub/staticandpub/media - โ No cron entries or long-running processes owned by the web user
- โ Web-server rules in place, verified with a real request from outside
- โ Legitimate traffic still returns
200โ home, category, product, cart, checkout - โ Poisoned logs archived, then truncated, with rotation configured
- โ The hotfix check added to your deploy routine
FAQ
Is this a fraud attempt on my payment gateway?
No. No card is presented and no money moves. The 0.0000 total is the giveaway: there is no real basket behind it.
A customer's card was declined at the same time. Related?
Almost certainly not. A genuine decline carries a real customer, a real total and a rendered address. Judge each email by the four markers at the top of this article.
The requests returned 200. Am I compromised?
Not by itself. GraphQL returns 200 for failed operations too. Work through Step 1 โ what matters is whether a file was written.
I am on Magento 2.4.9, the newest version. Am I safe?
Not automatically. CVE-2026-75650 affects 2.4.4 through 2.4.9, and a fresh install from composer does not carry the hotfix. We have seen a brand-new 2.4.9 store probed successfully within hours of going live, because the patch had not been reapplied after the last composer run.
The emails stopped on their own. Did the attacker give up?
More likely they moved to another target in their list, and will be back. Do the checks anyway: the first stage may have left something behind.
Can I just delete the emails with a mail rule?
You can, and then you will not notice the next campaign. Fix the cause, and the emails disappear as a consequence.



