From fc3089d98ec1ed5dcc81679fecd9e5e626e24cf2 Mon Sep 17 00:00:00 2001
From: Daniel Markstedt <daniel@mindani.net>
Date: Mon, 4 May 2026 20:25:06 +0200
Subject: [PATCH] CVE-2026-44047: cnid: protect against MySQL CNID filename SQL injection

Reported-by: @00redbeer
Signed-off-by: Daniel Markstedt <daniel@mindani.net>
---
 libatalk/cnid/mysql/cnid_mysql.c | 159 ++++++++++++++++++++-----------
 1 file changed, 106 insertions(+), 53 deletions(-)

diff --git a/libatalk/cnid/mysql/cnid_mysql.c b/libatalk/cnid/mysql/cnid_mysql.c
index 65122135b..5979c7ffd 100644
--- a/libatalk/cnid/mysql/cnid_mysql.c
+++ b/libatalk/cnid/mysql/cnid_mysql.c
@@ -275,6 +275,7 @@ int cnid_mysql_update(struct _cnid_db *cdb,
     EC_INIT;
     CNID_mysql_private *db;
     char *sql = NULL;
+    MYSQL_STMT *delete_stmt = NULL;
     cnid_t update_id = 0;
 
     if (!cdb || !(db = cdb->cnid_db_private) || !id || !st || !name) {
@@ -293,16 +294,32 @@ int cnid_mysql_update(struct _cnid_db *cdb,
     uint64_t ino = st->st_ino;
 
     do {
+        MYSQL_BIND delete_param[2];
+        unsigned long delete_name_len = len;
+        uint64_t delete_did = ntohl(did);
         EC_NEG1(asprintf(&sql, "DELETE FROM `%s` WHERE Id=%" PRIu32,
                          db->cnid_mysql_voluuid_str, ntohl(id)));
         EC_NEG1(cnid_mysql_execute(db->cnid_mysql_con, sql));
         free(sql);
         sql = NULL;
-        EC_NEG1(asprintf(&sql, "DELETE FROM `%s` WHERE Did=%" PRIu32 " AND Name='%s'",
-                         db->cnid_mysql_voluuid_str, ntohl(did), name));
-        EC_NEG1(cnid_mysql_execute(db->cnid_mysql_con, sql));
+        EC_NULL(delete_stmt = mysql_stmt_init(db->cnid_mysql_con));
+        EC_NEG1(asprintf(&sql, "DELETE FROM `%s` WHERE Did=? AND Name=?",
+                         db->cnid_mysql_voluuid_str));
+        EC_ZERO_LOG(mysql_stmt_prepare(delete_stmt, sql, strlen(sql)));
         free(sql);
         sql = NULL;
+        memset(delete_param, 0, sizeof(delete_param));
+        delete_param[0].buffer_type = MYSQL_TYPE_LONGLONG;
+        delete_param[0].buffer = &delete_did;
+        delete_param[0].is_unsigned = true;
+        delete_param[1].buffer_type = MYSQL_TYPE_STRING;
+        delete_param[1].buffer = (char *)name;
+        delete_param[1].buffer_length = len;
+        delete_param[1].length = &delete_name_len;
+        EC_ZERO_LOG(mysql_stmt_bind_param(delete_stmt, delete_param));
+        EC_ZERO_LOG(mysql_stmt_execute(delete_stmt));
+        mysql_stmt_close(delete_stmt);
+        delete_stmt = NULL;
         EC_NEG1(asprintf(&sql, "DELETE FROM `%s` WHERE DevNo=%" PRIu64 " AND InodeNo=%"
                          PRIu64,
                          db->cnid_mysql_voluuid_str, dev, ino));
@@ -341,6 +358,10 @@ int cnid_mysql_update(struct _cnid_db *cdb,
 
 EC_CLEANUP:
 
+    if (delete_stmt) {
+        mysql_stmt_close(delete_stmt);
+    }
+
     if (sql) {
         free(sql);
     }
@@ -648,8 +669,13 @@ cnid_t cnid_mysql_get(struct _cnid_db *cdb, cnid_t did, const char *name,
     CNID_mysql_private *db;
     char *sql = NULL;
     cnid_t id = CNID_INVALID;
-    MYSQL_RES *result = NULL;
-    MYSQL_ROW row;
+    MYSQL_STMT *stmt = NULL;
+    MYSQL_BIND param[2];
+    MYSQL_BIND result[1];
+    unsigned long name_len = len;
+    uint64_t did_param = ntohl(did);
+    unsigned long long result_id = 0;
+    bool have_result = false;
 
     if (!cdb || !(db = cdb->cnid_db_private) || !name) {
         LOG(log_error, logtype_cnid, "cnid_mysql_get: Parameter error");
@@ -666,30 +692,45 @@ cnid_t cnid_mysql_get(struct _cnid_db *cdb, cnid_t did, const char *name,
     LOG(log_debug, logtype_cnid,
         "cnid_mysql_get(did: %" PRIu32 ", name: \"%s\"): START",
         ntohl(did), name);
+    EC_NULL(stmt = mysql_stmt_init(db->cnid_mysql_con));
     EC_NEG1(asprintf(&sql, "SELECT Id FROM `%s` "
-                           "WHERE Name='%s' AND Did=%" PRIu32,
-                     db->cnid_mysql_voluuid_str, name, ntohl(did)));
-    EC_NEG1(cnid_mysql_execute(db->cnid_mysql_con, sql));
+                           "WHERE Name=? AND Did=?",
+                     db->cnid_mysql_voluuid_str));
+    EC_ZERO_LOG(mysql_stmt_prepare(stmt, sql, strlen(sql)));
     free(sql);
     sql = NULL;
+    memset(param, 0, sizeof(param));
+    param[0].buffer_type = MYSQL_TYPE_STRING;
+    param[0].buffer = (char *)name;
+    param[0].buffer_length = len;
+    param[0].length = &name_len;
+    param[1].buffer_type = MYSQL_TYPE_LONGLONG;
+    param[1].buffer = &did_param;
+    param[1].is_unsigned = true;
+    EC_ZERO_LOG(mysql_stmt_bind_param(stmt, param));
+    memset(result, 0, sizeof(result));
+    result[0].buffer_type = MYSQL_TYPE_LONGLONG;
+    result[0].buffer = &result_id;
+    result[0].is_unsigned = true;
+    EC_ZERO_LOG(mysql_stmt_bind_result(stmt, result));
+    EC_ZERO_LOG(mysql_stmt_execute(stmt));
+    EC_ZERO_LOG(mysql_stmt_store_result(stmt));
+    have_result = true;
 
-    if ((result = mysql_store_result(db->cnid_mysql_con)) == NULL) {
-        LOG(log_error, logtype_cnid, "MySQL query error: %s",
-            mysql_error(db->cnid_mysql_con));
-        errno = CNID_ERR_DB;
-        EC_FAIL;
-    }
-
-    if (mysql_num_rows(result)) {
-        row = mysql_fetch_row(result);
-        id = htonl(atoi(row[0]));
+    if (mysql_stmt_num_rows(stmt)) {
+        EC_ZERO(mysql_stmt_fetch(stmt));
+        id = htonl((uint32_t)result_id);
     }
 
 EC_CLEANUP:
     LOG(log_debug, logtype_cnid, "cnid_mysql_get: id: %" PRIu32, ntohl(id));
 
-    if (result) {
-        mysql_free_result(result);
+    if (have_result) {
+        mysql_stmt_free_result(stmt);
+    }
+
+    if (stmt) {
+        mysql_stmt_close(stmt);
     }
 
     if (sql) {
@@ -815,24 +856,30 @@ int cnid_mysql_find(struct _cnid_db *cdb, const char *name, size_t namelen,
                     void *buffer, size_t buflen)
 {
     EC_INIT;
-    CNID_mysql_private *db = cdb->cnid_db_private;
+    CNID_mysql_private *db = NULL;
     char *sql = NULL;
     char *namelike = NULL;
-    MYSQL_RES *result = NULL;
-    MYSQL_ROW row;
+    MYSQL_STMT *stmt = NULL;
+    MYSQL_BIND param[1];
+    MYSQL_BIND result[1];
     int count = 0;
     cnid_t *cnids = (cnid_t *)buffer;
     unsigned long max_results = buflen / sizeof(cnid_t);
-    LOG(log_maxdebug, logtype_cnid,
-        "cnid_mysql_find: called with name='%s', namelen=%zu, buflen=%zu", name,
-        namelen, buflen);
+    unsigned long namelike_len;
+    unsigned long long result_id = 0;
+    bool have_result = false;
+    int fetch_ret = MYSQL_NO_DATA;
 
-    if (!cdb || !db || !name) {
+    if (!cdb || !(db = cdb->cnid_db_private) || !name) {
         LOG(log_error, logtype_cnid, "cnid_mysql_find: Parameter error");
         errno = CNID_ERR_PARAM;
         EC_FAIL;
     }
 
+    LOG(log_maxdebug, logtype_cnid,
+        "cnid_mysql_find: called with name='%s', namelen=%zu, buflen=%zu", name,
+        namelen, buflen);
+
     if (namelen > MAXPATHLEN) {
         LOG(log_error, logtype_cnid,
             "cnid_mysql_find: Path name is too long (namelen=%zu)", namelen);
@@ -841,48 +888,54 @@ int cnid_mysql_find(struct _cnid_db *cdb, const char *name, size_t namelen,
     }
 
     EC_NEG1(asprintf(&namelike, "%%%s%%", name));
+    namelike_len = strlen(namelike);
     LOG(log_debug, logtype_cnid, "cnid_mysql_find: LIKE pattern is '%s'", namelike);
-    EC_NEG1(asprintf(&sql, "SELECT Id FROM `%s` WHERE Name LIKE '%s' ORDER BY Id",
-                     db->cnid_mysql_voluuid_str, namelike));
+    EC_NULL(stmt = mysql_stmt_init(db->cnid_mysql_con));
+    EC_NEG1(asprintf(&sql, "SELECT Id FROM `%s` WHERE Name LIKE ? ORDER BY Id",
+                     db->cnid_mysql_voluuid_str));
     LOG(log_maxdebug, logtype_cnid, "cnid_mysql_find: SQL query: %s", sql);
-
-    if (cnid_mysql_execute(db->cnid_mysql_con, sql) != 0) {
-        LOG(log_error, logtype_cnid, "cnid_mysql_find: SQL execution failed: %s",
-            mysql_error(db->cnid_mysql_con));
-        EC_FAIL;
-    }
-
+    EC_ZERO_LOG(mysql_stmt_prepare(stmt, sql, strlen(sql)));
     free(sql);
     sql = NULL;
-    result = mysql_store_result(db->cnid_mysql_con);
-
-    if (!result) {
-        LOG(log_error, logtype_cnid, "cnid_mysql_find: mysql_store_result failed: %s",
-            mysql_error(db->cnid_mysql_con));
-        EC_FAIL;
-    }
-
+    memset(param, 0, sizeof(param));
+    param[0].buffer_type = MYSQL_TYPE_STRING;
+    param[0].buffer = namelike;
+    param[0].buffer_length = namelike_len;
+    param[0].length = &namelike_len;
+    EC_ZERO_LOG(mysql_stmt_bind_param(stmt, param));
+    memset(result, 0, sizeof(result));
+    result[0].buffer_type = MYSQL_TYPE_LONGLONG;
+    result[0].buffer = &result_id;
+    result[0].is_unsigned = true;
+    EC_ZERO_LOG(mysql_stmt_bind_result(stmt, result));
+    EC_ZERO_LOG(mysql_stmt_execute(stmt));
+    EC_ZERO_LOG(mysql_stmt_store_result(stmt));
+    have_result = true;
     LOG(log_maxdebug, logtype_cnid, "cnid_mysql_find: mysql_num_rows=%lu",
-        (unsigned long)mysql_num_rows(result));
+        (unsigned long)mysql_stmt_num_rows(stmt));
 
-    while ((row = mysql_fetch_row(result)) && count < max_results) {
-        LOG(log_maxdebug, logtype_cnid, "cnid_mysql_find: row[%d] = '%s'", count,
-            row[0]);
-        cnids[count] = htonl(atoi(row[0]));
+    while (count < max_results && (fetch_ret = mysql_stmt_fetch(stmt)) == 0) {
+        LOG(log_maxdebug, logtype_cnid, "cnid_mysql_find: row[%d] = '%llu'", count,
+            result_id);
+        cnids[count] = htonl((uint32_t)result_id);
         count++;
     }
 
-    if (mysql_errno(db->cnid_mysql_con) != 0) {
+    if (count < max_results && fetch_ret != MYSQL_NO_DATA) {
         LOG(log_error, logtype_cnid, "cnid_mysql_find: MySQL error after fetch: %s",
-            mysql_error(db->cnid_mysql_con));
+            mysql_stmt_error(stmt));
         EC_FAIL;
     }
 
     LOG(log_debug, logtype_cnid, "cnid_mysql_find: returning %d matches", count);
 EC_CLEANUP:
 
-    if (result) {
-        mysql_free_result(result);
+    if (have_result) {
+        mysql_stmt_free_result(stmt);
+    }
+
+    if (stmt) {
+        mysql_stmt_close(stmt);
     }
 
     if (namelike) {
