Skip to content

Harden proxy auth handling and add bulk-request support#104

Merged
sgiehl merged 2 commits into
masterfrom
iphandling
Jun 16, 2026
Merged

Harden proxy auth handling and add bulk-request support#104
sgiehl merged 2 commits into
masterfrom
iphandling

Conversation

@sgiehl

@sgiehl sgiehl commented Jun 9, 2026

Copy link
Copy Markdown
Member

Summary

Hardens how the tracker-proxy handles authentication-controlled tracking parameters, and adds proper support for bulk tracking requests. The proxy no longer lets its privileged token_auth authorize parameters a client tried to set itself (IP / location / timestamp overrides), for both single and bulk requests, while still recording the correct visitor IP.

Background

The proxy forwards browser tracking requests to a hidden Matomo server and injects two things: the real visitor IP as cip, and a privileged token_auth (write/admin) needed to authorize that cip. The side effect is that the same token authorizes every auth-controlled parameter in the request. In Matomo, cip, country, region, city, lat, long, cdt and cdo are only honored for an authenticated request — otherwise the request is rejected (InvalidRequestParameterException → HTTP 400 for a single request; the offending entry is skipped for a bulk request). So a visitor who appended e.g. &country=ru&cdt=… would have those overrides authorized by the proxy's token and silently tracked.

A previous change (#103) addressed this for single requests by stripping the override params. That had two problems:

  1. Bulk requests were not covered. Matomo's JS tracker batches multiple actions into a single {"requests":[…]} POST by default, and the overrides live inside the nested entries — which the strip logic never inspected. The proxy's token (reached via Matomo's auth fallback) then authorized them.
  2. Type-juggling bypass. The "did the client send a token?" check used isset(), so token_auth= (empty) or token_auth[]=x (array) made the proxy skip stripping while Matomo treated the token as absent and fell back to the injected privileged token.

It also changed semantics in a questionable way: stripping turns a request Matomo would reject into one that tracks (minus the override).

What changed

Approach: withhold the token instead of stripping params. The proxy injects its token_auth only for requests/entries that did not try to override anything. Anything carrying its own auth-controlled param (or its own token_auth) gets no token from us, so Matomo applies its native rules — rejecting a single request, skipping an offending bulk entry — rather than us
laundering it.

  • Bulk requests (proxy.php injectVisitIpIntoBulkRequest): the body is parsed exactly the way Matomo's BulkTracking plugin parses it (json_decode, parse_url+parse_str for string entries, arrays used directly, same "requests" detection, sanitizeLineBreaks). Each clean entry gets cip + token_auth injected; offending entries are left verbatim. A client-supplied top-level token_auth makes the whole body pass through untouched (the client's token governs).
  • Two visitor-IP modes:
    • $http_ip_forward_header set → the proxy injects nothing; the visitor IP is sent in that header (now for GET as well as POST) and Matomo derives it from the connection. Cleanest, no token involved.
    • not set (default) → the IP is conveyed via cip, authorized by the token, only on clean requests/entries.
  • Type-juggling safe: a client token_auth counts only when it's a non-empty string (matching how Matomo reads it); auth-controlled params are detected by key presence, so empty/array values can't slip past. Includes both cdt and cdo.
  • Client cip is never overridden — if the client sent one, we don't add ours (an authorized client may set it; an unauthorized one is rejected).

Why this approach

We deliberately do not track a request Matomo would reject. If a client includes an auth-controlled override without valid auth, the proxy refuses to lend its token, so Matomo rejects/skips it — same outcome as sending it directly. Clean requests (the normal case) are unaffected and still get the correct visitor IP.

Verified against Matomo

  • Bulk entries are built only from the JSON body, so a GET cip never reaches them — per-entry cip injection is genuinely required (core/Tracker/RequestSet.php, plugins/BulkTracking/Tracker/Requests.php).
  • cdo alone backdates the visit (cdt = now - abs(cdo)) and requires auth if older than 24h (core/Tracker/Request.php::getCustomTimestamp).
  • Token resolution / conflict validation (core/Request/AuthenticationToken.php) — the proxy never creates a conflicting second token source.
  • The auth-gated parameter set matches Matomo's (RequestHandlerTrait::$fieldsThatRequireAuth + Request.php).

replaces #95

Checklist

  • [✔/✖/NA] I have understood, reviewed, and tested all AI outputs before use
  • [✔/✖/NA] All AI instructions respect security, IP, and privacy rules

Review

@sgiehl sgiehl marked this pull request as ready for review June 10, 2026 14:55
  The proxy injects a privileged token_auth plus the real visitor IP (cip)
  into the tracking requests it forwards. Because cip and the location/
  timestamp params (country, region, city, lat, long, cdt, cdo) are only
  honored by Matomo for an authenticated request, that token would also
  authorize the same params if a client smuggled them in - letting a visitor
  spoof their IP, location or timestamp. The previous fix stripped those
  params from single GET/POST requests, but did not cover bulk requests and
  could be bypassed with an empty/array token_auth.

  Rework the approach: instead of stripping params, the proxy now only lends
  its token to requests (and bulk entries) that did not try to override
  anything. Anything that carries its own auth-protected param or token gets
  no token from us, so Matomo rejects/skips it exactly as if it had been sent
  directly without authentication - we never silently track a request Matomo
  would reject.

  - Add bulk () handling: each clean nested entry gets
    cip + token injected; offending entries are left untouched. Parsing and
    bulk-detection mirror Matomo's BulkTracking plugin so the two cannot
    disagree about what an entry contains.
  - Two IP-forwarding modes: when  is set the proxy
    injects nothing and the visitor IP rides in that header (now sent for GET
    as well as POST); otherwise the IP is conveyed via cip authorized by the
    token.
  - Type-juggling safe: a client token counts only when a non-empty string
    (as Matomo reads it); override params are detected by key presence, so
    empty/array values cannot bypass the check. Covers cdt and cdo.
  - Don't override a client-supplied cip.

  Update the README (visitor-IP forwarding modes, auth-protected params,
  bulk content-type note) and config.php.example. Add tests covering single
  and bulk requests in both modes, and the type-juggling cases.
guzzle ^6.5 pulls in guzzlehttp/psr7 1.9.x, which composer 2.10+ refuses to
install due to security advisories, breaking `composer install` on every CI
job (PHPCS and all PHPUnit versions). guzzle ^7 resolves psr7 ^2 (unaffected).
Test-only dependency; ProxyTest's HTTP client usage is unchanged and the full
suite (45 tests) passes locally under guzzle 7.11.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@sgiehl sgiehl requested a review from mneudert June 15, 2026 13:10
@sgiehl sgiehl merged commit 55d6f77 into master Jun 16, 2026
8 checks passed
@sgiehl sgiehl deleted the iphandling branch June 16, 2026 11:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants