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

proftpd / proftpd / 26694586415

30 May 2026 08:48PM UTC coverage: 92.637% (-0.4%) from 93.024%
26694586415

push

github

web-flow
Implement some sanity checks on the length of extended attributes (xattrs) that can be requested via custom SFTP extensions.

Bankde Eakasit posited that this could be another vector to triggering excessive memory allocations.

47319 of 51080 relevant lines covered (92.64%)

235.33 hits per line

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

95.72
/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, ...) {
213✔
150
  pool *newpool = NULL;
213✔
151
  cmd_rec *cmd = NULL;
213✔
152
  int *xerrno = NULL;
213✔
153
  va_list args;
154

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

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

163
  cmd = pcalloc(newpool, sizeof(cmd_rec));
212✔
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

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

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

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

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

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

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

197
  return cmd;
212✔
198
}
199

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

213
  return 0;
1✔
214
}
215

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

223
  if (cmd->argc == 0 ||
2,746✔
224
      cmd->argv == NULL) {
1,372✔
225
    return 1;
226
  }
227

228
  /* The cmd ID is unknown; look it up. */
229
  if (cmd->cmd_id == 0) {
1,372✔
230
    cmd->cmd_id = pr_cmd_get_id(cmd->argv[0]);
154✔
231
  }
232

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

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

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

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

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

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

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

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

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

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

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

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

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

293
  return 0;
1✔
294
}
295

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

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

306
  if (cmd->argc == 0 ||
7✔
307
      cmd->argv == NULL) {
3✔
308
    return 1;
309
  }
310

311
  /* The cmd ID is unknown; look it up. */
312
  if (cmd->cmd_id == 0) {
3✔
313
    cmd->cmd_id = pr_cmd_get_id(cmd->argv[0]);
3✔
314
  }
315

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

319
    cmd_id = pr_cmd_get_id(cmd_name);
3✔
320

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

326
    return strncasecmp(cmd_name, cmd->argv[0],
2✔
327
      cmd_ids[cmd->cmd_id].cmd_namelen + 1);
2✔
328
  }
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) {
10✔
335
  const char *res;
336
  unsigned int argc;
337
  void **argv;
338
  pool *p;
339

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

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

351
    return res;
352
  }
353

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

358
  res = "";
8✔
359

360
  /* Check for "sensitive" commands. */
361
  if (pr_cmd_cmp(cmd, PR_CMD_PASS_ID) == 0 ||
15✔
362
      pr_cmd_cmp(cmd, PR_CMD_ADAT_ID) == 0) {
7✔
363
    argc = 2;
2✔
364
    argv[1] = "(hidden)";
2✔
365
  }
366

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

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

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

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

389
  return res;
390
}
391

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

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

402
  cmd_namelen = strlen(cmd_name);
281✔
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 ||
281✔
410
      cmd_namelen > PR_CMD_MAX_NAMELEN) {
411
    errno = ENOENT;
10✔
412
    return -1;
10✔
413
  }
414

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

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

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

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

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

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

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

452
  return known;
2✔
453
}
454

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

531
  if (strncmp(cmd_name, "SSH-2.0-", 8) == 0 ||
3✔
532
      strncmp(cmd_name, "SSH-1.99-", 9) == 0) {
1✔
533
    return TRUE;
534
  }
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