Security

My First Security Report on Lumberroom Cloud

October 1, 2026

On 29 September, I received my first security report for Lumberroom Cloud. It claimed critical account takeover through the OAuth registration endpoint. The reproduction showed anonymous registration, an attacker-chosen callback, and a sign-in screen that omitted that destination.

I traced the flow with AI reviewers. The takeover claim skipped consent, but the first screen gave an unverified client name too much weight. I changed the screens to make the destination visible before approval.

Why registration is open

Dynamic Client Registration (DCR) lets an app register with an OAuth server. When you connect Claude to Lumberroom, it registers its name and callback address, then sends you to sign in and approve access.

You can connect your AI to stored facts and preferences without asking me to register each app manually. Access requires your approval and follows the permissions you choose.

The registrant chooses the name. Anyone can type “Claude”, so the screen needs to help you recognise the connection you started.

What the report raised, and what it proved

The reproduction contained three accurate observations and a conclusion I could not reproduce:

  • Anonymous registration: confirmed. Anyone could introduce a client, which initially had no grant.
  • An attacker-chosen callback: confirmed for accepted redirect addresses. The client could request that an approved authorization code go to its own server.
  • No destination before sign-in: confirmed. The first screen repeated the supplied name. The later consent page already displayed the full callback address.
  • Account takeover merely by signing in: unsupported. The user still had to press Allow. Consent was bound to the authorization request and account session; exchanging the resulting code also required the client's PKCE proof.

What the phishing attempt looks like

An attacker registers an app named Claude with a callback at attacker.example.com. They send you an authorization link, perhaps claiming you need to reconnect your memory. You follow it, sign in, see “Give Claude access”, and press Allow because you recognise the name.

The attacker receives the code and can exchange it using the PKCE proof from their request. Their app gets the permissions you approved, potentially including reading and writing stored memories.

The earlier consent page showed the callback, but someone reading only the headline could miss it. The destination needed more prominence.

What changed before sign-in

I added the destination before sign-in. This comparison is reconstructed from the page code:

Earlier: “Claude” is asking to read and write your memory. Continue to sign in.

Hardened: “Claude” is asking to read and write your memory. If you allow it, it receives its code at attacker.example.com. This client registered itself; nobody vouched for the name.

The screen also says that closing it grants nothing and that consent comes next.

At consent, I placed “Codes go to…” directly below the headline. A self-registered client borrowing a known name and using an unfamiliar destination gets a red mismatch warning.

Try the trust decision

Same name. Different destination.

Play the registrant. Change the details and watch the consent cues respond.

Illustrative consent screenCheck the destination

Give Claude access?

Codes go to attacker.example

This client calls itself Claude, but attacker.example is not a recognised Claude destination. The name was chosen by its registrant.

Nothing has been granted. Closing the real consent page before approval grants nothing.

A borrowed name can look familiar. The destination gives you another fact to check before approving.

Local illustration. No registration or authorization happens here. Always check the destination and start connections from your own account.

A familiar service still needs context

An AI reviewer caught a mistake in my first pass: I had removed warnings for recognised domains. But claude.ai hosts many people's accounts. A familiar destination could still belong to a connection someone else started.

I kept a reminder: approve only if you just pressed Connect in your own account.

Smaller hardening

I stripped invisible and control characters from client names to reduce spoofing tricks. I also blocked framing, so another page cannot embed the consent screen and disguise the Allow button.

Registration now rejects public IP callbacks and the authorization server's own origin. A later review preserved local clients, including apps on the same host using a different port.

For private app schemes, the screen says the code goes to whichever installed app claims that scheme. A website-like string inside that URL can mislead.

These changes also address the potential consent-phishing weakness in the OSS engine. I merged them in Lumberroom OSS PR #87, then imported them into Cloud. Self-hosted users get the same hardening.

Timeline

  • 29 September 2026: received and investigated the report; confirmed the missing destination before sign-in and traced the consent requirement.
  • 29 September 2026: merged the hardening into the open-source engine and cloud service, then deployed it.
  • After deployment that day: repeated the registration and token probes and checked the live spoofed-client consent screen without approving it. A forged authorization code was rejected at the token endpoint.

Thanks

To the anonymous reporter: thank you for testing Lumberroom and sending clear reproduction steps. If you find anything else, I would welcome another report.

If you find a bug in Lumberroom, email [email protected].