Skip to content

fix: mask an encrypted custom field whatever its type - #895

Merged
blaipr merged 2 commits into
mainfrom
fix/an-encrypted-custom-field-is-masked-whatever-its-type
Aug 30, 2026
Merged

fix: mask an encrypted custom field whatever its type#895
blaipr merged 2 commits into
mainfrom
fix/an-encrypted-custom-field-is-masked-whatever-its-type

Conversation

@blaipr

@blaipr blaipr commented Aug 30, 2026

Copy link
Copy Markdown
Member

aux-customfields.inc masked a custom field's value inside its typeName === 'password' branch
and nowhere else. isEncrypted is a property of the definition — a checkbox that sits beside the
type select and is set independently of it — so "Encrypted" is on for plenty of fields that are not
typed password: a textarea holding a recovery phrase, a text field holding an API key, a url
holding a signed one.

ItemTrait::getCustomFieldsForItem() decrypts every such row before the view sees it, so by the
time the partial runs the plaintext is simply sitting in $field->value. Four of the five branches
printed it in full — textarea, the generic <input> that text, url, number, email and
date all fall through to, and color — to anybody who could open the item, whatever
CUSTOMFIELD_VIEW_PASS said about them.

The API is the sibling that has always had this right: CustomField::valueFor() decides on
isValueEncrypted and never looks at the type, so the same field was withheld from a REST caller
and handed over by the web page.

The change

The decision is computed once per field, above the type switch, and used by every branch that
prints a value:

$isSecret = $field->isValueEncrypted || $field->typeName === 'password';
$displayValue = !$showsValue && $isSecret && !empty($field->value) ? '***' : $field->value;

A password-typed field stays masked whether or not its row was encrypted — that is what the
partial did before, and it is the safer reading of somebody having chosen that type. An empty field
is left empty rather than masked into a *** that an edit form would save back as the literal
value.

Tests

EncryptedCustomFieldsAreMaskedTest renders the real partial through a real Template, the way
ViewpassEscapesTest does, because the masking is a property of the template file and of nothing
else. It asserts the mask across all five types, that the value is shown to someone holding the
permission (or the mask assertions would be satisfied by a partial that printed nothing), that an
unencrypted value is left alone, and the two edge cases above.

Mutation-verified: reverting the template fails exactly four of the fifteen — textarea, text,
url and color — while password keeps passing, which is the shape of the defect.

The web's custom field partial masked the value inside its `typeName === 'password'`
branch alone, and `isEncrypted` is a property of the definition — a checkbox beside the
type select, set independently of it. So an encrypted textarea, text, url or color field
was decrypted by `ItemTrait::getCustomFieldsForItem()` and then printed in full to
anybody who could open the item, whatever CUSTOMFIELD_VIEW_PASS said about them.

The API is the sibling that has always had it right: `CustomField::valueFor()` decides
on `isValueEncrypted` and never looks at the type, so the same field was withheld from a
REST caller and handed over by the web page.

The decision is now computed once per field, above the type switch, and used by every
branch that prints a value. A password-typed field stays masked whether or not its row
was encrypted, and an empty field is left empty rather than masked into a `***` that an
edit form would save back as the literal value.
@blaipr
blaipr merged commit 5a3bbb8 into main Aug 30, 2026
8 checks passed
@blaipr
blaipr deleted the fix/an-encrypted-custom-field-is-masked-whatever-its-type branch August 30, 2026 14:30
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.

1 participant