CVE-2019-7139: the Ambionics pre-auth SQL injection in Magento 2
Magekwik Security 6 min read
Charles Fol's PRODSECBUG-2198 turned a storefront product-tracking endpoint into unauthenticated SQL injection via a placeholder-reuse bug in prepareSqlCondition() — and a read-only leak into admin takeover. Root cause, exploitation, and the NVD-authoritative patch path. (The RCE that completed the chain was a separate CVE.)
What it is
CVE-2019-7139 (Magento tracker PRODSECBUG-2198) is a pre-authentication SQL injection reachable through the storefront catalog/product_frontend_action/synchronize controller. It was reported by Charles Fol of Ambionics Security and patched in the March 2019 quarterly release. No account, no CSRF token, and no admin surface is required: an anonymous request to a public product-view-tracking endpoint yields arbitrary read access to the database, and from there a practical path to full admin takeover and code execution.
Scope
Root cause
The bug is not in the controller but in the query builder shared by the whole framework: Magento\Framework\DB\Adapter\Pdo\Mysql::prepareSqlCondition(). When a condition specifies a bounded range using both from and to keys, the method builds the SQL in two steps. The second step re-quotes the already-partially-built fragment instead of quoting only the new value:
// $query already contains the rendered `from` fragment
$query = $this->_prepareQuotedSqlCondition(
$query . $conditionKeyMap['to'], $to, $fieldName
);
// every remaining `?` is replaced — including ones the attacker
// smuggled in through the `from` value
Placeholder substitution is positional and global over the string. Because the attacker controls the from value, planting a ? there causes the to value to be substituted at that position instead of at the intended bind point. The to value is concatenated without quoting, so the second half of the range condition is injected verbatim into the WHERE clause. The prepared-statement guarantee is silently broken by the framework's own condition assembler.
How attackers abuse it
The Synchronize action deserialises a JSON body of "frontend actions" (recently viewed / compared products) and passes the ids map straight into a collection filter, which lands in prepareSqlCondition(). A crafted range condition breaks out of the intended predicate:
type_id=recently_products
ids[0][added_at]=...
ids[0][product_id][from]=?
ids[0][product_id][to]=))) OR (SELECT ... UNION SELECT ...) -- -
The endpoint returns no query results to the client, so exploitation is blind — boolean- and time-based extraction via sqlmap or a bespoke oracle. Read access alone is enough to pivot: dump admin_user password hashes for offline cracking, or read admin_user_session / core_config_data to obtain the crypt key, forge or hijack an admin session, and recover a valid form_key. Fol later disclosed a separate remote-code-execution vulnerability — a distinct CVE, not CVE-2019-7139 itself — that uses an authenticated primitive (template/layout handling and PHAR-style object deserialization) to turn admin access into php on the box. Chained, the two bugs walked a store from an unauthenticated storefront request to a shell; but keep the CVE ids distinct — this post is about the SQL injection.
Who got hit / real examples
This did not stay theoretical for long. Within days of the 2.3.1 release, working proof-of-concept code and sqlmap tamper recipes were public (Vulhub shipped a reproducible 2.2 lab), which dropped the exploitation bar to commodity scanning. Magento's own advisory urged emergency patching precisely because the injection is unauthenticated and the endpoint is present on every default storefront. In the 2019 Magecart era, unpatched Magento SQLi and RCE were a primary route to planting card skimmers in checkout, and this class of bug — read the crypt key, forge admin, inject payment JavaScript — was exactly the pattern operators used at scale.
Why patch status is not enough
How to check
- Confirm the exact patch level:
bin/magento --version, and cross-check against 2.1.18 / 2.2.9 / 2.3.2 — the NVD-authoritative fixed baseline for this CVE. (2.3.1 / 2.2.8 / 2.1.17 carried Magento's PRODSECBUG-2198 patch, but NVD still lists 2.3.1 as affected, so the higher line is the safe floor.) - Verify the fix is actually present in code rather than trusting the version string, since patched-by-composer and vendor-patched trees diverge:
grep -n "_prepareQuotedSqlCondition" \
vendor/magento/framework/DB/Adapter/Pdo/Mysql.php
# Fixed builds quote only the `to` value, then concatenate:
# $query = $query . $this->_prepareQuotedSqlCondition(
# $conditionKeyMap['to'], $to, $fieldName);
- Review web-server and application logs for POST requests to
catalog/product_frontend_action/synchronizecontaining][from]/][to]with SQL keywords,UNION,SLEEP(, or stacked comment markers.
How to fix it
There is no configuration mitigation that fully closes this; the query builder must be corrected. Upgrade to a fixed release or apply the official patch.
composer require magento/product-community-edition 2.3.2 \
--no-update
composer update magento/framework --with-dependencies
bin/magento setup:upgrade
bin/magento setup:di:compile
bin/magento cache:flush
If you cannot take a full minor upgrade, apply the isolated vendor patch for PRODSECBUG-2198 (distributed via the official security package / quality patches) rather than hand-editing vendor/:
composer require magento/quality-patches
bin/magento setup:upgrade
./vendor/bin/magento-patches status
./vendor/bin/magento-patches apply MDVA-<id> # PRODSECBUG-2198 backport
After patching, treat the store as potentially compromised for the exposure window: rotate the crypt/key, invalidate all admin sessions, force admin password resets, and audit core_config_data and checkout templates for injected script. A read-only SQLi that leaks the crypt key does not "expire" when you patch — the stolen secret still decrypts saved sessions until it is rotated.
References & sources
Primary sources — advisories, vendor research, and the CVE record. Verify against these, not us.