v0.7.7 - Prevent sql attacks and rate limiting on subscriptions

This commit is contained in:
Your Name
2025-10-10 15:44:10 -04:00
parent 00a8f16262
commit 6709e229b3
11 changed files with 746 additions and 152 deletions

View File

@@ -126,6 +126,22 @@ int process_admin_event_in_config(cJSON* event, char* error_message, size_t erro
// Forward declaration for NIP-45 COUNT message handling
int handle_count_message(const char* sub_id, cJSON* filters, struct lws *wsi, struct per_session_data *pss);
// Parameter binding helpers for SQL queries
static void add_bind_param(char*** params, int* count, int* capacity, const char* value) {
if (*count >= *capacity) {
*capacity = *capacity == 0 ? 16 : *capacity * 2;
*params = realloc(*params, *capacity * sizeof(char*));
}
(*params)[(*count)++] = strdup(value);
}
static void free_bind_params(char** params, int count) {
for (int i = 0; i < count; i++) {
free(params[i]);
}
free(params);
}
// Forward declaration for enhanced admin event authorization
int is_authorized_admin_event(cJSON* event, char* error_message, size_t error_size);
@@ -726,7 +742,95 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru
log_error("REQ filters is not an array");
return 0;
}
// EARLY SUBSCRIPTION LIMIT CHECK - Check limits BEFORE any processing
if (pss) {
time_t current_time = time(NULL);
// Check if client is currently rate limited due to excessive failed attempts
if (pss->rate_limit_until > current_time) {
char rate_limit_msg[256];
int remaining_seconds = (int)(pss->rate_limit_until - current_time);
snprintf(rate_limit_msg, sizeof(rate_limit_msg),
"Rate limited due to excessive failed subscription attempts. Try again in %d seconds.", remaining_seconds);
// Send CLOSED notice for rate limiting
cJSON* closed_msg = cJSON_CreateArray();
cJSON_AddItemToArray(closed_msg, cJSON_CreateString("CLOSED"));
cJSON_AddItemToArray(closed_msg, cJSON_CreateString(sub_id));
cJSON_AddItemToArray(closed_msg, cJSON_CreateString("error: rate limited"));
cJSON_AddItemToArray(closed_msg, cJSON_CreateString(rate_limit_msg));
char* closed_str = cJSON_Print(closed_msg);
if (closed_str) {
size_t closed_len = strlen(closed_str);
unsigned char* buf = malloc(LWS_PRE + closed_len);
if (buf) {
memcpy(buf + LWS_PRE, closed_str, closed_len);
lws_write(wsi, buf + LWS_PRE, closed_len, LWS_WRITE_TEXT);
free(buf);
}
free(closed_str);
}
cJSON_Delete(closed_msg);
// Update rate limiting counters
pss->failed_subscription_attempts++;
pss->last_failed_attempt = current_time;
return 0;
}
// Check session subscription limits
if (pss->subscription_count >= g_subscription_manager.max_subscriptions_per_client) {
log_error("Maximum subscriptions per client exceeded");
// Update rate limiting counters for failed attempt
pss->failed_subscription_attempts++;
pss->last_failed_attempt = current_time;
pss->consecutive_failures++;
// Implement progressive backoff: 1s, 5s, 30s, 300s (5min) based on consecutive failures
int backoff_seconds = 1;
if (pss->consecutive_failures >= 10) backoff_seconds = 300; // 5 minutes
else if (pss->consecutive_failures >= 5) backoff_seconds = 30; // 30 seconds
else if (pss->consecutive_failures >= 3) backoff_seconds = 5; // 5 seconds
pss->rate_limit_until = current_time + backoff_seconds;
// Send CLOSED notice with backoff information
cJSON* closed_msg = cJSON_CreateArray();
cJSON_AddItemToArray(closed_msg, cJSON_CreateString("CLOSED"));
cJSON_AddItemToArray(closed_msg, cJSON_CreateString(sub_id));
cJSON_AddItemToArray(closed_msg, cJSON_CreateString("error: too many subscriptions"));
char backoff_msg[256];
snprintf(backoff_msg, sizeof(backoff_msg),
"Maximum subscriptions per client exceeded. Backoff for %d seconds.", backoff_seconds);
cJSON_AddItemToArray(closed_msg, cJSON_CreateString(backoff_msg));
char* closed_str = cJSON_Print(closed_msg);
if (closed_str) {
size_t closed_len = strlen(closed_str);
unsigned char* buf = malloc(LWS_PRE + closed_len);
if (buf) {
memcpy(buf + LWS_PRE, closed_str, closed_len);
lws_write(wsi, buf + LWS_PRE, closed_len, LWS_WRITE_TEXT);
free(buf);
}
free(closed_str);
}
cJSON_Delete(closed_msg);
return 0;
}
}
// Parameter binding helpers
char** bind_params = NULL;
int bind_param_count = 0;
int bind_param_capacity = 0;
// Check for kind 33334 configuration event requests BEFORE creating subscription
int config_events_sent = 0;
int has_config_request = 0;
@@ -770,32 +874,6 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru
// If only config events were requested, we can return early after sending EOSE
// But still create the subscription for future config updates
// Check session subscription limits
if (pss && pss->subscription_count >= g_subscription_manager.max_subscriptions_per_client) {
log_error("Maximum subscriptions per client exceeded");
// Send CLOSED notice
cJSON* closed_msg = cJSON_CreateArray();
cJSON_AddItemToArray(closed_msg, cJSON_CreateString("CLOSED"));
cJSON_AddItemToArray(closed_msg, cJSON_CreateString(sub_id));
cJSON_AddItemToArray(closed_msg, cJSON_CreateString("error: too many subscriptions"));
char* closed_str = cJSON_Print(closed_msg);
if (closed_str) {
size_t closed_len = strlen(closed_str);
unsigned char* buf = malloc(LWS_PRE + closed_len);
if (buf) {
memcpy(buf + LWS_PRE, closed_str, closed_len);
lws_write(wsi, buf + LWS_PRE, closed_len, LWS_WRITE_TEXT);
free(buf);
}
free(closed_str);
}
cJSON_Delete(closed_msg);
return has_config_request ? config_events_sent : 0;
}
// Create persistent subscription
subscription_t* subscription = create_subscription(sub_id, wsi, filters, pss ? pss->client_ip : "unknown");
if (!subscription) {
@@ -807,13 +885,13 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru
if (add_subscription_to_manager(subscription) != 0) {
log_error("Failed to add subscription to global manager");
free_subscription(subscription);
// Send CLOSED notice
cJSON* closed_msg = cJSON_CreateArray();
cJSON_AddItemToArray(closed_msg, cJSON_CreateString("CLOSED"));
cJSON_AddItemToArray(closed_msg, cJSON_CreateString(sub_id));
cJSON_AddItemToArray(closed_msg, cJSON_CreateString("error: subscription limit reached"));
char* closed_str = cJSON_Print(closed_msg);
if (closed_str) {
size_t closed_len = strlen(closed_str);
@@ -826,7 +904,15 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru
free(closed_str);
}
cJSON_Delete(closed_msg);
// Update rate limiting counters for failed attempt (global limit reached)
if (pss) {
time_t current_time = time(NULL);
pss->failed_subscription_attempts++;
pss->last_failed_attempt = current_time;
pss->consecutive_failures++;
}
return has_config_request ? config_events_sent : 0;
}
@@ -848,7 +934,13 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru
log_warning("Invalid filter object");
continue;
}
// Reset bind params for this filter
free_bind_params(bind_params, bind_param_count);
bind_params = NULL;
bind_param_count = 0;
bind_param_capacity = 0;
// Build SQL query based on filter - exclude ephemeral events (kinds 20000-29999) from historical queries
char sql[1024] = "SELECT id, pubkey, created_at, kind, content, sig, tags FROM events WHERE 1=1 AND (kind < 20000 OR kind >= 30000)";
char* sql_ptr = sql + strlen(sql);
@@ -888,56 +980,80 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru
// Handle authors filter
cJSON* authors = cJSON_GetObjectItem(filter, "authors");
if (authors && cJSON_IsArray(authors)) {
int author_count = cJSON_GetArraySize(authors);
int author_count = 0;
// Count valid authors
for (int a = 0; a < cJSON_GetArraySize(authors); a++) {
cJSON* author = cJSON_GetArrayItem(authors, a);
if (cJSON_IsString(author)) {
author_count++;
}
}
if (author_count > 0) {
snprintf(sql_ptr, remaining, " AND pubkey IN (");
sql_ptr += strlen(sql_ptr);
remaining = sizeof(sql) - strlen(sql);
for (int a = 0; a < author_count; a++) {
cJSON* author = cJSON_GetArrayItem(authors, a);
if (cJSON_IsString(author)) {
if (a > 0) {
snprintf(sql_ptr, remaining, ",");
sql_ptr++;
remaining--;
}
snprintf(sql_ptr, remaining, "'%s'", cJSON_GetStringValue(author));
sql_ptr += strlen(sql_ptr);
remaining = sizeof(sql) - strlen(sql);
if (a > 0) {
snprintf(sql_ptr, remaining, ",");
sql_ptr++;
remaining--;
}
snprintf(sql_ptr, remaining, "?");
sql_ptr += strlen(sql_ptr);
remaining = sizeof(sql) - strlen(sql);
}
snprintf(sql_ptr, remaining, ")");
sql_ptr += strlen(sql_ptr);
remaining = sizeof(sql) - strlen(sql);
// Add author values to bind params
for (int a = 0; a < cJSON_GetArraySize(authors); a++) {
cJSON* author = cJSON_GetArrayItem(authors, a);
if (cJSON_IsString(author)) {
add_bind_param(&bind_params, &bind_param_count, &bind_param_capacity, cJSON_GetStringValue(author));
}
}
}
}
// Handle ids filter
cJSON* ids = cJSON_GetObjectItem(filter, "ids");
if (ids && cJSON_IsArray(ids)) {
int id_count = cJSON_GetArraySize(ids);
int id_count = 0;
// Count valid ids
for (int i = 0; i < cJSON_GetArraySize(ids); i++) {
cJSON* id = cJSON_GetArrayItem(ids, i);
if (cJSON_IsString(id)) {
id_count++;
}
}
if (id_count > 0) {
snprintf(sql_ptr, remaining, " AND id IN (");
sql_ptr += strlen(sql_ptr);
remaining = sizeof(sql) - strlen(sql);
for (int i = 0; i < id_count; i++) {
cJSON* id = cJSON_GetArrayItem(ids, i);
if (cJSON_IsString(id)) {
if (i > 0) {
snprintf(sql_ptr, remaining, ",");
sql_ptr++;
remaining--;
}
snprintf(sql_ptr, remaining, "'%s'", cJSON_GetStringValue(id));
sql_ptr += strlen(sql_ptr);
remaining = sizeof(sql) - strlen(sql);
if (i > 0) {
snprintf(sql_ptr, remaining, ",");
sql_ptr++;
remaining--;
}
snprintf(sql_ptr, remaining, "?");
sql_ptr += strlen(sql_ptr);
remaining = sizeof(sql) - strlen(sql);
}
snprintf(sql_ptr, remaining, ")");
sql_ptr += strlen(sql_ptr);
remaining = sizeof(sql) - strlen(sql);
// Add id values to bind params
for (int i = 0; i < cJSON_GetArraySize(ids); i++) {
cJSON* id = cJSON_GetArrayItem(ids, i);
if (cJSON_IsString(id)) {
add_bind_param(&bind_params, &bind_param_count, &bind_param_capacity, cJSON_GetStringValue(id));
}
}
}
}
@@ -950,29 +1066,42 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru
const char* tag_name = filter_key + 1; // Get the tag name (e, p, t, type, etc.)
if (cJSON_IsArray(filter_item)) {
int tag_value_count = cJSON_GetArraySize(filter_item);
int tag_value_count = 0;
// Count valid tag values
for (int i = 0; i < cJSON_GetArraySize(filter_item); i++) {
cJSON* tag_value = cJSON_GetArrayItem(filter_item, i);
if (cJSON_IsString(tag_value)) {
tag_value_count++;
}
}
if (tag_value_count > 0) {
// Use EXISTS with LIKE to check for matching tags
snprintf(sql_ptr, remaining, " AND EXISTS (SELECT 1 FROM json_each(json(tags)) WHERE json_extract(value, '$[0]') = '%s' AND json_extract(value, '$[1]') IN (", tag_name);
// Use EXISTS with parameterized query
snprintf(sql_ptr, remaining, " AND EXISTS (SELECT 1 FROM json_each(json(tags)) WHERE json_extract(value, '$[0]') = ? AND json_extract(value, '$[1]') IN (");
sql_ptr += strlen(sql_ptr);
remaining = sizeof(sql) - strlen(sql);
for (int i = 0; i < tag_value_count; i++) {
cJSON* tag_value = cJSON_GetArrayItem(filter_item, i);
if (cJSON_IsString(tag_value)) {
if (i > 0) {
snprintf(sql_ptr, remaining, ",");
sql_ptr++;
remaining--;
}
snprintf(sql_ptr, remaining, "'%s'", cJSON_GetStringValue(tag_value));
sql_ptr += strlen(sql_ptr);
remaining = sizeof(sql) - strlen(sql);
if (i > 0) {
snprintf(sql_ptr, remaining, ",");
sql_ptr++;
remaining--;
}
snprintf(sql_ptr, remaining, "?");
sql_ptr += strlen(sql_ptr);
remaining = sizeof(sql) - strlen(sql);
}
snprintf(sql_ptr, remaining, "))");
sql_ptr += strlen(sql_ptr);
remaining = sizeof(sql) - strlen(sql);
// Add tag name and values to bind params
add_bind_param(&bind_params, &bind_param_count, &bind_param_capacity, tag_name);
for (int i = 0; i < cJSON_GetArraySize(filter_item); i++) {
cJSON* tag_value = cJSON_GetArrayItem(filter_item, i);
if (cJSON_IsString(tag_value)) {
add_bind_param(&bind_params, &bind_param_count, &bind_param_capacity, cJSON_GetStringValue(tag_value));
}
}
}
}
}
@@ -1048,6 +1177,11 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru
log_error(error_msg);
continue;
}
// Bind parameters
for (int i = 0; i < bind_param_count; i++) {
sqlite3_bind_text(stmt, i + 1, bind_params[i], -1, SQLITE_TRANSIENT);
}
int row_count = 0;
while (sqlite3_step(stmt) == SQLITE_ROW) {
@@ -1112,7 +1246,10 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru
sqlite3_finalize(stmt);
}
// Cleanup bind params
free_bind_params(bind_params, bind_param_count);
return events_sent;
}
/////////////////////////////////////////////////////////////////////////////////////////
@@ -1614,9 +1751,27 @@ int main(int argc, char* argv[]) {
// Initialize NIP-40 expiration configuration
init_expiration_config();
// Update subscription manager configuration
update_subscription_manager_config();
// Initialize subscription manager mutexes
if (pthread_mutex_init(&g_subscription_manager.subscriptions_lock, NULL) != 0) {
log_error("Failed to initialize subscription manager subscriptions lock");
cleanup_configuration_system();
nostr_cleanup();
close_database();
return 1;
}
if (pthread_mutex_init(&g_subscription_manager.ip_tracking_lock, NULL) != 0) {
log_error("Failed to initialize subscription manager IP tracking lock");
pthread_mutex_destroy(&g_subscription_manager.subscriptions_lock);
cleanup_configuration_system();
nostr_cleanup();
close_database();
return 1;
}
// Start WebSocket Nostr relay server (port from configuration)
@@ -1626,6 +1781,11 @@ int main(int argc, char* argv[]) {
cleanup_relay_info();
ginxsom_request_validator_cleanup();
cleanup_configuration_system();
// Cleanup subscription manager mutexes
pthread_mutex_destroy(&g_subscription_manager.subscriptions_lock);
pthread_mutex_destroy(&g_subscription_manager.ip_tracking_lock);
nostr_cleanup();
close_database();