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

neomutt / neomutt / 24648356273

18 Apr 2026 01:24PM UTC coverage: 42.851% (+0.5%) from 42.375%
24648356273

push

github

flatcap
merge: fix security issues

Raised by evilrabbit on Mutt devel mailing list.

 * security: fix GSSAPI buffer underflow on short unwrapped tokens
 * security: reject percent-encoded NUL bytes in URL decoding
 * security: skip CN fallback when SAN dNSName entries exist (RFC6125)
 * security: cap POP3 UIDL responses to prevent OOM from malicious server

3 of 7 new or added lines in 2 files covered. (42.86%)

3465 existing lines in 53 files now uncovered.

12428 of 29003 relevant lines covered (42.85%)

5272.14 hits per line

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

95.69
/expando/node_expando.c
1
/**
2
 * @file
3
 * Expando Node for an Expando
4
 *
5
 * @authors
6
 * Copyright (C) 2023-2024 Tóth János <gomba007@gmail.com>
7
 * Copyright (C) 2023-2025 Richard Russon <rich@flatcap.org>
8
 * Copyright (C) 2025 Thomas Klausner <wiz@gatalith.at>
9
 *
10
 * @copyright
11
 * This program is free software: you can redistribute it and/or modify it under
12
 * the terms of the GNU General Public License as published by the Free Software
13
 * Foundation, either version 2 of the License, or (at your option) any later
14
 * version.
15
 *
16
 * This program is distributed in the hope that it will be useful, but WITHOUT
17
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
18
 * FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
19
 * details.
20
 *
21
 * You should have received a copy of the GNU General Public License along with
22
 * this program.  If not, see <http://www.gnu.org/licenses/>.
23
 */
24

25
/**
26
 * @page expando_node_expando Expando Node
27
 *
28
 * Expando Node for an Expando
29
 */
30

31
#include "config.h"
32
#include <limits.h>
33
#include <stdio.h>
34
#include <string.h>
35
#include "mutt/lib.h"
36
#include "gui/lib.h"
37
#include "node_expando.h"
38
#include "color/lib.h"
39
#include "definition.h"
40
#include "format.h"
41
#include "helpers.h"
42
#include "node.h"
43
#include "parse.h"
44
#include "render.h"
45

46
/**
47
 * node_expando_private_new - Create new Expando private data
48
 * @retval ptr New Expando private data
49
 */
50
struct NodeExpandoPrivate *node_expando_private_new(void)
1✔
51
{
52
  struct NodeExpandoPrivate *priv = MUTT_MEM_CALLOC(1, struct NodeExpandoPrivate);
1✔
53

54
  // NOTE(g0mb4): Expando definition should contain this
55
  priv->color = -1;
95,962✔
56

57
  return priv;
1✔
58
}
59

60
/**
61
 * node_expando_private_free - Free Expando private data - Implements ExpandoNode::ndata_free()
62
 * @param ptr Data to free
63
 */
64
void node_expando_private_free(void **ptr)
95,852✔
65
{
66
  if (!ptr || !*ptr)
95,852✔
67
    return;
68

69
  FREE(ptr);
95,851✔
70
}
71

72
/**
73
 * node_expando_new - Create a new Expando ExpandoNode
74
 * @param fmt    Formatting data
75
 * @param did    Domain ID
76
 * @param uid    Unique ID
77
 * @retval ptr New Expando ExpandoNode
78
 */
79
struct ExpandoNode *node_expando_new(struct ExpandoFormat *fmt, int did, int uid)
95,961✔
80
{
81
  struct ExpandoNode *node = node_new();
95,961✔
82

83
  node->type = ENT_EXPANDO;
95,961✔
84
  node->did = did;
95,961✔
85
  node->uid = uid;
95,961✔
86
  node->render = node_expando_render;
95,961✔
87

88
  node->format = fmt;
95,961✔
89

90
  node->ndata = node_expando_private_new();
95,961✔
91
  node->ndata_free = node_expando_private_free;
95,961✔
92

93
  return node;
95,961✔
94
}
95

96
/**
97
 * node_expando_set_color - Set the colour for an Expando
98
 * @param node Node to alter
99
 * @param cid  Colour
100
 */
101
void node_expando_set_color(const struct ExpandoNode *node, int cid)
4✔
102
{
103
  if (!node || (node->type != ENT_EXPANDO) || !node->ndata)
4✔
104
    return;
105

106
  struct NodeExpandoPrivate *priv = node->ndata;
107

108
  priv->color = cid;
3✔
109
}
110

111
/**
112
 * node_expando_set_has_tree - Set the has_tree flag for an Expando
113
 * @param node     Node to alter
114
 * @param has_tree Flag to set
115
 */
116
void node_expando_set_has_tree(const struct ExpandoNode *node, bool has_tree)
2✔
117
{
118
  if (!node || (node->type != ENT_EXPANDO) || !node->ndata)
2✔
119
    return;
120

121
  struct NodeExpandoPrivate *priv = node->ndata;
122

123
  priv->has_tree = has_tree;
1✔
124
}
125

126
/**
127
 * parse_format - Parse a format string
128
 * @param[in]  str          String to parse
129
 * @param[out] parsed_until First character after the parsed string
130
 * @param[out] err          Buffer for errors
131
 * @retval ptr New ExpandoFormat object
132
 *
133
 * Parse a printf()-style format, e.g. '-15.20x'
134
 *
135
 * @note A trailing `_` (underscore) means lowercase the string
136
 */
137
struct ExpandoFormat *parse_format(const char *str, const char **parsed_until,
259,697✔
138
                                   struct ExpandoParseError *err)
139
{
140
  if (!str || !parsed_until || !err)
259,697✔
141
    return NULL;
142

143
  const char *start = str;
144

145
  struct ExpandoFormat *fmt = MUTT_MEM_CALLOC(1, struct ExpandoFormat);
259,696✔
146

147
  fmt->leader = ' ';
259,696✔
148
  fmt->justification = JUSTIFY_RIGHT;
259,696✔
149
  fmt->min_cols = 0;
259,696✔
150
  fmt->max_cols = -1;
259,696✔
151

152
  if (*str == '-')
259,696✔
153
  {
154
    fmt->justification = JUSTIFY_LEFT;
23,548✔
155
    str++;
23,548✔
156
  }
157
  else if (*str == '=')
236,148✔
158
  {
159
    fmt->justification = JUSTIFY_CENTER;
13✔
160
    str++;
13✔
161
  }
162

163
  if (*str == '0')
259,696✔
164
  {
165
    // Ignore '0' with left-justification
166
    if (fmt->justification != JUSTIFY_LEFT)
81✔
167
      fmt->leader = '0';
45✔
168
    str++;
81✔
169
  }
170

171
  // Parse the width (min_cols)
172
  if (mutt_isdigit(*str))
259,696✔
173
  {
174
    unsigned short number = 0;
62,707✔
175
    const char *end_ptr = mutt_str_atous(str, &number);
62,707✔
176

177
    if (!end_ptr || (number == USHRT_MAX))
62,707✔
178
    {
179
      err->position = str;
11✔
180
      snprintf(err->message, sizeof(err->message), _("Invalid number: %s"), str);
11✔
181
      FREE(&fmt);
11✔
182
      return NULL;
11✔
183
    }
184

185
    fmt->min_cols = number;
62,696✔
186
    str = end_ptr;
187
  }
188

189
  // Parse the precision (max_cols)
190
  if (*str == '.')
259,685✔
191
  {
192
    str++;
9,936✔
193

194
    unsigned short number = 1;
9,936✔
195

196
    if (mutt_isdigit(*str))
9,936✔
197
    {
198
      const char *end_ptr = mutt_str_atous(str, &number);
9,906✔
199

200
      if (!end_ptr || (number == USHRT_MAX))
9,906✔
201
      {
202
        err->position = str;
5✔
203
        snprintf(err->message, sizeof(err->message), _("Invalid number: %s"), str);
5✔
204
        FREE(&fmt);
5✔
205
        return NULL;
5✔
206
      }
207

208
      str = end_ptr;
209
    }
210
    else
211
    {
212
      number = 0;
30✔
213
    }
214

215
    fmt->leader = (number == 0) ? ' ' : '0';
9,931✔
216
    fmt->max_cols = number;
9,931✔
217
  }
218

219
  // A modifier of '_' before the letter means force lower case
220
  if (*str == '_')
259,680✔
221
  {
222
    fmt->lower = true;
11✔
223
    str++;
11✔
224
  }
225

226
  if (str == start) // Failed to parse anything
259,680✔
227
    FREE(&fmt);
196,894✔
228

229
  if (fmt && (fmt->min_cols == 0) && (fmt->max_cols == -1) && !fmt->lower)
259,680✔
230
    FREE(&fmt);
12✔
231

232
  *parsed_until = str;
259,680✔
233
  return fmt;
259,680✔
234
}
235

236
/**
237
 * parse_short_name - Create an expando by its short name
238
 * @param[in]  str          String to parse
239
 * @param[in]  defs         Expando definitions
240
 * @param[in]  flags        Flag for conditional expandos
241
 * @param[in]  fmt          Formatting info
242
 * @param[out] parsed_until First character after parsed string
243
 * @param[out] err          Buffer for errors
244
 * @retval ptr New ExpandoNode
245
 */
246
struct ExpandoNode *parse_short_name(const char *str, const struct ExpandoDefinition *defs,
98,732✔
247
                                     ExpandoParserFlags flags,
248
                                     struct ExpandoFormat *fmt, const char **parsed_until,
249
                                     struct ExpandoParseError *err)
250
{
251
  if (!str || !defs)
98,732✔
252
    return NULL;
253

254
  const struct ExpandoDefinition *def = defs;
255
  for (; def && (def->short_name || def->long_name); def++)
1,291,225✔
256
  {
257
    size_t len = mutt_str_len(def->short_name);
1,291,213✔
258
    if (len == 0)
1,291,213✔
UNCOV
259
      continue;
×
260

261
    if (mutt_strn_equal(def->short_name, str, len))
1,291,213✔
262
    {
263
      if (def->parse)
98,718✔
264
      {
265
        return def->parse(str, fmt, def->did, def->uid, flags, parsed_until, err);
8,663✔
266
      }
267
      else
268
      {
269
        *parsed_until = str + len;
90,055✔
270
        return node_expando_new(fmt, def->did, def->uid);
90,055✔
271
      }
272
    }
273
  }
274

275
  return NULL;
276
}
277

278
/**
279
 * parse_long_name - Create an expando by its long name
280
 * @param[in]  str          String to parse
281
 * @param[in]  defs         Expando definitions
282
 * @param[in]  flags        Flag for conditional expandos
283
 * @param[in]  fmt          Formatting info
284
 * @param[out] parsed_until First character after parsed string
285
 * @param[out] err          Buffer for errors
286
 * @retval ptr New ExpandoNode
287
 */
288
struct ExpandoNode *parse_long_name(const char *str, const struct ExpandoDefinition *defs,
669✔
289
                                    ExpandoParserFlags flags,
290
                                    struct ExpandoFormat *fmt, const char **parsed_until,
291
                                    struct ExpandoParseError *err)
292
{
293
  if (!str || !defs)
669✔
294
    return NULL;
295

296
  const struct ExpandoDefinition *def = defs;
297
  for (; def && (def->short_name || def->long_name); def++)
35,939✔
298
  {
299
    if (!def->long_name)
35,280✔
300
      continue;
2,636✔
301

302
    size_t len = mutt_str_len(def->long_name);
32,644✔
303

304
    if (mutt_strn_equal(def->long_name, str, len))
32,644✔
305
    {
306
      *parsed_until = str + len;
12✔
307
      if (def->parse)
12✔
308
      {
UNCOV
309
        struct ExpandoNode *node = def->parse(str, fmt, def->did, def->uid,
×
310
                                              flags, parsed_until, err);
311
        if (node || (err->message[0] != '\0'))
×
UNCOV
312
          return node;
×
313
      }
314
      else
315
      {
316
        if (str[len] != '}') // Not an exact match
12✔
317
          continue;
2✔
318

319
        return node_expando_new(fmt, def->did, def->uid);
10✔
320
      }
321
    }
322
  }
323

324
  return NULL;
325
}
326

327
/**
328
 * node_expando_parse - Parse an Expando format string
329
 * @param[in]  str          String to parse
330
 * @param[in]  defs         Expando definitions
331
 * @param[in]  flags        Flag for conditional expandos
332
 * @param[out] parsed_until First character after parsed string
333
 * @param[out] err          Buffer for errors
334
 * @retval ptr New ExpandoNode
335
 */
336
struct ExpandoNode *node_expando_parse(const char *str, const struct ExpandoDefinition *defs,
80,448✔
337
                                       ExpandoParserFlags flags, const char **parsed_until,
338
                                       struct ExpandoParseError *err)
339
{
340
  ASSERT(str[0] == '%');
80,448✔
341
  str++;
80,448✔
342

343
  struct ExpandoFormat *fmt = parse_format(str, parsed_until, err);
80,448✔
344
  if (err->position)
80,448✔
345
  {
346
    FREE(&fmt);
17✔
347
    return NULL;
17✔
348
  }
349

350
  str = *parsed_until;
80,431✔
351

352
  struct ExpandoNode *node = parse_short_name(str, defs, flags, fmt, parsed_until, err);
80,431✔
353
  if (node)
80,431✔
354
    return node;
355

356
  if (!err->position)
11✔
357
  {
358
    err->position = *parsed_until;
9✔
359
    // L10N: e.g. "Unknown expando: %Q"
360
    snprintf(err->message, sizeof(err->message), _("Unknown expando: %%%.1s"), *parsed_until);
9✔
361
  }
362

363
  FREE(&fmt);
11✔
364
  return NULL;
11✔
365
}
366

367
/**
368
 * node_expando_parse_name - Parse an Expando format string
369
 * @param[in]  str          String to parse
370
 * @param[in]  defs         Expando definitions
371
 * @param[in]  flags        Flag for conditional expandos
372
 * @param[out] parsed_until First character after parsed string
373
 * @param[out] err          Buffer for errors
374
 * @retval ptr New ExpandoNode
375
 */
376
struct ExpandoNode *node_expando_parse_name(const char *str,
80,440✔
377
                                            const struct ExpandoDefinition *defs,
378
                                            ExpandoParserFlags flags, const char **parsed_until,
379
                                            struct ExpandoParseError *err)
380
{
381
  ASSERT(str[0] == '%');
80,440✔
382
  str++;
80,440✔
383

384
  struct ExpandoFormat *fmt = parse_format(str, parsed_until, err);
80,440✔
385
  if (err->position)
80,440✔
386
    goto fail;
14✔
387

388
  str = *parsed_until;
80,426✔
389

390
  if (str[0] != '{')
80,426✔
391
    goto fail;
79,772✔
392

393
  str++;
654✔
394

395
  struct ExpandoNode *node = parse_long_name(str, defs, flags, fmt, parsed_until, err);
654✔
396
  if (!node)
654✔
397
  {
398
    if (!err->position)
654✔
399
    {
400
      const char *end = str + strspn(str, "abcdefghijklmnopqrstuvwxyz0123456789-");
654✔
401

402
      // Only report an error if the content looks like a long name (i.e. starts
403
      // with at least one valid character).  If it doesn't, the '{' is probably
404
      // a short-name expando like %{%b %d} — let node_expando_parse() handle it.
405
      if (end != str)
654✔
406
      {
407
        err->position = str;
2✔
408
        if (*end != '}')
2✔
409
        {
410
          snprintf(err->message, sizeof(err->message), _("Expando is missing closing '}'"));
1✔
411
        }
412
        else
413
        {
414
          // L10N: e.g. "Unknown expando: %{bad}"
415
          snprintf(err->message, sizeof(err->message),
1✔
416
                   _("Unknown expando: %%{%.*s}"), (int) (end - str), str);
1✔
417
        }
418
      }
419
    }
420
    goto fail;
654✔
421
  }
422

UNCOV
423
  fmt = NULL; // owned by the node, now
×
424

UNCOV
425
  if ((*parsed_until)[0] == '}')
×
426
  {
427
    (*parsed_until)++;
×
UNCOV
428
    return node;
×
429
  }
430

UNCOV
431
  node_free(&node);
×
432

433
fail:
80,440✔
434
  FREE(&fmt);
80,440✔
435
  return NULL;
80,440✔
436
}
437

438
/**
439
 * skip_until_ch - Search a string for a terminator character
440
 * @param str        Start of string
441
 * @param terminator Character to find
442
 * @retval ptr Position of terminator character, or end-of-string
443
 */
444
const char *skip_until_ch(const char *str, char terminator)
671✔
445
{
446
  while (str[0] != '\0')
4,044✔
447
  {
448
    if (*str == terminator)
4,041✔
449
      break;
450

451
    if (str[0] == '\\') // Literal character
3,374✔
452
    {
453
      if (str[1] == '\0')
2✔
454
        return str + 1;
1✔
455

456
      str++;
1✔
457
    }
458

459
    str++;
3,373✔
460
  }
461

462
  return str;
463
}
464

465
/**
466
 * node_expando_parse_enclosure - Parse an enclosed Expando
467
 * @param[in]  str          String to parse
468
 * @param[in]  did          Domain ID
469
 * @param[in]  uid          Unique ID
470
 * @param[in]  terminator   Terminating character
471
 * @param[in]  fmt          Formatting info
472
 * @param[out] parsed_until First character after the parsed string
473
 * @param[out] err          Buffer for errors
474
 * @retval ptr New ExpandoNode
475
 */
476
struct ExpandoNode *node_expando_parse_enclosure(const char *str, int did,
668✔
477
                                                 int uid, char terminator,
478
                                                 struct ExpandoFormat *fmt,
479
                                                 const char **parsed_until,
480
                                                 struct ExpandoParseError *err)
481

482
{
483
  str++; // skip opening char
668✔
484

485
  const char *expando_end = skip_until_ch(str, terminator);
668✔
486

487
  if (*expando_end != terminator)
668✔
488
  {
489
    err->position = expando_end;
2✔
490
    snprintf(err->message, sizeof(err->message),
2✔
491
             // L10N: Expando is missing a terminator character
492
             //       e.g. "%[..." is missing the final ']'
493
             _("Expando is missing terminator: '%c'"), terminator);
2✔
494
    return NULL;
2✔
495
  }
496

497
  *parsed_until = expando_end + 1;
666✔
498

499
  struct ExpandoNode *node = node_expando_new(fmt, did, uid);
666✔
500

501
  struct Buffer *buf = buf_pool_get();
666✔
502
  for (; str < expando_end; str++)
4,022✔
503
  {
504
    if (str[0] == '\\')
3,356✔
505
      continue;
1✔
506
    buf_addch(buf, str[0]);
3,355✔
507
  }
508

509
  node->text = buf_strdup(buf);
666✔
510
  buf_pool_release(&buf);
666✔
511

512
  return node;
666✔
513
}
514

515
/**
516
 * add_color - Add a colour code to a buffer
517
 * @param buf Buffer for colour code
518
 * @param cid Colour
519
 */
520
void add_color(struct Buffer *buf, enum ColorId cid)
25✔
521
{
522
  ASSERT(cid < MT_COLOR_MAX);
25✔
523

524
  buf_addch(buf, MUTT_SPECIAL_INDEX);
25✔
525
  buf_addch(buf, cid);
25✔
526
}
25✔
527

528
/**
529
 * node_expando_render - Render an Expando Node - Implements ExpandoNode::render() - @ingroup expando_render
530
 */
531
int node_expando_render(const struct ExpandoNode *node,
307✔
532
                        const struct ExpandoRenderCallback *erc, struct Buffer *buf,
533
                        int max_cols, void *data, MuttFormatFlags flags)
534
{
535
  ASSERT(node->type == ENT_EXPANDO);
307✔
536

537
  struct Buffer *buf_expando = buf_pool_get();
307✔
538
  struct Buffer *buf_format = buf_pool_get();
307✔
539

540
  const struct ExpandoFormat *fmt = node->format;
307✔
541
  const struct NodeExpandoPrivate *priv = node->ndata;
307✔
542

543
  // ---------------------------------------------------------------------------
544
  // Numbers and strings get treated slightly differently. We prefer strings.
545
  // This allows dates to be stored as 1729850182, but displayed as "2024-10-25".
546

547
  const struct ExpandoRenderCallback *erc_match = find_get_string(erc, node->did, node->uid);
307✔
548
  if (erc_match)
307✔
549
  {
550
    erc_match->get_string(node, data, flags, buf_expando);
188✔
551

552
    if (fmt && fmt->lower)
188✔
553
      buf_lower_special(buf_expando);
1✔
554
  }
555
  else
556
  {
557
    erc_match = find_get_number(erc, node->did, node->uid);
119✔
558
    ASSERT(erc_match && "Unknown UID");
119✔
559

560
    const long num = erc_match->get_number(node, data, flags);
119✔
561

562
    int precision = 1;
563

564
    if (fmt)
119✔
565
    {
566
      precision = fmt->max_cols;
106✔
567
      if ((precision < 0) && (fmt->leader == '0'))
106✔
568
        precision = fmt->min_cols;
6✔
569
    }
570

571
    if (num < 0)
119✔
572
      precision--; // Allow space for the '-' sign
39✔
573

574
    buf_printf(buf_expando, "%.*ld", precision, num);
119✔
575
  }
576

577
  // ---------------------------------------------------------------------------
578

579
  int max = max_cols;
580
  int min = 0;
581

582
  if (fmt)
137✔
583
  {
584
    min = fmt->min_cols;
124✔
585
    if (fmt->max_cols > 0)
124✔
586
      max = MIN(max_cols, fmt->max_cols);
49✔
587
  }
588

589
  const enum FormatJustify just = fmt ? fmt->justification : JUSTIFY_LEFT;
307✔
590

591
  int total_cols = format_string(buf_format, min, max, just, ' ', buf_string(buf_expando),
614✔
592
                                 buf_len(buf_expando), priv->has_tree);
307✔
593

594
  if (!buf_is_empty(buf_format))
307✔
595
  {
596
    if (priv->color > -1)
299✔
597
      add_color(buf, priv->color);
12✔
598

599
    buf_addstr(buf, buf_string(buf_format));
598✔
600

601
    if (priv->color > -1)
299✔
602
      add_color(buf, MT_COLOR_INDEX);
12✔
603
  }
604

605
  buf_pool_release(&buf_format);
307✔
606
  buf_pool_release(&buf_expando);
307✔
607

608
  return total_cols;
307✔
609
}
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