• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

proftpd / proftpd / 26182518137

20 May 2026 06:49PM UTC coverage: 93.024% (+0.4%) from 92.635%
26182518137

push

github

51329 of 55178 relevant lines covered (93.02%)

226.63 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.65
/src/cmd.c
1
/*
2
 * ProFTPD - FTP server daemon
3
 * Copyright (c) 2009-2026 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, see <https://www.gnu.org/licenses/>.
17
 *
18
 * As a special exemption, The ProFTPD Project team and other respective
19
 * copyright holders give permission to link this program with OpenSSL, and
20
 * distribute the resulting executable, without including the source code for
21
 * OpenSSL in the source distribution.
22
 */
23

24
#include "conf.h"
25

26
/* This struct and the list of such structs are used to try to reduce
27
 * the use of the following idiom to identify which command a given
28
 * cmd_rec is:
29
 *
30
 *  if (strcmp(cmd->argv[0], C_USER) == 0)
31
 *
32
 * Rather than using strcmp(3) so freely, try to reduce the command to
33
 * a fixed ID (an index into the struct list); this ID can then be compared
34
 * rather than using strcmp(3).  For commands not in the list, strcmp(3)
35
 * can always be used as a fallback.
36
 *
37
 * A future improvement would be to sort the entries in the table so that
38
 * the most common commands appear earlier in the table, and make the
39
 * linear scan even shorter.  But I'd need to collect better metrics in
40
 * order to do that.
41
 */
42

43
struct cmd_entry {
44
  const char *cmd_name;
45
  size_t cmd_namelen;
46
};
47

48
static struct cmd_entry cmd_ids[] = {
49
  { " ",        1 },        /* Index 0 is intentionally filled with a sentinel */
50
  { C_USER,        4 },        /* PR_CMD_USER_ID (1) */
51
  { C_PASS,        4 },        /* PR_CMD_PASS_ID (2) */
52
  { C_ACCT,        4 },        /* PR_CMD_ACCT_ID (3) */
53
  { C_CWD,        3 },        /* PR_CMD_CWD_ID (4) */
54
  { C_XCWD,        4 },        /* PR_CMD_XCWD_ID (5) */
55
  { C_CDUP,        4 },        /* PR_CMD_CDUP_ID (6) */
56
  { C_XCUP,        4 },        /* PR_CMD_XCUP_ID (7) */
57
  { C_SMNT,        4 },        /* PR_CMD_SMNT_ID (8) */
58
  { C_REIN,        4 },        /* PR_CMD_REIN_ID (9) */
59
  { C_QUIT,        4 },        /* PR_CMD_QUIT_ID (10) */
60
  { C_PORT,        4 },        /* PR_CMD_PORT_ID (11) */
61
  { C_EPRT,        4 },        /* PR_CMD_EPRT_ID (12) */
62
  { C_PASV,        4 },        /* PR_CMD_PASV_ID (13) */
63
  { C_EPSV,        4 },        /* PR_CMD_EPSV_ID (14) */
64
  { C_TYPE,        4 },        /* PR_CMD_TYPE_ID (15) */
65
  { C_STRU,        4 },        /* PR_CMD_STRU_ID (16) */
66
  { C_MODE,        4 },        /* PR_CMD_MODE_ID (17) */
67
  { C_RETR,        4 },        /* PR_CMD_RETR_ID (18) */
68
  { C_STOR,        4 },        /* PR_CMD_STOR_ID (19) */
69
  { C_STOU,        4 },        /* PR_CMD_STOU_ID (20) */
70
  { C_APPE,        4 },        /* PR_CMD_APPE_ID (21) */
71
  { C_ALLO,        4 },        /* PR_CMD_ALLO_ID (22) */
72
  { C_REST,        4 },        /* PR_CMD_REST_ID (23) */
73
  { C_RNFR,        4 },        /* PR_CMD_RNFR_ID (24) */
74
  { C_RNTO,        4 },        /* PR_CMD_RNTO_ID (25) */
75
  { C_ABOR,        4 },        /* PR_CMD_ABOR_ID (26) */
76
  { C_DELE,        4 },        /* PR_CMD_DELE_ID (27) */
77
  { C_MDTM,        4 },        /* PR_CMD_MDTM_ID (28) */
78
  { C_RMD,        3 },        /* PR_CMD_RMD_ID (29) */
79
  { C_XRMD,        4 },        /* PR_CMD_XRMD_ID (30) */
80
  { C_MKD,        3 },        /* PR_CMD_MKD_ID (31) */
81
  { C_MLSD,        4 },        /* PR_CMD_MLSD_ID (32) */
82
  { C_MLST,        4 },        /* PR_CMD_MLST_ID (33) */
83
  { C_XMKD,        4 },        /* PR_CMD_XMKD_ID (34) */
84
  { C_PWD,        3 },        /* PR_CMD_PWD_ID (35) */
85
  { C_XPWD,        4 },        /* PR_CMD_XPWD_ID (36) */
86
  { C_SIZE,        4 },        /* PR_CMD_SIZE_ID (37) */
87
  { C_LIST,        4 },        /* PR_CMD_LIST_ID (38) */
88
  { C_NLST,        4 },        /* PR_CMD_NLST_ID (39) */
89
  { C_SITE,        4 },        /* PR_CMD_SITE_ID (40) */
90
  { C_SYST,        4 },        /* PR_CMD_SYST_ID (41) */
91
  { C_STAT,        4 },        /* PR_CMD_STAT_ID (42) */
92
  { C_HELP,        4 },        /* PR_CMD_HELP_ID (43) */
93
  { C_NOOP,        4 },        /* PR_CMD_NOOP_ID (44) */
94
  { C_FEAT,        4 },        /* PR_CMD_FEAT_ID (45) */
95
  { C_OPTS,        4 },        /* PR_CMD_OPTS_ID (46) */
96
  { C_LANG,        4 },        /* PR_CMD_LANG_ID (47) */
97
  { C_ADAT,        4 },        /* PR_CMD_ADAT_ID (48) */
98
  { C_AUTH,        4 },        /* PR_CMD_AUTH_ID (49) */
99
  { C_CCC,        3 },        /* PR_CMD_CCC_ID (50) */
100
  { C_CONF,        4 },        /* PR_CMD_CONF_ID (51) */
101
  { C_ENC,        3 },        /* PR_CMD_ENC_ID (52) */
102
  { C_MIC,        3 },        /* PR_CMD_MIC_ID (53) */
103
  { C_PBSZ,        4 },        /* PR_CMD_PBSZ_ID (54) */
104
  { C_PROT,        4 },        /* PR_CMD_PROT_ID (55) */
105
  { C_MFF,        3 },        /* PR_CMD_MFF_ID (56) */
106
  { C_MFMT,        4 },        /* PR_CMD_MFMT_ID (57) */
107
  { C_HOST,        4 },        /* PR_CMD_HOST_ID (58) */
108
  { C_CLNT,        4 },        /* PR_CMD_CLNT_ID (59) */
109
  { C_RANG,        4 },        /* PR_CMD_RANG_ID (60) */
110
  { C_CSID,        4 },        /* PR_CMD_CSID_ID (61) */
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

149
cmd_rec *pr_cmd_alloc(pool *p, unsigned int argc, ...) {
150
  pool *newpool = NULL;
213✔
151
  cmd_rec *cmd = NULL;
213✔
152
  int *xerrno = NULL;
213✔
153
  va_list args;
213✔
154

213✔
155
  if (p == NULL) {
156
    errno = EINVAL;
213✔
157
    return NULL;
1✔
158
  }
1✔
159

160
  newpool = make_sub_pool(p);
161
  pr_pool_tag(newpool, "cmd_rec pool");
212✔
162

212✔
163
  cmd = pcalloc(newpool, sizeof(cmd_rec));
164
  cmd->argc = argc;
212✔
165
  cmd->stash_index = -1;
212✔
166
  cmd->stash_hash = 0;
212✔
167
  cmd->pool = newpool;
212✔
168
  cmd->tmp_pool = make_sub_pool(cmd->pool);
212✔
169
  pr_pool_tag(cmd->tmp_pool, "cmd_rec tmp pool");
212✔
170

212✔
171
  if (argc > 0) {
172
    register unsigned int i = 0;
212✔
173

210✔
174
    cmd->argv = pcalloc(cmd->pool, sizeof(void *) * (argc + 1));
175
    va_start(args, argc);
210✔
176

210✔
177
    for (i = 0; i < argc; i++) {
178
      cmd->argv[i] = va_arg(args, void *);
451✔
179
    }
241✔
180

181
    va_end(args);
182
    cmd->argv[argc] = NULL;
210✔
183

210✔
184
    pr_pool_tag(cmd->pool, cmd->argv[0]);
185
  }
210✔
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

212✔
192
  /* Initialize the "errno" note to be zero, so that it is always present. */
193
  xerrno = palloc(cmd->pool, sizeof(int));
194
  *xerrno = 0;
212✔
195
  (void) pr_table_add(cmd->notes, "errno", xerrno, sizeof(int));
212✔
196

212✔
197
  return cmd;
198
}
212✔
199

200
int pr_cmd_clear_cache(cmd_rec *cmd) {
201
  if (cmd == NULL) {
2✔
202
    errno = EINVAL;
2✔
203
    return -1;
1✔
204
  }
1✔
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);
1✔
212

1✔
213
  return 0;
214
}
1✔
215

216
int pr_cmd_cmp(cmd_rec *cmd, int cmd_id) {
217
  if (cmd == NULL ||
1,378✔
218
      cmd_id <= 0) {
1,378✔
219
    errno = EINVAL;
1,378✔
220
    return -1;
4✔
221
  }
4✔
222

223
  if (cmd->argc == 0 ||
224
      cmd->argv == NULL) {
1,374✔
225
    return 1;
1,372✔
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]);
1,372✔
231
  }
154✔
232

233
  /* The cmd ID is known to be unknown. */
234
  if (cmd->cmd_id < 0) {
235
    return 1;
1,372✔
236
  }
237

238
  if (cmd->cmd_id == cmd_id) {
239
    return 0;
984✔
240
  }
241

242
  return cmd->cmd_id < cmd_id ? -1 : 1;
243
}
854✔
244

245
int pr_cmd_get_errno(cmd_rec *cmd) {
246
  void *v;
4✔
247
  int *xerrno;
4✔
248

4✔
249
  if (cmd == NULL) {
250
    errno = EINVAL;
4✔
251
    return -1;
1✔
252
  }
1✔
253

254
  v = (void *) pr_table_get(cmd->notes, "errno", NULL);
255
  if (v == NULL) {
3✔
256
    errno = ENOENT;
3✔
257
    return -1;
1✔
258
  }
1✔
259

260
  xerrno = v;
261
  return *xerrno;
2✔
262
}
2✔
263

264
int pr_cmd_set_errno(cmd_rec *cmd, int xerrno) {
265
  void *v;
3✔
266

3✔
267
  if (cmd == NULL ||
268
      cmd->notes == NULL) {
3✔
269
    errno = EINVAL;
2✔
270
    return -1;
1✔
271
  }
1✔
272

273
  v = (void *) pr_table_get(cmd->notes, "errno", NULL);
274
  if (v == NULL) {
2✔
275
    errno = ENOENT;
2✔
276
    return -1;
×
277
  }
×
278

279
  *((int *) v) = xerrno;
280
  return 0;
2✔
281
}
2✔
282

283
int pr_cmd_set_name(cmd_rec *cmd, const char *cmd_name) {
284
  if (cmd == NULL ||
3✔
285
      cmd_name == NULL) {
3✔
286
    errno = EINVAL;
3✔
287
    return -1;
2✔
288
  }
2✔
289

290
  cmd->argv[0] = (char *) cmd_name;
291
  cmd->cmd_id = pr_cmd_get_id(cmd->argv[0]);
1✔
292

1✔
293
  return 0;
294
}
1✔
295

296
int pr_cmd_strcmp(cmd_rec *cmd, const char *cmd_name) {
297
  int cmd_id;
5✔
298
  size_t cmd_namelen;
5✔
299

5✔
300
  if (cmd == NULL ||
301
      cmd_name == NULL) {
5✔
302
    errno = EINVAL;
5✔
303
    return -1;
1✔
304
  }
1✔
305

306
  if (cmd->argc == 0 ||
307
      cmd->argv == NULL) {
4✔
308
    return 1;
3✔
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]);
3✔
314
  }
3✔
315

316
  if (cmd->cmd_id > 0) {
317
    int res;
3✔
318

3✔
319
    cmd_id = pr_cmd_get_id(cmd_name);
320

3✔
321
    res = pr_cmd_cmp(cmd, cmd_id);
322
    if (res == 0) {
3✔
323
      return 0;
3✔
324
    }
325

326
    return strncasecmp(cmd_name, cmd->argv[0],
327
      cmd_ids[cmd->cmd_id].cmd_namelen + 1);
2✔
328
  }
2✔
329

330
  cmd_namelen = strlen(cmd_name);
331
  return strncmp(cmd->argv[0], cmd_name, cmd_namelen + 1);
×
332
}
×
333

334
const char *pr_cmd_get_displayable_str(cmd_rec *cmd, size_t *str_len) {
335
  const char *res;
10✔
336
  unsigned int argc;
10✔
337
  void **argv;
10✔
338
  pool *p;
10✔
339

10✔
340
  if (cmd == NULL) {
341
    errno = EINVAL;
10✔
342
    return NULL;
1✔
343
  }
1✔
344

345
  res = pr_table_get(cmd->notes, "displayable-str", NULL);
346
  if (res != NULL) {
9✔
347
    if (str_len != NULL) {
9✔
348
      *str_len = strlen(res);
1✔
349
    }
×
350

351
    return res;
352
  }
1✔
353

354
  argc = cmd->argc;
355
  argv = cmd->argv;
8✔
356
  p = cmd->pool;
8✔
357

8✔
358
  res = "";
359

8✔
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) {
15✔
363
    argc = 2;
7✔
364
    argv[1] = "(hidden)";
2✔
365
  }
2✔
366

367
  if (argc > 0) {
368
    register unsigned int i;
8✔
369

7✔
370
    res = pstrcat(p, res, pr_fs_decode_path(p, argv[0]), NULL);
371

7✔
372
    for (i = 1; i < argc; i++) {
373
      res = pstrcat(p, res, " ", pr_fs_decode_path(p, argv[i]), NULL);
20✔
374
    }
6✔
375
  }
376

377
  if (pr_table_add(cmd->notes, pstrdup(cmd->pool, "displayable-str"),
378
      pstrdup(cmd->pool, res), 0) < 0) {
8✔
379
    if (errno != EEXIST) {
8✔
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);
8✔
387
  }
2✔
388

389
  return res;
390
}
391

392
int pr_cmd_get_id(const char *cmd_name) {
393
  register unsigned int i;
284✔
394
  size_t cmd_namelen;
284✔
395
  char first_letter;
284✔
396

284✔
397
  if (cmd_name == NULL) {
398
    errno = EINVAL;
284✔
399
    return -1;
3✔
400
  }
3✔
401

402
  cmd_namelen = strlen(cmd_name);
403

281✔
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) {
281✔
411
    errno = ENOENT;
412
    return -1;
10✔
413
  }
10✔
414

415
  first_letter = cmd_name[0];
416
  if (PR_ISALPHA((int) first_letter)) {
271✔
417
    first_letter = toupper((int) first_letter);
271✔
418
  }
271✔
419

420
  for (i = 1; cmd_ids[i].cmd_name != NULL; i++) {
421
    if (cmd_ids[i].cmd_namelen != cmd_namelen) {
8,768✔
422
      continue;
8,723✔
423
    }
3,146✔
424

425
    if (cmd_ids[i].cmd_name[0] != first_letter) {
426
      continue;
5,577✔
427
    }
4,946✔
428

429
    if (strcasecmp(cmd_ids[i].cmd_name, cmd_name) == 0) {
430
      return i;
631✔
431
    }
226✔
432
  }
433

434
  errno = ENOENT;
435
  return -1;
45✔
436
}
45✔
437

438
static int is_known_cmd(struct cmd_entry *known_cmds, const char *cmd_name,
439
    size_t cmd_namelen) {
2✔
440
  register unsigned int i;
441
  int known = FALSE;
2✔
442

2✔
443
  for (i = 0; known_cmds[i].cmd_name != NULL; i++) {
444
    if (cmd_namelen == known_cmds[i].cmd_namelen) {
10✔
445
      if (strncmp(cmd_name, known_cmds[i].cmd_name, cmd_namelen + 1) == 0) {
10✔
446
        known = TRUE;
6✔
447
        break;
448
      }
449
    }
450
  }
451

452
  return known;
453
}
2✔
454

455
int pr_cmd_is_http(cmd_rec *cmd) {
456
  const char *cmd_name;
4✔
457
  size_t cmd_namelen;
4✔
458

4✔
459
  if (cmd == NULL) {
460
    errno = EINVAL;
4✔
461
    return -1;
1✔
462
  }
1✔
463

464
  cmd_name = cmd->argv[0];
465
  if (cmd_name == NULL) {
3✔
466
    errno = EINVAL;
3✔
467
    return -1;
1✔
468
  }
1✔
469

470
  if (cmd->cmd_id == 0) {
471
    cmd->cmd_id = pr_cmd_get_id(cmd_name);
2✔
472
  }
2✔
473

474
  if (cmd->cmd_id >= 0) {
475
    return FALSE;
2✔
476
  }
477

478
  cmd_namelen = strlen(cmd_name);
479
  return is_known_cmd(http_ids, cmd_name, cmd_namelen);
1✔
480
}
1✔
481

482
int pr_cmd_is_smtp(cmd_rec *cmd) {
483
  const char *cmd_name;
4✔
484
  size_t cmd_namelen;
4✔
485

4✔
486
  if (cmd == NULL) {
487
    errno = EINVAL;
4✔
488
    return -1;
1✔
489
  }
1✔
490

491
  cmd_name = cmd->argv[0];
492
  if (cmd_name == NULL) {
3✔
493
    errno = EINVAL;
3✔
494
    return -1;
1✔
495
  }
1✔
496

497
  if (cmd->cmd_id == 0) {
498
    cmd->cmd_id = pr_cmd_get_id(cmd_name);
2✔
499
  }
2✔
500

501
  if (cmd->cmd_id >= 0) {
502
    return FALSE;
2✔
503
  }
504

505
  cmd_namelen = strlen(cmd_name);
506
  return is_known_cmd(smtp_ids, cmd_name, cmd_namelen);
1✔
507
}
1✔
508

509
int pr_cmd_is_ssh2(cmd_rec *cmd) {
510
  const char *cmd_name;
5✔
511

5✔
512
  if (cmd == NULL) {
513
    errno = EINVAL;
5✔
514
    return -1;
1✔
515
  }
1✔
516

517
  cmd_name = cmd->argv[0];
518
  if (cmd_name == NULL) {
4✔
519
    errno = EINVAL;
4✔
520
    return -1;
1✔
521
  }
1✔
522

523
  if (cmd->cmd_id == 0) {
524
    cmd->cmd_id = pr_cmd_get_id(cmd_name);
3✔
525
  }
3✔
526

527
  if (cmd->cmd_id >= 0) {
528
    return FALSE;
3✔
529
  }
530

531
  if (strncmp(cmd_name, "SSH-2.0-", 8) == 0 ||
532
      strncmp(cmd_name, "SSH-1.99-", 9) == 0) {
2✔
533
    return TRUE;
1✔
534
  }
2✔
535

536
  return FALSE;
537
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc