Skip to content

Commit 494dd01

Browse files
committed
fix(utils): reject CR/LF in CACAO SIWE single-line fields
EIP-4361 / CAIP-122 single-line fields (domain, statement, aud, version, nonce, iat, exp, nbf, requestId) must not embed carriage returns or newlines, or a caller can forge later message lines. Fail closed before formatting. Parity with walletconnect-monorepo#7311 / utils#280.
1 parent 1f28261 commit 494dd01

2 files changed

Lines changed: 84 additions & 0 deletions

File tree

Sources/WalletConnectUtils/SIWE/SIWEFromCacaoPayloadFormatter.swift

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ public typealias SignWithXFormatter = SIWEFromCacaoPayloadFormatter
2121
/// Previously SIWEFromCacaoPayloadFormatter - now supports Sign with X (CAIP-122)
2222
public struct SIWEFromCacaoPayloadFormatter: SignWithXFormatting {
2323

24+
public enum Errors: Error, Equatable {
25+
case invalidSingleLineField(String)
26+
}
27+
2428
public init() {}
2529

2630
public func formatMessage(from payload: CacaoPayload, includeRecapInTheStatement: Bool) throws -> String {
@@ -30,6 +34,27 @@ public struct SIWEFromCacaoPayloadFormatter: SignWithXFormatting {
3034
let chainId = account.reference
3135
let namespace = account.namespace
3236

37+
// EIP-4361 / CAIP-122 single-line fields must not embed CR/LF, or a
38+
// caller can forge later lines (URI, Nonce, …). Parity with
39+
// walletconnect-monorepo #7311 / walletconnect-utils #280.
40+
try assertSingleLine(payload.domain, field: "domain")
41+
try assertSingleLine(payload.aud, field: "aud")
42+
try assertSingleLine(payload.version, field: "version")
43+
try assertSingleLine(payload.nonce, field: "nonce")
44+
try assertSingleLine(payload.iat, field: "iat")
45+
if let statement = payload.statement {
46+
try assertSingleLine(statement, field: "statement")
47+
}
48+
if let exp = payload.exp {
49+
try assertSingleLine(exp, field: "exp")
50+
}
51+
if let nbf = payload.nbf {
52+
try assertSingleLine(nbf, field: "nbf")
53+
}
54+
if let requestId = payload.requestId {
55+
try assertSingleLine(requestId, field: "requestId")
56+
}
57+
3358
// Determine chain-specific account type for CAIP-122
3459
let accountType = getAccountType(for: namespace)
3560

@@ -51,6 +76,12 @@ public struct SIWEFromCacaoPayloadFormatter: SignWithXFormatting {
5176
return formattedMessage
5277
}
5378

79+
private func assertSingleLine(_ value: String, field: String) throws {
80+
if value.contains("\r") || value.contains("\n") {
81+
throw Errors.invalidSingleLineField(field)
82+
}
83+
}
84+
5485
// CAIP-122: Map chain namespace to account type
5586
private func getAccountType(for namespace: String) -> String {
5687
switch namespace {

Tests/WalletConnectSignTests/SIWEFromCacaoPayloadFormatterTests.swift

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,4 +289,57 @@ class SIWEFromCacaoPayloadFormatterTests: XCTestCase {
289289
let message = try sut.formatMessage(from: cacaoPayload)
290290
XCTAssertEqual(message, expectedMessage)
291291
}
292+
293+
// MARK: - EIP-4361 single-line field integrity
294+
295+
func testRejectsDomainWithEmbeddedNewline() throws {
296+
let payload = try smugglingPayload(domain: "service.invalid\nURI: https://evil.invalid")
297+
XCTAssertThrowsError(try sut.formatMessage(from: payload)) { error in
298+
XCTAssertEqual(
299+
error as? SIWEFromCacaoPayloadFormatter.Errors,
300+
.invalidSingleLineField("domain")
301+
)
302+
}
303+
}
304+
305+
func testRejectsStatementWithEmbeddedCarriageReturn() throws {
306+
let payload = try smugglingPayload(statement: "I accept\rURI: https://evil.invalid")
307+
XCTAssertThrowsError(try sut.formatMessage(from: payload)) { error in
308+
XCTAssertEqual(
309+
error as? SIWEFromCacaoPayloadFormatter.Errors,
310+
.invalidSingleLineField("statement")
311+
)
312+
}
313+
}
314+
315+
func testRejectsAudWithEmbeddedNewline() throws {
316+
let payload = try smugglingPayload(aud: "https://service.invalid/login\nNonce: forged")
317+
XCTAssertThrowsError(try sut.formatMessage(from: payload)) { error in
318+
XCTAssertEqual(
319+
error as? SIWEFromCacaoPayloadFormatter.Errors,
320+
.invalidSingleLineField("aud")
321+
)
322+
}
323+
}
324+
325+
private func smugglingPayload(
326+
domain: String = "service.invalid",
327+
aud: String = "https://service.invalid/login",
328+
statement: String? = "I accept the ServiceOrg Terms of Service: https://service.invalid/tos"
329+
) throws -> CacaoPayload {
330+
let account = Account.stub()
331+
return CacaoPayload(
332+
iss: account.did,
333+
domain: domain,
334+
aud: aud,
335+
version: "1",
336+
nonce: "32891756",
337+
iat: "2021-09-30T16:25:24Z",
338+
nbf: nil,
339+
exp: nil,
340+
statement: statement,
341+
requestId: nil,
342+
resources: nil
343+
)
344+
}
292345
}

0 commit comments

Comments
 (0)