# SECURE: Django views without exec() usage
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_protect
from django.contrib.auth.decorators import login_required, permission_required
from django.views.decorators.http import require_http_methods
from django.core.cache import cache
from django.contrib.auth.models import User
from django.utils.decorators import method_decorator
from django.views import View
from django import forms
import json
import ast
import operator
import time
import logging
logger = logging.getLogger(__name__)
# SECURE: Safe expression evaluator
class SafeMathEvaluator:
ALLOWED_OPERATIONS = {
ast.Add: operator.add,
ast.Sub: operator.sub,
ast.Mult: operator.mul,
ast.Div: operator.truediv,
ast.Mod: operator.mod,
ast.Pow: operator.pow,
ast.USub: operator.neg,
ast.UAdd: operator.pos
}
ALLOWED_FUNCTIONS = {
'abs': abs,
'round': round,
'min': min,
'max': max
}
def evaluate(self, expression: str, max_length: int = 100):
if len(expression) > max_length:
raise ValueError('Expression too long')
try:
tree = ast.parse(expression, mode='eval')
return self._eval_node(tree.body)
except (SyntaxError, ValueError) as e:
raise ValueError(f'Invalid expression: {e}')
def _eval_node(self, node):
if isinstance(node, ast.Constant):
if isinstance(node.value, (int, float)):
if abs(node.value) > 1e10:
raise ValueError('Number too large')
return node.value
raise ValueError('Only numbers allowed')
elif isinstance(node, ast.BinOp):
left = self._eval_node(node.left)
right = self._eval_node(node.right)
op = self.ALLOWED_OPERATIONS.get(type(node.op))
if op:
try:
result = op(left, right)
if abs(result) > 1e10:
raise ValueError('Result too large')
return result
except ZeroDivisionError:
raise ValueError('Division by zero')
raise ValueError('Operation not allowed')
elif isinstance(node, ast.UnaryOp):
operand = self._eval_node(node.operand)
op = self.ALLOWED_OPERATIONS.get(type(node.op))
if op:
return op(operand)
raise ValueError('Unary operation not allowed')
elif isinstance(node, ast.Call):
if isinstance(node.func, ast.Name):
func_name = node.func.id
if func_name in self.ALLOWED_FUNCTIONS:
args = [self._eval_node(arg) for arg in node.args]
if len(args) > 5:
raise ValueError('Too many function arguments')
return self.ALLOWED_FUNCTIONS[func_name](*args)
raise ValueError('Function not allowed')
raise ValueError('Expression type not allowed')
# SECURE: Data operations with allowlisting
class SecureDataOperations:
ALLOWED_OPERATIONS = {
'user_count': 'get_user_count',
'user_info': 'get_user_info',
'system_status': 'get_system_status'
}
def execute(self, operation: str, params: dict):
if operation not in self.ALLOWED_OPERATIONS:
raise ValueError(f'Operation not allowed: {operation}')
method_name = self.ALLOWED_OPERATIONS[operation]
method = getattr(self, method_name)
return method(params)
def get_user_count(self, params: dict):
return {'total_users': User.objects.count()}
def get_user_info(self, params: dict):
user_id = params.get('user_id')
if not isinstance(user_id, int) or user_id <= 0:
raise ValueError('Valid user_id required')
try:
user = User.objects.get(id=user_id)
return {
'username': user.username,
'date_joined': user.date_joined.isoformat(),
'is_active': user.is_active
}
except User.DoesNotExist:
raise ValueError('User not found')
def get_system_status(self, params: dict):
return {
'timestamp': time.time(),
'status': 'operational'
}
# Form validation
class CalculationForm(forms.Form):
expression = forms.CharField(max_length=100, strip=True)
def clean_expression(self):
expr = self.cleaned_data['expression']
# Basic validation
if not expr:
raise forms.ValidationError('Expression cannot be empty')
# Check for dangerous patterns
dangerous_patterns = ['import', 'exec', 'eval', '__', 'open', 'file']
expr_lower = expr.lower()
for pattern in dangerous_patterns:
if pattern in expr_lower:
raise forms.ValidationError('Expression contains forbidden keywords')
return expr
class DataOperationForm(forms.Form):
operation = forms.ChoiceField(choices=[
('user_count', 'User Count'),
('user_info', 'User Info'),
('system_status', 'System Status')
])
user_id = forms.IntegerField(min_value=1, required=False)
# SECURE: Rate limiting decorator
def rate_limit(max_requests=10, window=60):
def decorator(view_func):
def wrapper(request, *args, **kwargs):
if request.user.is_authenticated:
key = f'rate_limit_{request.user.id}'
else:
key = f'rate_limit_{request.META.get("REMOTE_ADDR")}'
current = cache.get(key, 0)
if current >= max_requests:
return JsonResponse({'error': 'Rate limit exceeded'}, status=429)
cache.set(key, current + 1, window)
return view_func(request, *args, **kwargs)
return wrapper
return decorator
# SECURE: Views with proper validation
@require_http_methods(['POST'])
@csrf_protect
@rate_limit(max_requests=5, window=60)
def safe_calculate(request):
try:
data = json.loads(request.body)
form = CalculationForm(data)
if not form.is_valid():
return JsonResponse({
'error': 'Validation failed',
'details': form.errors
}, status=400)
expression = form.cleaned_data['expression']
# Use safe evaluator
evaluator = SafeMathEvaluator()
result = evaluator.evaluate(expression)
return JsonResponse({
'expression': expression,
'result': result
})
except json.JSONDecodeError:
return JsonResponse({'error': 'Invalid JSON'}, status=400)
except ValueError as e:
return JsonResponse({'error': str(e)}, status=400)
except Exception as e:
logger.error(f'Calculation error: {e}')
return JsonResponse({'error': 'Calculation failed'}, status=500)
@require_http_methods(['POST'])
@csrf_protect
@login_required
@permission_required('auth.view_user', raise_exception=True)
@rate_limit(max_requests=20, window=60)
def secure_data_operation(request):
try:
data = json.loads(request.body)
form = DataOperationForm(data)
if not form.is_valid():
return JsonResponse({
'error': 'Validation failed',
'details': form.errors
}, status=400)
# Execute operation safely
operations = SecureDataOperations()
result = operations.execute(
form.cleaned_data['operation'],
form.cleaned_data
)
return JsonResponse({
'operation': form.cleaned_data['operation'],
'result': result
})
except json.JSONDecodeError:
return JsonResponse({'error': 'Invalid JSON'}, status=400)
except ValueError as e:
return JsonResponse({'error': str(e)}, status=400)
except Exception as e:
logger.error(f'Data operation error: {e}')
return JsonResponse({'error': 'Operation failed'}, status=500)
@require_http_methods(['GET'])
def list_operations(request):
operations = SecureDataOperations()
return JsonResponse({
'available_operations': list(operations.ALLOWED_OPERATIONS.keys()),
'math_functions': list(SafeMathEvaluator.ALLOWED_FUNCTIONS.keys())
})
# URLs configuration
from django.urls import path
urlpatterns = [
path('api/calculate/', safe_calculate, name='safe_calculate'),
path('api/data/', secure_data_operation, name='secure_data'),
path('api/operations/', list_operations, name='list_operations'),
]