summaryrefslogtreecommitdiffstats
path: root/debian/patches/76-01-fix-string_is_ip_address-CVE-2023-42117-Bug-3031.patch
blob: cbddde0f369f09eb1967730957b0f399f4e55aa3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
From a95acb1c19c2e3600ef327c71318e33316d34440 Mon Sep 17 00:00:00 2001
From: "Heiko Schlittermann (HS12-RIPE)" <hs@schlittermann.de>
Date: Thu, 5 Oct 2023 22:49:57 +0200
Subject: [PATCH 1/3] fix: string_is_ip_address (CVE-2023-42117) Bug 3031

---
 doc/ChangeLog | 206 ++++++++++++++++++++++++++++++++++++++++++
 src/expand.c      |  14 ++-
 src/functions.h   |   1 +
 src/string.c      | 200 +++++++++++++++++++++-------------------
 4 files changed, 323 insertions(+), 98 deletions(-)

diff --git a/src/expand.c b/src/expand.c
index 36c9f423b..4986e4657 100644
--- a/src/expand.c
+++ b/src/expand.c
@@ -2646,17 +2646,25 @@ switch(cond_type = identify_operator(&s, &opname))
       }
     *yield = (Ustat(sub[0], &statbuf) == 0) == testfor;
     break;
 
     case ECOND_ISIP:
     case ECOND_ISIP4:
     case ECOND_ISIP6:
-    rc = string_is_ip_address(sub[0], NULL);
-    *yield = ((cond_type == ECOND_ISIP)? (rc != 0) :
-             (cond_type == ECOND_ISIP4)? (rc == 4) : (rc == 6)) == testfor;
+    {
+      const uschar *errp;
+      const uschar **errpp;
+      DEBUG(D_expand) errpp = &errp; else errpp = 0;
+      if (0 == (rc = string_is_ip_addressX(sub[0], NULL, errpp)))
+        DEBUG(D_expand) debug_printf("failed: %s\n", errp);
+
+      *yield = ( cond_type == ECOND_ISIP  ? rc != 0 :
+                 cond_type == ECOND_ISIP4 ? rc == 4 : rc == 6) == testfor;
+    }
+
     break;
 
     /* Various authentication tests - all optionally compiled */
 
     case ECOND_PAM:
     #ifdef SUPPORT_PAM
     rc = auth_call_pam(sub[0], &expand_string_message);
diff --git a/src/functions.h b/src/functions.h
index 224666cb1..3c8104d25 100644
--- a/src/functions.h
+++ b/src/functions.h
@@ -552,14 +552,15 @@ extern gstring *string_catn(gstring *, const uschar *, int) WARN_UNUSED_RESULT;
 extern int     string_compare_by_pointer(const void *, const void *);
 extern uschar *string_copy_dnsdomain(uschar *);
 extern uschar *string_copy_malloc(const uschar *);
 extern uschar *string_dequote(const uschar **);
 extern uschar *string_format_size(int, uschar *);
 extern int     string_interpret_escape(const uschar **);
 extern int     string_is_ip_address(const uschar *, int *);
+extern int     string_is_ip_addressX(const uschar *, int *, const uschar **);
 #ifdef SUPPORT_I18N
 extern BOOL    string_is_utf8(const uschar *);
 #endif
 extern const uschar *string_printing2(const uschar *, int);
 extern uschar *string_split_message(uschar *);
 extern uschar *string_unprinting(uschar *);
 #ifdef SUPPORT_I18N
diff --git a/src/string.c b/src/string.c
index a5161bb31..9aefc2b58 100644
--- a/src/string.c
+++ b/src/string.c
@@ -25,131 +25,141 @@ address (assuming HAVE_IPV6 is set). If a mask is permitted and one is present,
 and maskptr is not NULL, its offset is placed there.
 
 Arguments:
   s         a string
   maskptr   NULL if no mask is permitted to follow
             otherwise, points to an int where the offset of '/' is placed
             if there is no / followed by trailing digits, *maskptr is set 0
+  errp      NULL if no diagnostic information is required, and if the netmask
+            length should not be checked. Otherwise it is set pointing to a short
+            descriptive text.
 
 Returns:    0 if the string is not a textual representation of an IP address
             4 if it is an IPv4 address
             6 if it is an IPv6 address
-*/
 
+The legacy string_is_ip_address() function follows below.
+*/
 int
-string_is_ip_address(const uschar *s, int *maskptr)
-{
-int yield = 4;
+string_is_ip_addressX(const uschar *ip_addr, int *maskptr, const uschar **errp) {
+  struct addrinfo hints;
+  struct addrinfo *res;
 
-/* If an optional mask is permitted, check for it. If found, pass back the
-offset. */
+  uschar *slash, *percent;
 
-if (maskptr)
+  uschar *endp = 0;
+  long int mask = 0;
+  const uschar *addr = 0;
+
+  /* If there is a slash, but we didn't request a (optional) netmask,
+  we return failure, as we do if the mask isn't a pure numerical value,
+  or if it is negative. The actual length is checked later, once we know
+  the address family. */
+  if (slash = Ustrchr(ip_addr, '/'))
   {
-  const uschar *ss = s + Ustrlen(s);
-  *maskptr = 0;
-  if (s != ss && isdigit(*(--ss)))
+    if (!maskptr)
     {
-    while (ss > s && isdigit(ss[-1])) ss--;
-    if (ss > s && *(--ss) == '/') *maskptr = ss - s;
+      if (errp) *errp = "netmask found, but not requested";
+      return 0;
     }
-  }
-
-/* A colon anywhere in the string => IPv6 address */
-
-if (Ustrchr(s, ':') != NULL)
-  {
-  BOOL had_double_colon = FALSE;
-  BOOL v4end = FALSE;
-
-  yield = 6;
-
-  /* An IPv6 address must start with hex digit or double colon. A single
-  colon is invalid. */
-
-  if (*s == ':' && *(++s) != ':') return 0;
-
-  /* Now read up to 8 components consisting of up to 4 hex digits each. There
-  may be one and only one appearance of double colon, which implies any number
-  of binary zero bits. The number of preceding components is held in count. */
 
-  for (int count = 0; count < 8; count++)
+    uschar *rest;
+    mask = Ustrtol(slash+1, &rest, 10);
+    if (*rest || mask < 0)
     {
-    /* If the end of the string is reached before reading 8 components, the
-    address is valid provided a double colon has been read. This also applies
-    if we hit the / that introduces a mask or the % that introduces the
-    interface specifier (scope id) of a link-local address. */
-
-    if (*s == 0 || *s == '%' || *s == '/') return had_double_colon ? yield : 0;
-
-    /* If a component starts with an additional colon, we have hit a double
-    colon. This is permitted to appear once only, and counts as at least
-    one component. The final component may be of this form. */
-
-    if (*s == ':')
-      {
-      if (had_double_colon) return 0;
-      had_double_colon = TRUE;
-      s++;
-      continue;
-      }
-
-    /* If the remainder of the string contains a dot but no colons, we
-    can expect a trailing IPv4 address. This is valid if either there has
-    been no double-colon and this is the 7th component (with the IPv4 address
-    being the 7th & 8th components), OR if there has been a double-colon
-    and fewer than 6 components. */
-
-    if (Ustrchr(s, ':') == NULL && Ustrchr(s, '.') != NULL)
-      {
-      if ((!had_double_colon && count != 6) ||
-          (had_double_colon && count > 6)) return 0;
-      v4end = TRUE;
-      yield = 6;
-      break;
-      }
-
-    /* Check for at least one and not more than 4 hex digits for this
-    component. */
-
-    if (!isxdigit(*s++)) return 0;
-    if (isxdigit(*s) && isxdigit(*(++s)) && isxdigit(*(++s))) s++;
-
-    /* If the component is terminated by colon and there is more to
-    follow, skip over the colon. If there is no more to follow the address is
-    invalid. */
-
-    if (*s == ':' && *(++s) == 0) return 0;
+      if (errp) *errp = "netmask not numeric or <0";
+      return 0;
     }
 
-  /* If about to handle a trailing IPv4 address, drop through. Otherwise
-  all is well if we are at the end of the string or at the mask or at a percent
-  sign, which introduces the interface specifier (scope id) of a link local
-  address. */
+    *maskptr = slash - ip_addr;     /* offset of the slash */
+    endp = slash;
+  } else if (maskptr) *maskptr = 0; /* no slash found */
 
-  if (!v4end)
-    return (*s == 0 || *s == '%' ||
-           (*s == '/' && maskptr != NULL && *maskptr != 0))? yield : 0;
+  /* The interface-ID suffix (%<id>) is optional (for IPv6). If it
+  exists, we check it syntactically. Later, if we know the address
+  family is IPv4, we might reject it.
+  The interface-ID is mutually exclusive with the netmask, to the
+  best of my knowledge. */
+  if (percent = Ustrchr(ip_addr, '%'))
+  {
+    if (slash)
+    {
+      if (errp) *errp = "interface-ID and netmask are mutually exclusive";
+      return 0;
+    }
+    for (uschar *p = percent+1; *p; p++)
+        if (!isalnum(*p) && !ispunct(*p))
+        {
+          if (errp) *errp = "interface-ID must match [[:alnum:][:punct:]]";
+          return 0;
+        }
+    endp = percent;
   }
 
-/* Test for IPv4 address, which may be the tail-end of an IPv6 address. */
-
-for (int i = 0; i < 4; i++)
+  /* inet_pton() can't parse netmasks and interface IDs, so work on a shortened copy
+  allocated on the current stack */
+  if (endp) {
+    ptrdiff_t l = endp - ip_addr;
+    if (l > 255)
+    {
+      if (errp) *errp = "rudiculous long ip address string";
+      return 0;
+    }
+    addr = alloca(l+1); /* *BSD does not have strndupa() */
+    Ustrncpy((uschar *)addr, ip_addr, l);
+    ((uschar*)addr)[l] = '\0';
+  } else addr = ip_addr;
+
+  int af;
+  union { /* we do not need this, but inet_pton() needs a place for storage */
+    struct in_addr sa4;
+    struct in6_addr sa6;
+  } sa;
+
+  af = Ustrchr(addr, ':') ? AF_INET6 : AF_INET;
+  if (!inet_pton(af, addr, &sa))
   {
-  long n;
-  uschar * end;
-
-  if (i != 0 && *s++ != '.') return 0;
-  n = strtol(CCS s, CSS &end, 10);
-  if (n > 255 || n < 0 || end <= s || end > s+3) return 0;
-  s = end;
+    if (errp) *errp = af == AF_INET6 ? "IP address string not parsable as IPv6"
+                                     : "IP address string not parsable IPv4";
+    return 0;
   }
+  /* we do not check the values of the mask here, as
+  this is done on the callers side (but I don't understand why), so
+  actually I'd like to do it here, but it breaks at least 0002 */
+  switch (af)
+  {
+    case AF_INET6:
+        if (errp && mask > 128)
+        {
+          *errp = "IPv6 netmask value must not be >128";
+          return 0;
+        }
+        return 6;
+    case AF_INET:
+        if (percent)
+        {
+          if (errp) *errp = "IPv4 address string must not have an interface-ID";
+          return 0;
+        }
+        if (errp && mask > 32) {
+          *errp = "IPv4 netmask value must not be >32";
+          return 0;
+        }
+        return 4;
+    default:
+        if (errp) *errp = "unknown address family (should not happen)";
+        return 0;
+ }
+}
 
-return !*s || (*s == '/' && maskptr && *maskptr != 0) ? yield : 0;
+int
+string_is_ip_address(const uschar *ip_addr, int *maskptr) {
+  return string_is_ip_addressX(ip_addr, maskptr, 0);
 }
+
 #endif  /* COMPILE_UTILITY */
 
 
 /*************************************************
 *              Format message size               *
 *************************************************/
 
-- 
2.42.0