mirror of
https://github.com/thecyberlearn/quantum-ai-v2.git
synced 2026-08-18 14:12:59 +00:00
🔒 Enhance marketplace security with comprehensive protections
- Add rate limiting to marketplace views (60/min) and API (30/min) - Implement input validation for category and search parameters - Add server-side search with sanitization and length validation - Secure API data exposure with pagination and essential fields only - Add comprehensive security logging for monitoring suspicious activity - Update marketplace template with secure form-based search - Preserve search queries in category filter navigation - Add security-specific loggers for better monitoring Security improvements: • Rate limiting prevents abuse and DoS attacks • Input validation prevents injection attacks • Server-side search replaces vulnerable client-side filtering • API pagination limits response size and data exposure • Enhanced logging enables security monitoring • Template updates maintain UX while improving security 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
f4df8f935b
commit
c61263e7b7
@ -2,18 +2,57 @@ from django.shortcuts import render, redirect, get_object_or_404
|
||||
from django.contrib import messages
|
||||
from django.http import JsonResponse
|
||||
from django.db.models import Q
|
||||
from django_ratelimit.decorators import ratelimit
|
||||
from django_ratelimit import UNSAFE
|
||||
from .models import BaseAgent
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger('agent_base.security')
|
||||
|
||||
|
||||
@ratelimit(key='ip', rate='60/m', method='GET', block=False)
|
||||
def marketplace_view(request):
|
||||
"""Professional marketplace view with agent system"""
|
||||
"""Professional marketplace view with agent system - Rate limited to 60 requests per minute per IP"""
|
||||
# Check if rate limited
|
||||
if getattr(request, 'limited', False):
|
||||
logger.warning(f"Marketplace rate limit exceeded for IP {request.META.get('REMOTE_ADDR')}")
|
||||
messages.error(request, 'Too many requests. Please wait a moment before refreshing.')
|
||||
# Still show marketplace but with warning
|
||||
|
||||
# Get all agents for marketplace with optimized query
|
||||
agents_queryset = BaseAgent.objects.filter(is_active=True).select_related().order_by('category', 'name')
|
||||
|
||||
# Filter by category if specified
|
||||
# Server-side search with validation
|
||||
search_query = request.GET.get('search', '').strip()
|
||||
if search_query:
|
||||
# Validate search query (max length and safe characters)
|
||||
if len(search_query) > 100:
|
||||
logger.warning(f"Search query too long: {len(search_query)} characters")
|
||||
messages.error(request, 'Search query too long. Please keep it under 100 characters.')
|
||||
search_query = search_query[:100]
|
||||
|
||||
# Remove potential SQL injection patterns and sanitize
|
||||
import re
|
||||
search_query = re.sub(r'[^\w\s\-\.]', '', search_query)
|
||||
|
||||
if search_query:
|
||||
agents_queryset = agents_queryset.filter(
|
||||
Q(name__icontains=search_query) |
|
||||
Q(description__icontains=search_query)
|
||||
)
|
||||
logger.info(f"Marketplace search performed: '{search_query}'")
|
||||
|
||||
# Filter by category if specified with validation
|
||||
category = request.GET.get('category')
|
||||
if category:
|
||||
# Validate category against allowed choices
|
||||
valid_categories = [choice[0] for choice in BaseAgent.CATEGORIES]
|
||||
if category in valid_categories:
|
||||
agents_queryset = agents_queryset.filter(category=category)
|
||||
logger.info(f"Marketplace filtered by valid category: {category}")
|
||||
else:
|
||||
logger.warning(f"Invalid category parameter attempted: {category}")
|
||||
category = None # Reset to show all agents
|
||||
|
||||
# Get agents and categories in single query
|
||||
agents = list(agents_queryset)
|
||||
@ -24,37 +63,98 @@ def marketplace_view(request):
|
||||
'agents': agents,
|
||||
'categories': categories,
|
||||
'selected_category': category,
|
||||
'search_query': search_query if 'search_query' in locals() else '',
|
||||
}
|
||||
|
||||
return render(request, 'agent_base/marketplace.html', context)
|
||||
|
||||
|
||||
|
||||
@ratelimit(key='ip', rate='30/m', method='GET', block=False)
|
||||
def agents_api_view(request):
|
||||
"""API endpoint for agents list"""
|
||||
"""API endpoint for agents list - Rate limited to 30 requests per minute per IP"""
|
||||
# Check if rate limited
|
||||
if getattr(request, 'limited', False):
|
||||
logger.warning(f"Agents API rate limit exceeded for IP {request.META.get('REMOTE_ADDR')}")
|
||||
return JsonResponse({
|
||||
'error': 'Rate limit exceeded. Please try again later.',
|
||||
'agents': [],
|
||||
'total_count': 0,
|
||||
}, status=429)
|
||||
|
||||
agents = BaseAgent.objects.filter(is_active=True)
|
||||
|
||||
# Filter by category if specified
|
||||
# Server-side search with validation for API
|
||||
search_query = request.GET.get('search', '').strip()
|
||||
if search_query:
|
||||
# Validate search query (max length and safe characters)
|
||||
if len(search_query) > 100:
|
||||
logger.warning(f"API search query too long: {len(search_query)} characters")
|
||||
return JsonResponse({
|
||||
'error': 'Search query too long. Maximum 100 characters allowed.',
|
||||
'agents': [],
|
||||
'total_count': 0,
|
||||
}, status=400)
|
||||
|
||||
# Remove potential SQL injection patterns and sanitize
|
||||
import re
|
||||
search_query = re.sub(r'[^\w\s\-\.]', '', search_query)
|
||||
|
||||
if search_query:
|
||||
agents = agents.filter(
|
||||
Q(name__icontains=search_query) |
|
||||
Q(description__icontains=search_query)
|
||||
)
|
||||
logger.info(f"API search performed: '{search_query}'")
|
||||
|
||||
# Filter by category if specified with validation
|
||||
category = request.GET.get('category')
|
||||
if category:
|
||||
# Validate category against allowed choices
|
||||
valid_categories = [choice[0] for choice in BaseAgent.CATEGORIES]
|
||||
if category in valid_categories:
|
||||
agents = agents.filter(category=category)
|
||||
logger.info(f"API filtered by valid category: {category}")
|
||||
else:
|
||||
logger.warning(f"Invalid category parameter in API: {category}")
|
||||
return JsonResponse({
|
||||
'error': 'Invalid category parameter',
|
||||
'valid_categories': valid_categories,
|
||||
'agents': [],
|
||||
'total_count': 0,
|
||||
}, status=400)
|
||||
|
||||
# Add pagination for security (limit large responses) with validation
|
||||
try:
|
||||
page_size = min(int(request.GET.get('limit', 50)), 100) # Max 100 agents per request
|
||||
offset = max(int(request.GET.get('offset', 0)), 0)
|
||||
except (ValueError, TypeError):
|
||||
logger.warning(f"Invalid pagination parameters in API request")
|
||||
return JsonResponse({
|
||||
'error': 'Invalid pagination parameters. Limit and offset must be integers.',
|
||||
'agents': [],
|
||||
'total_count': 0,
|
||||
}, status=400)
|
||||
|
||||
agents_page = agents[offset:offset + page_size]
|
||||
|
||||
# Only return essential data (minimize information disclosure)
|
||||
agents_data = []
|
||||
for agent in agents:
|
||||
for agent in agents_page:
|
||||
agents_data.append({
|
||||
'id': str(agent.id),
|
||||
'name': agent.name,
|
||||
'slug': agent.slug,
|
||||
'description': agent.description,
|
||||
'description': agent.description[:200], # Limit description length
|
||||
'category': agent.category,
|
||||
'price': float(agent.price),
|
||||
'icon': agent.icon,
|
||||
'rating': float(agent.rating),
|
||||
'review_count': agent.review_count,
|
||||
'agent_type': agent.agent_type,
|
||||
})
|
||||
|
||||
return JsonResponse({
|
||||
'agents': agents_data,
|
||||
'total_count': len(agents_data),
|
||||
'total_count': agents.count(),
|
||||
'returned_count': len(agents_data),
|
||||
'offset': offset,
|
||||
'limit': page_size,
|
||||
})
|
||||
@ -386,5 +386,25 @@ LOGGING = {
|
||||
'level': 'INFO',
|
||||
'propagate': False,
|
||||
},
|
||||
'agent_base.security': {
|
||||
'handlers': ['console', 'file'],
|
||||
'level': 'INFO',
|
||||
'propagate': False,
|
||||
},
|
||||
'authentication.security': {
|
||||
'handlers': ['console', 'file'],
|
||||
'level': 'INFO',
|
||||
'propagate': False,
|
||||
},
|
||||
'core.security': {
|
||||
'handlers': ['console', 'file'],
|
||||
'level': 'INFO',
|
||||
'propagate': False,
|
||||
},
|
||||
'wallet.security': {
|
||||
'handlers': ['console', 'file'],
|
||||
'level': 'INFO',
|
||||
'propagate': False,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -68,11 +68,27 @@ body {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
.search-button {
|
||||
position: absolute;
|
||||
right: clamp(16px, 4vw, 24px);
|
||||
right: clamp(8px, 2vw, 12px);
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: clamp(8px, 2vw, 12px);
|
||||
border-radius: clamp(8px, 2vw, 12px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.search-button:hover {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
width: clamp(20px, 5vw, 24px);
|
||||
height: clamp(20px, 5vw, 24px);
|
||||
color: #9ca3af;
|
||||
|
||||
@ -26,22 +26,27 @@
|
||||
|
||||
<!-- Search -->
|
||||
<div class="search-container">
|
||||
<div class="search-wrapper">
|
||||
<input type="text" placeholder="Search AI assistants..." class="search-input" id="search-input">
|
||||
<form method="GET" action="{% url 'agent_base:marketplace' %}" class="search-wrapper">
|
||||
{% if selected_category %}
|
||||
<input type="hidden" name="category" value="{{ selected_category }}">
|
||||
{% endif %}
|
||||
<input type="text" name="search" placeholder="Search AI assistants..." class="search-input" id="search-input" value="{{ search_query }}" maxlength="100">
|
||||
<button type="submit" class="search-button">
|
||||
<svg class="search-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Category Filters -->
|
||||
<div class="category-filters">
|
||||
<a href="{% url 'agent_base:marketplace' %}" class="category-button {% if not selected_category %}active{% endif %}" data-category="all">
|
||||
<a href="{% url 'agent_base:marketplace' %}{% if search_query %}?search={{ search_query }}{% endif %}" class="category-button {% if not selected_category %}active{% endif %}" data-category="all">
|
||||
<span class="category-emoji">🤖</span> All Assistants
|
||||
</a>
|
||||
{% if categories %}
|
||||
{% for category_value, category_display in categories %}
|
||||
<a href="{% url 'agent_base:marketplace' %}?category={{ category_value }}"
|
||||
<a href="{% url 'agent_base:marketplace' %}?category={{ category_value }}{% if search_query %}&search={{ search_query }}{% endif %}"
|
||||
class="category-button {% if selected_category == category_value %}active{% endif %}"
|
||||
data-category="{{ category_value }}">
|
||||
<span class="category-emoji">
|
||||
|
||||
Loading…
Reference in New Issue
Block a user