Skip to content

Commit 7a09a16

Browse files
ekscryptoclaude
andcommitted
Security: reject Plane 1-3 supplementary noncharacters in local parts
Unicode §23.7 permanently reserves U+nFFFE and U+nFFFF for every plane (n=1..16) as noncharacters forbidden in open interchange. The explicit scalar guards covered Planes 4-13 (U+40000-U+DFFFF) and SSP/PUA (U+E0000+), but missed the six noncharacters in Planes 1-3: U+1FFFE/U+1FFFF (SMP), U+2FFFE/U+2FFFF (SIP), U+3FFFE/U+3FFFF (TIP). Add explicit value checks for all six in extractDotAtom and the extractQuotedString inline guard. Fix the incorrect XCTAssertNotNil for U+3FFFF (a §23.7 noncharacter, not an assigned scalar). Add testSupplementaryNonCharactersPlanes1Through3RejectedInLocalPart to cover all six values plus boundary confirmation that U+1FFFD, U+2FFFD, and U+3FFFD remain accepted. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 52c845a commit 7a09a16

2 files changed

Lines changed: 61 additions & 3 deletions

File tree

Sources/SwiftEmailValidator/EmailSyntaxValidator.swift

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -412,11 +412,17 @@ public final class EmailSyntaxValidator {
412412
// Reject supplementary-plane ranges excluded from allowedCharacterSet via
413413
// explicit scalar guards (Foundation CharacterSet.contains() is reliable for
414414
// individual scalars, but belt-and-suspenders for these security-sensitive ranges):
415+
// U+1FFFE-U+1FFFF: Plane 1 (SMP) noncharacters — Unicode §23.7 permanently reserved
416+
// U+2FFFE-U+2FFFF: Plane 2 (SIP) noncharacters — Unicode §23.7 permanently reserved
417+
// U+3FFFE-U+3FFFF: Plane 3 (TIP) noncharacters — Unicode §23.7 permanently reserved
415418
// U+40000-U+DFFFF: Planes 4-13 (entirely unassigned in Unicode)
416419
// U+E0000-U+EFFFF: entire SSP (Tags block, unassigned gaps, VS Supplement)
417420
// U+F0000-U+10FFFF: Supplementary PUA-A/B
418421
&& !label.unicodeScalars.contains(where: {
419-
($0.value >= 0x40000 && $0.value <= 0xDFFFF) // Planes 4-13 (entirely unassigned)
422+
($0.value == 0x1FFFE || $0.value == 0x1FFFF) // Plane 1 noncharacters (§23.7)
423+
|| ($0.value == 0x2FFFE || $0.value == 0x2FFFF) // Plane 2 noncharacters (§23.7)
424+
|| ($0.value == 0x3FFFE || $0.value == 0x3FFFF) // Plane 3 noncharacters (§23.7)
425+
|| ($0.value >= 0x40000 && $0.value <= 0xDFFFF) // Planes 4-13 (entirely unassigned)
420426
|| ($0.value >= 0xE0000 && $0.value <= 0x10FFFF) // Entire SSP + PUA-A/B
421427
})
422428
})
@@ -465,6 +471,9 @@ public final class EmailSyntaxValidator {
465471
(s.value == 0x2028 || s.value == 0x2029) || // U+2028 Line Sep, U+2029 Para Sep
466472
(s.value >= 0xFDD0 && s.value <= 0xFDEF) || // U+FDD0-U+FDEF Unicode noncharacters
467473
(s.value == 0xFFFE || s.value == 0xFFFF) || // U+FFFE/U+FFFF BMP noncharacters
474+
(s.value == 0x1FFFE || s.value == 0x1FFFF) || // Plane 1 noncharacters (§23.7)
475+
(s.value == 0x2FFFE || s.value == 0x2FFFF) || // Plane 2 noncharacters (§23.7)
476+
(s.value == 0x3FFFE || s.value == 0x3FFFF) || // Plane 3 noncharacters (§23.7)
468477
(s.value >= 0x40000 && s.value <= 0xDFFFF) || // Planes 4-13 (entirely unassigned)
469478
(s.value >= 0xE0000 && s.value <= 0x10FFFF) // Entire SSP (Tags, unassigned gaps, VS Sup) + PUA-A/B
470479
}) else {

Tests/SwiftEmailValidatorTests/EmailSyntaxValidatorTests.swift

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1133,10 +1133,59 @@ final class EmailSyntaxValidatorTests: XCTestCase {
11331133
compatibility: .unicode, domainValidator: permissive),
11341134
"U+1F600 (emoji, SMP Plane 1) must still be accepted"
11351135
)
1136-
XCTAssertNotNil(
1136+
// U+3FFFF is a Unicode §23.7 noncharacter (U+nFFFF pattern, n=3) and must be rejected.
1137+
// It was previously incorrectly asserted as accepted.
1138+
XCTAssertNil(
11371139
EmailSyntaxValidator.mailbox(from: "user\u{3FFFF}@site.com",
11381140
compatibility: .unicode, domainValidator: permissive),
1139-
"U+3FFFF (last scalar of Plane 3 / TIP, assigned range boundary) must still be accepted"
1141+
"U+3FFFF (Plane 3 noncharacter per §23.7) must be rejected"
1142+
)
1143+
}
1144+
1145+
// MARK: - Plane 1–3 supplementary noncharacter exclusions
1146+
1147+
func testSupplementaryNonCharactersPlanes1Through3RejectedInLocalPart() {
1148+
// Unicode §23.7: for every plane n=1..16, U+nFFFE and U+nFFFF are permanently reserved
1149+
// noncharacters "forbidden for use in open interchange of Unicode text data."
1150+
// Planes 1 (SMP), 2 (SIP), and 3 (TIP) each have two such noncharacters that were
1151+
// previously not covered by the Planes 4-13 or SSP/PUA scalar guards.
1152+
let permissive: (String) -> Bool = { _ in true }
1153+
let plane123NonChars: [(Unicode.Scalar, String)] = [
1154+
(Unicode.Scalar(0x1FFFE)!, "U+1FFFE (Plane 1 / SMP noncharacter)"),
1155+
(Unicode.Scalar(0x1FFFF)!, "U+1FFFF (Plane 1 / SMP noncharacter)"),
1156+
(Unicode.Scalar(0x2FFFE)!, "U+2FFFE (Plane 2 / SIP noncharacter)"),
1157+
(Unicode.Scalar(0x2FFFF)!, "U+2FFFF (Plane 2 / SIP noncharacter)"),
1158+
(Unicode.Scalar(0x3FFFE)!, "U+3FFFE (Plane 3 / TIP noncharacter)"),
1159+
(Unicode.Scalar(0x3FFFF)!, "U+3FFFF (Plane 3 / TIP noncharacter)"),
1160+
]
1161+
for (scalar, name) in plane123NonChars {
1162+
let char = String(scalar)
1163+
XCTAssertNil(
1164+
EmailSyntaxValidator.mailbox(from: "user\(char)@site.com",
1165+
compatibility: .unicode, domainValidator: permissive),
1166+
"\(name) must be rejected in dot-atom local part (Unicode §23.7 noncharacter)"
1167+
)
1168+
XCTAssertNil(
1169+
EmailSyntaxValidator.mailbox(from: "\"user\(char)\"@site.com",
1170+
compatibility: .unicode, domainValidator: permissive),
1171+
"\(name) must be rejected in quoted-string local part (Unicode §23.7 noncharacter)"
1172+
)
1173+
}
1174+
// Confirm adjacent assigned scalars remain accepted
1175+
XCTAssertNotNil(
1176+
EmailSyntaxValidator.mailbox(from: "user\u{1FFFD}@site.com",
1177+
compatibility: .unicode, domainValidator: permissive),
1178+
"U+1FFFD (last assigned Plane 1 scalar, Linear B Syllabary) must remain accepted"
1179+
)
1180+
XCTAssertNotNil(
1181+
EmailSyntaxValidator.mailbox(from: "user\u{2FFFD}@site.com",
1182+
compatibility: .unicode, domainValidator: permissive),
1183+
"U+2FFFD (last assigned Plane 2 scalar) must remain accepted"
1184+
)
1185+
XCTAssertNotNil(
1186+
EmailSyntaxValidator.mailbox(from: "user\u{3FFFD}@site.com",
1187+
compatibility: .unicode, domainValidator: permissive),
1188+
"U+3FFFD (last assigned Plane 3 scalar) must remain accepted"
11401189
)
11411190
}
11421191
}

0 commit comments

Comments
 (0)