Back to Blog
critical SEVERITY4 min read

heatmap.php SQL Injection: $_REQUEST Parameters in Unparameterized

A critical SQL injection vulnerability in the heatmap data retrieval endpoint allowed attackers to execute arbitrary database commands by manipulating coordinate bounds or time range parameters. The vulnerability affected all six user-controlled $_REQUEST parameters passed directly into query construction without parameterization.

O
By Orbis AppSec
Published September 22, 2026Reviewed September 22, 2026

Answer Summary

This first-party code vulnerability affects the heatmap data endpoint prior to the security fix. An attacker achieves arbitrary SQL command execution by injecting malicious input into the days, dataset, latitude_min, latitude_max, longitude_min, longitude_max, or clustered request parameters. The fix replaces string concatenation with mysqli_prepare(), mysqli_stmt_bind_param(), and mysqli_stmt_execute(), using type-specific parameter binding. CWE-89.

Vulnerability at a Glance

cweCWE-89
fixParameterized queries using mysqli_prepare() with mysqli_stmt_bind_param()
riskCritical — arbitrary SQL execution leading to data exfiltration, modification, or server compromise
languagePHP
root causeDirect interpolation of $_REQUEST parameters into SQL query strings
vulnerabilitySQL injection (CWE-89)

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:

  1. intval() coercion for days and clustered before they reach the database layer, ensuring these are integers even if parameter binding somehow fails
  2. Parameterized query structure with ? placeholders that completely separate SQL logic from data values
  3. 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 DAY requires integer validation, not just quote escaping—parameterization is the only safe approach
  • Type strings expose schema assumptions: The isddddi signature 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.

Prevention and further reading

Frequently Asked Questions

Which $_REQUEST parameter in the heatmap endpoint was most dangerous for SQL injection, and why?

The "days" parameter was particularly dangerous because it was used in an INTERVAL expression without quotes, allowing direct injection of SQL operators and additional clauses without needing to escape quote characters.

Why did the fix use "isddddi" as the type string for mysqli_stmt_bind_param()?

The type string specifies: integer for $days, string for $_REQUEST["dataset"], double for each coordinate bound (latitude_min, latitude_max, longitude_min, longitude_max), and integer for $clustered—matching the database schema's expected types.

Does the fix change how the clustered parameter is processed before reaching the database layer?

Yes, the fix adds intval() sanitization for $clustered before parameter binding, ensuring it is coerced to an integer even if mysqli_stmt_bind_param() receives unexpected input.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #93

Related Articles

critical

Actual Budget addTransaction.sh SQL Injection via Shell Variable

A critical SQL injection vulnerability in Actual Budget's transaction automation script allowed attackers to manipulate database records through shell variables interpolated directly into SQL strings. The fix introduces proper escaping functions and numeric validation to prevent injection through unquoted fields.

critical

SQL's Insert() and Update() Methods Used F-String Interpolation in u2share_batch_give_sugar

The SQL helper class in u2share_batch_give_sugar used Python f-strings to construct INSERT and UPDATE queries, creating SQL injection vulnerabilities even though values appeared to come from internal constants. The fix replaces all f-string query construction with sqlite3 parameterized queries using `?` placeholders, eliminating string interpolation entirely from the database path.

high

TrackOptionsManager DDL: Template-Literal SQL Injection Closed

The `TrackOptionsManager` service built its `CREATE TABLE` and `ALTER TABLE ... ADD COLUMN alias` statements by interpolating a `DEFAULT_ALIAS` constant directly inside single quotes in a JavaScript template literal, and its private `_query()` helper had no parameter channel at all. The fix routes the default value through `mysql.escape()` and gives `_query(q, params = [])` a real bound-parameter argument that is forwarded to `db.query()`. This removes an injection primitive on a schema-bootstra

high

How SQL injection via template literals happens in Node.js SQLite and how to fix it

A SQL injection vulnerability in `src/lib/codex-state.mjs` allowed dynamic column names to reach SQL queries through JavaScript template literals. The fix implements defense-in-depth with strict identifier validation using `SAFE_IDENTIFIER` regex before query construction.

high

How Python SQLAlchemy Raw Query SQL Injection happens and how to fix it

A high-severity SQL injection vulnerability was fixed in the `skills/last30days/scripts/store.py` file where untrusted input was being concatenated directly into raw SQL queries. The fix replaces string concatenation with SQLAlchemy's TextualSQL prepared statements using named parameters, preventing attackers from manipulating database queries through malicious input.

critical

docker_rpc.uc Command Injection: Unsanitized RPC Parameters

A critical command injection vulnerability in the Docker RPC handler allowed authenticated attackers to execute arbitrary system commands by injecting shell metacharacters into container ID, port, user ID, or command parameters. The fix validates all user-supplied inputs against strict whitelist patterns before interpolating them into shell commands.