Skip to content

Commit 7b3adc6

Browse files
committed
imparse: reject HTTP methods as initial tag only
1 parent 1e17543 commit 7b3adc6

File tree

5 files changed

+95
-11
lines changed

5 files changed

+95
-11
lines changed

cunit/imparse.testc

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -78,36 +78,50 @@ static void test_isatom(void)
7878
static void test_istag(void)
7979
{
8080
const char hello[] = "hello";
81+
const char *const forbidden_http_methods[] = {
82+
"ACL", "BIND", "LOCK", "MKCALENDAR", "MKCOL", "PATCH", "POST",
83+
"PROPFIND", "PROPPATCH", "PUT", "REPORT", "SEARCH", "UNBIND",
84+
};
85+
const int n_forbidden_http_methods = sizeof(forbidden_http_methods)
86+
/ sizeof(forbidden_http_methods[0]);
8187
char tmp[2] = { 0 };
8288
int i;
8389

8490
/* if it's not an atom, it definitely can't be a tag */
8591
for (i = 0; i <= 0xff; i++) {
8692
tmp[0] = (char) i;
8793
if (!imparse_isatom(tmp))
88-
CU_ASSERT_EQUAL(imparse_istag(tmp), 0);
94+
CU_ASSERT_EQUAL(imparse_istag(tmp, 0), 0);
8995
}
9096

9197
/* there used to be an explicit (albeit redundant) check for this case */
9298
tmp[0] = '*';
93-
CU_ASSERT_EQUAL(imparse_istag(tmp), 0);
99+
CU_ASSERT_EQUAL(imparse_istag(tmp, 0), 0);
94100

95101
/* "." tag idiomatic when telnetting to imap server, don't break that */
96102
tmp[0] = '.';
97-
CU_ASSERT_NOT_EQUAL(imparse_istag(tmp), 0);
103+
CU_ASSERT_NOT_EQUAL(imparse_istag(tmp, 0), 0);
98104

99105
/* angle brackets exploitable in cross-protocol reflection attacks */
100106
tmp[0] = '<';
101-
CU_ASSERT_EQUAL(imparse_istag(tmp), 0);
107+
CU_ASSERT_EQUAL(imparse_istag(tmp, 0), 0);
102108
tmp[0] = '>';
103-
CU_ASSERT_EQUAL(imparse_istag(tmp), 0);
109+
CU_ASSERT_EQUAL(imparse_istag(tmp, 0), 0);
104110

105111
/* colon character in tag suggests confused HTTP client */
106112
tmp[0] = ':';
107-
CU_ASSERT_EQUAL(imparse_istag(tmp), 0);
113+
CU_ASSERT_EQUAL(imparse_istag(tmp, 0), 0);
108114

109115
/* make sure it doesn't just always return zero... */
110-
CU_ASSERT_NOT_EQUAL(imparse_istag(hello), 0);
116+
CU_ASSERT_NOT_EQUAL(imparse_istag(hello, 0), 0);
117+
118+
for (i = 0; i < n_forbidden_http_methods; i++) {
119+
/* reject forbidden HTTP method used as tag on the first command */
120+
CU_ASSERT_EQUAL(imparse_istag(forbidden_http_methods[i], 0), 0);
121+
122+
/* but permit during an established session */
123+
CU_ASSERT_NOT_EQUAL(imparse_istag(forbidden_http_methods[i], 1), 0);
124+
}
111125
}
112126

113127
static void test_parse_range(void)

imap/httpd.c

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -510,7 +510,11 @@ static struct sasl_callback mysasl_cb[] = {
510510
{ SASL_CB_LIST_END, NULL, NULL }
511511
};
512512

513-
/* Array of HTTP methods known by our server. */
513+
/* Array of HTTP methods known by our server.
514+
* Keep this up to date with reject_http_method_tag() in imparse.c
515+
* XXX Would be nice if this table were in libcyrus somewhere rather than
516+
* XXX directly in httpd, so that imparse could use it directly.
517+
*/
514518
const struct known_meth_t http_methods[] = {
515519
{ "ACL", 0, CYRUS_HTTP_ACL_TOTAL },
516520
{ "BIND", 0, CYRUS_HTTP_BIND_TOTAL },

imap/imapd.c

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1493,6 +1493,7 @@ static void cmdloop(void)
14931493
int readonly = config_getswitch(IMAPOPT_READONLY);
14941494
int syntax_errors = 0;
14951495
const int syntax_errors_limit = 10; /* XXX make this configurable? */
1496+
unsigned command_count = 0;
14961497

14971498
prot_printf(imapd_out, "* OK [CAPABILITY");
14981499
capa_response(CAPA_PREAUTH);
@@ -1599,7 +1600,7 @@ static void cmdloop(void)
15991600
}
16001601
goto done;
16011602
}
1602-
if (c != ' ' || !imparse_istag(tag.s)) {
1603+
if (c != ' ' || !imparse_istag(tag.s, command_count)) {
16031604
syntax_errors ++;
16041605
prot_printf(imapd_out, "* BAD Invalid tag\r\n");
16051606
eatline(imapd_in, c);
@@ -1618,6 +1619,10 @@ static void cmdloop(void)
16181619
xstrncpy(cmdname, cmd.s, 99);
16191620
cmd.s[0] = toupper((unsigned char) cmd.s[0]);
16201621

1622+
/* that looks like a command, count it (but saturate, not overflow) */
1623+
if (command_count != UINT_MAX)
1624+
command_count ++;
1625+
16211626
if (config_getswitch(IMAPOPT_CHATTY))
16221627
syslog(LOG_NOTICE, "command: %s %s", tag.s, cmd.s);
16231628

@@ -2312,6 +2317,9 @@ static void cmdloop(void)
23122317
}
23132318
cmd_starttls(tag.s, 0);
23142319

2320+
/* reset command count, the real imap session starts here */
2321+
command_count = 0;
2322+
23152323
prometheus_increment(CYRUS_IMAP_STARTTLS_TOTAL);
23162324
continue;
23172325
}

lib/imparse.c

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
#include <config.h>
4444
#include <errno.h>
4545
#include <stdio.h>
46+
#include <string.h>
4647

4748
#include "imparse.h"
4849
#include "util.h"
@@ -216,10 +217,64 @@ EXPORTED int imparse_isnumber(const char *s)
216217
return 1;
217218
}
218219

220+
static int reject_http_method_tag(const char *s)
221+
{
222+
/* Don't like tags that match HTTP methods that accept a request body!
223+
* Keep this up to date with http_methods[] in httpd.c
224+
* and test_istag() in imparse.testc
225+
*/
226+
switch (s[0]) {
227+
case 'A':
228+
if (0 == strcmp(s, "ACL"))
229+
return 1;
230+
break;
231+
case 'B':
232+
if (0 == strcmp(s, "BIND"))
233+
return 1;
234+
break;
235+
case 'L':
236+
if (0 == strcmp(s, "LOCK"))
237+
return 1;
238+
break;
239+
case 'M':
240+
if (0 == strcmp(s, "MKCALENDAR"))
241+
return 1;
242+
else if (0 == strcmp(s, "MKCOL"))
243+
return 1;
244+
break;
245+
case 'P':
246+
if (0 == strcmp(s, "PATCH"))
247+
return 1;
248+
else if (0 == strcmp(s, "POST"))
249+
return 1;
250+
else if (0 == strcmp(s, "PROPFIND"))
251+
return 1;
252+
else if (0 == strcmp(s, "PROPPATCH"))
253+
return 1;
254+
else if (0 == strcmp(s, "PUT"))
255+
return 1;
256+
break;
257+
case 'R':
258+
if (0 == strcmp(s, "REPORT"))
259+
return 1;
260+
break;
261+
case 'S':
262+
if (0 == strcmp(s, "SEARCH"))
263+
return 1;
264+
break;
265+
case 'U':
266+
if (0 == strcmp(s, "UNBIND"))
267+
return 1;
268+
break;
269+
}
270+
271+
return 0;
272+
}
273+
219274
/*
220275
* Return nonzero if we like 's' as an IMAP tag
221276
*/
222-
EXPORTED int imparse_istag(const char *s)
277+
EXPORTED int imparse_istag(const char *s, unsigned command_count)
223278
{
224279
static const char reject[] = {
225280
/* 0 1 2 3 4 5 6 7 8 9 A B C D E F */
@@ -242,6 +297,9 @@ EXPORTED int imparse_istag(const char *s)
242297
return 0;
243298
}
244299

300+
if (command_count == 0 && reject_http_method_tag(s))
301+
return 0;
302+
245303
return 1;
246304
}
247305

lib/imparse.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ extern int imparse_isnatom (const char *s, int len);
5656
extern int imparse_isatom (const char *s);
5757
extern int imparse_issequence (const char *s);
5858
extern int imparse_isnumber (const char *s);
59-
extern int imparse_istag (const char *s);
59+
extern int imparse_istag (const char *s, unsigned command_count);
6060
extern int imparse_range (const char *s, range_t *range);
6161

6262
#endif /* INCLUDED_IMPARSE_H */

0 commit comments

Comments
 (0)