|
| 1 | +--- |
| 2 | +name: secure-email-validation |
| 3 | +description: Generate secure email address validation code. Enforces secure generation of code validating an email address. Invoke when writing any email address validation related code. |
| 4 | +allowed-tools: Read Grep Glob |
| 5 | +metadata: |
| 6 | + category: security |
| 7 | +--- |
| 8 | + |
| 9 | +# Secure Email Address Validation Code Generation Rules |
| 10 | + |
| 11 | +Apply **all** rules below when generating or reviewing any code related to validation of an email address. |
| 12 | + |
| 13 | +## 1. Email address validation (CRITICAL) |
| 14 | + |
| 15 | +- ALWAYS ensure that the email address is a valid email address, from a parser perspective, following RFCs on email addresses. |
| 16 | +- ALWAYS ensure that the email address is not using "Encoded-word" format. |
| 17 | +- ALWAYS ensure that the email address is not using comment format. |
| 18 | +- ALWAYS ensure that the email address is not using "Punycode" format. |
| 19 | +- ALWAYS ensure that the email address is not using UUCP style addresses. |
| 20 | +- ALWAYS ensure that the email address is not using address literals. |
| 21 | +- ALWAYS ensure that the email address is not using source routes. |
| 22 | +- ALWAYS ensure that the email address is not using the "percent hack". |
| 23 | +- ALWAYS enforce RFC 5321 length limits: local part ≤ 64 characters, domain ≤ 255 characters, total address ≤ 320 characters. |
| 24 | +- ALWAYS ensure that the email address does not contain newline or carriage-return characters (CRLF injection prevention). |
| 25 | +- ALWAYS ensure that the domain part contains at least one dot (reject single-label domains such as localhost or internal hostnames). |
| 26 | +- ALWAYS ensure that the local part is not a quoted string (i.e. not wrapped in double quotes). |
| 27 | + |
| 28 | +```java |
| 29 | +// BAD: No validation is applied |
| 30 | +import jakarta.mail.internet.InternetAddress; |
| 31 | + |
| 32 | +public static InternetAddress readEmailInsecure(String address) throws AddressException { |
| 33 | + return new InternetAddress(address); |
| 34 | +} |
| 35 | + |
| 36 | +// GOOD: All points are applied |
| 37 | +import jakarta.mail.internet.AddressException; |
| 38 | +import jakarta.mail.internet.InternetAddress; |
| 39 | + |
| 40 | +public static InternetAddress readEmailSecure(String email) throws AddressException { |
| 41 | + if (email == null || email.isBlank()) { |
| 42 | + throw new AddressException("Email address must not be null or blank"); |
| 43 | + } |
| 44 | + |
| 45 | + // 1. Parse and validate via RFC-compliant parser |
| 46 | + InternetAddress address = new InternetAddress(email, true); |
| 47 | + address.validate(); |
| 48 | + |
| 49 | + String raw = address.getAddress(); |
| 50 | + |
| 51 | + // 2. No encoded-word format: =?charset?encoding?text?= |
| 52 | + if (email.contains("=?") && email.contains("?=")) { |
| 53 | + throw new AddressException("Encoded-word format is not allowed: " + email); |
| 54 | + } |
| 55 | + |
| 56 | + // 3. No comment format: parentheses ( ) |
| 57 | + if (raw.contains("(") || raw.contains(")")) { |
| 58 | + throw new AddressException("Comment format is not allowed: " + email); |
| 59 | + } |
| 60 | + |
| 61 | + // 4. No Punycode format: xn-- in domain |
| 62 | + String domain = raw.substring(raw.lastIndexOf('@') + 1); |
| 63 | + if (domain.toLowerCase().contains("xn--")) { |
| 64 | + throw new AddressException("Punycode format is not allowed: " + email); |
| 65 | + } |
| 66 | + |
| 67 | + // 5. No UUCP style addresses: bang paths using ! |
| 68 | + if (raw.contains("!")) { |
| 69 | + throw new AddressException("UUCP-style addresses are not allowed: " + email); |
| 70 | + } |
| 71 | + |
| 72 | + // 6. No address literals: domain in square brackets [...] |
| 73 | + if (domain.startsWith("[") && domain.endsWith("]")) { |
| 74 | + throw new AddressException("Address literals are not allowed: " + email); |
| 75 | + } |
| 76 | + |
| 77 | + // 7. No source routes: input starts with @ (e.g. @relay:user@domain or @r1,@r2:user@domain) |
| 78 | + if (email.startsWith("@")) { |
| 79 | + throw new AddressException("Source routes are not allowed: " + email); |
| 80 | + } |
| 81 | + |
| 82 | + // 8. No percent hack: % in the local part |
| 83 | + String localPart = raw.substring(0, raw.lastIndexOf('@')); |
| 84 | + if (localPart.contains("%")) { |
| 85 | + throw new AddressException("Percent hack is not allowed: " + email); |
| 86 | + } |
| 87 | + |
| 88 | + // 9. RFC 5321 length limits |
| 89 | + if (localPart.length() > 64) { |
| 90 | + throw new AddressException("Local part exceeds 64 characters: " + email); |
| 91 | + } |
| 92 | + if (domain.length() > 255) { |
| 93 | + throw new AddressException("Domain exceeds 255 characters: " + email); |
| 94 | + } |
| 95 | + if (raw.length() > 320) { |
| 96 | + throw new AddressException("Email address exceeds 320 characters: " + email); |
| 97 | + } |
| 98 | + |
| 99 | + // 10. No CRLF injection: newline or carriage-return characters |
| 100 | + if (email.contains("\n") || email.contains("\r")) { |
| 101 | + throw new AddressException("Newline characters are not allowed: " + email); |
| 102 | + } |
| 103 | + |
| 104 | + // 11. No single-label domain: domain must contain at least one dot |
| 105 | + if (!domain.contains(".")) { |
| 106 | + throw new AddressException("Single-label domains are not allowed: " + email); |
| 107 | + } |
| 108 | + |
| 109 | + // 12. No quoted local part: local part must not be wrapped in double quotes |
| 110 | + if (localPart.startsWith("\"") && localPart.endsWith("\"")) { |
| 111 | + throw new AddressException("Quoted local parts are not allowed: " + email); |
| 112 | + } |
| 113 | + |
| 114 | + return address; |
| 115 | +} |
| 116 | +``` |
| 117 | + |
| 118 | +## 2. Output Checklist |
| 119 | + |
| 120 | +Before finalizing generated code, verify: |
| 121 | + |
| 122 | +- [ ] The email address is a valid email address, from a parser perspective, following RFCs on email addresses. |
| 123 | +- [ ] The email address is not using "Encoded-word" format. |
| 124 | +- [ ] The email address is not using comment format. |
| 125 | +- [ ] The email address is not using "Punycode" format. |
| 126 | +- [ ] The email address is not using UUCP style addresses. |
| 127 | +- [ ] The email address is not using address literals. |
| 128 | +- [ ] The email address is not using source routes. |
| 129 | +- [ ] The email address is not using the "percent hack". |
| 130 | +- [ ] The local part is ≤ 64 characters, the domain is ≤ 255 characters, and the total address is ≤ 320 characters (RFC 5321). |
| 131 | +- [ ] The email address does not contain newline or carriage-return characters. |
| 132 | +- [ ] The domain part contains at least one dot (no single-label domains). |
| 133 | +- [ ] The local part is not a quoted string (not wrapped in double quotes). |
| 134 | + |
| 135 | +## References |
| 136 | + |
| 137 | +- [Research on email address parser bypass from PortSwigger](https://portswigger.net/research/splitting-the-email-atom). |
| 138 | +- [MIME (Multipurpose Internet Mail Extensions) part three:Message Header extensions for Non-ASCII text from IETF](https://datatracker.ietf.org/doc/html/rfc2047). |
| 139 | +- [Syntax of encoded-words from IETF](https://datatracker.ietf.org/doc/html/rfc2047#section-2). |
| 140 | +- [Anatomy of an email address from Jochen Topf](https://www.jochentopf.com/email/address.html). |
| 141 | +- [Email address from Wikipedia](https://en.wikipedia.org/wiki/Email_address). |
| 142 | +- [RFC 5321 - Simple Mail Transfer Protocol (size limits) from IETF](https://datatracker.ietf.org/doc/html/rfc5321#section-4.5.3). |
0 commit comments