fix(privacy): hash IP addresses in referral tracking for GDPR compliance

- ReferralController now stores ip_hash (SHA-256) instead of raw IP in session
- Cookie excludes IP entirely (only stores provider/model/timestamp)
- PlantTreeForAgentReferral uses hashed IPs in tree metadata
- Updated test to verify hashed IP storage

Raw IPs should not be stored in cookies or persisted unnecessarily.
Session-only hashed IP is sufficient for fraud detection.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Snider 2026-01-26 00:39:07 +00:00
parent c8dfc2a8a9
commit edb34e38d5
3 changed files with 21 additions and 9 deletions

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Core\Mod\Tenant\Controllers;
use Core\Helpers\PrivacyHelper;
use Core\Mod\Trees\Models\TreePlanting;
use Core\Mod\Trees\Models\TreePlantingStats;
use Illuminate\Http\RedirectResponse;
@ -55,24 +56,32 @@ class ReferralController extends \Core\Front\Controller
$provider = strtolower($provider);
$model = $model ? strtolower($model) : null;
// Build referral data
// Build referral data for session (includes hashed IP for fraud detection)
$referral = [
'provider' => $provider,
'model' => $model,
'referred_at' => now()->toIso8601String(),
'ip' => $request->ip(),
'ip_hash' => PrivacyHelper::hashIp($request->ip()),
];
// Track the referral visit in stats (raw inbound count)
TreePlantingStats::incrementReferrals($provider, $model);
// Store in session (primary)
// Store in session (primary) - includes hashed IP
$request->session()->put(self::REFERRAL_SESSION, $referral);
// Cookie data - exclude IP for privacy (GDPR compliance)
// Provider/model is sufficient for referral attribution
$cookieData = [
'provider' => $provider,
'model' => $model,
'referred_at' => $referral['referred_at'],
];
// Set 30-day cookie (backup for session expiry)
$cookie = Cookie::make(
name: self::REFERRAL_COOKIE,
value: json_encode($referral),
value: json_encode($cookieData),
minutes: self::COOKIE_LIFETIME,
path: '/',
domain: config('session.domain'),
@ -90,7 +99,7 @@ class ReferralController extends \Core\Front\Controller
/**
* Get the agent referral from session or cookie.
*
* @return array{provider: string, model: ?string, referred_at: string, ip: string}|null
* @return array{provider: string, model: ?string, referred_at: string, ip_hash?: string}|null
*/
public static function getReferral(Request $request): ?array
{

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Core\Mod\Trees\Listeners;
use Core\Helpers\PrivacyHelper;
use Core\Mod\Tenant\Controllers\ReferralController;
use Core\Mod\Trees\Models\TreePlanting;
use Core\Mod\Tenant\Models\AgentReferralBonus;
@ -64,8 +65,8 @@ class PlantTreeForAgentReferral
'status' => $status,
'metadata' => [
'referred_at' => $referral['referred_at'] ?? now()->toIso8601String(),
'referral_ip' => $referral['ip'] ?? null,
'signup_ip' => $request->ip(),
'referral_ip_hash' => $referral['ip_hash'] ?? null,
'signup_ip_hash' => PrivacyHelper::hashIp($request->ip()),
],
]);

View file

@ -69,11 +69,13 @@ describe('Referral Route', function () {
expect($referral['referred_at'])->not->toBeNull();
});
it('stores client IP in referral', function () {
it('stores hashed client IP in referral for privacy', function () {
$response = $this->get('/ref/anthropic');
$referral = $response->getSession()->get('agent_referral');
expect($referral['ip'])->not->toBeNull();
expect($referral['ip_hash'])->not->toBeNull();
// Verify it's a hash (64 chars for SHA-256)
expect(strlen($referral['ip_hash']))->toBe(64);
});
});