~/posts/web/cve-2026-108926-baptism-edit-sql-injection.md

CVE-2026-108926: SQL injection in the baptism edit page

Trace id from the URL into SQL and the rendered form. Vendor source and a constant marker explain the 28-column UNION, the last-row helper, evidence limits, and a parameterized repair.

date[31:24]
read[23:16]
6 min
cat[15:8]
Web security
tags[7:0]
Contents
  1. 0x00Record and verification scope
  2. 0x01From input to rendered value
  3. 0x02Why the UNION has 28 columns
  4. 0x03Minimal verification and regression criteria
  5. 0x04Impact and limits
  6. 0x05Repairing the query boundary
  7. 0x06Disclosure timeline
  8. 0x07References

The edit page in itsourcecode Baptism Information Management System 1.0 appends the URL parameter id directly to a SQL predicate. A value intended to select one baptism record can instead change the query structure. The public report uses a constant-only UNION query to place a chosen marker in the edit form.

CVE-2026-108926, reported by YuLi, is tracked as VDB-416749. The vendor download, the published request, and the response evidence explain both the injection and the less obvious role of a database helper that claims to return a single result.

Record and verification scope

CVE-2026-108926CVSS 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 GET /ebaptism.php
Input Case-sensitive URL query parameter id
Query target The bapt_pers_spon view
Classification CWE-89, SQL injection
Scoring source VulDB, the CNA in the CVE record
Fixed version Not specified in the advisories checked

The CNA also supplies a CVSS 3.1 base score of 6.3. This article and the vulnerability wall consistently use its CVSS 4.0 record of 5.3. The vector includes PR:L and E:P; scores from different versions are not interchangeable.

The product page links to baptism.zip, with SHA-256 22294a9f4470b22da0c31f0dc59f655bdb08452899d13c3e50705a2cdb48c3ab. Line numbers below refer to that download snapshot. It provides static evidence for the code path, not proof that every deployed file in the original test was identical. The request and screenshot are evidence from the published report. Source inspection, hexadecimal decoding, and column counting were checked separately; the original application environment was not rerun.

From input to rendered value

ebaptism.php:16-18php
$id = $_GET['id'];
$mydb->setQuery("SELECT * from bapt_pers_spon where b_id=".$id);
$res = $mydb->loadSingleResult();

There is no quoted string to break out of here: id occupies an unquoted numeric-expression position. An ordinary value such as 1 produces WHERE b_id=1. SQL operators, subqueries, or a UNION clause become grammar rather than an opaque record identifier.

In includes/database.php:40-47, setQuery() merely stores the string in sql_string, and executeQuery() passes it to mysql_query(). Neither layer binds a parameter. A wrapper method does not establish a type boundary.

includes/database.php:74-82php
function loadSingleResult() {
    $cur = $this->executeQuery();
    while ($row = mysql_fetch_object($cur)) {
        $data = $row;
    }
    mysql_free_result($cur);
    return $data;
}

The single-result helper actually iterates over the result set and overwrites $data, returning the last row it reads. The public response shows the marker in the hidden bapt_id field and its adjacent label. Those sinks correspond to $res->bapt_id at ebaptism.php:42-43. The evidence therefore extends beyond a suspicious request: query output reaches the page through the fetched object.

Without ORDER BY, SQL does not promise a general row order. The screenshot establishes which value rendered in that test; another dataset or execution plan still requires observation. The helper should not be mistaken for a reliable sorting mechanism.

Why the UNION has 28 columns

The following request preserves the report's parameter value and replaces the host with a documentation domain. It projects a chosen constant rather than business data. It is an excerpt from the report, not a new request sent to a deployed system.

Request
GET /ebaptism.php?id=1+UNION+ALL+SELECT+CONCAT(0x716b627071,0x616b744970674e674f594a6a4a7366766a454744786e6f7775576a737a7a6654636b657858765a73,0x7171766b71),NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL--+- HTTP/1.1
Content-Type: application/json
Host: target.example

Both sides of a UNION must have the same number of projected expressions. This payload has one CONCAT(...) and 27 NULL expressions: 28 columns in total. The view definition in the download also has 28 projections, with bapt_id first, matching the observed output position.

payload-analysis.txttext
projection_count = 28
null_count = 27
prefix = qkbpq
value = aktIpgNgOYJjJsfvjEGDxnowuWjszzfTckexXvZs
suffix = qqvkq
marker = qkbpqaktIpgNgOYJjJsfvjEGDxnowuWjszzfTckexXvZsqqvkq

The hexadecimal literals decode to a prefix, a constant value, and a suffix. Their concatenation contains no database secret, password, or token. The remaining NULL values fill the required projection positions. Query-string decoding turns the --+- suffix into a line-comment form with the whitespace MySQL expects.

The screenshot shows the evaluated marker in the field and label, not the full SQL text. That agrees with the source-to-database-to-output path. Plain reflection would generally return the supplied text itself. An independent check should retain the normal page, the modified page, and query evidence rather than relying on a visual difference alone.

Minimal verification and regression criteria

Use disposable records, establish that a normal record renders, then replay the constant-only request. Keep the session, database contents, and application files fixed while changing only id. Look for the complete marker at the bapt_id output location, not merely in a request inspector or log.

Input or state Acceptance criterion after remediation
Authorized, existing integer ID The matching record renders
Valid but absent ID A consistent not-found response
Missing, array, negative, or out-of-range ID Rejected before the query
ID containing SQL syntax Never becomes query structure
Missing session or record permission Execution stops before data access
Database exception No SQL or connection details in the page

The report says both that valid credentials are used and that authentication is unnecessary. Its request excerpt has no session header, but an abbreviated request does not establish the complete authentication flow. This article retains the CNA's PR:L assessment rather than claiming unauthenticated exploitation. Server-side authentication and object authorization require separate deployment checks.

Impact and limits

The demonstrated effect is an attacker-chosen SQL expression result reaching the edit form. The view joins baptism records, personal details, and sponsor information. Loss of the query boundary can expose or affect data within the application account's privileges; reachable objects depend on those privileges and the statement context.

The constant marker does not demonstrate a database dump, arbitrary file writes, or operating-system command execution. Broad claims of complete system control need additional evidence and are not treated as observed outcomes here. The affected version remains 1.0 as recorded by the CNA, without extending the conclusion to every derivative project.

Repairing the query boundary

This is an illustrative replacement, not a vendor patch. It uses modern PHP and an existing PDO MySQL connection. An application built on mysql_* needs a database-layer migration rather than a literal drop-in copy.

parameterized-read.phpphp
<?php
// Illustrative replacement; $pdo is an existing PDO MySQL connection.
$raw = $_GET['id'] ?? null;
$id = is_string($raw)
    ? filter_var($raw, FILTER_VALIDATE_INT, [
        'options' => ['min_range' => 1, 'max_range' => 2147483647]
    ])
    : false;
if ($id === false) {
    http_response_code(400);
    exit('Invalid id');
}
// Enforce session and object authorization before this query.
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$stmt = $pdo->prepare(
    'SELECT * FROM bapt_pers_spon WHERE b_id = ?'
);
$stmt->bindValue(1, $id, PDO::PARAM_INT);
$stmt->execute();
$res = $stmt->fetch(PDO::FETCH_OBJ);
if ($res === false) {
    http_response_code(404);
    exit('Not found');
}

Integer validation enforces the business domain; parameter binding separates data from SQL grammar. Casting alone can silently turn malformed input into a valid identifier. Explicit rejection is easier to audit. Production code should catch database exceptions centrally, return a generic response, and log a correlation identifier server-side.

The page also needs record-level authorization and context-appropriate HTML output encoding. Authorization determines which record a user may read; output encoding protects the HTML context. Neither substitutes for SQL parameterization. Run the regression matrix and audit other dynamic predicates passed to setQuery().

Disclosure timeline

Date Event What the evidence establishes
2026-09-24 GitHub report #1 created Platform creation time, not a vendor receipt date
2026-10-11 CNA timeline: Advisory disclosed The disclosure date used on the wall
2026-10-12 CVE record published The official datePublished field

The submission number is 997397 and the accepted entry is VDB-416749. The available material does not establish a complete vendor correspondence history or a fixed release. GitHub creation predates the CNA disclosure date; these are distinct events, not a single verified first-publication date.

References

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