Skip to content

fix(base-account): parse the SIWE nonce correctly and bind verification to the app domain - #1794

Open
Dusk1e wants to merge 1 commit into
base:masterfrom
Dusk1e:fix/siwe-domain-binding-and-nonce-parsing
Open

fix(base-account): parse the SIWE nonce correctly and bind verification to the app domain#1794
Dusk1e wants to merge 1 commit into
base:masterfrom
Dusk1e:fix/siwe-domain-binding-and-nonce-parsing

Conversation

@Dusk1e

@Dusk1e Dusk1e commented Aug 10, 2026

Copy link
Copy Markdown

Correction (2026-08-11): an earlier version of this description said issue #1502 had no PR. That was wrong — five PRs already target this file. I missed them because I listed open PRs with a limit that truncated before reaching May. The section at the bottom now sets out how this PR relates to them, and I'm happy to close this in favour of one of them.

The Authenticate users guide ships an Express example that rejects every valid login, and a verification step that accepts signatures a user produced on someone else's site. Both are in code a developer is meant to copy into a production auth endpoint.

1. The nonce is never extracted, so /auth/verify always returns 400

docs/base-account/guides/authenticate-users.mdx:207 reads the nonce with:

const nonce = message.match(/at (\w{32})$/)?.[1];

In an EIP-4361 message the nonce is on its own Nonce: <value> line and Issued At: comes after it, so nothing sits at the end of the string for $ to anchor to. The match is always null, the guard below it fires, and the endpoint answers 400 Invalid or reused nonce for a completely valid login.

Using the message format this repo documents at docs/base-account/reference/core/capabilities/signInWithEthereum.mdx:128:

const message =
  "localhost:3000 wants you to sign in with your Ethereum account:\n" +
  "0x1234567890123456789012345678901234567890\n\n" +
  "Sign in with Ethereum to the app.\n\n" +
  "URI: http://localhost:3000\nVersion: 1\nChain ID: 8453\n" +
  "Nonce: abc123def456\nIssued At: 2024-01-15T10:30:00Z";

message.match(/at (\w{32})$/)?.[1];   // undefined  <- the guide
message.match(/Nonce: (\w+)/)?.[1];   // 'abc123def456'

Two other pages in this repo already read the nonce correctly — signInWithEthereum.mdx:216 and framework-integrations/privy/authentication.mdx:227 — so this guide was the only one reading it incorrectly. This PR uses viem's parseSiweMessage rather than another regex.

2. Verification is not bound to your domain

Both snippets verified with:

const valid = await client.verifyMessage({ address, message, signature });

verifyMessage only answers "does this signature match this message". It never looks at the domain field, so a SIWE signature a user was asked to produce on evil.com verifies against your endpoint as well. EIP-4361 requires the relying party to check domain against its own host (issue #1502).

Swapped to verifySiweMessage, which validates domain, nonce and expiry before checking the signature. It still routes through verifyHash internally, so ERC-6492 and ERC-1271 signatures from not-yet-deployed Base Accounts keep verifying exactly as before — the guide's own note at line 62 stays accurate.

Verification

I ran the proposed server code unmodified against a local node, signing real messages with viem:

Case Before After
Legitimate login 400 Invalid or reused nonce 200 { ok: true }
Replay of the same signature 400 400 Invalid or reused nonce
Signature minted for evil.com 200 { ok: true } 401 Invalid signature
Nonce the server never issued 400 400 Invalid or reused nonce

Row 1 is the functional bug, row 3 is the security one. Rows 2 and 4 confirm the existing replay protection still behaves.

Relationship to the existing PRs on this file

Five PRs already touch this guide, all opened in May and all still unreviewed. For whoever triages this, here is what each actually changes, checked against viem 2.55.13:

PR Fixes the nonce regex Adds domain binding Note
#1503 no yes Destructures const { isValid } = await verifySiweMessage(...). That function returns a boolean, so isValid is undefined and the endpoint would answer 401 for every login.
#1542 yes yes Closest to this PR. Moves nonces.delete(nonce) to after verification, so a nonce survives a failed attempt and two concurrent requests can both pass the has() check.
#1553 no yes Manual parseSiweMessage(...).domain comparison; keeps verifyMessage.
#1559 no yes Passes nonce to verifySiweMessage, but that nonce still comes from the broken regex.
#1560 no no Separate issue (Math.random() nonces), no overlap.

#1542 covers the same two defects this PR does and is the older submission. The differences here are that the nonce stays an atomic check-and-consume (nonces.delete as the guard, as in the current guide), the domain constant does not default to localhost:3000, and the behaviour is backed by the run above. If you would rather take #1542, this can be closed — the important thing is that one of them lands, because the guide is currently broken for every reader who copies it.

Notes

@cb-heimdall

Copy link
Copy Markdown
Collaborator

🟡 Heimdall Review Status

Requirement Status More Info
Reviews 🟡 0/1
Denominator calculation
Show calculation
1 if user is bot 0
1 if user is external 0
2 if repo is sensitive 0
From .codeflow.yml 1
Additional review requirements
Show calculation
Max 0
0
From CODEOWNERS 0
Global minimum 0
Max 1
1
1 if commit is unverified 0
Sum 1

…on to the app domain

The "Authenticate users" guide ships a server example that cannot
authenticate anyone, and a verification step that accepts signatures
minted for other sites.

Nonce extraction used `/at (\w{32})$/`. In an EIP-4361 message the nonce
sits on a `Nonce: <value>` line with `Issued At:` after it, so the regex
never matches and `/auth/verify` answers 400 "Invalid or reused nonce"
for every valid login. Two other pages in this repo already read the
nonce correctly, so the guide was the odd one out. Switching to viem's
`parseSiweMessage` removes the regex entirely.

Verification called `client.verifyMessage`, which only checks that the
signature matches the message. It does not look at the `domain` field,
so a signature a user produced on another site verifies against this
endpoint too. EIP-4361 requires the relying party to check `domain`.
`verifySiweMessage` checks domain, nonce and expiry, and still routes
through `verifyHash`, so ERC-6492 and ERC-1271 signatures from
undeployed Base Accounts keep working.

Verified against a local node with real signatures: a legitimate login
returns 200, a replayed signature 400, a signature minted for another
domain 401, and an unissued nonce 400. Before the change the first case
returned 400 and the third returned 200.

Fixes base#1502
@Dusk1e
Dusk1e force-pushed the fix/siwe-domain-binding-and-nonce-parsing branch from c5f7105 to 32bddba Compare August 10, 2026 23:56
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.

2 participants