1 /*
2 * ProFTPD - FTP server daemon
3 * Copyright (c) 2009-2020 The ProFTPD Project team
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program; if not, write to the Free Software
17 * Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA.
18 *
19 * As a special exemption, The ProFTPD Project team and other respective
20 * copyright holders give permission to link this program with OpenSSL, and
21 * distribute the resulting executable, without including the source code for
22 * OpenSSL in the source distribution.
23 */
24
25 #include "conf.h"
26
27 /* This struct and the list of such structs are used to try to reduce
28 * the use of the following idiom to identify which command a given
29 * cmd_rec is:
30 *
31 * if (strcmp(cmd->argv[0], C_USER) == 0)
32 *
33 * Rather than using strcmp(3) so freely, try to reduce the command to
34 * a fixed ID (an index into the struct list); this ID can then be compared
35 * rather than using strcmp(3). For commands not in the list, strcmp(3)
36 * can always be used as a fallback.
37 *
38 * A future improvement would be to sort the entries in the table so that
39 * the most common commands appear earlier in the table, and make the
40 * linear scan even shorter. But I'd need to collect better metrics in
41 * order to do that.
42 */
43
44 struct cmd_entry {
45 const char *cmd_name;
46 size_t cmd_namelen;
47 };
48
49 static struct cmd_entry cmd_ids[] = {
50 { " ", 1 }, /* Index 0 is intentionally filled with a sentinel */
51 { C_USER, 4 }, /* PR_CMD_USER_ID (1) */
52 { C_PASS, 4 }, /* PR_CMD_PASS_ID (2) */
53 { C_ACCT, 4 }, /* PR_CMD_ACCT_ID (3) */
54 { C_CWD, 3 }, /* PR_CMD_CWD_ID (4) */
55 { C_XCWD, 4 }, /* PR_CMD_XCWD_ID (5) */
56 { C_CDUP, 4 }, /* PR_CMD_CDUP_ID (6) */
57 { C_XCUP, 4 }, /* PR_CMD_XCUP_ID (7) */
58 { C_SMNT, 4 }, /* PR_CMD_SMNT_ID (8) */
59 { C_REIN, 4 }, /* PR_CMD_REIN_ID (9) */
60 { C_QUIT, 4 }, /* PR_CMD_QUIT_ID (10) */
61 { C_PORT, 4 }, /* PR_CMD_PORT_ID (11) */
62 { C_EPRT, 4 }, /* PR_CMD_EPRT_ID (12) */
63 { C_PASV, 4 }, /* PR_CMD_PASV_ID (13) */
64 { C_EPSV, 4 }, /* PR_CMD_EPSV_ID (14) */
65 { C_TYPE, 4 }, /* PR_CMD_TYPE_ID (15) */
66 { C_STRU, 4 }, /* PR_CMD_STRU_ID (16) */
67 { C_MODE, 4 }, /* PR_CMD_MODE_ID (17) */
68 { C_RETR, 4 }, /* PR_CMD_RETR_ID (18) */
69 { C_STOR, 4 }, /* PR_CMD_STOR_ID (19) */
70 { C_STOU, 4 }, /* PR_CMD_STOU_ID (20) */
71 { C_APPE, 4 }, /* PR_CMD_APPE_ID (21) */
72 { C_ALLO, 4 }, /* PR_CMD_ALLO_ID (22) */
73 { C_REST, 4 }, /* PR_CMD_REST_ID (23) */
74 { C_RNFR, 4 }, /* PR_CMD_RNFR_ID (24) */
75 { C_RNTO, 4 }, /* PR_CMD_RNTO_ID (25) */
76 { C_ABOR, 4 }, /* PR_CMD_ABOR_ID (26) */
77 { C_DELE, 4 }, /* PR_CMD_DELE_ID (27) */
78 { C_MDTM, 4 }, /* PR_CMD_MDTM_ID (28) */
79 { C_RMD, 3 }, /* PR_CMD_RMD_ID (29) */
80 { C_XRMD, 4 }, /* PR_CMD_XRMD_ID (30) */
81 { C_MKD, 3 }, /* PR_CMD_MKD_ID (31) */
82 { C_MLSD, 4 }, /* PR_CMD_MLSD_ID (32) */
83 { C_MLST, 4 }, /* PR_CMD_MLST_ID (33) */
84 { C_XMKD, 4 }, /* PR_CMD_XMKD_ID (34) */
85 { C_PWD, 3 }, /* PR_CMD_PWD_ID (35) */
86 { C_XPWD, 4 }, /* PR_CMD_XPWD_ID (36) */
87 { C_SIZE, 4 }, /* PR_CMD_SIZE_ID (37) */
88 { C_LIST, 4 }, /* PR_CMD_LIST_ID (38) */
89 { C_NLST, 4 }, /* PR_CMD_NLST_ID (39) */
90 { C_SITE, 4 }, /* PR_CMD_SITE_ID (40) */
91 { C_SYST, 4 }, /* PR_CMD_SYST_ID (41) */
92 { C_STAT, 4 }, /* PR_CMD_STAT_ID (42) */
93 { C_HELP, 4 }, /* PR_CMD_HELP_ID (43) */
94 { C_NOOP, 4 }, /* PR_CMD_NOOP_ID (44) */
95 { C_FEAT, 4 }, /* PR_CMD_FEAT_ID (45) */
96 { C_OPTS, 4 }, /* PR_CMD_OPTS_ID (46) */
97 { C_LANG, 4 }, /* PR_CMD_LANG_ID (47) */
98 { C_ADAT, 4 }, /* PR_CMD_ADAT_ID (48) */
99 { C_AUTH, 4 }, /* PR_CMD_AUTH_ID (49) */
100 { C_CCC, 3 }, /* PR_CMD_CCC_ID (50) */
101 { C_CONF, 4 }, /* PR_CMD_CONF_ID (51) */
102 { C_ENC, 3 }, /* PR_CMD_ENC_ID (52) */
103 { C_MIC, 3 }, /* PR_CMD_MIC_ID (53) */
104 { C_PBSZ, 4 }, /* PR_CMD_PBSZ_ID (54) */
105 { C_PROT, 4 }, /* PR_CMD_PROT_ID (55) */
106 { C_MFF, 3 }, /* PR_CMD_MFF_ID (56) */
107 { C_MFMT, 4 }, /* PR_CMD_MFMT_ID (57) */
108 { C_HOST, 4 }, /* PR_CMD_HOST_ID (58) */
109 { C_CLNT, 4 }, /* PR_CMD_CLNT_ID (59) */
110 { C_RANG, 4 }, /* PR_CMD_RANG_ID (60) */
111
112 { NULL, 0 }
113 };
114
115 /* Due to potential XSS issues (see Bug#4143), we want to explicitly
116 * check for commands from other text-based protocols (e.g. HTTP and SMTP);
117 * if we see these, we want to close the connection with extreme prejudice.
118 */
119
120 static struct cmd_entry http_ids[] = {
121 { " ", 1 }, /* Index 0 is intentionally filled with a sentinel */
122 { "CONNECT", 7 },
123 { "DELETE", 6 },
124 { "GET", 3 },
125 { "HEAD", 4 },
126 { "OPTIONS", 7 },
127 { "PATCH", 5 },
128 { "POST", 4 },
129 { "PUT", 3 },
130
131 { NULL, 0 }
132 };
133
134 static struct cmd_entry smtp_ids[] = {
135 { " ", 1 }, /* Index 0 is intentionally filled with a sentinel */
136 { "DATA", 4 },
137 { "EHLO", 4 },
138 { "HELO", 4 },
139 { "MAIL", 4 },
140 { "RCPT", 4 },
141 { "RSET", 4 },
142 { "VRFY", 4 },
143
144 { NULL, 0 }
145 };
146
147 static const char *trace_channel = "command";
148
pr_cmd_alloc(pool * p,unsigned int argc,...)149 cmd_rec *pr_cmd_alloc(pool *p, unsigned int argc, ...) {
150 pool *newpool = NULL;
151 cmd_rec *cmd = NULL;
152 int *xerrno = NULL;
153 va_list args;
154
155 if (p == NULL) {
156 errno = EINVAL;
157 return NULL;
158 }
159
160 newpool = make_sub_pool(p);
161 pr_pool_tag(newpool, "cmd_rec pool");
162
163 cmd = pcalloc(newpool, sizeof(cmd_rec));
164 cmd->argc = argc;
165 cmd->stash_index = -1;
166 cmd->stash_hash = 0;
167 cmd->pool = newpool;
168 cmd->tmp_pool = make_sub_pool(cmd->pool);
169 pr_pool_tag(cmd->tmp_pool, "cmd_rec tmp pool");
170
171 if (argc > 0) {
172 register unsigned int i = 0;
173
174 cmd->argv = pcalloc(cmd->pool, sizeof(void *) * (argc + 1));
175 va_start(args, argc);
176
177 for (i = 0; i < argc; i++) {
178 cmd->argv[i] = va_arg(args, void *);
179 }
180
181 va_end(args);
182 cmd->argv[argc] = NULL;
183
184 pr_pool_tag(cmd->pool, cmd->argv[0]);
185 }
186
187 /* This table will not contain that many entries, so a low number
188 * of chains should suffice.
189 */
190 cmd->notes = pr_table_nalloc(cmd->pool, 0, 8);
191
192 /* Initialize the "errno" note to be zero, so that it is always present. */
193 xerrno = palloc(cmd->pool, sizeof(int));
194 *xerrno = 0;
195 (void) pr_table_add(cmd->notes, "errno", xerrno, sizeof(int));
196
197 return cmd;
198 }
199
pr_cmd_clear_cache(cmd_rec * cmd)200 int pr_cmd_clear_cache(cmd_rec *cmd) {
201 if (cmd == NULL) {
202 errno = EINVAL;
203 return -1;
204 }
205
206 /* Clear the strings that have been cached for this command in the
207 * notes table.
208 */
209
210 (void) pr_table_remove(cmd->notes, "displayable-str", NULL);
211 (void) pr_cmd_set_errno(cmd, 0);
212
213 return 0;
214 }
215
pr_cmd_cmp(cmd_rec * cmd,int cmd_id)216 int pr_cmd_cmp(cmd_rec *cmd, int cmd_id) {
217 if (cmd == NULL ||
218 cmd_id <= 0) {
219 errno = EINVAL;
220 return -1;
221 }
222
223 if (cmd->argc == 0 ||
224 cmd->argv == NULL) {
225 return 1;
226 }
227
228 /* The cmd ID is unknown; look it up. */
229 if (cmd->cmd_id == 0) {
230 cmd->cmd_id = pr_cmd_get_id(cmd->argv[0]);
231 }
232
233 /* The cmd ID is known to be unknown. */
234 if (cmd->cmd_id < 0) {
235 return 1;
236 }
237
238 if (cmd->cmd_id == cmd_id) {
239 return 0;
240 }
241
242 return cmd->cmd_id < cmd_id ? -1 : 1;
243 }
244
pr_cmd_get_errno(cmd_rec * cmd)245 int pr_cmd_get_errno(cmd_rec *cmd) {
246 void *v;
247 int *xerrno;
248
249 if (cmd == NULL) {
250 errno = EINVAL;
251 return -1;
252 }
253
254 v = (void *) pr_table_get(cmd->notes, "errno", NULL);
255 if (v == NULL) {
256 errno = ENOENT;
257 return -1;
258 }
259
260 xerrno = v;
261 return *xerrno;
262 }
263
pr_cmd_set_errno(cmd_rec * cmd,int xerrno)264 int pr_cmd_set_errno(cmd_rec *cmd, int xerrno) {
265 void *v;
266
267 if (cmd == NULL ||
268 cmd->notes == NULL) {
269 errno = EINVAL;
270 return -1;
271 }
272
273 v = (void *) pr_table_get(cmd->notes, "errno", NULL);
274 if (v == NULL) {
275 errno = ENOENT;
276 return -1;
277 }
278
279 *((int *) v) = xerrno;
280 return 0;
281 }
282
pr_cmd_set_name(cmd_rec * cmd,const char * cmd_name)283 int pr_cmd_set_name(cmd_rec *cmd, const char *cmd_name) {
284 if (cmd == NULL ||
285 cmd_name == NULL) {
286 errno = EINVAL;
287 return -1;
288 }
289
290 cmd->argv[0] = (char *) cmd_name;
291 cmd->cmd_id = pr_cmd_get_id(cmd->argv[0]);
292
293 return 0;
294 }
295
pr_cmd_strcmp(cmd_rec * cmd,const char * cmd_name)296 int pr_cmd_strcmp(cmd_rec *cmd, const char *cmd_name) {
297 int cmd_id;
298 size_t cmd_namelen;
299
300 if (cmd == NULL ||
301 cmd_name == NULL) {
302 errno = EINVAL;
303 return -1;
304 }
305
306 if (cmd->argc == 0 ||
307 cmd->argv == NULL) {
308 return 1;
309 }
310
311 /* The cmd ID is unknown; look it up. */
312 if (cmd->cmd_id == 0) {
313 cmd->cmd_id = pr_cmd_get_id(cmd->argv[0]);
314 }
315
316 if (cmd->cmd_id > 0) {
317 int res;
318
319 cmd_id = pr_cmd_get_id(cmd_name);
320
321 res = pr_cmd_cmp(cmd, cmd_id);
322 if (res == 0) {
323 return 0;
324 }
325
326 return strncasecmp(cmd_name, cmd->argv[0],
327 cmd_ids[cmd->cmd_id].cmd_namelen + 1);
328 }
329
330 cmd_namelen = strlen(cmd_name);
331 return strncmp(cmd->argv[0], cmd_name, cmd_namelen + 1);
332 }
333
pr_cmd_get_displayable_str(cmd_rec * cmd,size_t * str_len)334 const char *pr_cmd_get_displayable_str(cmd_rec *cmd, size_t *str_len) {
335 const char *res;
336 unsigned int argc;
337 void **argv;
338 pool *p;
339
340 if (cmd == NULL) {
341 errno = EINVAL;
342 return NULL;
343 }
344
345 res = pr_table_get(cmd->notes, "displayable-str", NULL);
346 if (res != NULL) {
347 if (str_len != NULL) {
348 *str_len = strlen(res);
349 }
350
351 return res;
352 }
353
354 argc = cmd->argc;
355 argv = cmd->argv;
356 p = cmd->pool;
357
358 res = "";
359
360 /* Check for "sensitive" commands. */
361 if (pr_cmd_cmp(cmd, PR_CMD_PASS_ID) == 0 ||
362 pr_cmd_cmp(cmd, PR_CMD_ADAT_ID) == 0) {
363 argc = 2;
364 argv[1] = "(hidden)";
365 }
366
367 if (argc > 0) {
368 register unsigned int i;
369
370 res = pstrcat(p, res, pr_fs_decode_path(p, argv[0]), NULL);
371
372 for (i = 1; i < argc; i++) {
373 res = pstrcat(p, res, " ", pr_fs_decode_path(p, argv[i]), NULL);
374 }
375 }
376
377 if (pr_table_add(cmd->notes, pstrdup(cmd->pool, "displayable-str"),
378 pstrdup(cmd->pool, res), 0) < 0) {
379 if (errno != EEXIST) {
380 pr_trace_msg(trace_channel, 4,
381 "error setting 'displayable-str' command note: %s", strerror(errno));
382 }
383 }
384
385 if (str_len != NULL) {
386 *str_len = strlen(res);
387 }
388
389 return res;
390 }
391
pr_cmd_get_id(const char * cmd_name)392 int pr_cmd_get_id(const char *cmd_name) {
393 register unsigned int i;
394 size_t cmd_namelen;
395 char first_letter;
396
397 if (cmd_name == NULL) {
398 errno = EINVAL;
399 return -1;
400 }
401
402 cmd_namelen = strlen(cmd_name);
403
404 /* Take advantage of the fact that we know, a priori, that the shortest
405 * command name in the list is 3 characters, and that the longest is 4
406 * characters. No need to scan the list if we know that the given name
407 * is not within that length range.
408 */
409 if (cmd_namelen < PR_CMD_MIN_NAMELEN ||
410 cmd_namelen > PR_CMD_MAX_NAMELEN) {
411 errno = ENOENT;
412 return -1;
413 }
414
415 first_letter = toupper(cmd_name[0]);
416
417 for (i = 1; cmd_ids[i].cmd_name != NULL; i++) {
418 if (cmd_ids[i].cmd_namelen != cmd_namelen) {
419 continue;
420 }
421
422 if (cmd_ids[i].cmd_name[0] != first_letter) {
423 continue;
424 }
425
426 if (strcasecmp(cmd_ids[i].cmd_name, cmd_name) == 0) {
427 return i;
428 }
429 }
430
431 errno = ENOENT;
432 return -1;
433 }
434
is_known_cmd(struct cmd_entry * known_cmds,const char * cmd_name,size_t cmd_namelen)435 static int is_known_cmd(struct cmd_entry *known_cmds, const char *cmd_name,
436 size_t cmd_namelen) {
437 register unsigned int i;
438 int known = FALSE;
439
440 for (i = 0; known_cmds[i].cmd_name != NULL; i++) {
441 if (cmd_namelen == known_cmds[i].cmd_namelen) {
442 if (strncmp(cmd_name, known_cmds[i].cmd_name, cmd_namelen + 1) == 0) {
443 known = TRUE;
444 break;
445 }
446 }
447 }
448
449 return known;
450 }
451
pr_cmd_is_http(cmd_rec * cmd)452 int pr_cmd_is_http(cmd_rec *cmd) {
453 const char *cmd_name;
454 size_t cmd_namelen;
455
456 if (cmd == NULL) {
457 errno = EINVAL;
458 return -1;
459 }
460
461 cmd_name = cmd->argv[0];
462 if (cmd_name == NULL) {
463 errno = EINVAL;
464 return -1;
465 }
466
467 if (cmd->cmd_id == 0) {
468 cmd->cmd_id = pr_cmd_get_id(cmd_name);
469 }
470
471 if (cmd->cmd_id >= 0) {
472 return FALSE;
473 }
474
475 cmd_namelen = strlen(cmd_name);
476 return is_known_cmd(http_ids, cmd_name, cmd_namelen);
477 }
478
pr_cmd_is_smtp(cmd_rec * cmd)479 int pr_cmd_is_smtp(cmd_rec *cmd) {
480 const char *cmd_name;
481 size_t cmd_namelen;
482
483 if (cmd == NULL) {
484 errno = EINVAL;
485 return -1;
486 }
487
488 cmd_name = cmd->argv[0];
489 if (cmd_name == NULL) {
490 errno = EINVAL;
491 return -1;
492 }
493
494 if (cmd->cmd_id == 0) {
495 cmd->cmd_id = pr_cmd_get_id(cmd_name);
496 }
497
498 if (cmd->cmd_id >= 0) {
499 return FALSE;
500 }
501
502 cmd_namelen = strlen(cmd_name);
503 return is_known_cmd(smtp_ids, cmd_name, cmd_namelen);
504 }
505
pr_cmd_is_ssh2(cmd_rec * cmd)506 int pr_cmd_is_ssh2(cmd_rec *cmd) {
507 const char *cmd_name;
508
509 if (cmd == NULL) {
510 errno = EINVAL;
511 return -1;
512 }
513
514 cmd_name = cmd->argv[0];
515 if (cmd_name == NULL) {
516 errno = EINVAL;
517 return -1;
518 }
519
520 if (cmd->cmd_id == 0) {
521 cmd->cmd_id = pr_cmd_get_id(cmd_name);
522 }
523
524 if (cmd->cmd_id >= 0) {
525 return FALSE;
526 }
527
528 if (strncmp(cmd_name, "SSH-2.0-", 8) == 0 ||
529 strncmp(cmd_name, "SSH-1.99-", 9) == 0) {
530 return TRUE;
531 }
532
533 return FALSE;
534 }
535