Skip to content

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

Open
ar2rsawseen wants to merge 1 commit into
masterfrom
security/compliance-hub-fixed-projection
Open

security(compliance-hub): fix the consents table projection server side#7859
ar2rsawseen wants to merge 1 commit into
masterfrom
security/compliance-hub-fixed-projection

Conversation

@ar2rsawseen

Copy link
Copy Markdown
Member

Reported via security@count.ly. Assessed against master 321a6645591e21c6e41f9834614d56126b780f45.

The issue

/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. 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 (lp/lpa/tp/tpc), city/region, acquisition source and device details.

The Compliance Hub UI never sends a projection, so the parameter was attacker-facing only.

What changes

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 frontend/public/templates/user.html (did, d, av, consent, lac) 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, still 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. The now-unreachable JSON.parse of the caller's value is removed so the pattern does not look like it is still there.

Worth noting for reviewers

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

Tests

Four cases in plugins/compliance-hub/tests.js: project={}, an explicit projection naming name/email/username/custom, and the projection alias, each asserting only allowlisted fields come back. _id is in the allowed set because Mongo returns it unless explicitly excluded.

Verified locally: npx eslint plugins/compliance-hub/ and node --check pass. The plugin suite runs in CI (test-api-plugins).

🤖 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:28

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.

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.

2 participants