Unicode security hardening & RFC 2047 fixes
This release consolidates a series of Unicode and RFC compliance fixes that hardened the validator against spoofing, parser-confusion, and out-of-spec inputs. Earlier 1.1.x versions accept characters and encoded sequences that should be rejected; users should upgrade.
Security
Unicode spoofing prevention (local part)
- Zs-category space characters rejected. U+00A0 (NO-BREAK SPACE), U+1680 (OGHAM SPACE MARK), U+2000–U+200A (EN QUAD … HAIR SPACE), U+202F (NARROW NO-BREAK SPACE), U+205F (MEDIUM MATHEMATICAL SPACE), U+3000 (IDEOGRAPHIC SPACE) are visually indistinguishable from U+0020 in most fonts and could be used to register lookalike accounts.
- Reserved format character U+2065 rejected. The previous block (U+2060–U+2064) left U+2065 reachable; the range is now U+2060–U+2065.
- Plane 1–3 supplementary noncharacters rejected. U+1FFFE/U+1FFFF, U+2FFFE/U+2FFFF, U+3FFFE/U+3FFFF (Unicode §23.7 permanently reserved noncharacters).
- Planes 4–13 (U+40000–U+DFFFF) rejected. Entirely unassigned in Unicode; should never appear in interchange.
- Full Supplementary Special-purpose Plane and Supplementary PUA rejected. U+E0000–U+10FFFF blocked as a single guarded range, covering Tags, Variation Selectors Supplement, and Private Use Areas A/B.
- Variation Selectors rejected. U+FE00–U+FE0F are invisible combining characters that produce no glyph (same spoofing risk as ZWJ/ZWNJ).
- U+FDD0–U+FDEF and U+FFFE/U+FFFF rejected. BMP §23.7 permanently reserved noncharacters.
RFC 2047 decoder hardening
- DEL (0x7F) rejected from Q-encoded content. The prior
value >= 0x20guard admitted 0x7F. - C1 control bytes (0x80–0x9F) rejected from Q-encoded ISO-8859-1/2 content. Previously decoded to U+0080–U+009F C1 controls, which RFC 5198 §2 forbids in network interchange.
- 75-character encoded-word limit enforced per RFC 2047 §2 (the prior limit allowed 76).
- Underscore-as-space decoding in Q encoding per RFC 2047 §4.2.
Quoted-string parser hardening
- Per-scalar character-set check.
extractQuotedStringnow usesunicodeScalars.allSatisfyinstead ofrangeOfCharacter(which only inspected the first scalar of a grapheme cluster), preventing security-excluded scalars from slipping through as combining elements. - Escaped quoted-pair restricted to a single ASCII scalar per RFC 5321 (
quoted-pair = "\" (VCHAR / WSP)). - Inline scalar guard in
extractQuotedStringmatches the dot-atom guard, ensuring identical security posture across both local-part forms.
IPv6 / address-literal hardening
- Zone identifiers rejected (e.g.
fe80::1%eth0) per RFC 5321 §4.1.3. - Empty
IPv6:literal, non-IPv6 strings after theIPv6:tag, and non-standard literal types (e.g.[SMTP:…]) are rejected, with regression tests added.
Fixed
- CharacterSet construction order. Foundation's
CharacterSethas a bug where calling.subtracting()on a set containing supplementary Unicode planes corrupts the supplementary-plane bitmap. All exclusion sets are now subtracted beforesupplementaryPlanesis added via.union(). - Domain octet limits enforced (RFC 1035 §2.3.4): per-label ≤63 octets, total domain ≤253 octets — measured in UTF-8 bytes, not character count.
- Total email length enforced at 254 UTF-8 bytes (RFC 5321 §4.5.3.1.3).
- Quoted-string local part length enforced at 64 UTF-8 bytes.
- ASCII-only domain labels in
.asciimode. Unicode U-labels are now rejected whencompatibility == .ascii; Punycode (xn--…) labels remain accepted. - Source-route addresses rejected (
@relay.host:user@domain). - Empty host, double-
@, empty domain labels, leading/trailing dots and hyphens in domain labels are rejected regardless of thedomainValidatorclosure.
Documentation
mailbox(from:)andcorrectlyFormatted(_:)doc comments corrected. Prior comments referenced astrategy:parameter andValidationStrategytype that do not exist. Documentation now matches the actualoptions:parameter andOptionsenum.
Tests
- Test count grew from 77 to 125 (all passing).
- New thematic coverage for: Zs space spoofing, supplementary noncharacters, Variation Selectors, the Unicode Tags block, source routes, address-literal edge cases, byte-vs-character length boundaries, RFC 2047 75-char limit and C1 rejection.
Release hygiene
SECURITY.mdrefreshed.- README install snippet bumped to 1.2.0.
- CI bumped:
actions/checkout@v4,codecov-action@v4,swift-coverage-action@v4,upload-sarif@v3,ubuntu-latest.