CVE-2025-14770 – Unauthenticated SQL Injection via “city” Parameter

WordPress Shipping Rate By Cities Plugin
Overview
- CVE ID: CVE-2025-14770
- Affected Plugin: Shipping Rate By Cities
- Affected Versions: ≤ 2.0.0
- Vulnerability Type: Unauthenticated SQL Injection
- Attack Vector: Network
- Authentication Required: No
Description
The Shipping Rate By Cities WordPress plugin contains an unauthenticated SQL Injection vulnerability in versions up to 2.0.0.
The issue originates from unsafe handling of the city parameter, which is concatenated directly into an SQL query without proper preparation.
Because the vulnerable code is reachable during the WooCommerce checkout flow, an attacker does not need authentication to exploit it.
Successful exploitation allows an attacker to manipulate SQL queries, potentially leading to:
- Sensitive data disclosure
- Database enumeration
- Service degradation via time-based payloads
Patch & Commit Analysis

Based on the image provided, here is the technical analysis of the patch for CVE-2025-14770 in the “Shipping Rate by Cities” WordPress plugin.
The Vulnerability: SQL Injection (SQLi)
Looking at the right side (the old version), the code for the getCityFee function was:
1 | public function getCityFee($city_name){ |
- The Problem: The variable $city_name was directly concatenated into the SQL query string.
- The Risk: Since $city_name likely comes from a user-controlled source (like a checkout form), an attacker could input something like ‘ OR 1=1 – to manipulate the query, bypass logic, or extract sensitive data from the database.
The Patch: Secure Coding Practices
The left side (the new version) introduces several layers of defense to mitigate this vulnerability
Input Sanitization
The patch adds:
1 | $city_name = sanitize_text_field($city_name); |
- This is the first line of defense. It strips out HTML tags and characters that shouldn’t be in a simple text field, reducing the attack surface.
Prepared Statements (The Core Fix)
The most critical change is the shift to the $wpdb->prepare() method:
1 | $result = $wpdb->get_row( |
- How it works: Instead of building a string, it uses a placeholder (%s). WordPress then handles the data binding, ensuring that the value of $city_name is treated strictly as data, not as part of the SQL command. This effectively kills the SQL Injection vector.
Implementation of Object Caching
The patch also introduces the WordPress Cache API:
1 | $cached = wp_cache_get($cache_key, $cache_group); |
- While primarily introduced for performance optimization, object caching may reduce repeated identical queries, but it should not be considered a security mitigation against SQL Injection or DoS attacks, as attackers can still bypass cache by varying input values.
Attack Flow
I have categorized the data flow into two main processes:
- Admin Process: How data is stored in the database (Configuration).

- User/Checkout Process: How data is retrieved and used (The Vulnerable Path).


SINK -> SOURCE (Backtrace)
I present this part in the report to explain “The path of malicious data
- File: shiprate-cities-method-class.php
- Class/Method: ShipRate_FlatShipRateCity_Method::getCityFee($city_name)

Caller (Function to call and transmit data)
After the breakpoint was hit it will call ShipRate_FlatShipRateCity_Method::calculate_shipping($package) method.
1 | public function calculate_shipping( $package = array() ) { |
The variable $package is an array containing cart information and shipping address. This variable has not been strictly controlled at this step.
Trigger (WordPress/WooCommerce Hook Mechanism)
- Mechanism: The plugin registers this method into the WooCommerce system through the hooks
woocommerce_shipping_initandwoocommerce_shipping_methods(in the main plugin file). - Flow: When WooCommerce needs to recalculate the total amount (when the user changes address, adds items…), it will loop through all activated Shipping Methods and call the calculate_shipping($package) function of each one.

SOURCE (Input Point & Entry Point)
Unlike standard WordPress Form submissions ($_POST), JSON data sent via REST API is not subjected to WordPress’s default wp_magic_quotes() mechanism. This allows raw single quotes (‘) to reach the vulnerable function without being escaped as ', making the SQL Injection possible.
- Entry Point: Hacker sends Request to REST API
/wp-json/wc/store/v1/cart/update-customer. - Controller:
Automattic\WooCommerce\StoreApi\Routes\V1\CartUpdateCustomer::get_response(This is the core code of WooCommerce).
POC And Debug
When a user updates their shipping information on a WooCommerce site, the browser sends a REST API request to all action.
Endpoint:/wp-json/wc/store/v1/cart

After proceed checkout, we can see that it calls a REST API to check the cart and check for every value like address or state in personal customer details.

When i change shipping address value and change shipping method, i got an API called /wp-json/wc/store/v1/batch?_locale=site, as we saw at the image above, it will request to another API called /wc/store/v1/cart/update-customer and this this the place that called entry point because its function was to changed the customer details. Besides, the vulnerable value is city.
Why send an update-customer request?
- Because the
getCityFeeerror function usesCityas input. - The update-customer API is the standard endpoint to change City to enable Shipping Calculation.
Once the API receives this data, the following “domino effect” occurs inside WordPress:
- Data Processing: WooCommerce receives the JSON and updates the session/cart object with the new address.
- Shipping Recalculation: WooCommerce triggers a recalculation of shipping costs to reflect the new address. It looks for active shipping methods.
- The Hook: The “Shipping Rate by Cities” plugin is activated. It retrieves the city name from the cart to look up the specific rate in its database table.
- The Vulnerable Call: The plugin passes the raw, unsanitized string from the API directly into the function we saw in the diff:
1 | $this->getCityFee($city_name); |
Using that logic vulnerable at city variable, now i can inject SQL query to trigger vulnerability.

The response time is roughly 4x the sleep value. This behavior is consistent with WooCommerce’s cart recalculation lifecycle, where shipping methods may be evaluated multiple times per request (e.g., for billing and shipping contexts), resulting in repeated execution of the vulnerable query.
To further validate this behavior, the sleep duration was modified to observe the corresponding change in response time.

After change payload’s sleep time to 5 and sent the request, look at the response show that the time response was decrease about half and it took just 21s. That prove that POC success. Now create a python script to make exploit process be automated.
Click see script in details
1 | import requests |

Successful dump the database version.
Remediation & Recommendations
For Site Administrators
- Immediate Update: If you are using the “Shipping Rate by Cities” plugin, ensure you have updated to version 2.0.1 or higher. This version includes the critical security patches analyzed in this report.
- Deploy a Web Application Firewall (WAF): Use a WAF (such as Cloudflare, Wordfence, or Sucuri) to detect and block common SQL injection patterns (e.g., SLEEP(), UNION SELECT), while acknowledging that WAFs should not be relied upon as the sole defense.
- Audit Database Permissions: Ensure the database user for WordPress has the least privilege necessary. This limits the potential damage if an SQL injection vulnerability is exploited.
For Developers
- Never Trust User Input: Always treat data coming from APIs or forms as untrusted. Use built-in WordPress sanitization functions like sanitize_text_field() or absint() as a first layer of defense.
- Mandatory use of $wpdb->prepare(): Never concatenate variables directly into SQL queries. The $wpdb->prepare() method is the industry standard for preventing SQLi in WordPress by ensuring that data is safely escaped and handled as a literal value.
- Implement Rate Limiting: Since this vulnerability is unauthenticated, implement rate limiting on sensitive endpoints like /wp-json/wc/store/v1/cart/update-customer to prevent automated scanning and brute-forcing.