fix(analytics): validate telemetry event names and reject unknown body keys - #6435
claude[bot] wants to merge 1 commit into
Conversation
…y keys POST /analytics/send-event and /analytics/send-page are unauthenticated and passed the caller's event name straight through to the Segment/Amplitude pipeline. The pipe was created without whitelist/forbidNonWhitelisted, so unknown body keys rode along too: `traits` was spread into Amplitude user properties, and `nonTracking` let a caller override the user's analytics consent. Constrain the event name to the shape real event names use, drop the `traits` pass-through (nothing ever set it), and stop accepting `nonTracking` from the request body - internal emitters keep it on ITelemetryEvent. The server-stamped `telemetry` trait is unchanged. `eventData` stays free-form, but must now be validated with @isObject() rather than a no-op @ValidateNested(), because a nested validation under whitelist rejects every key inside it. Page names are validated with a separate pattern via a new SendPageDto, since they are human readable labels ("Search and Query", "Pub/Sub") rather than SCREAMING_SNAKE_CASE identifiers.
|
|
Code Coverage - Backend unit tests
Test suite run success3779 tests passing in 325 suites. Report generated by 🧪jest coverage report action from a4a17f2 |
Code Coverage - Integration Tests
|
Code Coverage - Frontend unit tests
Test suite run success8049 tests passing in 872 suites. Report generated by 🧪jest coverage report action from a4a17f2 |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a4a17f2c27
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| * HTTP is constrained to that shape to keep arbitrary caller supplied strings out of | ||
| * the analytics pipeline. | ||
| */ | ||
| export const TELEMETRY_EVENT_NAME_PATTERN = /^[A-Z0-9_]{1,128}$/; |
There was a problem hiding this comment.
Enforce an allowlist for unauthenticated event names
When analytics consent is enabled, an unauthenticated caller can still submit arbitrary names such as UNRECOGNIZED_EVENT_123, because this pattern validates only their syntax; the controller then forwards them to Segment and returns 204. The same issue exists for the page-name pattern below, so an adapted client can continue polluting Amplitude with invented events and consuming the ingestion allowance this change is intended to protect. Validate against the application's actual event/page allowlists, or add authentication/rate limiting rather than relying only on name shape.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3ceb926cae
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
3ceb926 to
a4a17f2
Compare
|
On the review point that the event-name patterns validate shape but not membership: confirmed, and deferring the fix to a follow-up rather than doing it here. The finding is real. I booted the real I attempted a genuine allowlist on this branch and reverted it — the branch is back at
Worth recording for the follow-up: an allowlist bounds the set of names but not the volume. The endpoint stays unauthenticated and unthrottled, and the API binds Generated by Claude Code |
Requested by Olga Lopaci, Dimo Georgiev · Slack thread
What
Before.
POST /api/analytics/send-eventandPOST /api/analytics/send-pageaccept requests from anyone — there is no authentication on either route — and whatever string the caller put ineventbecame the event name in Segment, and from there the event name in Amplitude. The request body was not restricted to the fields the DTO declared, so callers could also send atraitsobject, which was spread verbatim intocontext.traitsand landed in Amplitude as user properties, and anonTracking: trueflag, which made the backend send the event even when the user had not granted analytics consent. A vulnerability scanner found the endpoint and posted an out-of-band command-injection probe as the event name; those probe strings were ingested as real event names in the production Amplitude project and consumed the month's ingestion allowance.After. Both endpoints only accept event names in the shape the application's own event names actually use, and reject any body field that is not part of the request contract. The scanner's probe payload now gets a
400, nothing reaches Segment, and the analytics consent decision can no longer be set by the caller. All real event names and page names continue to be accepted unchanged.In one sentence: this validates the telemetry event name and closes the endpoints to unknown request-body fields.
How.
eventis now constrained with@Matches()—/^[A-Z0-9_]{1,128}$/for events, which areSCREAMING_SNAKE_CASEconstants, and a separate/^[A-Za-z0-9][A-Za-z0-9 /_-]{0,127}$/for page views, which are human-readable labels likeSearch and QueryandPub/Sub. Because the two routes have genuinely different name shapes,send-pagenow binds a dedicatedSendPageDtoinstead of reusingSendEventDto. The controller'sValidationPipegainswhitelist: true, forbidNonWhitelisted: true(matchingdatabase.controller.ts), so unknown keys are rejected rather than passed through.traitsis removed from the request DTO and from the UI sender that was including the key on every request; the server-stampedtelemetry: enabled|disabledtrait is untouched, since dashboards depend on it.nonTrackingis removed from the request DTO but stays on the internalITelemetryEvent, so internal emitters that legitimately mark non-tracking events keep working.One subtlety worth flagging for review:
eventDatahad a@ValidateNested()decorator with no@Type()target. Underwhitelist: truethat combination makes class-validator reject every key insideeventData, which would have rejected all real events. It is now@IsObject(), which keeps the contents free-form while still requiring an object. There is a comment in the DTO recording why.Deliberately not changed here, to keep this reviewable:
eventDatacontents remain free-form. Real events carry many different shapes, so pinning them down needs its own pass.PATCH /api/settingshas the same gap:settings.controller.tsalso constructs itsValidationPipewithoutwhitelist, so unknown keys sent to it are persisted into the settings record.app.enableCors()inmain.tsis called with no origin allowlist, so any origin can make these calls from a browser.Testing
Automated, added in this PR — both endpoints now assert that a malformed event/page name, a
traitsobject, and a caller-suppliednonTrackingflag each return400with the expected message. The pre-existing generated validation cases and the happy-path cases still pass unchanged.The event-name pattern was checked against every event-name literal in the codebase rather than assumed: all 481 unique names in
api/src/constants/telemetry-events.tsandui/src/telemetry/events.tsmatch, and all 15 page names inui/src/telemetry/pageViews.tsmatch the page pattern. Note the longest real event name is 66 characters (CONFIG_DATABASES_REDIS_CLOUD_AUTODISCOVERY_SUBSCRIPTIONS_SUCCEEDED), which is why the bound is 128 and not 64.Manually, booting the controller over HTTP: real events with arbitrary
eventData, the 66-character event name, and page names containing spaces and/all return204; probe-style event names,traits, andnonTrackingall return400. Againstmainthe same payloads all returned204and reached the analytics service with the attacker-controlled event name, the injected user properties, andnonTracking: true.Membership validation against an allowlist of known event and page names was attempted on this branch and reverted — it is not part of this diff. See the deferral note for why it is follow-up work.