~/posts/web/cve-2026-108927-baptism-update-sql-injection.md

CVE-2026-108927: SQL injection in the baptism update handler

Trace b_id into an unbound UPDATE predicate despite attribute escaping. Examine the published timing evidence, multi-table side effects, and the repairs needed for validation, binding, and atomic writes.

date[31:24]
read[23:16]
7 min
cat[15:8]
Web security
tags[7:0]
Contents
  1. 0x00Record and evidence scope
  2. 0x01Two input locations feed one update
  3. 0x02The WHERE parameter escapes the sanitizer
  4. 0x03Reading the published timing probe
  5. 0x04Verification design and impact
  6. 0x05Repairing the model and multi-table write
  7. 0x06Disclosure timeline
  8. 0x07References

The save handler in itsourcecode Baptism Information Management System 1.0 receives business fields through POST but takes b_id from the URL query string. It passes that value to a model method that concatenates an UPDATE predicate. Escaping the form attributes does not cover this separate record selector.

CVE-2026-108927, reported by YuLi, is tracked as VDB-416750. The public report probes expression execution with SLEEP(5). The source explains why sanitized SET values leave the WHERE clause exposed, and why testing this endpoint requires tracking both elapsed time and data changes.

Record and evidence scope

CVE-2026-108927CVSS 5.3 MEDIUM
AVNALP
ACLH
ATNP
PRNLH
UINPA
VCHLN
VIHLN
VAHLN
SCHLN
SIHLN
SAHLN
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:L/VI:L/VA:L/SC:N/SI:N/SA:N/E:P
Affected
itsourcecode Baptism Information Management System 1.0
CWE
CWE-89
  1. disclosed
Item Confirmed detail
Entry point POST /pbaptism.php?b_id=...
Branch condition The request body contains submit
Injection input URL parameter b_id, not POST field bapt_id
SQL sink The WHERE clause in Baptism::update()
Fixed version Not specified in the advisories checked

VulDB is the CNA. Its CVSS 4.0 record is 5.3, medium, matching the wall; the CVE also lists a CVSS 3.1 base score of 6.3. The official PR:L assessment is retained rather than changed because a request excerpt omits a cookie.

The product page's baptism.zip has SHA-256 22294a9f4470b22da0c31f0dc59f655bdb08452899d13c3e50705a2cdb48c3ab. The screenshot contains X-Powered-By: PHP/5.6.40, an environment clue for that response, not evidence of a particular database version. Code analysis below uses the download snapshot. Timing observations belong to the public report, not a newly reproduced application environment.

Two input locations feed one update

pbaptism.php:5-81 (excerpt)php
if (isset($_POST['submit'])) {
    $b_id = $_GET['b_id'];
    // ... populate the Baptism object from form fields ...
    $baptism->update($b_id);
    // ... update personal and sponsor details ...
}

This excerpt preserves the relevant control flow while omitting assignments and later updates. Line 5 checks isset($_POST['submit']), line 7 reads $_GET['b_id'], and line 59 passes it to $baptism->update($b_id). Query-string and form-body values coexist in one request and must be traced separately.

bapt_id is a business field; b_id selects the row for UPDATE. Their similar spelling masks distinct paths. Inspecting only the body, validating only the business identifier, or relying on browser field restrictions can miss the actual sink.

The body also contains personal and sponsor details. Later code calls secdetails->update() and sponsor->update(). This is a write-path test: even when the main UPDATE matches no row, the request can still have side effects.

The WHERE parameter escapes the sanitizer

includes/baptism.php:89-102php
public function update($id=0) {
    global $mydb;
    $attributes = $this->sanitized_attributes();
    $attribute_pairs = array();
    foreach ($attributes as $key => $value) {
        $attribute_pairs[] = "{$key}='{$value}'";
    }
    $sql = "UPDATE ".self::$tbl_name." SET ";
    $sql .= join(", ", $attribute_pairs);
    $sql .= " WHERE b_id=". $id;
    $mydb->setQuery($sql);
    if (!$mydb->executeQuery()) return false;
}

sanitized_attributes() iterates over object attributes and passes their values through escape_value(). The $id appended at line 98 is a method argument outside that collection. It has neither a bound placeholder nor integer validation. SET values and the WHERE predicate therefore follow different trust paths.

Database::executeQuery() sends the assembled string to mysql_query(). A method named sanitized only addresses values that actually pass through it. Its guarantee does not extend to a condition or identifier appended afterward.

query-shape.sqlsql
UPDATE baptism
SET /* existing assignments */
WHERE b_id = (SELECT SLEEP(5));

This SQL illustrates the statement shape; it is not a complete query log. The supplied value occupies an unquoted expression position, allowing a subquery to participate in predicate evaluation without first escaping a string literal.

Reading the published timing probe

This is the report's request with its host replaced by target.example. Form values are preserved to show the processing context. Such verification belongs in a resettable data copy because the request carries update fields.

Request
POST /pbaptism.php?b_id=(SELECT%20SLEEP(5)) HTTP/1.1
Host: target.example
Content-Type: application/x-www-form-urlencoded
Content-Length: 370

submit=1&bapt_id=BAP001&bkNum=1&Page=1&line=1&lName=Test&fName=Test&mName=Test&sName=Test&cur_date=2024-01-01&minister=Test&stipend=100&remarks=Test&pers_id=999999&sponsor_id=999999&birth_date=2024-01-01&age=1&birth_place=Test&father=Test&f_Origin=Test&Mother=Test&m_Origin=Test&legitimacy=Test&sponsName1=Test&s_origin1=Test&sponsName2=Test&s_origin2=Test&s_others=Test

Offline calculation confirms a 370-byte ASCII body, matching Content-Length: 370. The submit=1 field enables the branch. The changed input is the URL value b_id=(SELECT SLEEP(5)); the form fields should stay fixed during timing comparisons.

The screenshot shows HTTP 200 and approximately 27,007 ms in the tool's timing indicator, not an exact five-second delay. The argument to SLEEP(5) describes one invocation. UPDATE scanning, expression evaluation count, later business operations, networking, and database waits can all affect total latency. Without baseline distributions, repetitions, or query traces, the image does not establish an invocation count.

MySQL SLEEP() returns 0 on normal completion. Even if the main update matches no record, personal and sponsor updates may still run. The success JavaScript is also not reliable proof of SQL success or changed rows: the surrounding flow does not gate the entire success message on the first update's result.

Verification design and impact

Prepare identifiable test records in a disposable database and snapshot all three groups of related data. Establish normal-request timing with the same session and body, then use a small number of serial expression comparisons. Record latency, response status, database-side statements, and before/after row differences so that network variation is not mistaken for an SQL timing channel.

Check Question to answer
Branch reachability Is submit present, and is the update branch reached?
Input location Is only URL b_id changing, not body bapt_id?
Timing evidence Are baselines, repeated samples, and query traces available?
Side effects Did any of the three record groups change?
Remediation Are non-integers rejected before querying while valid saves still work?
Authorization Are session and record permissions checked before writes?

The source establishes the expression-injection path, while the screenshot contributes a timing observation. Together they support the SQL-injection finding. They do not demonstrate data extraction, arbitrary command execution, or complete system control. Further confidentiality, integrity, and availability effects depend on UPDATE context and database privileges.

The report contains conflicting statements about authenticated versus unauthenticated access. Pending an independent session check, this article retains the official low-privilege prerequisite. An excerpt without a cookie is not a substitute for authentication evidence.

Repairing the model and multi-table write

The model should accept a validated integer ID and bind it in the WHERE clause. Editable values also need parameters; column names should come from a fixed application allowlist, never the request. The following example updates one field to show the pattern. It is neither a full business patch nor a vendor release.

parameterized-update.phpphp
<?php
// Illustrative excerpt: update only remarks, not the full form.
$raw = $_GET['b_id'] ?? null;
$id = is_string($raw)
    ? filter_var($raw, FILTER_VALIDATE_INT, [
        'options' => ['min_range' => 1, 'max_range' => 2147483647]
    ])
    : false;
$remarks = $_POST['remarks'] ?? null;
if ($id === false || !is_string($remarks)) {
    http_response_code(400);
    exit('Invalid input');
}
// Require an authenticated, authorized session and validate CSRF first.
// Resolve related record IDs from the authorized database record.
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$stmt = $pdo->prepare(
    'UPDATE baptism SET remarks = ? WHERE b_id = ?'
);
$stmt->bindValue(1, $remarks, PDO::PARAM_STR);
$stmt->bindValue(2, $id, PDO::PARAM_INT);
$stmt->execute();

Resolve pers_id and sponsor_id from the authorized baptism record and verify their relationships. Hidden fields are not authoritative. A complete repair also needs CSRF validation, stop-on-error handling, field bounds, and correct HTML output encoding.

The snapshot defines the baptism table as MyISAM. Wrapping those writes in a transaction does not make MyISAM changes rollback-capable. Atomic updates across the three record groups require checking every participating table, migrating to a transactional engine, and using one connection with explicit error paths. Until that work is verified, restricting the save endpoint and testing restoration against backups can limit exposure.

The central regression requirement is that direct requests to the save endpoint still treat identifiers as data, and that a failed update is not concealed by a success message.

Disclosure timeline

Date Event Qualification
2026-09-24 GitHub report #2 created Platform metadata, not proof of vendor receipt
2026-10-11 CNA: Advisory disclosed The date used on the wall
2026-10-12 CVE record published The official datePublished field

The submission is 997398 and the accepted entry is VDB-416750. The material does not establish a first fix date, fixed version, or vendor acknowledgment. The screenshot's server clock is not used as a discovery date; only events with a clear source appear above.

References

NORMAL~/posts/web/cve-2026-108927-baptism-update-sql-injection.md§--
0%en