fix(chat): validate complete signed profile before publishing read output

This commit is contained in:
evgeny committed 2026-10-27 06:13:12 +01:00
1 parent 30d58a4bde
commit b52e5f5489
3 files changed
+78 -16

No files matched your search

+42
View File
@@ -30,6 +30,7 @@
#include <string.h>
#include <stdio.h>
#include <limits.h>
#include "../../lib/platform_compat.h"
/* ─── хелперы ─── */
@@ -379,6 +380,47 @@ static int json_read_text(sqlite3_stmt* st, int column, const char** out) {
*out = text; return SQLITE_OK;
}
int chat_json_strings(sqlite3* db, const char* text, size_t limit,
int (*visit)(const char* key, const char* value, void* arg), void* arg) {
if (!db || !text || limit > INT_MAX || strnlen(text, limit + 1) > limit) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "flat JSON: invalid arguments/size limit=%zu", limit); return -1;
}
sqlite3_stmt* st = NULL; const char* stage = "validate prepare";
int rc = sqlite3_prepare_v2(db, "SELECT json_valid(?1),CASE WHEN json_valid(?1) THEN json_type(?1) END", -1, &st, NULL);
if (rc == SQLITE_OK) rc = sqlite3_bind_text(st, 1, text, -1, SQLITE_STATIC);
if (rc == SQLITE_OK) rc = sqlite3_step(st);
if (rc != SQLITE_ROW) goto error;
const char* type = NULL; rc = json_read_text(st, 1, &type); if (rc != SQLITE_OK) goto error;
if (!sqlite3_column_int(st, 0) || !type || strcmp(type, "object")) { stage = "not a valid object"; rc = SQLITE_CONSTRAINT; goto error; }
rc = sqlite3_finalize(st); st = NULL; if (rc != SQLITE_OK) goto error;
stage = "field schema";
rc = sqlite3_prepare_v2(db, "SELECT count(*),count(DISTINCT key),coalesce(sum(type<>'text'),0) FROM json_each(?1)", -1, &st, NULL);
if (rc == SQLITE_OK) rc = sqlite3_bind_text(st, 1, text, -1, SQLITE_STATIC);
if (rc == SQLITE_OK) rc = sqlite3_step(st);
if (rc != SQLITE_ROW) goto error;
if (sqlite3_column_int64(st, 0) != sqlite3_column_int64(st, 1) || sqlite3_column_int64(st, 2)) {
stage = "duplicate/nonstring fields"; rc = SQLITE_CONSTRAINT; goto error;
}
rc = sqlite3_finalize(st); st = NULL; if (rc != SQLITE_OK) goto error;
stage = "fields";
rc = sqlite3_prepare_v2(db, "SELECT key,value FROM json_each(?1)", -1, &st, NULL);
if (rc == SQLITE_OK) rc = sqlite3_bind_text(st, 1, text, -1, SQLITE_STATIC);
if (rc != SQLITE_OK) goto error;
while ((rc = sqlite3_step(st)) == SQLITE_ROW) {
const char *key, *value; rc = json_read_text(st, 0, &key); if (rc == SQLITE_OK) rc = json_read_text(st, 1, &value);
if (rc != SQLITE_OK) goto error;
if (!key || !value || (visit && visit(key, value, arg))) { stage = "field callback"; rc = SQLITE_CONSTRAINT; goto error; }
}
if (rc != SQLITE_DONE) goto error;
stage = "finalize"; rc = sqlite3_finalize(st); st = NULL; if (rc != SQLITE_OK) goto error;
return 0;
error:
DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "flat JSON: stage=%s rc=%d: %s", stage, rc, sqlite3_errmsg(db));
rc = sqlite3_finalize(st);
if (rc != SQLITE_OK) DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "flat JSON: cleanup finalize rc=%d", rc);
return -1;
}
/* Сохранённый канал может ещё не иметь history-таблицы при отложенной runtime-подготовке. */
static int json_message_count(sqlite3* db, const char* channel, sqlite3_int64* count) {
char table[80], sql[128]; msg_table_name(channel, table, sizeof(table)); sqlite3_stmt* st = NULL;
+6
View File
@@ -124,6 +124,12 @@ void peers_table_name(const char* ch_id, char* buf, size_t sz);
* Использующий модуль логирует ошибку с контекстом своей операции. */
uint64_t chat_channel_id(const char* text);
/* Проверить весь JSON-object<=limit:уникальные ключи,только строковые значения,без скрытого NUL.
* SQLite заимствована;0/-1 с логом,callback может быть NULL. Callback получает строки до следующего step,
* строит только временный результат и возвращает0/-1;при поздней ошибке caller отбрасывает весь результат. */
int chat_json_strings(sqlite3* db, const char* text, size_t limit,
int (*visit)(const char* key, const char* value, void* arg), void* arg);
/* Байты подписи описания:канонический ID+NUL,имя<=255байт+NUL,owner native-u64,X25519/Ed25519 public[32].
* Возвращает длину/-1;аргументы обязательны,при отказе out не меняется. Общий формат create/JOIN/invite. */
int chat_channel_description_data(const char* channel, const char* name, uint64_t owner,
+30 -16
View File
@@ -104,26 +104,40 @@ int chat_core_name_userinfo(const char* name, char* out, size_t capacity) {
int chat_core_get_my_profile(struct UTUN_INSTANCE* inst, const char* channel, char* out, size_t capacity,
uint8_t signature[64], uint64_t* timestamp) {
struct chat_core_ctx* cc = CC(inst);
if (!cc || !cc->db || !channel || !out || !signature || !timestamp) {
struct chat_core_ctx* cc = inst ? CC(inst) : NULL;
if (!cc || !cc->initialized || !cc->db || !chat_channel_id(channel) || !out || !capacity || !signature || !timestamp) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: invalid read request"); return -1;
}
char table[80], sql[256]; peers_table_name(channel, table, sizeof(table));
snprintf(sql, sizeof(sql), "SELECT userinfo,update_sig,update_ts FROM \"%s\" WHERE node_id=?", table);
sqlite3_stmt* st = NULL; int result = -1;
if (sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL) == SQLITE_OK) {
sqlite3_bind_int64(st, 1, (sqlite3_int64)cc->my_node_id);
if (sqlite3_step(st) == SQLITE_ROW) {
const char* json = (const char*)sqlite3_column_text(st, 0); size_t size = json ? strlen(json) : 0;
if (json && size <= MS_USERINFO_MAX && size < capacity && sqlite3_column_bytes(st, 1) == 64 && sqlite3_column_int64(st, 2) > 0) {
memcpy(out, json, size + 1); memcpy(signature, sqlite3_column_blob(st, 1), 64);
*timestamp = (uint64_t)sqlite3_column_int64(st, 2); result = 0;
}
}
sqlite3_finalize(st);
snprintf(sql, sizeof(sql), "SELECT userinfo,update_sig,update_ts,join_sig,ed25519_pubkey,source FROM \"%s\" WHERE node_id=?", table);
sqlite3_stmt* st = NULL; const char* stage = "prepare";
char profile[MS_USERINFO_MAX + 1]; uint8_t update[64], join[64], body[MS_USERINFO_MAX + 73]; uint64_t version;
int rc = sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL);
if (rc == SQLITE_OK) { stage = "bind"; rc = sqlite3_bind_int64(st, 1, (sqlite3_int64)cc->my_node_id); }
if (rc == SQLITE_OK) { stage = "step"; rc = sqlite3_step(st); }
if (rc != SQLITE_ROW) goto error;
stage = "fields"; rc = SQLITE_CORRUPT;
int bytes = sqlite3_column_bytes(st, 0); const char* json = (const char*)sqlite3_column_text(st, 0);
const void *sig = sqlite3_column_blob(st, 1), *joined = sqlite3_column_blob(st, 3), *key = sqlite3_column_blob(st, 4);
if (sqlite3_column_type(st, 0) != SQLITE_TEXT || !json || bytes > MS_USERINFO_MAX || strlen(json) != (size_t)bytes ||
sqlite3_column_bytes(st, 1) != 64 || !sig || sqlite3_column_bytes(st, 3) != 64 || !joined ||
sqlite3_column_bytes(st, 4) != 32 || !key || memcmp(key, inst->my_ed25519_pubkey, 32) ||
sqlite3_column_int64(st, 2) <= 0 || sqlite3_column_int(st, 5)) goto error;
if ((size_t)bytes >= capacity) { stage = "capacity"; rc = SQLITE_TOOBIG; goto error; }
memcpy(profile, json, (size_t)bytes + 1); memcpy(update, sig, 64); memcpy(join, joined, 64);
version = (uint64_t)sqlite3_column_int64(st, 2);
stage = "finalize"; rc = sqlite3_finalize(st); st = NULL; if (rc != SQLITE_OK) goto error;
int length = member_sync_build_update_msg(join, version, profile, body, sizeof(body));
if (length < 0 || sc_ed25519_verify(inst->my_ed25519_pubkey, body, (size_t)length, update) != SC_OK ||
chat_json_strings(cc->db, profile, MS_USERINFO_MAX, NULL, NULL)) {
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: stored signature/JSON invalid channel=%s", channel); return -1;
}
if (result) DEBUG_WARN(DEBUG_CATEGORY_CHAT, "profile: signed local profile unavailable channel=%s capacity=%zu", channel, capacity);
return result;
memcpy(out, profile, (size_t)bytes + 1); memcpy(signature, update, 64); *timestamp = version; return 0;
error:
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: read channel=%s stage=%s rc=%d capacity=%zu: %s", channel, stage, rc, capacity, sqlite3_errmsg(cc->db));
rc = sqlite3_finalize(st);
if (rc != SQLITE_OK) DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: read cleanup finalize rc=%d", rc);
return -1;
}
/* Обновить профиль целиком одной подписанной записью, сохранив не затронутые поля. */