Skip to content

Commit 5ef2c6d

Browse files
committed
fix(siwe): reject a comma in the scheme
`[a-zA-Z0-9+-.]` reads `+-.` as a character range, so it matches `+ , - .`. The intended set is the RFC 3986 one, `+`, `-` and `.`, so a comma was allowed in a SIWE scheme by both sides: createSiweMessage({ ..., scheme: 'ht,tps' }) // accepted parseSiweMessage('ht,tps://example.com ...') // scheme: 'ht,tps' createSiweMessage validates the scheme specifically to enforce RFC 3986 3.1, and its own error message says so, so accepting a comma is not intentional leniency. siwe/utils.ts already writes the same class correctly as `[a-z0-9+\\-.]` in isUri. Escape the hyphen in both. The domain class is left as it is: RFC 3986 reg-name does allow a comma via sub-delims, and parseSiweMessage is deliberately lenient there.
1 parent 200fbe1 commit 5ef2c6d

5 files changed

Lines changed: 62 additions & 2 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"viem": patch
3+
---
4+
5+
Fixed the SIWE scheme check accepting a comma. `[a-zA-Z0-9+-.]` reads `+-.` as a character range (`+` `,` `-` `.`), so `createSiweMessage` did not reject a scheme such as `ht,tps` despite validating against RFC 3986, and `parseSiweMessage` returned it as `scheme`.

src/utils/siwe/createSiweMessage.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,30 @@ test('behavior: invalid scheme', () => {
348348
`)
349349
})
350350

351+
test('behavior: invalid scheme (comma)', () => {
352+
// `[a-zA-Z0-9+-.]` reads `+-.` as a range (+ , - .), so a comma slipped past the
353+
// RFC 3986 scheme check this error is about.
354+
expect(() =>
355+
createSiweMessage({ ...message, scheme: 'ht,tps' }),
356+
).toThrowErrorMatchingInlineSnapshot(`
357+
[SiweInvalidMessageFieldError: Invalid Sign-In with Ethereum message field "scheme".
358+
359+
- Scheme must be an RFC 3986 URI scheme.
360+
- See https://www.rfc-editor.org/rfc/rfc3986#section-3.1
361+
362+
Provided value: ht,tps
363+
364+
Version: viem@x.y.z]
365+
`)
366+
})
367+
368+
test('behavior: valid scheme punctuation is still accepted', () => {
369+
// RFC 3986 §3.1: ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
370+
for (const scheme of ['https', 'a+b', 'a-b', 'a.b']) {
371+
expect(() => createSiweMessage({ ...message, scheme })).not.toThrow()
372+
}
373+
})
374+
351375
test('behavior: invalid statement', () => {
352376
expect(() =>
353377
createSiweMessage({ ...message, statement: 'foo\nbar' }),

src/utils/siwe/createSiweMessage.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,4 +175,4 @@ const ipRegex =
175175
/^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(:[0-9]{1,5})?$/
176176
const localhostRegex = /^localhost(:[0-9]{1,5})?$/
177177
const nonceRegex = /^[a-zA-Z0-9]{8,}$/
178-
const schemeRegex = /^([a-zA-Z][a-zA-Z0-9+-.]*)$/
178+
const schemeRegex = /^([a-zA-Z][a-zA-Z0-9+\-.]*)$/

src/utils/siwe/parseSiweMessage.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,37 @@ Issued At: 2023-02-01T00:00:00.000Z`
4141
expect(parsed.scheme).toMatchInlineSnapshot(`"https"`)
4242
})
4343

44+
test('behavior: scheme with a comma is not a scheme', () => {
45+
// `[a-zA-Z0-9+-.]` reads `+-.` as a range, which also matches `,`. RFC 3986 §3.1
46+
// allows only `+`, `-` and `.` after the leading letter, so this is not a scheme
47+
// and the prefix does not parse.
48+
const message = `ht,tps://example.com wants you to sign in with your Ethereum account:
49+
0xA0Cf798816D4b9b9866b5330EEa46a18382f251e
50+
51+
URI: https://example.com/path
52+
Version: 1
53+
Chain ID: 1
54+
Nonce: foobarbaz
55+
Issued At: 2023-02-01T00:00:00.000Z`
56+
const parsed = parseSiweMessage(message)
57+
expect(parsed.scheme).toBeUndefined()
58+
expect(parsed.domain).toBeUndefined()
59+
})
60+
61+
test('behavior: schemes using the punctuation RFC 3986 does allow', () => {
62+
for (const scheme of ['https', 'a+b', 'a-b', 'a.b']) {
63+
const message = `${scheme}://example.com wants you to sign in with your Ethereum account:
64+
0xA0Cf798816D4b9b9866b5330EEa46a18382f251e
65+
66+
URI: https://example.com/path
67+
Version: 1
68+
Chain ID: 1
69+
Nonce: foobarbaz
70+
Issued At: 2023-02-01T00:00:00.000Z`
71+
expect(parseSiweMessage(message).scheme).toBe(scheme)
72+
}
73+
})
74+
4475
test('behavior: domain with port', () => {
4576
const message = `example.com:8080 wants you to sign in with your Ethereum account:
4677
0xA0Cf798816D4b9b9866b5330EEa46a18382f251e

src/utils/siwe/parseSiweMessage.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ export function parseSiweMessage(
6565

6666
// https://regexr.com/80gdj
6767
const prefixRegex =
68-
/^(?:(?<scheme>[a-zA-Z][a-zA-Z0-9+-.]*):\/\/)?(?<domain>[a-zA-Z0-9+-.]*(?::[0-9]{1,5})?) (?:wants you to sign in with your Ethereum account:\n)(?<address>0x[a-fA-F0-9]{40})\n\n(?:(?<statement>.*)\n\n)?/
68+
/^(?:(?<scheme>[a-zA-Z][a-zA-Z0-9+\-.]*):\/\/)?(?<domain>[a-zA-Z0-9+-.]*(?::[0-9]{1,5})?) (?:wants you to sign in with your Ethereum account:\n)(?<address>0x[a-fA-F0-9]{40})\n\n(?:(?<statement>.*)\n\n)?/
6969

7070
// https://regexr.com/80gf9
7171
const suffixRegex =

0 commit comments

Comments
 (0)