Make analytics endpoint dynamic: /t/{token} derived from APP_SECRET

The endpoint path is now /t/<8-char hash of APP_SECRET> instead of
static /t. Token is injected via data-e attribute on body, read by JS.
Server validates token on every hit, returns 404 if invalid.
Changes with each APP_SECRET = impossible to hardcode in a blocker.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Serreau Jovann
2026-03-26 12:22:59 +01:00
parent 0952bc6e17
commit a139f86b90
4 changed files with 20 additions and 4 deletions

View File

@@ -1,4 +1,4 @@
const ENDPOINT = '/t' let ENDPOINT = '/t'
const SK_UID = '_u' const SK_UID = '_u'
const SK_HASH = '_h' const SK_HASH = '_h'
@@ -86,7 +86,9 @@ async function trackPageView(visitor) {
export async function initAnalytics() { export async function initAnalytics() {
const keyB64 = document.body.dataset.k const keyB64 = document.body.dataset.k
if (!keyB64) return const ep = document.body.dataset.e
if (!keyB64 || !ep) return
ENDPOINT = ep
try { try {
encKey = await importKey(keyB64) encKey = await importKey(keyB64)

View File

@@ -11,17 +11,25 @@ use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
class AnalyticsController extends AbstractController class AnalyticsController extends AbstractController
{ {
#[Route('/t', name: 'app_analytics_track', methods: ['POST'])] #[Route('/t/{token}', name: 'app_analytics_track', methods: ['POST'])]
public function track( public function track(
string $token,
#[Autowire('%kernel.secret%')] string $appSecret,
Request $request, Request $request,
AnalyticsCryptoService $crypto, AnalyticsCryptoService $crypto,
EntityManagerInterface $em, EntityManagerInterface $em,
MessageBusInterface $bus, MessageBusInterface $bus,
): Response { ): Response {
$expectedToken = substr(hash('sha256', $appSecret.'_endpoint'), 0, 8);
if (!hash_equals($expectedToken, $token)) {
return new Response('', 404);
}
$body = $request->getContent(); $body = $request->getContent();
$envelope = json_decode($body, true); $envelope = json_decode($body, true);

View File

@@ -3,20 +3,26 @@
namespace App\Twig; namespace App\Twig;
use App\Service\AnalyticsCryptoService; use App\Service\AnalyticsCryptoService;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Twig\Extension\AbstractExtension; use Twig\Extension\AbstractExtension;
use Twig\Extension\GlobalsInterface; use Twig\Extension\GlobalsInterface;
class AnalyticsExtension extends AbstractExtension implements GlobalsInterface class AnalyticsExtension extends AbstractExtension implements GlobalsInterface
{ {
private string $endpointToken;
public function __construct( public function __construct(
private AnalyticsCryptoService $crypto, private AnalyticsCryptoService $crypto,
#[Autowire('%kernel.secret%')] string $appSecret,
) { ) {
$this->endpointToken = substr(hash('sha256', $appSecret.'_endpoint'), 0, 8);
} }
public function getGlobals(): array public function getGlobals(): array
{ {
return [ return [
'analytics_key' => $this->crypto->getKeyForJs(), 'analytics_key' => $this->crypto->getKeyForJs(),
'analytics_endpoint' => '/t/'.$this->endpointToken,
]; ];
} }
} }

View File

@@ -84,7 +84,7 @@
{% endblock %} {% endblock %}
{% block head %}{% endblock %} {% block head %}{% endblock %}
</head> </head>
<body class="min-h-screen flex flex-col bg-[#fbfbfb] text-[#111827]" data-env="{{ app.environment }}" data-k="{{ analytics_key }}"{% if app.user and app.user.id is defined and app.request.cookies.get('e_ticket_consent') == 'accepted' %} data-uid="{{ app.user.id }}"{% endif %}> <body class="min-h-screen flex flex-col bg-[#fbfbfb] text-[#111827]" data-env="{{ app.environment }}" data-k="{{ analytics_key }}" data-e="{{ analytics_endpoint }}"{% if app.user and app.user.id is defined and app.request.cookies.get('e_ticket_consent') == 'accepted' %} data-uid="{{ app.user.id }}"{% endif %}>
<header class="sticky top-0 z-50 bg-white border-b-4 border-gray-900"> <header class="sticky top-0 z-50 bg-white border-b-4 border-gray-900">
<nav class="mx-auto px-4 lg:px-8" role="navigation" aria-label="Navigation principale" itemscope itemtype="https://schema.org/SiteNavigationElement"> <nav class="mx-auto px-4 lg:px-8" role="navigation" aria-label="Navigation principale" itemscope itemtype="https://schema.org/SiteNavigationElement">
<div class="flex justify-between items-center h-20"> <div class="flex justify-between items-center h-20">