@@ -49,6 +49,16 @@ public static ValidationResult validateSyntax(ACMEIdentifier id) {
4949 * _policy_ is checked elsewhere.
5050 */
5151 private static ValidationResult validateSyntaxDNS (String value ) {
52+
53+ // dns identifiers must be hostnames, not IP literals (RFC 8555).
54+ // Accepting IPs enables SSRF via HTTP-01 (see dogtagpki/pki#5407).
55+ if (isIpLiteral (value )) {
56+ ACMEError error = new ACMEError ();
57+ error .setType ("urn:ietf:params:acme:error:malformed" );
58+ error .setDetail ("DNS identifier must not be an IP address: " + value );
59+ return ValidationResult .fail (error );
60+ }
61+
5262 String [] labels = value .split ("\\ ." );
5363
5464 if (labels .length < 1 ) {
@@ -109,6 +119,113 @@ private static ValidationResult validateSyntaxDNS(String value) {
109119 return ValidationResult .ok ();
110120 }
111121
122+ /**
123+ * True if value is an IPv4/IPv6 literal (optional [], optional leading "*.").
124+ * Wildcard is stripped because newOrder removes "*." before HTTP-01.
125+ * Parsing is lexical only (no InetAddress/DNS) and covers the decimal IPv4
126+ * forms Java accepts (d, d.d, d.d.d, d.d.d.d).
127+ */
128+ static boolean isIpLiteral (String value ) {
129+ String host = value .startsWith ("*." ) ? value .substring (2 ) : value ;
130+ if (host .startsWith ("[" ) && host .endsWith ("]" ) && host .length () > 2 ) {
131+ host = host .substring (1 , host .length () - 1 );
132+ }
133+ if (host .indexOf (':' ) >= 0 ) {
134+ return isIpv6Literal (host );
135+ }
136+ return isIpv4Literal (host );
137+ }
138+
139+ /**
140+ * Match Java Inet4Address decimal IPv4 text forms (historical inet_aton style):
141+ * d (32-bit), d.d (8+24), d.d.d (8+8+16), or d.d.d.d (four bytes).
142+ * Digits only; leading zeros allowed. See java.net.Inet4Address.
143+ */
144+ private static boolean isIpv4Literal (String host ) {
145+ if (host .isEmpty ()) {
146+ return false ;
147+ }
148+ for (int i = 0 ; i < host .length (); i ++) {
149+ char c = host .charAt (i );
150+ if (c != '.' && (c < '0' || c > '9' )) {
151+ return false ;
152+ }
153+ }
154+ String [] parts = host .split ("\\ ." , -1 );
155+ if (parts .length < 1 || parts .length > 4 ) {
156+ return false ;
157+ }
158+ for (String part : parts ) {
159+ if (part .isEmpty ()) {
160+ return false ;
161+ }
162+ }
163+ switch (parts .length ) {
164+ case 1 : // d -> 32-bit value (e.g. 2130706433)
165+ return inRange (parts [0 ], 0xFFFFFFFFL );
166+ case 2 : // d.d -> 8-bit + 24-bit (e.g. 127.1)
167+ return inRange (parts [0 ], 255 ) && inRange (parts [1 ], 0xFFFFFFL );
168+ case 3 : // d.d.d -> 8-bit + 8-bit + 16-bit (e.g. 127.0.1)
169+ return inRange (parts [0 ], 255 ) && inRange (parts [1 ], 255 )
170+ && inRange (parts [2 ], 0xFFFFL );
171+ case 4 : // d.d.d.d -> four 8-bit octets (e.g. 127.0.0.1)
172+ return inRange (parts [0 ], 255 ) && inRange (parts [1 ], 255 )
173+ && inRange (parts [2 ], 255 ) && inRange (parts [3 ], 255 );
174+ default :
175+ return false ;
176+ }
177+ }
178+
179+ private static boolean inRange (String part , long max ) {
180+ try {
181+ long value = Long .parseLong (part );
182+ return value >= 0 && value <= max ;
183+ } catch (NumberFormatException e ) {
184+ return false ;
185+ }
186+ }
187+
188+ /**
189+ * Simplified lexical IPv6 text check (RFC 4291 / RFC 5952 style):
190+ * hex digits and ':', at most one "::" compression, optional dotted IPv4
191+ * tail for mapped forms (e.g. ::ffff:127.0.0.1). Not a full RFC parser.
192+ */
193+ private static boolean isIpv6Literal (String host ) {
194+ int colons = 0 ;
195+ int doubleColon = 0 ;
196+ for (int i = 0 ; i < host .length (); i ++) {
197+ char c = host .charAt (i );
198+ if (c == ':' ) {
199+ colons ++;
200+ // "::" may appear at most once
201+ if (i + 1 < host .length () && host .charAt (i + 1 ) == ':' ) {
202+ doubleColon ++;
203+ i ++;
204+ colons ++;
205+ }
206+ } else if (!isHex (c ) && c != '.' ) {
207+ // '.' only for IPv4-mapped / IPv4-compatible tails
208+ return false ;
209+ }
210+ }
211+ // At least one ":" pair worth of colons; reject multiple "::"
212+ if (colons < 2 || doubleColon > 1 ) {
213+ return false ;
214+ }
215+ // If an IPv4 tail is present after the last ':', validate it as IPv4
216+ int lastColon = host .lastIndexOf (':' );
217+ if (lastColon >= 0 && host .indexOf ('.' , lastColon ) >= 0 ) {
218+ return isIpv4Literal (host .substring (lastColon + 1 ));
219+ }
220+ return true ;
221+ }
222+
223+ private static boolean isHex (char c ) {
224+ return c >= '0' && c <= '9'
225+ || c >= 'a' && c <= 'f'
226+ || c >= 'A' && c <= 'F' ;
227+ }
228+
112229 /* helper predicates for "dns" identifier validity */
113230 private static boolean isLetter (char c ) { return c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z' ; }
114231 private static boolean isDigit (char c ) { return c >= '0' && c <= '9' ; }
0 commit comments