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().
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.
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(...)).
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.
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
projectionquery 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 inprojection. 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/recordsis@Public+@AllowAnonymous().apps/nestjs-backend/src/features/share/share.service.ts:264(getViewRecords):projection: query?.projection ?? filteredFields.map((f) => f.id). The hidden-field maskfilteredFieldsis only the default; the clientquery.projection(retained byshareViewRecordsRoSchema, which only omitsviewId/ignoreViewQuery) overrides it.apps/nestjs-backend/src/features/record/record.service.ts:1083-1085(getRecords): whenquery.projectionis present, the per-view hidden-field maskgetViewProjection()is skipped (query.projection ? convertProjection(...) : getViewProjection(...)).apps/nestjs-backend/src/features/record/record.service.ts:1980-1982(getSnapshotBulkWithPermission): a supplied projection passes through unfiltered (projection ?? convertEnabledFieldIdsToProjection(...)). The siblinggetDocIdsByQuery(~2121-2124) does intersect (query.projection.filter((id) => enabledFieldIds.includes(id))); the snapshot path lacks this intersection.In the OSS build,
RecordPermissionService.wrapViewreturnsenabledFieldIds: 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 anonymousGET /api/share/:shareId/viewresponse, whoseview.columnMetaenumerates 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. IngetViewRecords(share.service.ts:264) restrict a suppliedquery.projectiontofilteredFieldsids (reject ids not in the visible set), and/or ingetSnapshotBulkWithPermissionapplyprojection ∩ enabledFieldIds. Also mask hidden field ids out ofview.columnMetaingetShareView.