Skip to content

security(compliance-hub): fix the consents table projection server side (24.05) - #7860

Merged
ar2rsawseen merged 3 commits into
release.24.05from
backport/compliance-hub-fixed-projection-2405
Jul 30, 2026
Merged

security(compliance-hub): fix the consents table projection server side (24.05)#7860
ar2rsawseen merged 3 commits into
release.24.05from
backport/compliance-hub-fixed-projection-2405

Conversation

@ar2rsawseen

Copy link
Copy Markdown
Member

Backport of #7859 to release.24.05. The plugin code is identical on this branch, so the fix applied cleanly; only the CHANGELOG heading needed resolving.

What changes

/o/app_users/consents built its projection as
params.qstring.project || params.qstring.projection || {defaults}, so the narrow default applied only when the caller omitted both parameters. A query-string value arrives as a string, so project={} is truthy and short-circuits the chain, and an empty projection means "all fields" to MongoDB, which was then forwarded unvalidated into appUsers.search().

A member holding nothing but compliance_hub read on an app could turn a consent-status table into full app_users documents, including name, email, username, phone, customer-defined custom properties, payment totals and location.

The projection is now a server-side constant matching the columns the users table actually renders, and any project/projection on the request is ignored. /o/consent/search gets the same treatment with its response shape unchanged.

Tests

Same four regression cases as #7859, asserting only allowlisted fields are returned for project={}, an explicit projection naming extra fields, and the projection alias.

Verified locally: npx eslint plugins/compliance-hub/ and node --check pass.

Note for reviewers

The CHANGELOG entry went under the current 24.05.51 heading rather than being appended to the 24.05.50 "backport of #7535" Security Fixes block, since this is a separate fix. Happy to consolidate if you prefer.

🤖 Generated with Claude Code

/o/app_users/consents built its projection as
`params.qstring.project || params.qstring.projection || {defaults}`, so the
narrow default applied only when the caller omitted both parameters. Because a
query-string value arrives as a string, `project={}` is truthy and short-circuits
the chain, and an empty projection means "all fields" to MongoDB. The projection
was then forwarded unvalidated into appUsers.search() and on to
find(query, project).

A member holding nothing but compliance_hub read on an app could therefore turn a
consent-status table into full app_users documents: name, email, username, phone,
organization, customer-defined custom properties, payment totals, city/region,
acquisition source and device details. The Compliance Hub UI never sends a
projection, so the parameter was attacker-facing only.

The consent table renders a fixed set of columns, so the projection is now a
server-side constant and any project/projection on the request is ignored. The
allowlist matches the columns bound in the users table template, plus uid and
appUserExport for the per-row history and export actions.

/o/consent/search is fixed the same way. Its response shape is unchanged - full
consent_history documents, which hold only consent state and the device context
it changed in - but the projection is no longer taken from the request, and the
now-unreachable JSON.parse of the caller's value is removed.

Note the surrounding hardening pass validated user-supplied *queries* via
common.parseUserQuery but never covered projections; the only projection
sanitizer in the tree (dbviewer's sanitizeProjection) constrains values to 0/1
and not which fields may be requested, so it would not have closed this.

Regression tests assert that project={}, an explicit projection naming
name/email/username/custom, and the projection alias parameter all return only
the allowed fields.

Co-Authored-By: Claude <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 29, 2026 20:30

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

ar2rsawseen and others added 2 commits July 30, 2026 11:04
Addresses review feedback on the countly-platform port. The consent-search
endpoints were changed to a fixed empty projection for tidiness, which was scope
creep: their default was already an empty projection, so a caller could only ever
narrow the response and there was no vulnerability there to fix. Forcing {} removed
the ability to narrow.

Restored to their original behaviour, including the JSON.parse of a caller-supplied
value. The only change left is the one that fixes the actual issue:
/o/app_users/consents returns a fixed set of fields.

Co-Authored-By: Claude <noreply@anthropic.com>
@ar2rsawseen
ar2rsawseen merged commit c0171d6 into release.24.05 Jul 30, 2026
6 of 7 checks passed
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