fix(chat): validate complete signed profile before publishing read output
This commit is contained in:
1 parent
30d58a4bde
commit
b52e5f5489
3 files changed
+78
-16
No files matched your search
@@ -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;
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
/* Обновить профиль целиком одной подписанной записью, сохранив не затронутые поля. */
|
||||
|
||||
Reference in new issue
Block a user