Skip to content

Security considerations

Most of these are inherent to any client-side analytics beacon (the same holds for GA4’s collect endpoint). They are listed so they are an informed choice, not a surprise.

The visitor id is a bearer value, not a credential

Section titled “The visitor id is a bearer value, not a credential”

X-Visitor-Id and visitor_id are checked for format only (TokenResolver::isValidFormat()), never for authenticity, and they win over the cookie. Anyone who learns another visitor’s id — through XSS, logs, a shared link with ?visitor_id= — can record events under that identity.

The impact is limited to the anonymous tracking identity: the id never authenticates anyone in your app. It can, however, affect which user a visitor row points to. Since 0.13.2 an ordinary tracked request only links a visitor that has no user yet; a visitor already linked to someone else is moved only by Login or Visits::identify(). A victim’s id that is still unlinked, or sent along with the attacker’s own login, still ends up on the attacker’s account. Treat the user link on visitors as analytics, not as an audit trail.

The cookie is encrypted and httpOnly like every Laravel cookie. visits.js deliberately keeps the same id in localStorage — readable by any script on the page — because that is what makes cross-origin and cached-page tracking work. An XSS on the page can read it either way.

POST /visits/collect accepts whatever type, name, url and meta a caller sends; nothing confirms that a reported page view or conversion happened. rate_limit.endpoint and rate_limit.visitor_budget bound the volume, not the authenticity. collect.allowed_origins filters by Origin/Referer, which non-browser clients forge freely — a filter against casual misuse, not authentication.

Record conversions that matter (orders, payments) server-side with Visits::track(), and treat beacon events as signals.

A client retry or a webhook delivered twice records the event twice. Guard calls yourself where a conversion must not be counted twice.

matomo/device-detector classifies by User-Agent, which any client sets freely. It keeps crawler noise out of the dashboard; it does not stop anyone.

  • The dashboard has no authentication by default — dashboard.middleware is ['web']. It shows IPs, visitor ids, linked users and full URLs, including query strings. Protect it before deploying (Dashboard).
  • /visits/whoami is public and unauthenticated, throttled per IP by rate_limit.whoami (60,1). Every uncached ?ip= costs a geo lookup. Tune the throttle, protect it, or disable it with whoami.enabled = false.

Visitor and session rows contain IP addresses, User-Agents, approximate location and full URLs. Under GDPR and similar laws these are personal data: gate tracking behind consent where required, add routes whose URLs carry secrets (password reset links, signed URLs) to exclude_paths, set geo.store_coordinates = false if you don’t need coordinates, and schedule visits:prune for a retention period (Rollups, sessions & retention).