diff --git a/application/controllers/Search.php b/application/controllers/Search.php index 95e0ec550..25b1e6263 100644 --- a/application/controllers/Search.php +++ b/application/controllers/Search.php @@ -107,7 +107,25 @@ class Search extends CI_Controller { $this->db->where('id', xss_clean($this->input->post('id'))); $sql = $this->db->get('queries')->result(); - $data['qsos'] = $this->db->query($sql[0]->query); + $query = $sql[0]->query; + + // Security: Validate query only accesses allowed tables + if (!$this->_validate_query_tables($query)) { + show_error("Invalid query: unauthorized table access detected", 403); + return; + } + + // Security: Block dangerous SQL keywords to prevent SQL injection + // Note: 'join' is NOT blocked because legitimate queries use JOINs + $blocked = ['insert', 'drop', 'alter', 'create', 'exec', 'script', 'into outfile', 'load_file']; + foreach ($blocked as $word) { + if (stristr($query, $word)) { + show_error("Invalid query: contains blocked keyword", 403); + return; + } + } + + $data['qsos'] = $this->db->query($query); $this->load->view('adif/data/exportall', $data); } @@ -116,7 +134,24 @@ class Search extends CI_Controller { $sql = $this->db->get('queries')->result(); $sql = $sql[0]->query; + // Security: Only allow SELECT queries if (stristr($sql, 'select') && !stristr($sql, 'delete') && !stristr($sql, 'update')) { + // Security: Validate query only accesses allowed tables + if (!$this->_validate_query_tables($sql)) { + show_error("Invalid query: unauthorized table access detected", 403); + return; + } + + // Security: Block dangerous SQL keywords to prevent SQL injection + // Note: 'join' is NOT blocked because legitimate queries use JOINs + $blocked = ['insert', 'drop', 'alter', 'create', 'exec', 'script', 'into outfile', 'load_file']; + foreach ($blocked as $word) { + if (stristr($sql, $word)) { + show_error("Invalid query: contains blocked keyword", 403); + return; + } + } + if (!(strpos(strtolower($sql),'limit'))) { $sql.=' limit 5000'; } @@ -312,4 +347,83 @@ class Search extends CI_Controller { } return $query; } + + /** + * Validates that query only accesses allowed tables + * Prevents SQL injection via UNION-based attacks on other tables + * + * @param string $sql The SQL query to validate + * @return bool TRUE if query only uses allowed tables, FALSE otherwise + */ + private function _validate_query_tables($sql) { + // Whitelist of allowed tables - users can only query these + $allowed_tables = [ + $this->config->item('table_name'), // Main logbook table (e.g., TABLE_HRD_CONTACTS_V01) + 'station_profile', + 'dxcc_entities', + 'lotw_users', + 'queries' + ]; + + // Convert to lowercase for case-insensitive comparison + $allowed_tables_lower = array_map('strtolower', $allowed_tables); + $main_table_lower = strtolower($this->config->item('table_name')); + + // Normalize the SQL: remove newlines and extra spaces for easier parsing + $normalized_sql = preg_replace('/\s+/', ' ', trim($sql)); + + // Pattern 1: Check for UNION/INTO OUTFILE/LOAD FILE - these are always blocked + if (preg_match('/\bunion\b.*?\bselect.*?\bfrom\s+(\w+)/i', $normalized_sql, $matches)) { + $union_table = strtolower($matches[1]); + if (!in_array($union_table, $allowed_tables_lower)) { + log_message('error', "Search query blocked: UNION with unauthorized table - '$union_table'"); + return FALSE; + } + } + + // Pattern 2: Extract all table names after FROM and JOIN keywords + // This handles: FROM `table`, JOIN `table`, FROM table, JOIN table + preg_match_all('/\b(?:FROM|JOIN)\s+`?(\w+)`?/i', $normalized_sql, $from_join_matches); + $found_tables = $from_join_matches[1]; + + // Pattern 3: Extract table.column references that are NOT in the whitelist + // This catches things like "users.password" or "admin.secret" + preg_match_all('/(\w+)\.\w+/i', $normalized_sql, $column_refs); + foreach ($column_refs[1] as $potential_table) { + $potential_table_lower = strtolower($potential_table); + // Only add if it's not a SQL keyword and not the main table + $sql_keywords = ['select', 'where', 'order', 'group', 'having', 'limit', 'offset', + 'and', 'or', 'not', 'null', 'like', 'in', 'between', 'exists', 'case', + 'when', 'then', 'else', 'end', 'as', 'on', 'desc', 'asc', 'left', 'right', 'inner', 'outer']; + if (!in_array($potential_table_lower, $sql_keywords) && $potential_table_lower !== $main_table_lower) { + $found_tables[] = $potential_table; + } + } + + // Check all found tables are in whitelist + $found_tables = array_unique($found_tables); + foreach ($found_tables as $table) { + if (empty($table)) { + continue; + } + + $table_lower = strtolower($table); + + // Skip common SQL keywords that might match + $sql_keywords = ['select', 'where', 'order', 'group', 'having', 'limit', 'offset', + 'and', 'or', 'not', 'null', 'like', 'in', 'between', 'exists', 'case', + 'when', 'then', 'else', 'end', 'as', 'on', 'desc', 'asc', 'left', 'right', 'inner', 'outer']; + if (in_array($table_lower, $sql_keywords)) { + continue; + } + + if (!in_array($table_lower, $allowed_tables_lower)) { + log_message('error', "Search query blocked: unauthorized table access detected - '$table'"); + log_message('error', "Full query: $sql"); + return FALSE; + } + } + + return TRUE; + } }