v0.7.7 - Prevent sql attacks and rate limiting on subscriptions
This commit is contained in:
296
src/main.c
296
src/main.c
@@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user