~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0

App-password sessions can sign and submit PLC operations

Open
Open chadtmiller.com opened this issue 0 comments

What happens

An app-password session (scope com.atproto.appPass) can call all of these:

  • com.atproto.identity.requestPlcOperationSignature
  • com.atproto.identity.signPlcOperation
  • com.atproto.identity.submitPlcOperation
  • com.atproto.identity.getRecommendedDidCredentials

Each route in packages/core/src/handlers/xrpc-identity.js (around l. 788–806) is auth: 'required', and its handler checks only that auth is present, never its scope. A PLC operation can replace an account's rotation keys, signing key and PDS endpoint. So anyone holding a leaked app password can take the identity over for good, and revoking the app password afterwards doesn't undo it.

The email confirmation doesn't stop this. canConfirmByEmail() is false on a PDS with no mailer or no verified address, and then requestPlcOperationSignature answers {} and signPlcOperation asks for no token.

This was seen on grain-pds (grain.social) on 2026-11-01:

  • createSession with an app password returned scope com.atproto.appPass.
  • requestPlcOperationSignature answered 200 {}.
  • signPlcOperation returned a signed operation.
  • submitPlcOperation answered 200.

That operation was deliberate: it added a recovery key. It shows the path is open.

What the reference PDS does

@atproto/pds refuses app passwords on these methods: they need a full access session (its accessFull auth). Clients such as goat, and the Bluesky app's account settings, sign in with the account password for PLC changes for that reason.

Fix

Refuse an app-password scope on the four routes, privileged app passwords included. Return 403 InvalidToken, as requireFullSession in xrpc-server.js already does for app-password management. Ideally the identity handlers would share that helper, or auth: 'full' would become a route option, so the next privileged route can't forget it.

com.atproto.identity.updateHandle is worth checking too. The reference PDS lets an app password change the handle only when it is privileged.

Tests should show that an app-password session gets 403 from each route and a password session still works.