Coverage Report

Created: 2025-07-11 06:08

/src/proftpd/src/cmd.c
Line
Count
Source (jump to first uncovered line)
1
/*
2
 * ProFTPD - FTP server daemon
3
 * Copyright (c) 2009-2022 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
  { C_CSID, 4 },  /* PR_CMD_CSID_ID (61) */
112
113
  { NULL, 0 }
114
};
115
116
/* Due to potential XSS issues (see Bug#4143), we want to explicitly
117
 * check for commands from other text-based protocols (e.g. HTTP and SMTP);
118
 * if we see these, we want to close the connection with extreme prejudice.
119
 */
120
121
static struct cmd_entry http_ids[] = {
122
  { " ",  1 },    /* Index 0 is intentionally filled with a sentinel */
123
  { "CONNECT",  7 },
124
  { "DELETE", 6 },
125
  { "GET",  3 },
126
  { "HEAD", 4 },
127
  { "OPTIONS",  7 },
128
  { "PATCH",  5 },
129
  { "POST", 4 },
130
  { "PUT",  3 },
131
132
  { NULL, 0 }
133
};
134
135
static struct cmd_entry smtp_ids[] = {
136
  { " ",  1 },    /* Index 0 is intentionally filled with a sentinel */
137
  { "DATA", 4 },
138
  { "EHLO", 4 },
139
  { "HELO", 4 },
140
  { "MAIL", 4 },
141
  { "RCPT", 4 },
142
  { "RSET", 4 },
143
  { "VRFY", 4 },
144
145
  { NULL, 0 }
146
};
147
148
static const char *trace_channel = "command";
149
150
0
cmd_rec *pr_cmd_alloc(pool *p, unsigned int argc, ...) {
151
0
  pool *newpool = NULL;
152
0
  cmd_rec *cmd = NULL;
153
0
  int *xerrno = NULL;
154
0
  va_list args;
155
156
0
  if (p == NULL) {
157
0
    errno = EINVAL;
158
0
    return NULL;
159
0
  }
160
161
0
  newpool = make_sub_pool(p);
162
0
  pr_pool_tag(newpool, "cmd_rec pool");
163
164
0
  cmd = pcalloc(newpool, sizeof(cmd_rec));
165
0
  cmd->argc = argc;
166
0
  cmd->stash_index = -1;
167
0
  cmd->stash_hash = 0;
168
0
  cmd->pool = newpool;
169
0
  cmd->tmp_pool = make_sub_pool(cmd->pool);
170
0
  pr_pool_tag(cmd->tmp_pool, "cmd_rec tmp pool");
171
172
0
  if (argc > 0) {
173
0
    register unsigned int i = 0;
174
175
0
    cmd->argv = pcalloc(cmd->pool, sizeof(void *) * (argc + 1));
176
0
    va_start(args, argc);
177
178
0
    for (i = 0; i < argc; i++) {
179
0
      cmd->argv[i] = va_arg(args, void *);
180
0
    }
181
182
0
    va_end(args);
183
0
    cmd->argv[argc] = NULL;
184
185
0
    pr_pool_tag(cmd->pool, cmd->argv[0]);
186
0
  }
187
188
  /* This table will not contain that many entries, so a low number
189
   * of chains should suffice.
190
   */
191
0
  cmd->notes = pr_table_nalloc(cmd->pool, 0, 8);
192
193
  /* Initialize the "errno" note to be zero, so that it is always present. */
194
0
  xerrno = palloc(cmd->pool, sizeof(int));
195
0
  *xerrno = 0;
196
0
  (void) pr_table_add(cmd->notes, "errno", xerrno, sizeof(int));
197
198
0
  return cmd;
199
0
}
200
201
0
int pr_cmd_clear_cache(cmd_rec *cmd) {
202
0
  if (cmd == NULL) {
203
0
    errno = EINVAL;
204
0
    return -1;
205
0
  }
206
207
  /* Clear the strings that have been cached for this command in the
208
   * notes table.
209
   */
210
211
0
  (void) pr_table_remove(cmd->notes, "displayable-str", NULL);
212
0
  (void) pr_cmd_set_errno(cmd, 0);
213
214
0
  return 0;
215
0
}
216
217
0
int pr_cmd_cmp(cmd_rec *cmd, int cmd_id) {
218
0
  if (cmd == NULL ||
219
0
      cmd_id <= 0) {
220
0
    errno = EINVAL;
221
0
    return -1;
222
0
  }
223
224
0
  if (cmd->argc == 0 ||
225
0
      cmd->argv == NULL) {
226
0
    return 1;
227
0
  }
228
229
  /* The cmd ID is unknown; look it up. */
230
0
  if (cmd->cmd_id == 0) {
231
0
    cmd->cmd_id = pr_cmd_get_id(cmd->argv[0]);
232
0
  }
233
234
  /* The cmd ID is known to be unknown. */
235
0
  if (cmd->cmd_id < 0) {
236
0
    return 1;
237
0
  }
238
239
0
  if (cmd->cmd_id == cmd_id) {
240
0
    return 0;
241
0
  }
242
243
0
  return cmd->cmd_id < cmd_id ? -1 : 1;
244
0
}
245
246
0
int pr_cmd_get_errno(cmd_rec *cmd) {
247
0
  void *v;
248
0
  int *xerrno;
249
250
0
  if (cmd == NULL) {
251
0
    errno = EINVAL;
252
0
    return -1;
253
0
  }
254
255
0
  v = (void *) pr_table_get(cmd->notes, "errno", NULL);
256
0
  if (v == NULL) {
257
0
    errno = ENOENT;
258
0
    return -1;
259
0
  }
260
261
0
  xerrno = v;
262
0
  return *xerrno;
263
0
}
264
265
0
int pr_cmd_set_errno(cmd_rec *cmd, int xerrno) {
266
0
  void *v;
267
268
0
  if (cmd == NULL ||
269
0
      cmd->notes == NULL) {
270
0
    errno = EINVAL;
271
0
    return -1;
272
0
  }
273
274
0
  v = (void *) pr_table_get(cmd->notes, "errno", NULL);
275
0
  if (v == NULL) {
276
0
    errno = ENOENT;
277
0
    return -1;
278
0
  }
279
280
0
  *((int *) v) = xerrno;
281
0
  return 0;
282
0
}
283
284
0
int pr_cmd_set_name(cmd_rec *cmd, const char *cmd_name) {
285
0
  if (cmd == NULL ||
286
0
      cmd_name == NULL) {
287
0
    errno = EINVAL;
288
0
    return -1;
289
0
  }
290
291
0
  cmd->argv[0] = (char *) cmd_name;
292
0
  cmd->cmd_id = pr_cmd_get_id(cmd->argv[0]);
293
294
0
  return 0;
295
0
}
296
297
0
int pr_cmd_strcmp(cmd_rec *cmd, const char *cmd_name) {
298
0
  int cmd_id;
299
0
  size_t cmd_namelen;
300
301
0
  if (cmd == NULL ||
302
0
      cmd_name == NULL) {
303
0
    errno = EINVAL;
304
0
    return -1;
305
0
  }
306
307
0
  if (cmd->argc == 0 ||
308
0
      cmd->argv == NULL) {
309
0
    return 1;
310
0
  }
311
312
  /* The cmd ID is unknown; look it up. */
313
0
  if (cmd->cmd_id == 0) {
314
0
    cmd->cmd_id = pr_cmd_get_id(cmd->argv[0]);
315
0
  }
316
317
0
  if (cmd->cmd_id > 0) {
318
0
    int res;
319
320
0
    cmd_id = pr_cmd_get_id(cmd_name);
321
322
0
    res = pr_cmd_cmp(cmd, cmd_id);
323
0
    if (res == 0) {
324
0
      return 0;
325
0
    }
326
327
0
    return strncasecmp(cmd_name, cmd->argv[0],
328
0
      cmd_ids[cmd->cmd_id].cmd_namelen + 1);
329
0
  }
330
331
0
  cmd_namelen = strlen(cmd_name);
332
0
  return strncmp(cmd->argv[0], cmd_name, cmd_namelen + 1);
333
0
}
334
335
0
const char *pr_cmd_get_displayable_str(cmd_rec *cmd, size_t *str_len) {
336
0
  const char *res;
337
0
  unsigned int argc;
338
0
  void **argv;
339
0
  pool *p;
340
341
0
  if (cmd == NULL) {
342
0
    errno = EINVAL;
343
0
    return NULL;
344
0
  }
345
346
0
  res = pr_table_get(cmd->notes, "displayable-str", NULL);
347
0
  if (res != NULL) {
348
0
    if (str_len != NULL) {
349
0
      *str_len = strlen(res);
350
0
    }
351
352
0
    return res;
353
0
  }
354
355
0
  argc = cmd->argc;
356
0
  argv = cmd->argv;
357
0
  p = cmd->pool;
358
359
0
  res = "";
360
361
  /* Check for "sensitive" commands. */
362
0
  if (pr_cmd_cmp(cmd, PR_CMD_PASS_ID) == 0 ||
363
0
      pr_cmd_cmp(cmd, PR_CMD_ADAT_ID) == 0) {
364
0
    argc = 2;
365
0
    argv[1] = "(hidden)";
366
0
  }
367
368
0
  if (argc > 0) {
369
0
    register unsigned int i;
370
371
0
    res = pstrcat(p, res, pr_fs_decode_path(p, argv[0]), NULL);
372
373
0
    for (i = 1; i < argc; i++) {
374
0
      res = pstrcat(p, res, " ", pr_fs_decode_path(p, argv[i]), NULL);
375
0
    }
376
0
  }
377
378
0
  if (pr_table_add(cmd->notes, pstrdup(cmd->pool, "displayable-str"),
379
0
      pstrdup(cmd->pool, res), 0) < 0) {
380
0
    if (errno != EEXIST) {
381
0
      pr_trace_msg(trace_channel, 4,
382
0
        "error setting 'displayable-str' command note: %s", strerror(errno));
383
0
    }
384
0
  }
385
386
0
  if (str_len != NULL) {
387
0
    *str_len = strlen(res);
388
0
  }
389
390
0
  return res;
391
0
}
392
393
0
int pr_cmd_get_id(const char *cmd_name) {
394
0
  register unsigned int i;
395
0
  size_t cmd_namelen;
396
0
  char first_letter;
397
398
0
  if (cmd_name == NULL) {
399
0
    errno = EINVAL;
400
0
    return -1;
401
0
  }
402
403
0
  cmd_namelen = strlen(cmd_name);
404
405
  /* Take advantage of the fact that we know, a priori, that the shortest
406
   * command name in the list is 3 characters, and that the longest is 4
407
   * characters.  No need to scan the list if we know that the given name
408
   * is not within that length range.
409
   */
410
0
  if (cmd_namelen < PR_CMD_MIN_NAMELEN ||
411
0
      cmd_namelen > PR_CMD_MAX_NAMELEN) {
412
0
    errno = ENOENT;
413
0
    return -1;
414
0
  }
415
416
0
  first_letter = toupper((int) cmd_name[0]);
417
418
0
  for (i = 1; cmd_ids[i].cmd_name != NULL; i++) {
419
0
    if (cmd_ids[i].cmd_namelen != cmd_namelen) {
420
0
      continue;
421
0
    }
422
423
0
    if (cmd_ids[i].cmd_name[0] != first_letter) {
424
0
      continue;
425
0
    }
426
427
0
    if (strcasecmp(cmd_ids[i].cmd_name, cmd_name) == 0) {
428
0
      return i;
429
0
    }
430
0
  }
431
432
0
  errno = ENOENT;
433
0
  return -1;
434
0
}
435
436
static int is_known_cmd(struct cmd_entry *known_cmds, const char *cmd_name,
437
0
    size_t cmd_namelen) {
438
0
  register unsigned int i;
439
0
  int known = FALSE;
440
441
0
  for (i = 0; known_cmds[i].cmd_name != NULL; i++) {
442
0
    if (cmd_namelen == known_cmds[i].cmd_namelen) {
443
0
      if (strncmp(cmd_name, known_cmds[i].cmd_name, cmd_namelen + 1) == 0) {
444
0
        known = TRUE;
445
0
        break;
446
0
      }
447
0
    }
448
0
  }
449
450
0
  return known;
451
0
}
452
453
0
int pr_cmd_is_http(cmd_rec *cmd) {
454
0
  const char *cmd_name;
455
0
  size_t cmd_namelen;
456
457
0
  if (cmd == NULL) {
458
0
    errno = EINVAL;
459
0
    return -1;
460
0
  }
461
462
0
  cmd_name = cmd->argv[0];
463
0
  if (cmd_name == NULL) {
464
0
    errno = EINVAL;
465
0
    return -1;
466
0
  }
467
468
0
  if (cmd->cmd_id == 0) {
469
0
    cmd->cmd_id = pr_cmd_get_id(cmd_name);
470
0
  }
471
472
0
  if (cmd->cmd_id >= 0) {
473
0
    return FALSE;
474
0
  }
475
476
0
  cmd_namelen = strlen(cmd_name);
477
0
  return is_known_cmd(http_ids, cmd_name, cmd_namelen);
478
0
}
479
480
0
int pr_cmd_is_smtp(cmd_rec *cmd) {
481
0
  const char *cmd_name;
482
0
  size_t cmd_namelen;
483
484
0
  if (cmd == NULL) {
485
0
    errno = EINVAL;
486
0
    return -1;
487
0
  }
488
489
0
  cmd_name = cmd->argv[0];
490
0
  if (cmd_name == NULL) {
491
0
    errno = EINVAL;
492
0
    return -1;
493
0
  }
494
495
0
  if (cmd->cmd_id == 0) {
496
0
    cmd->cmd_id = pr_cmd_get_id(cmd_name);
497
0
  }
498
499
0
  if (cmd->cmd_id >= 0) {
500
0
    return FALSE;
501
0
  }
502
503
0
  cmd_namelen = strlen(cmd_name);
504
0
  return is_known_cmd(smtp_ids, cmd_name, cmd_namelen);
505
0
}
506
507
0
int pr_cmd_is_ssh2(cmd_rec *cmd) {
508
0
  const char *cmd_name;
509
510
0
  if (cmd == NULL) {
511
0
    errno = EINVAL;
512
0
    return -1;
513
0
  }
514
515
0
  cmd_name = cmd->argv[0];
516
0
  if (cmd_name == NULL) {
517
0
    errno = EINVAL;
518
0
    return -1;
519
0
  }
520
521
0
  if (cmd->cmd_id == 0) {
522
0
    cmd->cmd_id = pr_cmd_get_id(cmd_name);
523
0
  }
524
525
0
  if (cmd->cmd_id >= 0) {
526
0
    return FALSE;
527
0
  }
528
529
0
  if (strncmp(cmd_name, "SSH-2.0-", 8) == 0 ||
530
0
      strncmp(cmd_name, "SSH-1.99-", 9) == 0) {
531
0
    return TRUE;
532
0
  }
533
534
0
  return FALSE;
535
0
}