1 /*        $NetBSD: valid_hostname.c,v 1.4 2025/02/25 19:15:52 christos Exp $    */
2 
3 /*++
4 /* NAME
5 /*        valid_hostname 3
6 /* SUMMARY
7 /*        network name validation
8 /* SYNOPSIS
9 /*        #include <valid_hostname.h>
10 /*
11 /*        int       valid_hostname(name, flags)
12 /*        const char *name;
13 /*        int       flags;
14 /*
15 /*        int       valid_hostaddr(addr, gripe)
16 /*        const char *addr;
17 /*        int       gripe;
18 /*
19 /*        int       valid_ipv4_hostaddr(addr, gripe)
20 /*        const char *addr;
21 /*        int       gripe;
22 /*
23 /*        int       valid_ipv6_hostaddr(addr, gripe)
24 /*        const char *addr;
25 /*        int       gripe;
26 /*
27 /*        int       valid_hostport(port, gripe)
28 /*        const char *port;
29 /*        int       gripe;
30 /* DESCRIPTION
31 /*        valid_hostname() scrutinizes a hostname: the name should
32 /*        be no longer than VALID_HOSTNAME_LEN characters, should
33 /*        contain only letters, digits, dots and hyphens, no adjacent
34 /*        dots, no leading or trailing dots or hyphens, no labels
35 /*        longer than VALID_LABEL_LEN characters, and it should not
36 /*        be all numeric.
37 /*        The flags argument is the bit-wise or of zero or more of
38 /*        DO_GRIPE or DO_WILDCARD (the latter allows the "*." name
39 /*        prefix, which is rare but valid in some DNS responses and
40 /*        queries).
41 /*
42 /*        valid_hostaddr() requires that the input is a valid string
43 /*        representation of an IPv4 or IPv6 network address as
44 /*        described next.
45 /*
46 /*        valid_ipv4_hostaddr() and valid_ipv6_hostaddr() implement
47 /*        protocol-specific address syntax checks. A valid IPv4
48 /*        address is in dotted-quad decimal form. A valid IPv6 address
49 /*        has 16-bit hexadecimal fields separated by ":", and does not
50 /*        include the RFC 2821 style "IPv6:" prefix.
51 /*
52 /*        These routines operate silently unless the gripe parameter
53 /*        specifies a non-zero value. The macros DO_GRIPE and DONT_GRIPE
54 /*        provide suitable constants.
55 /*
56 /*        valid_hostport() requires that the input is a valid string
57 /*        representation of a TCP or UDP port number.
58 /* BUGS
59 /*        valid_hostmumble() does not guarantee that string lengths
60 /*        fit the buffer sizes defined in myaddrinfo(3h).
61 /* DIAGNOSTICS
62 /*        All functions return zero if they disagree with the input.
63 /* SEE ALSO
64 /*        RFC 952, RFC 1123, RFC 1035, RFC 2373.
65 /* LICENSE
66 /* .ad
67 /* .fi
68 /*        The Secure Mailer license must be distributed with this software.
69 /* AUTHOR(S)
70 /*        Wietse Venema
71 /*        IBM T.J. Watson Research
72 /*        P.O. Box 704
73 /*        Yorktown Heights, NY 10598, USA
74 /*--*/
75 
76 /* System library. */
77 
78 #include <sys_defs.h>
79 #include <string.h>
80 #include <ctype.h>
81 #include <stdlib.h>
82 
83 /* Utility library. */
84 
85 #include "msg.h"
86 #include "mymalloc.h"
87 #include "stringops.h"
88 #include "valid_hostname.h"
89 
90 /* valid_hostname - screen out bad hostnames */
91 
valid_hostname(const char * name,int flags)92 int     valid_hostname(const char *name, int flags)
93 {
94     const char *myname = "valid_hostname";
95     const char *cp;
96     int     label_length = 0;
97     int     label_count = 0;
98     int     non_numeric = 0;
99     int     ch;
100     int     gripe = flags & DO_GRIPE;
101 
102     /*
103      * Trivial cases first.
104      */
105     if (*name == 0) {
106           if (gripe)
107               msg_warn("%s: empty hostname", myname);
108           return (0);
109     }
110 
111     /*
112      * Find bad characters or label lengths. Find adjacent delimiters.
113      */
114     for (cp = name; (ch = *(unsigned char *) cp) != 0; cp++) {
115           if (ISALNUM(ch) || ch == '_') {                   /* grr.. */
116               if (label_length == 0)
117                     label_count++;
118               label_length++;
119               if (label_length > VALID_LABEL_LEN) {
120                     if (gripe)
121                         msg_warn("%s: hostname label too long: %.100s", myname, name);
122                     return (0);
123               }
124               if (!ISDIGIT(ch))
125                     non_numeric = 1;
126           } else if ((flags & DO_WILDCARD) && ch == '*') {
127               if (label_length || label_count || (cp[1] && cp[1] != '.')) {
128                     if (gripe)
129                         msg_warn("%s: '*' can be the first label only: %.100s", myname, name);
130                     return (0);
131               }
132               label_count++;
133               label_length++;
134               non_numeric = 1;
135           } else if (ch == '.') {
136               if (label_length == 0 || cp[1] == 0) {
137                     if (gripe)
138                         msg_warn("%s: misplaced delimiter: %.100s", myname, name);
139                     return (0);
140               }
141               label_length = 0;
142           } else if (ch == '-') {
143               non_numeric = 1;
144               label_length++;
145               if (label_length == 1 || cp[1] == 0 || cp[1] == '.') {
146                     if (gripe)
147                         msg_warn("%s: misplaced hyphen: %.100s", myname, name);
148                     return (0);
149               }
150           }
151 #ifdef SLOPPY_VALID_HOSTNAME
152           else if (ch == ':' && valid_ipv6_hostaddr(name, DONT_GRIPE)) {
153               non_numeric = 0;
154               break;
155           }
156 #endif
157           else {
158               if (gripe)
159                     msg_warn("%s: invalid character %d(decimal): %.100s",
160                                myname, ch, name);
161               return (0);
162           }
163     }
164 
165     if (non_numeric == 0) {
166           if (gripe)
167               msg_warn("%s: numeric hostname: %.100s", myname, name);
168 #ifndef SLOPPY_VALID_HOSTNAME
169           return (0);
170 #endif
171     }
172     if (cp - name > VALID_HOSTNAME_LEN) {
173           if (gripe)
174               msg_warn("%s: bad length %d for %.100s...",
175                          myname, (int) (cp - name), name);
176           return (0);
177     }
178     return (1);
179 }
180 
181 /* valid_hostaddr - verify numerical address syntax */
182 
valid_hostaddr(const char * addr,int gripe)183 int     valid_hostaddr(const char *addr, int gripe)
184 {
185     const char *myname = "valid_hostaddr";
186 
187     /*
188      * Trivial cases first.
189      */
190     if (*addr == 0) {
191           if (gripe)
192               msg_warn("%s: empty address", myname);
193           return (0);
194     }
195 
196     /*
197      * Protocol-dependent processing next.
198      */
199     if (strchr(addr, ':') != 0)
200           return (valid_ipv6_hostaddr(addr, gripe));
201     else
202           return (valid_ipv4_hostaddr(addr, gripe));
203 }
204 
205 /* valid_ipv4_hostaddr - test dotted quad string for correctness */
206 
valid_ipv4_hostaddr(const char * addr,int gripe)207 int     valid_ipv4_hostaddr(const char *addr, int gripe)
208 {
209     const char *cp;
210     const char *myname = "valid_ipv4_hostaddr";
211     int     in_byte = 0;
212     int     byte_count = 0;
213     int     byte_val = 0;
214     int     ch;
215 
216 #define BYTES_NEEDED          4
217 
218     /*
219      * Scary code to avoid sscanf() overflow nasties.
220      *
221      * This routine is called by valid_ipv6_hostaddr(). It must not call that
222      * routine, to avoid deadly recursion.
223      */
224     for (cp = addr; (ch = *(unsigned const char *) cp) != 0; cp++) {
225           if (ISDIGIT(ch)) {
226               if (in_byte == 0) {
227                     in_byte = 1;
228                     byte_val = 0;
229                     byte_count++;
230               }
231               byte_val *= 10;
232               byte_val += ch - '0';
233               if (byte_val > 255) {
234                     if (gripe)
235                         msg_warn("%s: invalid octet value: %.100s", myname, addr);
236                     return (0);
237               }
238           } else if (ch == '.') {
239               if (in_byte == 0 || cp[1] == 0) {
240                     if (gripe)
241                         msg_warn("%s: misplaced dot: %.100s", myname, addr);
242                     return (0);
243               }
244               /* XXX Allow 0.0.0.0 but not 0.1.2.3 */
245               if (byte_count == 1 && byte_val == 0 && addr[strspn(addr, "0.")]) {
246                     if (gripe)
247                         msg_warn("%s: bad initial octet value: %.100s", myname, addr);
248                     return (0);
249               }
250               in_byte = 0;
251           } else {
252               if (gripe)
253                     msg_warn("%s: invalid character %d(decimal): %.100s",
254                                myname, ch, addr);
255               return (0);
256           }
257     }
258 
259     if (byte_count != BYTES_NEEDED) {
260           if (gripe)
261               msg_warn("%s: invalid octet count: %.100s", myname, addr);
262           return (0);
263     }
264     return (1);
265 }
266 
267 /* valid_ipv6_hostaddr - validate IPv6 address syntax */
268 
valid_ipv6_hostaddr(const char * addr,int gripe)269 int     valid_ipv6_hostaddr(const char *addr, int gripe)
270 {
271     const char *myname = "valid_ipv6_hostaddr";
272     int     null_field = 0;
273     int     field = 0;
274     unsigned char *cp = (unsigned char *) addr;
275     int     len = 0;
276 
277     /*
278      * FIX 200501 The IPv6 patch validated syntax with getaddrinfo(), but I
279      * am not confident that everyone's system library routines are robust
280      * enough, like buffer overflow free. Remember, the valid_hostmumble()
281      * routines are meant to protect Postfix against malformed information in
282      * data received from the network.
283      *
284      * We require eight-field hex addresses of the form 0:1:2:3:4:5:6:7,
285      * 0:1:2:3:4:5:6a.6b.7c.7d, or some :: compressed version of the same.
286      *
287      * Note: the character position is advanced inside the loop. I have added
288      * comments to show why we can't get stuck.
289      */
290     for (;;) {
291           switch (*cp) {
292           case 0:
293               /* Terminate the loop. */
294               if (field < 2) {
295                     if (gripe)
296                         msg_warn("%s: too few `:' in IPv6 address: %.100s",
297                                    myname, addr);
298                     return (0);
299               } else if (len == 0 && null_field != field - 1) {
300                     if (gripe)
301                         msg_warn("%s: bad null last field in IPv6 address: %.100s",
302                                    myname, addr);
303                     return (0);
304               } else
305                     return (1);
306           case '.':
307               /* Terminate the loop. */
308               if (field < 2 || field > 6) {
309                     if (gripe)
310                         msg_warn("%s: malformed IPv4-in-IPv6 address: %.100s",
311                                    myname, addr);
312                     return (0);
313               } else
314                     /* NOT: valid_hostaddr(). Avoid recursion. */
315                     return (valid_ipv4_hostaddr((char *) cp - len, gripe));
316           case ':':
317               /* Advance by exactly 1 character position or terminate. */
318               if (field == 0 && len == 0 && ISALNUM(cp[1])) {
319                     if (gripe)
320                         msg_warn("%s: bad null first field in IPv6 address: %.100s",
321                                    myname, addr);
322                     return (0);
323               }
324               field++;
325               if (field > 7) {
326                     if (gripe)
327                         msg_warn("%s: too many `:' in IPv6 address: %.100s",
328                                    myname, addr);
329                     return (0);
330               }
331               cp++;
332               len = 0;
333               if (*cp == ':') {
334                     if (null_field > 0) {
335                         if (gripe)
336                               msg_warn("%s: too many `::' in IPv6 address: %.100s",
337                                          myname, addr);
338                         return (0);
339                     }
340                     null_field = field;
341               }
342               break;
343           default:
344               /* Advance by at least 1 character position or terminate. */
345               len = strspn((char *) cp, "0123456789abcdefABCDEF");
346               if (len /* - strspn((char *) cp, "0") */ > 4) {
347                     if (gripe)
348                         msg_warn("%s: malformed IPv6 address: %.100s",
349                                    myname, addr);
350                     return (0);
351               }
352               if (len <= 0) {
353                     if (gripe)
354                         msg_warn("%s: invalid character %d(decimal) in IPv6 address: %.100s",
355                                    myname, *cp, addr);
356                     return (0);
357               }
358               cp += len;
359               break;
360           }
361     }
362 }
363 
364 /* valid_hostport - validate numeric port */
365 
valid_hostport(const char * str,int gripe)366 int     valid_hostport(const char *str, int gripe)
367 {
368     const char *myname = "valid_hostport";
369     int     port;
370 
371     if (str[0] == '0' && str[1] != 0) {
372           if (gripe)
373               msg_warn("%s: leading zero in port number: %.100s", myname, str);
374           return (0);
375     }
376     if (alldig(str) == 0) {
377           if (gripe)
378               msg_warn("%s: non-numeric port number: %.100s", myname, str);
379           return (0);
380     }
381     if (strlen(str) > strlen("65535")
382           || (port = atoi(str)) > 65535 || port < 0) {
383           if (gripe)
384               msg_warn("%s: out-of-range port number: %.100s", myname, str);
385           return (0);
386     }
387     return (1);
388 }
389 
390 #ifdef TEST
391 
392  /*
393   * Test program - reads hostnames from stdin, reports invalid hostnames to
394   * stderr.
395   */
396 #include <stdlib.h>
397 
398 #include "vstring.h"
399 #include "vstream.h"
400 #include "vstring_vstream.h"
401 #include "msg_vstream.h"
402 
main(int unused_argc,char ** argv)403 int     main(int unused_argc, char **argv)
404 {
405     VSTRING *buffer = vstring_alloc(1);
406 
407     msg_vstream_init(argv[0], VSTREAM_ERR);
408     msg_verbose = 1;
409 
410     while (vstring_fgets_nonl(buffer, VSTREAM_IN)) {
411           msg_info("testing: \"%s\"", vstring_str(buffer));
412           valid_hostname(vstring_str(buffer), DO_GRIPE | DO_WILDCARD);
413           if (strchr(vstring_str(buffer), '*') == 0)
414               valid_hostaddr(vstring_str(buffer), DO_GRIPE);
415     }
416     exit(0);
417 }
418 
419 #endif
420