Affected Versions
| Affected | not applicable (first-party code) |
| Fixed in | not applicable (first-party code) — see security fix commit |
| Ecosystem | PHP |
| CVE / GHSA | not assigned |
| CWE | CWE-89 (Improper Neutralization of Special Elements in SQL Command) |
Introduction
The heatmap visualization endpoint processes geographic and temporal filters from web requests to return density data for map rendering. A critical flaw in this request handler allowed attackers to rewrite the entire SQL query by manipulating coordinate bounds or time range parameters. Six distinct $_REQUEST values—days, dataset, latitude_min, latitude_max, longitude_min, and longitude_max—were concatenated directly into the query string, with only the clustered flag receiving any processing.
This pattern is particularly dangerous in visualization endpoints because they typically expose rich data sets and often run with database credentials that can read large portions of the schema. The endpoint's purpose—returning geographic heatmap data—means it naturally touches tables containing location tracking, user activity, or sensor measurements.
The Vulnerability Explained
The vulnerable code constructed the entire WHERE clause through string concatenation:
$query = "SELECT latitude, longitude, value FROM heatmap.data WHERE date >= NOW() - INTERVAL " . $_REQUEST["days"] . " DAY AND map_name = '" . $_REQUEST["dataset"] . "'" .
" AND latitude >= " . $_REQUEST["latitude_min"] .
" AND latitude <= " . $_REQUEST["latitude_max"] .
" AND longitude >= " . $_REQUEST["longitude_min"] .
" AND longitude <= " . $_REQUEST["longitude_max"] .
" AND clustered = " . $clustered;
The days parameter is especially problematic: it sits unquoted in an INTERVAL expression, meaning attackers need not escape any quotes to inject SQL. A payload like 1 DAY UNION SELECT username,password,1 FROM admin_users-- would execute immediately.
The coordinate parameters (latitude_min, latitude_max, longitude_min, longitude_max) are similarly unquoted, treating them as numeric literals. While this avoids quote-escaping complexity, it permits injection of SQL operators and subqueries. The dataset parameter is quoted but without parameterized binding, leaving room for quote-escaping attacks.
An attacker exploiting this could:
- Extract the entire database schema via information_schema queries
- Pivot to the underlying server through LOAD_FILE() or INTO OUTFILE if file privileges exist
- Modify or delete heatmap data by appending ; UPDATE ... or ; DELETE ... statements
The Fix
The remediation replaces string concatenation with MySQLi prepared statements and proper type binding:
Before:
$clustered = isset($_REQUEST["clustered"]) ? $_REQUEST["clustered"] : 0;
// ... direct concatenation into $query
$result = mysqli_query($connection, $query);
After:
$clustered = isset($_REQUEST["clustered"]) ? intval($_REQUEST["clustered"]) : 0;
$days = intval($_REQUEST["days"]);
$query = "SELECT latitude, longitude, value FROM heatmap.data WHERE date >= NOW() - INTERVAL ? DAY AND map_name = ?" .
" AND latitude >= ? AND latitude <= ? AND longitude >= ? AND longitude <= ? AND clustered = ?";
$stmt = mysqli_prepare($connection, $query);
mysqli_stmt_bind_param($stmt, "isddddi", $days, $_REQUEST["dataset"], $_REQUEST["latitude_min"], $_REQUEST["latitude_max"], $_REQUEST["longitude_min"], $_REQUEST["longitude_max"], $clustered);
mysqli_stmt_execute($stmt);
$result = mysqli_stmt_get_result($stmt);
The fix introduces three defensive layers:
- intval() coercion for
daysandclusteredbefore they reach the database layer, ensuring these are integers even if parameter binding somehow fails - Parameterized query structure with
?placeholders that completely separate SQL logic from data values - Type-bound execution via
mysqli_stmt_bind_param()with the type string"isddddi"—integer, string, then four doubles, then integer—matching the expected schema types
The isddddi type signature is deliberate: days as integer, dataset as string (map names), all coordinate bounds as doubles (floating-point precision), and clustered as integer.
Key Takeaways
- INTERVAL expressions are injection hotspots: The unquoted numeric position in
INTERVAL N DAYrequires integer validation, not just quote escaping—parameterization is the only safe approach - Type strings expose schema assumptions: The
isddddisignature documents the expected types and fails safely if wrong types are passed - Visualization endpoints are high-value targets: Rich geographic data combined with complex filtering logic creates natural SQL injection surfaces
- mysqli_prepare() requires complete conversion: Partial fixes that parameterize some fields while concatenating others remain vulnerable
- intval() before bind_param() provides defense in depth: Even with parameterization, coercing expected types at the application layer prevents type confusion attacks
How Orbis AppSec Detected This
Source: The $_REQUEST superglobal—specifically $_REQUEST["days"], $_REQUEST["dataset"], $_REQUEST["latitude_min"], $_REQUEST["latitude_max"], $_REQUEST["longitude_min"], and $_REQUEST["longitude_max"]
Sink: The mysqli_query() function invoked with a query string built through direct concatenation of user-controlled input
Missing control: No use of mysqli_prepare(), mysqli_stmt_bind_param(), or equivalent parameterized query mechanisms; no input validation or type coercion before query construction
CWE: CWE-89 (Improper Neutralization of Special Elements in SQL Command)
Fix: Replaced string concatenation with mysqli_prepare(), mysqli_stmt_bind_param() using the type string "isddddi", and mysqli_stmt_execute(), adding intval() sanitization for numeric parameters
Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.
Conclusion
This vulnerability demonstrates how visualization endpoints—with their natural need for complex, user-controlled filtering—become SQL injection targets when developers prioritize query construction convenience over security. The specific pattern of unquoted numeric parameters in INTERVAL expressions and geographic bounds is particularly dangerous because it bypasses the quote-escaping that might catch simpler injection attempts. The complete conversion to MySQLi prepared statements with explicit type binding closes all injection vectors while maintaining the endpoint's functionality.