fix(vault): retire a secret's plaintext when the sidebar loads another - #573
Merged
rubenvdlinde merged 1 commit intoAug 31, 2026
Merged
Conversation
PasswordField decrypts lazily and then caches the plaintext for its own
lifetime: toggle() only resolves while `plain` is still null, and
`revealed` persists too. That was harmless while the detail was a full
page that remounted per secret.
SecretDetailSidebar deliberately does NOT remount. Its own secretId
watcher says so: clicking another row "swaps the :id segment without
remounting this component". So the cache outlived the secret it belonged
to, in two ways that both matter for a password manager:
- Edit the open secret: load() refreshes the secret, the field kept
showing the OLD plaintext. This is what the e2e caught, asserting
the edited value and receiving the previous one.
- Click another row while revealed: the panel showed secret B's name
with secret A's plaintext, and Copy resolved A's value.
The watcher already reset cardRevealed and bsnRevealed for exactly this
reason. It could not reach inside PasswordField, so the password row was
the one field that kept its cleartext across the swap.
Binding :key to a token bumped on every successful load() remounts the
field, which resets `plain` and `revealed` together. A changed secret is
re-masked until the user asks for it again.
Also: click the Move menuitem by role instead of dispatching el.click()
at a node guessed from NcActionButton's markup. The old approach broke
silently after the restyle. The trace shows the testid resolving in 0.1s
and .move-form then timing out for 10s, with the menu still expanded and
menuitem "Move" present in the snapshot: a click landing on the wrong
node is indistinguishable from a dialog that refuses to open. The
accessibility tree exposes the item whatever element renders it.
Fixes two e2e failures on development introduced by the Stage-8 restyle,
and two pre-existing lint errors in the same spec file.
Contributor
Quality Report — ConductionNL/keepiq @
|
| Check | PHP | Vue | Security | License | Tests |
|---|---|---|---|---|---|
| lint | ✅ | ||||
| phpcs | ✅ | ||||
| phpmd | ✅ | ||||
| psalm | ✅ | ||||
| phpstan | ✅ | ||||
| phpmetrics | ✅ | ||||
| eslint | ✅ | ||||
| stylelint | ✅ | ||||
| build | ✅ | ||||
| check-manifest | ✅ | ||||
| test-l10n | ✅ | ||||
| format | ✅ | ||||
| check-l10n-js | ✅ | ||||
| check-schema-l10n | ✅ | ||||
| composer | ✅ | ✅ 111/111 | |||
| npm | ✅ | ✅ 536/536 | |||
| app:check-code | ⏭️ | ||||
| info.xml | ✅ | ||||
| REUSE | ❌ | ||||
| PHPUnit | ✅ | ||||
| Newman | ✅ | ||||
| Playwright | ⏭️ deferred — runs on the promotion into beta/main, not on a pull request into development | ||||
| Hydra gates | ✅ |
Quality workflow — 2026-08-31 14:42 UTC
Download the full PDF report from the workflow artifacts.
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.
Two e2e failures appeared on development when the Stage-8 restyle landed (#479). One of them is a real disclosure bug, not a test problem.
The sidebar served one secret's plaintext under another secret's name
PasswordField decrypts lazily and caches:
toggle()resolves only whileplainis still null, andrevealedpersists for the component's lifetime. That was harmless while the detail was a full page that remounted per secret.SecretDetailSidebar deliberately does not remount. Its own
secretIdwatcher says so: clicking another row swaps the :id segment without remounting this component. So the cache outlives the secret it belongs to:load()refreshes the secret, the field keeps showing the old plaintext. This is what the e2e caught: it asserted the edited value and received the previous one.The watcher already resets
cardRevealedandbsnRevealedfor exactly this reason. It could not reach inside PasswordField, so the password row was the one field that kept its cleartext across the swap.Binding
:keyto a token bumped on every successfulload()remounts the field, resettingplainandrevealedtogether. A changed secret is re-masked until the user asks for it again, which is the behaviour the rest of the panel already has.The Move menuitem was clicked by guessing at markup
The spec dispatched
el.click()at a node derived from NcActionButton's structure. After the restyle that stopped reaching the handler, and it failed invisibly: the trace shows the testid resolving in 0.1s and.move-formthen timing out for 10s, with the menu still[expanded]andmenuitem "Move"present in the snapshot. A click landing on the wrong node looks exactly like a dialog that refuses to open.Asking for the menuitem by role targets whatever element renders it, and uses a real Playwright click with its actionability checks rather than a dispatched event.
Verified
npm run buildexits 0One caveat worth recording:
SecretRequestFill > encrypts on submitfailed once in a fourth run and passed in the other three with identical code, so it is flaky rather than affected by this change. Worth a look on its own, since it sits in the encryption path.