ekofyi
Sell Digital Products "Securely" — Unless Someone Submits a Coupon Code
Security Research8 min read

Sell Digital Products "Securely" — Unless Someone Submits a Coupon Code

The Premium Packages plugin for WordPress lets unauthenticated attackers dump your database through a coupon code field. Here's how the SQL injection works and why REST API endpoints keep getting this wrong.

The plugin is called Premium Packages – Sell Digital Products Securely. And it has a REST API endpoint that takes a coupon code from anyone on the internet, shoves it straight into a raw SQL query, and hands the result back. No login. No nonce. No $wpdb->prepare(). Just $_POST['code'] into a WHERE clause.

Here's the thing: I get that WordPress plugin development is a wild mix of junior devs shipping features fast and solo maintainers fighting a backlog of 200 issues. But in 2026, an unauthenticated SQL injection in a premium plugin with "securely" in its name isn't just ironic — it's a symptom of a pattern that refuses to die. And the pattern isn't just about forgetting parameterized queries. It's about slapping REST API endpoints onto plugin functionality without thinking through the attack surface.

The CVE landed today: CVE-2026-12800, CVSS 7.5 HIGH, affecting Premium Packages versions up to and including 6.2.0. Disclosure by Wordfence. The vulnerable endpoint is POST /wp-json/wpdmpp/v1/cart/coupon. The code parameter is injected directly into a query built inside CouponCodes::find() via something like this (actual code from the version 6.2.0 tag):

php
// CouponCodes.php line ~26 (vulnerable)
$sql = "SELECT * FROM {$wpdb->prefix}ahm_coupons WHERE code = '{$code}' AND status = 1";
$result = $wpdb->get_row($sql);

No escaping. No prepared statement. The only thing standing between an attacker and your entire database is the single quotes around {$code}. And those quotes? Trivial to break out of.

What Makes This One Ugly

Look, not all SQLi is created equal. This one ticks several boxes that make it a high-severity treasure for an attacker:

  1. Unauthenticated. No authentication cookie, no nonce check, no authorization logic on the REST route. Anyone can hit it. The disclosure mentions it's in MiniCartAPI.php line 398, where the route callback is registered without any permission callback.
  2. Blind-friendly. Even if error display is off, an attacker can use time-based or boolean-based blind injection. The fact that the coupon code find() method likely returns a result or empty set gives a clear true/false oracle.
  3. Direct database extraction possible. With UNION SELECT or error-based techniques (if error reporting is on — and on many hosts, it is), an attacker can dump tables like wp_users, orders, or the plugin's own stored digital products and license keys.
  4. Widely used plugin? The changelog suggests this is a paid plugin with a substantial user base. The premium nature makes it worse — people paid for this, and the "securely" branding implies they trusted it more than they should have.

The attacker payload might look like a simple curl:

bash
curl -X POST https://target.com/wp-json/wpdmpp/v1/cart/coupon \
  -d "code=' OR 1=1 -- -"

That's it. No arcane bypass, no exotic encoding. The code is placed directly into the string, and the single quote breaks out. Everything after the -- - is a comment, neutralizing the trailing quote and the rest of the query. The result? The query becomes SELECT * FROM wp_ahm_coupons WHERE code = '' OR 1=1 -- -' AND status = 1, returning all coupons (or at least the first row) — and that's just the start. A real attacker would use sqlmap or a crafted UNION to pull user tables.

The Actual Vulnerable Code (Because Seeing It Matters)

The references point to three key files in the tags/6.2.0 branch:

  • CouponCodes.php line 26 — the unescaped interpolation into a raw SQL string.
  • CouponCodes.php line 82 — where the static find() method is defined and constructs the query.
  • MiniCartAPI.php line 398 — the REST route registration that calls CouponCodes::find($_POST['code']) without sanitization.

If you pull up the trac browser links, the pattern is painfully clear:

php
// CouponCodes.php#L82 (simplified from source)
public static function find($code) {
    global $wpdb;
    $sql = "SELECT * FROM {$wpdb->prefix}ahm_coupons WHERE code = '{$code}' AND status = 1";
    return $wpdb->get_row($sql);
}

The fix in the changeset 3595291 is equally obvious: wrap the variable in $wpdb->prepare() and pass the value as a placeholder.

php
// fixed version
$sql = $wpdb->prepare(
    "SELECT * FROM {$wpdb->prefix}ahm_coupons WHERE code = %s AND status = 1",
    $code
);

That's the entire fix. One function call. A junior dev could have caught this in code review. So why didn't they?

The REST API Endpoint Blind Spot

Let's step back from this specific CVE and talk about the bigger problem: WordPress plugins and custom REST API endpoints.

Since WordPress 4.7, the REST API has been a first-class citizen, and plugin developers have embraced it for AJAX-like functionality. But there's a mental model gap. Many developers treat AJAX handlers as if they're still behind admin-ajax.php with its implicit nonce requirements and user context. They forget that REST API endpoints are public by default unless you explicitly add a permission_callback that returns a WP_Error for unauthorized requests.

In this case, the register_rest_route() call in MiniCartAPI.php appears to lack any permission callback. That means WordPress will happily let anyone hit /wp-json/wpdmpp/v1/cart/coupon. The endpoint was designed to validate a coupon code during checkout — a seemingly harmless operation. But because it directly interacted with the database using unsanitized input, it became a direct pipeline to SQL injection.

I've seen this pattern over and over in my own bug bounty experience: a plugin adds a public REST endpoint for some innocuous front-end feature (like checking a promo code, subscribing to a newsletter, or looking up product availability). The developer assumes that because it's only returning data that should be public, sanitization doesn't matter. But SQL injection isn't about what the endpoint is supposed to return; it's about what the database engine can be tricked into executing.

But The Plugin Is "Premium"! Surely They Know Better?

Here's where the opinion gets sharp. Premium does not mean secure. In fact, I'd argue that premium plugins, especially those sold on marketplaces like CodeCanyon, often have _worse_ security than many popular free plugins. Why? Because the free ones on the WordPress.org repo undergo at least some level of automated scanning and community scrutiny thanks to the plugin review team and the public trac system. Premium plugins are a black box until someone like Wordfence takes a look.

And that's exactly what happened here. Wordfence disclosed it, presumably after finding it during a routine audit or from a customer report. The vulnerability sat in the code for who knows how long — maybe since the REST endpoint was first added. All those stores selling digital products, maybe collecting customer emails and transaction records, with the database wide open to anyone who can craft a curl command.

What to Do Right Now

If you run Premium Packages on your site, here's the straightforward advice:

  1. Update immediately to version 6.2.1 or higher (the changeset shows the fix is in the next release). The plugin's developer pushed a patch quickly after disclosure, which is good.
  2. If you can't update for some reason, block access to the REST endpoint at the web server level. This is a hack, not a fix:

``apache # In .htaccess or equivalent RewriteRule ^wp-json/wpdmpp/v1/cart/coupon$ - [F] `` Or better, use a Web Application Firewall rule to block requests containing SQL metacharacters in that specific parameter. But really, just update.

  1. Audit your database logs. Look for suspicious POST requests to that endpoint with unusual code values that contain SQL fragments (like ' OR '1'='1). If you see any, assume your database may have been exfiltrated. Rotate all secrets, salts, and force password resets for all users.
  2. If you're a developer building custom REST endpoints, internalize this: every public endpoint that touches the database needs parameterized queries. No exceptions. The $wpdb->prepare() function exists for exactly this reason, and it's trivial to use. If you're copying code from a Stack Overflow answer that uses string interpolation, you're part of the problem.

The Broader Lesson: Security Isn't a Feature You Bolt On

There's a name for this class of vulnerability: it's not a new attack vector, it's not a clever bypass. It's a basic, textbook SQL injection. And yet it shipped in a paid product, passed whatever internal QA they had, and likely affected thousands of sites.

I think it's worth saying out loud: if you sell a plugin and put "securely" in the title, you are making a promise. That promise means your code should survive at the very least a cursory security review. It means you shouldn't have SQL injection in an unauthenticated endpoint. It means the first time someone typed $sql = "SELECT * FROM ... WHERE code = '{$code}'", an alarm should have gone off.

This CVE is a reminder that security isn't about the complexity of the defense; it's about the consistency of the basics. Use parameterized queries. Escape output. Check permissions. Do those three things everywhere, every time, and you eliminate the vast majority of vulnerabilities in web applications.

The irony of this particular plugin name is going to stick with me. Sell Digital Products Securely. Take a moment, check your sites, update that plugin, and maybe have a quiet laugh at the absurdity of it all.

Then go look at your own code and make sure you're not making the same mistake somewhere else. Because you probably are.

Related posts

Written by Eko

If you found this useful, follow @ekofyi on X for more notes like this — or get in touch if you have a problem to solve.