Skip to content

Unicode security hardening & RFC 2047 fixes

Choose a tag to compare

@ekscrypto ekscrypto released this 23 Apr 00:15
· 64 commits to main since this release

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 >= 0x20 guard 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. extractQuotedString now uses unicodeScalars.allSatisfy instead of rangeOfCharacter (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 extractQuotedString matches 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 the IPv6: tag, and non-standard literal types (e.g. [SMTP:…]) are rejected, with regression tests added.

Fixed

  • CharacterSet construction order. Foundation's CharacterSet has a bug where calling .subtracting() on a set containing supplementary Unicode planes corrupts the supplementary-plane bitmap. All exclusion sets are now subtracted before supplementaryPlanes is 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 .ascii mode. Unicode U-labels are now rejected when compatibility == .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 the domainValidator closure.

Documentation

  • mailbox(from:) and correctlyFormatted(_:) doc comments corrected. Prior comments referenced a strategy: parameter and ValidationStrategy type that do not exist. Documentation now matches the actual options: parameter and Options enum.

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.md refreshed.
  • 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.