Skip to content

fix(vault): retire a secret's plaintext when the sidebar loads another - #573

Merged
rubenvdlinde merged 1 commit into
developmentfrom
fix/sidebar-plaintext-cache-and-move-menu
Aug 31, 2026
Merged

fix(vault): retire a secret's plaintext when the sidebar loads another#573
rubenvdlinde merged 1 commit into
developmentfrom
fix/sidebar-plaintext-cache-and-move-menu

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

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 while plain is still null, and revealed persists 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 secretId watcher says so: clicking another row swaps the :id segment without remounting this component. So the cache outlives the secret it belongs to:

  • Edit the open secret. 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.
  • Click another row while revealed. The panel shows secret B's name with secret A's plaintext, and Copy resolves A's value.

The watcher already resets 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, resetting plain and revealed together. 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-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 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 build exits 0
  • eslint clean on both files, including two pre-existing errors in the spec that are fixed here
  • prettier clean
  • unit suite 678/678 across three full runs

One caveat worth recording: SecretRequestFill > encrypts on submit failed 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.

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.
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/keepiq @ c08aa92

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.

@rubenvdlinde
rubenvdlinde merged commit 759edc1 into development Aug 31, 2026
49 checks passed
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