Files
offline-db/billing/views.py
Barış Keserandgoogle-labs-jules[bot] de7f0fc33b 🛡️ Sentinel: Fix IP Spoofing and Rate Limit Bypass (#260)
This commit introduces a `get_client_ip` utility function to securely extract the user's IP address from the `HTTP_X_FORWARDED_FOR` header, falling back to `REMOTE_ADDR`.

Previously, if the app was behind a reverse proxy, `REMOTE_ADDR` would be the proxy's IP, which allowed attackers to exhaust the rate limit for all users or spoof IP logs. This fix updates the `rate_limit_ip` decorator and the shopier callback logger to use the new utility function.

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2026-03-10 21:25:39 +03:00

101 lines
3.4 KiB
Python

from django.shortcuts import render, get_object_or_404
from django.http import HttpResponse, HttpResponseBadRequest
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
from django.utils import timezone
from .models import ShopierPayment
from users.models import User
from django.conf import settings
from core.utils import get_client_ip
import hmac
import hashlib
import base64
import logging
logger = logging.getLogger(__name__)
def verify_shopier_signature(post_data):
"""
Verifies the Shopier signature.
Since the exact algorithm is not documented in the codebase, we use a generic HMAC-SHA256
verification of the transaction ID and status with the secret key.
"""
secret = settings.SHOPIER_SECRET
if not secret:
# Critical security check: Do not allow proceeding without a secret, even in DEBUG.
# This forces developers to configure the environment properly and prevents accidental deployments with insecure settings.
logger.error("SHOPIER_SECRET is missing. Signature verification failed.")
return False
signature = post_data.get('signature')
if not signature:
return False
transaction_id = post_data.get('platform_order_id', '')
status = post_data.get('status', '')
# Construct the payload to sign.
# NOTE: This is an assumption based on common practices.
# Real Shopier integration usually involves random_nr and other fields.
# Without docs, we standardize on: transaction_id + status
payload = f"{transaction_id}{status}"
expected_signature = base64.b64encode(
hmac.new(
secret.encode('utf-8'),
payload.encode('utf-8'),
hashlib.sha256
).digest()
).decode('utf-8')
return hmac.compare_digest(signature, expected_signature)
@csrf_exempt
@require_POST
def shopier_callback(request):
"""
Callback for Shopier payment.
Expects POST data with transaction_id and status.
Note: Rate limiting is not applied here as this is a server-to-server webhook
that must handle bursts of legitimate payment notifications. Security is provided
by HMAC signature verification in verify_shopier_signature().
"""
# Log the IP address for security auditing
ip = get_client_ip(request)
logger.info(f"Shopier callback received from IP: {ip}")
# Verify signature
if not verify_shopier_signature(request.POST):
logger.warning(f"Invalid signature for Shopier callback from IP: {ip}")
return HttpResponseBadRequest("Invalid signature")
# Parse Shopier payload
transaction_id = request.POST.get('platform_order_id') # Assuming this maps to our transaction_id
status = request.POST.get('status') # success or failure
if not transaction_id:
return HttpResponseBadRequest("Missing transaction_id")
try:
payment = ShopierPayment.objects.get(transaction_id=transaction_id)
if status == 'success':
payment.status = 'success'
payment.processed_at = timezone.now()
payment.save()
# Activate Premium
user = payment.user
user.is_premium = True
user.save()
return HttpResponse("OK")
else:
payment.status = 'failed'
payment.save()
return HttpResponse("OK")
except ShopierPayment.DoesNotExist:
return HttpResponseBadRequest("Payment not found")