fix: mask a custom field's secret for an API caller who may not see it - #830
Merged
Merged
Conversation
`account/view?customFields=1` answered with the decrypted value of an encrypted custom field, whoever asked. `ItemTrait::getCustomFieldsForItem()` decrypts whenever the row carries a key, without asking who is looking — the deciding is left to whoever renders it. The theme does decide: `aux-customfields.inc` prints `***` unless `showViewCustomPass`, which `AccountHelper` sets from the account's own view-password permission, and `CUSTOMFIELD_VIEW_PASS` maps to the same `isAccViewPass` profile flag. That flag is consulted in five web controllers and nowhere else. Nothing decided on the API path. What makes it more than an omission is that the API is deliberate about the account's own password: the account transform returns login, url, notes and metadata but not `pass`, because retrieving a password is `account/viewPass` — a separate action, with a separate token. A token for `account/view` was handing out what that separation exists to gate. `Api::setupUser()` puts the profile in context, so the check was possible all along. The ACL is threaded through the Adapter base, which the three adapters that include custom fields already autowire, and the masking matches the theme's `***` rather than dropping the key — the response keeps its shape and still reports `encrypted: true`, so a caller can tell a masked secret from an empty field. A value that was never encrypted is returned as it is; masking every custom field would have been a different change, and the third test pins that. Six existing adapter tests construct these positionally and broke on the new parameter, which is what a full-suite run is for.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
account/view?customFields=1answered with the decrypted value of an encrypted custom field, whoever asked.ItemTrait::getCustomFieldsForItem()decrypts whenever the row carries a key, without asking who is looking — the deciding is left to whoever renders it. The theme does decide:AccountHelpersetsshowViewCustomPassfrom the account's own view-password permission, andCUSTOMFIELD_VIEW_PASSmaps to the sameisAccViewPassprofile flag. That flag is consulted in five web view controllers and nowhere else. Nothing decided on the API path, so the adapter returned'value' => $data->value.Why this is more than an omission
The API is otherwise deliberate about exactly this. Its account transform returns
login,url,notesand metadata — but notpass, because retrieving a password isaccount/viewPass, a separate action needing a separate token. A token scoped toaccount/viewwas handing out what that separation exists to gate.Api::setupUser()loads the user's profile into context, so the check was possible all along.The change
The ACL is threaded through the
Adapterbase — the three adapters that include custom fields (Account,Client,Category) already autowire it, so DI needed no change, and the wiring tests confirm the container still builds them.Masking matches the theme's
***rather than dropping the key: the response keeps its shape and still reportsencrypted: true, so a caller can tell a masked secret from an empty field.A value that was never encrypted is returned as it is. Masking every custom field would have been a different change and a wrong one —
testAValueThatWasNeverEncryptedIsReturnedRegardlesspins that.Scope left open, deliberately
The web applies the per-account view-password permission; this applies the profile-level
CUSTOMFIELD_VIEW_PASS. Closing that last gap means threading the account's permission object into an adapter shared with Client and Category, which have no per-account notion. The substantive leak — a caller with no password permission at all reading secrets — is closed; the residual is recorded rather than silently half-done.Verification
Reverting the mask fails
testAnEncryptedValueIsMaskedForACallerWhoMayNotViewPasswords. Six existing adapter tests construct these positionally and broke on the new parameter — which is what a full-suite run is for.3994 unit + 976 integration pass; PHPStan level 6 on
srcand PHPCS clean.