Skip to content

Unauthenticated hidden-field disclosure in share views via client-supplied projection #3335

@geo-chen

Description

@geo-chen

Summary

A view can mark fields hidden, and share views are meant to never expose hidden field values to viewers. The share-view records endpoint masks hidden fields by passing a default projection of only the non-hidden fields, but it lets a client-supplied projection query parameter override that default entirely, and the snapshot read path does not intersect a supplied projection with the allowed field set. An anonymous attacker holding a share link can therefore read the values of fields the owner explicitly hid by naming their field ids in projection. The hidden field ids are discoverable from the same anonymous share metadata (view.columnMeta). Confirmed against the NestJS backend and Postgres: an anonymous request read a hidden field's value.

Details

The endpoint GET /api/share/:shareId/view/records is @Public + @AllowAnonymous().

  1. apps/nestjs-backend/src/features/share/share.service.ts:264 (getViewRecords): projection: query?.projection ?? filteredFields.map((f) => f.id). The hidden-field mask filteredFields is only the default; the client query.projection (retained by shareViewRecordsRoSchema, which only omits viewId/ignoreViewQuery) overrides it.
  2. apps/nestjs-backend/src/features/record/record.service.ts:1083-1085 (getRecords): when query.projection is present, the per-view hidden-field mask getViewProjection() is skipped (query.projection ? convertProjection(...) : getViewProjection(...)).
  3. apps/nestjs-backend/src/features/record/record.service.ts:1980-1982 (getSnapshotBulkWithPermission): a supplied projection passes through unfiltered (projection ?? convertEnabledFieldIdsToProjection(...)). The sibling getDocIdsByQuery (~2121-2124) does intersect (query.projection.filter((id) => enabledFieldIds.includes(id))); the snapshot path lacks this intersection.

In the OSS build, RecordPermissionService.wrapView returns enabledFieldIds: undefined, so the share-service default projection is the only defense, which is exactly what the client projection overrides. The hidden field id is discoverable from the anonymous GET /api/share/:shareId/view response, whose view.columnMeta enumerates all field ids including hidden ones.

The boundary crossed is an anonymous share-link holder reading field values the owner deliberately hid from the view.

PoC

Impact

An anonymous attacker with a share-view link reads field values the share owner hid from the view (CWE-200/639). Hidden fields routinely hold data deliberately withheld from viewers (PII, internal notes, formulas), so this is an unauthenticated confidentiality breach of the share's intended column-level isolation.

Remediation

Intersect a caller-supplied projection with the allowed field set on the share/snapshot path, mirroring getDocIdsByQuery. In getViewRecords (share.service.ts:264) restrict a supplied query.projection to filteredFields ids (reject ids not in the visible set), and/or in getSnapshotBulkWithPermission apply projection ∩ enabledFieldIds. Also mask hidden field ids out of view.columnMeta in getShareView.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions