All news
    SecurityEngineering

    Three security reports. One uncomfortable lesson. Thank you, squ11z1.

    Three reports from squ11z1 exposed gaps in Pwdly's invite flow, MFA enforcement and passkey lifecycle. David explains what went wrong, what changed, and why zero-knowledge has to hold beyond the cipher.

    The Pwdly Team
    Engineering & Security
    15 October 2026
    6 min read
    A translucent teal shield with three reinforced seams in front of a steel vault, beside sealed key capsules

    Last week, I received three emails from a security researcher using the handle squ11z1. Each identified a security issue in Pwdly. These weren't vague suggestions to add another header: they came with clear evidence of problems in project invitations, second-factor enforcement and passkey registration.

    Within 20 minutes of receiving the emails, I took the Pwdly dashboard and browser extensions completely offline for maintenance. I worked through the night. Ten hours after the reports arrived, all three issues were fixed, the maintenance page was removed, and the dashboard and extensions were back in service.

    Those ten hours weren't only about the fixes. I also built out our internal end-to-end (e2e) test tooling, expanding the code test suite we already had into something much more in-depth. The previous tests existed; these reports made clear that they needed to go further.

    The expanded e2e suite now runs automatically for every future feature release or bug fix. New code cannot be released unless the entire suite passes. That gives us a mandatory check against regressions before changes reach users — not a promise that tests can prove every future bug impossible, but a stronger release safeguard than we had before.

    That is not an email collection any password-manager founder hopes to receive. It is also exactly the kind of work a security reporting channel needs to make possible. Thank you, squ11z1, for taking the time to investigate and report these responsibly.

    I confirmed in my response that all three issues were fixed in production. This post explains what was wrong, what changed, and why the biggest lesson is about the boundaries around encryption — not the cipher itself.

    The distinction matters: these reports did not describe credential data being read or the encryption algorithms being broken. Credential fields remained stored as ciphertext. The findings concerned the controls around that ciphertext — including an invitation flaw that could expose the key needed to decrypt project secrets. We fixed those weaknesses; encrypted storage and protection of the keys both matter.

    The uncomfortable part: our invite flow contradicted our own model

    FIND-PW01 identified a plaintext invite_code stored in the pending_invites table. In the previous flow, the code and the invitee's email were inputs to Argon2id, which derived an Invite Key used to unwrap the Project Key.

    Our explanation said the code would be shared separately and that our systems would never hold both halves of the handoff. The implementation did not uphold that statement. With access to the stored code and the other required invitation material, someone with database or relevant log access could derive the Invite Key, recover the Project Key and decrypt that project's secrets.

    Argon2id wasn't broken. XChaCha20-Poly1305 wasn't broken. We had put material needed to unlock an encrypted key on the same side of the boundary as the encrypted key. A good primitive cannot rescue a protocol that hands over its inputs.

    The fix was to remove the invite_code column and replace code-derived Project Key wrapping with sealing to the invitee's public key. Opening the sealed key requires the corresponding private key; the handoff no longer relies on a plaintext invite code stored by our server.

    The distinction matters: recipient public-key sealing is the invitation handoff. Members' ongoing copies of the Project Key are still wrapped with their own Master Keys. And nobody needs the project owner's 3-word login phrase to join.

    We've updated the sharing explanation on our Security page to describe this change and acknowledge what the previous wording got wrong. Leaving a reassuring diagram online after its assumptions fail would be another mistake.

    Two-factor authentication has to be enforced where the data lives

    FIND-PW11 concerned Authenticator Assurance Level 2, or AAL2. In plain English: the server needs to distinguish a session that has passed the initial login factor (AAL1) from one that has completed the second factor (AAL2). Showing an MFA screen in the browser is not enough.

    The report showed that an AAL1 session could still access sensitive material, including the encrypted master_key_blob and KDF pepper. It also identified a path for unenrolling a user's TOTP factor without the required second-factor assurance. A stolen or leftover lower-assurance session could therefore bypass protections users expected MFA to provide.

    An encrypted Master Key blob is not a plaintext Master Key, and retrieving it does not by itself decrypt a vault. The report identified unauthorised access to encrypted key material and the KDF pepper, not a break in the cipher. Those access controls still needed correcting: encryption and second-factor enforcement are separate protections, and both should do their job.

    We hardened server-side second-factor enforcement, including Data API and row-level access policies that refuse AAL1 sessions for vault tables and KDF material, addressing the reported MFA bypass and TOTP unenrollment issue. The lesson is simple: controls need to reject the request at the server, not merely hide the button.

    A logged-out token must not be able to create a lasting way back in

    FIND-PW12 examined passkey registration. The report described programmatic WebAuthn attestations using ‘none’ and Ed25519, but the important security boundary was session validity: the registration endpoint accepted tokens from sessions that had already been revoked through logout.

    This allowed brief token access to become a persistent login method. The registered passkey could then obtain a full session and survive a password change. Account access is not, by itself, the same as decrypting credentials: local decryption still requires the relevant encryption keys. The issue was not that Ed25519 or ‘none’ attestation is inherently a vulnerability; it was that an invalid session could register a credential, and that credential could outlive the user's expected reset boundary.

    Passkey registration now requires a live, recent session — within 10 minutes — and a completed second factor. Password resets now invalidate all associated passkeys. Those are two separate protections: one controls creation of a new login method; the other removes it during a reset.

    What ‘zero-knowledge’ still has to earn

    Pwdly uses a machine-generated, random 3-word login phrase to unlock encryption locally. It isn't a recovery phrase or a password chosen from your dog's name, birthday and hometown. But the phrase alone does not make every surrounding flow safe. Project-key handoff, second-factor checks and passkey lifecycle all have to preserve their own boundaries.

    That is the part I want to be candid about. Security is not a badge you earn once by choosing libsodium and then stop thinking about. It's a collection of assumptions that need to survive contact with somebody who asks a better question than you did.

    These findings were serious. Acknowledging the researcher and shipping fixes does not erase the original mistakes. It does let us explain them without pretending that an implementation flaw is somehow somebody else's fault.

    The cryptography was not broken. These reports reinforced why protecting encryption keys is just as important as encrypting the data: if someone obtains the right key, they can decrypt the ciphertext without defeating the cipher. That is why we corrected the invitation handoff as well as the authentication controls, rather than treating encrypted storage alone as the whole answer.

    Thank you, squ11z1

    I'm grateful for the clear evidence and the responsible way these reports were raised. The handle squ11z1 now has a place in our public security acknowledgements, alongside the three findings: FIND-PW01, FIND-PW11 and FIND-PW12.

    You can read the public changelog and our updated security documentation for the concise version of the changes.

    If you find a security issue, please use our coordinated disclosure channel. We want to hear about it. A difficult email is considerably better than a flaw nobody tells us about.

    Secure by design, not by marketing. That line has to include being honest when the design — or its implementation — needs correcting.

    — David - Pwdly Founder

    #security-disclosure#zero-knowledge#project-key-sharing#mfa#passkeys#squ11z1

    Related reading

    No cookies. No tracking. No banners (almost).

    We use privacy-friendly, cookieless analytics (Umami) to count page views — no personal data, no profiling, no third-party scripts. Read more.