diff -Nru sudo-1.9.9/debian/changelog sudo-1.9.9/debian/changelog --- sudo-1.9.9/debian/changelog 2023-03-01 13:59:37.000000000 +0000 +++ sudo-1.9.9/debian/changelog 2023-04-03 18:00:44.000000000 +0000 @@ -1,3 +1,19 @@ +sudo (1.9.9-1ubuntu2.4) jammy-security; urgency=medium + + * SECURITY UPDATE: does not escape control characters + - debian/patches/CVE-2023-2848x-1.patch: escape control characters in + log messages and sudoreplay output in docs/sudoers.man.in, + docs/sudoers.mdoc.in, docs/sudoreplay.man.in, + docs/sudoreplay.mdoc.in, include/sudo_lbuf.h, + lib/eventlog/eventlog.c, lib/iolog/iolog_json.c, lib/util/lbuf.c, + lib/util/util.exp.in, plugins/sudoers/sudoreplay.c. + - debian/patches/CVE-2023-2848x-2.patch: fix regression in + lib/eventlog/eventlog.c. + - CVE-2023-28486 + - CVE-2023-28487 + + -- Marc Deslauriers Mon, 03 Apr 2023 14:00:44 -0400 + sudo (1.9.9-1ubuntu2.3) jammy-security; urgency=medium * SECURITY UPDATE: double free with per-command chroot sudoers rules diff -Nru sudo-1.9.9/debian/patches/CVE-2023-2848x-1.patch sudo-1.9.9/debian/patches/CVE-2023-2848x-1.patch --- sudo-1.9.9/debian/patches/CVE-2023-2848x-1.patch 1970-01-01 00:00:00.000000000 +0000 +++ sudo-1.9.9/debian/patches/CVE-2023-2848x-1.patch 2023-04-03 18:00:29.000000000 +0000 @@ -0,0 +1,951 @@ +Backport of: + +From 334daf92b31b79ce68ed75e2ee14fca265f029ca Mon Sep 17 00:00:00 2001 +From: "Todd C. Miller" +Date: Wed, 18 Jan 2023 08:21:34 -0700 +Subject: [PATCH] Escape control characters in log messages and "sudoreplay -l" + output. The log message contains user-controlled strings that could include + things like terminal control characters. Space characters in the command + path are now also escaped. + +Command line arguments that contain spaces are surrounded with +single quotes and any literal single quote or backslash characters +are escaped with a backslash. This makes it possible to distinguish +multiple command line arguments from a single argument that contains +spaces. + +Issue found by Matthieu Barjole and Victor Cutillas of Synacktiv +(https://synacktiv.com). +--- + docs/sudoers.man.in | 44 ++++++-- + docs/sudoers.mdoc.in | 38 +++++-- + docs/sudoreplay.man.in | 9 ++ + docs/sudoreplay.mdoc.in | 10 ++ + include/sudo_lbuf.h | 7 ++ + lib/eventlog/eventlog.c | 210 ++++++++++------------------------- + lib/iolog/iolog_json.c | 39 ------- + lib/util/lbuf.c | 106 ++++++++++++++++++ + lib/util/util.exp.in | 1 + + plugins/sudoers/sudoreplay.c | 144 ++++++++++++++++++++---- + 10 files changed, 383 insertions(+), 225 deletions(-) + +--- a/docs/sudoers.man.in ++++ b/docs/sudoers.man.in +@@ -5338,14 +5338,31 @@ can log events via + syslog(3), + to a local log file, or both. + The log format is almost identical in both cases. ++Any control characters present in the log data are formatted in octal ++with a leading ++\(oq#\(cq ++character. ++For example, a horizontal tab is stored as ++\(oq#011\(cq ++and an embedded carriage return is stored as ++\(oq#015\(cq. ++In addition, space characters in the command path are stored as ++\(oq#040\(cq. ++Command line arguments that contain spaces are enclosed in single quotes ++(''). ++This makes it possible to distinguish multiple command line arguments ++from a single argument that contains spaces. ++Literal single quotes and backslash characters ++(\(oq\e\(cq) ++in command line arguments are escaped with a backslash. + .SS "Accepted command log entries" + Commands that sudo runs are logged using the following format (split + into multiple lines for readability): + .nf + .sp + .RS 4n +-date hostname progname: username : TTY=ttyname ; PWD=cwd ; \e +- USER=runasuser ; GROUP=runasgroup ; TSID=logid ; \e ++date hostname progname: username : TTY=ttyname ; CHROOT=chroot ; \e ++ PWD=cwd ; USER=runasuser ; GROUP=runasgroup ; TSID=logid ; \e + ENV=env_vars COMMAND=command + .RE + .fi +@@ -5394,6 +5411,9 @@ was run on, or + \(lqunknown\(rq + if there was no terminal present. + .TP 14n ++chroot ++The root directory that the command was run in, if one was specified. ++.TP 14n + cwd + The current working directory that + \fBsudo\fR +@@ -5418,7 +5438,7 @@ A list of environment variables specifie + if specified. + .TP 14n + command +-The actual command that was executed. ++The actual command that was executed, including any command line arguments. + .PP + Messages are logged using the locale specified by + \fIsudoers_locale\fR, +@@ -5654,17 +5674,21 @@ with a few important differences: + 1.\& + The + \fIprogname\fR +-and +-\fIhostname\fR +-fields are not present. ++field is not present. + .TP 5n + 2.\& +-If the +-\fIlog_year\fR +-option is enabled, +-the date will also include the year. ++The ++\fIhostname\fR ++is only logged if the ++\fIlog_host\fR ++option is enabled. + .TP 5n + 3.\& ++The date does not include the year unless the ++\fIlog_year\fR ++option is enabled. ++.TP 5n ++4.\& + Lines that are longer than + \fIloglinelen\fR + characters (80 by default) are word-wrapped and continued on the +--- a/docs/sudoers.mdoc.in ++++ b/docs/sudoers.mdoc.in +@@ -4980,12 +4980,29 @@ can log events via + .Xr syslog 3 , + to a local log file, or both. + The log format is almost identical in both cases. ++Any control characters present in the log data are formatted in octal ++with a leading ++.Ql # ++character. ++For example, a horizontal tab is stored as ++.Ql #011 ++and an embedded carriage return is stored as ++.Ql #015 . ++In addition, space characters in the command path are stored as ++.Ql #040 . ++Command line arguments that contain spaces are enclosed in single quotes ++.Pq '' . ++This makes it possible to distinguish multiple command line arguments ++from a single argument that contains spaces. ++Literal single quotes and backslash characters ++.Pq Ql \e ++in command line arguments are escaped with a backslash. + .Ss Accepted command log entries + Commands that sudo runs are logged using the following format (split + into multiple lines for readability): + .Bd -literal -offset 4n +-date hostname progname: username : TTY=ttyname ; PWD=cwd ; \e +- USER=runasuser ; GROUP=runasgroup ; TSID=logid ; \e ++date hostname progname: username : TTY=ttyname ; CHROOT=chroot ; \e ++ PWD=cwd ; USER=runasuser ; GROUP=runasgroup ; TSID=logid ; \e + ENV=env_vars COMMAND=command + .Ed + .Pp +@@ -5028,6 +5045,8 @@ or + was run on, or + .Dq unknown + if there was no terminal present. ++.It chroot ++The root directory that the command was run in, if one was specified. + .It cwd + The current working directory that + .Nm sudo +@@ -5047,7 +5066,7 @@ option is enabled. + A list of environment variables specified on the command line, + if specified. + .It command +-The actual command that was executed. ++The actual command that was executed, including any command line arguments. + .El + .Pp + Messages are logged using the locale specified by +@@ -5269,14 +5288,17 @@ with a few important differences: + .It + The + .Em progname +-and ++field is not present. ++.It ++The + .Em hostname +-fields are not present. ++is only logged if the ++.Em log_host ++option is enabled. + .It +-If the ++The date does not include the year unless the + .Em log_year +-option is enabled, +-the date will also include the year. ++option is enabled. + .It + Lines that are longer than + .Em loglinelen +--- a/docs/sudoreplay.man.in ++++ b/docs/sudoreplay.man.in +@@ -168,6 +168,15 @@ In this mode, + will list available sessions in a format similar to the + \fBsudo\fR + log file format, sorted by file name (or sequence number). ++Any control characters present in the log data are formated in octal ++with a leading ++\(oq#\(cq ++character. ++For example, a horizontal tab is displayed as ++\(oq#011\(cq ++and an embedded carriage return is displayed as ++\(oq#015\(cq. ++.sp + If a + \fIsearch expression\fR + is specified, it will be used to restrict the IDs that are displayed. +--- a/docs/sudoreplay.mdoc.in ++++ b/docs/sudoreplay.mdoc.in +@@ -160,6 +160,16 @@ In this mode, + will list available sessions in a format similar to the + .Nm sudo + log file format, sorted by file name (or sequence number). ++Any control characters present in the log data are formatted in octal ++with a leading ++.Ql # ++character. ++For example, a horizontal tab is displayed as ++.Ql #011 ++and an embedded carriage return is displayed as ++.Ql #015 . ++Space characters in the command name and arguments are also formatted in octal. ++.Pp + If a + .Ar search expression + is specified, it will be used to restrict the IDs that are displayed. +--- a/include/sudo_lbuf.h ++++ b/include/sudo_lbuf.h +@@ -36,9 +36,15 @@ struct sudo_lbuf { + + typedef int (*sudo_lbuf_output_t)(const char *); + ++/* Flags for sudo_lbuf_append_esc() */ ++#define LBUF_ESC_CNTRL 0x01 ++#define LBUF_ESC_BLANK 0x02 ++#define LBUF_ESC_QUOTE 0x04 ++ + sudo_dso_public void sudo_lbuf_init_v1(struct sudo_lbuf *lbuf, sudo_lbuf_output_t output, int indent, const char *continuation, int cols); + sudo_dso_public void sudo_lbuf_destroy_v1(struct sudo_lbuf *lbuf); + sudo_dso_public bool sudo_lbuf_append_v1(struct sudo_lbuf *lbuf, const char *fmt, ...) __printflike(2, 3); ++sudo_dso_public bool sudo_lbuf_append_esc_v1(struct sudo_lbuf *lbuf, int flags, const char *fmt, ...) __printflike(3, 4); + sudo_dso_public bool sudo_lbuf_append_quoted_v1(struct sudo_lbuf *lbuf, const char *set, const char *fmt, ...) __printflike(3, 4); + sudo_dso_public void sudo_lbuf_print_v1(struct sudo_lbuf *lbuf); + sudo_dso_public bool sudo_lbuf_error_v1(struct sudo_lbuf *lbuf); +@@ -47,6 +53,7 @@ sudo_dso_public void sudo_lbuf_clearerr_ + #define sudo_lbuf_init(_a, _b, _c, _d, _e) sudo_lbuf_init_v1((_a), (_b), (_c), (_d), (_e)) + #define sudo_lbuf_destroy(_a) sudo_lbuf_destroy_v1((_a)) + #define sudo_lbuf_append sudo_lbuf_append_v1 ++#define sudo_lbuf_append_esc sudo_lbuf_append_esc_v1 + #define sudo_lbuf_append_quoted sudo_lbuf_append_quoted_v1 + #define sudo_lbuf_print(_a) sudo_lbuf_print_v1((_a)) + #define sudo_lbuf_error(_a) sudo_lbuf_error_v1((_a)) +--- a/lib/eventlog/eventlog.c ++++ b/lib/eventlog/eventlog.c +@@ -1,7 +1,7 @@ + /* + * SPDX-License-Identifier: ISC + * +- * Copyright (c) 1994-1996, 1998-2021 Todd C. Miller ++ * Copyright (c) 1994-1996, 1998-2023 Todd C. Miller + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above +@@ -51,24 +51,13 @@ + #include "sudo_compat.h" + #include "sudo_debug.h" + #include "sudo_eventlog.h" ++#include "sudo_lbuf.h" + #include "sudo_fatal.h" + #include "sudo_gettext.h" + #include "sudo_json.h" + #include "sudo_queue.h" + #include "sudo_util.h" + +-#define LL_HOST_STR "HOST=" +-#define LL_TTY_STR "TTY=" +-#define LL_CHROOT_STR "CHROOT=" +-#define LL_CWD_STR "PWD=" +-#define LL_USER_STR "USER=" +-#define LL_GROUP_STR "GROUP=" +-#define LL_ENV_STR "ENV=" +-#define LL_CMND_STR "COMMAND=" +-#define LL_TSID_STR "TSID=" +-#define LL_EXIT_STR "EXIT=" +-#define LL_SIGNAL_STR "SIGNAL=" +- + #define IS_SESSID(s) ( \ + isalnum((unsigned char)(s)[0]) && isalnum((unsigned char)(s)[1]) && \ + (s)[2] == '/' && \ +@@ -93,26 +82,28 @@ new_logline(int event_type, int flags, s + const struct eventlog *evlog) + { + const struct eventlog_config *evl_conf = eventlog_getconf(); +- char *line = NULL, *evstr = NULL; + const char *iolog_file; + const char *tty, *tsid = NULL; + char exit_str[(((sizeof(int) * 8) + 2) / 3) + 2]; + char sessid[7], offsetstr[64] = ""; +- size_t len = 0; ++ struct sudo_lbuf lbuf; + int i; + debug_decl(new_logline, SUDO_DEBUG_UTIL); + ++ sudo_lbuf_init(&lbuf, NULL, 0, NULL, 0); ++ + if (ISSET(flags, EVLOG_RAW) || evlog == NULL) { + if (args->reason != NULL) { + if (args->errstr != NULL) { +- if (asprintf(&line, "%s: %s", args->reason, args->errstr) == -1) +- goto oom; ++ sudo_lbuf_append_esc(&lbuf, LBUF_ESC_CNTRL, "%s: %s", ++ args->reason, args->errstr); + } else { +- if ((line = strdup(args->reason)) == NULL) +- goto oom; ++ sudo_lbuf_append_esc(&lbuf, LBUF_ESC_CNTRL, "%s", args->reason); + } ++ if (sudo_lbuf_error(&lbuf)) ++ goto oom; + } +- debug_return_str(line); ++ debug_return_str(lbuf.buf); + } + + /* A TSID may be a sudoers-style session ID or a free-form string. */ +@@ -150,169 +141,90 @@ new_logline(int event_type, int flags, s + } + + /* +- * Compute line length ++ * Format the log line as an lbuf, escaping control characters in ++ * octal form (#0nn). Error checking (ENOMEM) is done at the end. + */ +- if (args->reason != NULL) +- len += strlen(args->reason) + 3; +- if (args->errstr != NULL) +- len += strlen(args->errstr) + 3; +- if (evlog->submithost != NULL && !evl_conf->omit_hostname) +- len += sizeof(LL_HOST_STR) + 2 + strlen(evlog->submithost); +- if (tty != NULL) +- len += sizeof(LL_TTY_STR) + 2 + strlen(tty); +- if (evlog->runchroot != NULL) +- len += sizeof(LL_CHROOT_STR) + 2 + strlen(evlog->runchroot); +- if (evlog->runcwd != NULL) +- len += sizeof(LL_CWD_STR) + 2 + strlen(evlog->runcwd); +- if (evlog->runuser != NULL) +- len += sizeof(LL_USER_STR) + 2 + strlen(evlog->runuser); +- if (evlog->rungroup != NULL) +- len += sizeof(LL_GROUP_STR) + 2 + strlen(evlog->rungroup); +- if (tsid != NULL) { +- len += sizeof(LL_TSID_STR) + 2 + strlen(tsid) + strlen(offsetstr); +- } +- if (evlog->env_add != NULL) { +- size_t evlen = 0; +- char * const *ep; +- +- for (ep = evlog->env_add; *ep != NULL; ep++) +- evlen += strlen(*ep) + 1; +- if (evlen != 0) { +- if ((evstr = malloc(evlen)) == NULL) +- goto oom; +- ep = evlog->env_add; +- if (strlcpy(evstr, *ep, evlen) >= evlen) +- goto toobig; +- while (*++ep != NULL) { +- if (strlcat(evstr, " ", evlen) >= evlen || +- strlcat(evstr, *ep, evlen) >= evlen) +- goto toobig; +- } +- len += sizeof(LL_ENV_STR) + 2 + evlen; +- } +- } +- if (evlog->command != NULL) { +- len += sizeof(LL_CMND_STR) - 1 + strlen(evlog->command); +- if (evlog->argv != NULL && evlog->argv[0] != NULL) { +- for (i = 1; evlog->argv[i] != NULL; i++) +- len += strlen(evlog->argv[i]) + 1; +- } +- if (event_type == EVLOG_EXIT) { +- if (evlog->signal_name != NULL) +- len += sizeof(LL_SIGNAL_STR) + 2 + strlen(evlog->signal_name); +- if (evlog->exit_value != -1) { +- (void)snprintf(exit_str, sizeof(exit_str), "%d", evlog->exit_value); +- len += sizeof(LL_EXIT_STR) + 2 + strlen(exit_str); +- } +- } +- } +- +- /* +- * Allocate and build up the line. +- */ +- if ((line = malloc(++len)) == NULL) +- goto oom; +- line[0] = '\0'; +- + if (args->reason != NULL) { +- if (strlcat(line, args->reason, len) >= len || +- strlcat(line, args->errstr ? " : " : " ; ", len) >= len) +- goto toobig; ++ sudo_lbuf_append_esc(&lbuf, LBUF_ESC_CNTRL, "%s%s", args->reason, ++ args->errstr ? " : " : " ; "); + } + if (args->errstr != NULL) { +- if (strlcat(line, args->errstr, len) >= len || +- strlcat(line, " ; ", len) >= len) +- goto toobig; ++ sudo_lbuf_append_esc(&lbuf, LBUF_ESC_CNTRL, "%s ; ", args->errstr); + } + if (evlog->submithost != NULL && !evl_conf->omit_hostname) { +- if (strlcat(line, LL_HOST_STR, len) >= len || +- strlcat(line, evlog->submithost, len) >= len || +- strlcat(line, " ; ", len) >= len) +- goto toobig; ++ sudo_lbuf_append_esc(&lbuf, LBUF_ESC_CNTRL, "HOST=%s ; ", ++ evlog->submithost); + } + if (tty != NULL) { +- if (strlcat(line, LL_TTY_STR, len) >= len || +- strlcat(line, tty, len) >= len || +- strlcat(line, " ; ", len) >= len) +- goto toobig; ++ sudo_lbuf_append_esc(&lbuf, LBUF_ESC_CNTRL, "TTY=%s ; ", tty); + } + if (evlog->runchroot != NULL) { +- if (strlcat(line, LL_CHROOT_STR, len) >= len || +- strlcat(line, evlog->runchroot, len) >= len || +- strlcat(line, " ; ", len) >= len) +- goto toobig; ++ sudo_lbuf_append_esc(&lbuf, LBUF_ESC_CNTRL, "CHROOT=%s ; ", ++ evlog->runchroot); + } + if (evlog->runcwd != NULL) { +- if (strlcat(line, LL_CWD_STR, len) >= len || +- strlcat(line, evlog->runcwd, len) >= len || +- strlcat(line, " ; ", len) >= len) +- goto toobig; ++ sudo_lbuf_append_esc(&lbuf, LBUF_ESC_CNTRL, "PWD=%s ; ", ++ evlog->runcwd); + } + if (evlog->runuser != NULL) { +- if (strlcat(line, LL_USER_STR, len) >= len || +- strlcat(line, evlog->runuser, len) >= len || +- strlcat(line, " ; ", len) >= len) +- goto toobig; ++ sudo_lbuf_append_esc(&lbuf, LBUF_ESC_CNTRL, "USER=%s ; ", ++ evlog->runuser); + } + if (evlog->rungroup != NULL) { +- if (strlcat(line, LL_GROUP_STR, len) >= len || +- strlcat(line, evlog->rungroup, len) >= len || +- strlcat(line, " ; ", len) >= len) +- goto toobig; ++ sudo_lbuf_append_esc(&lbuf, LBUF_ESC_CNTRL, "GROUP=%s ; ", ++ evlog->rungroup); + } + if (tsid != NULL) { +- if (strlcat(line, LL_TSID_STR, len) >= len || +- strlcat(line, tsid, len) >= len || +- strlcat(line, offsetstr, len) >= len || +- strlcat(line, " ; ", len) >= len) +- goto toobig; +- } +- if (evstr != NULL) { +- if (strlcat(line, LL_ENV_STR, len) >= len || +- strlcat(line, evstr, len) >= len || +- strlcat(line, " ; ", len) >= len) +- goto toobig; +- free(evstr); +- evstr = NULL; ++ sudo_lbuf_append_esc(&lbuf, LBUF_ESC_CNTRL, "TSID=%s%s ; ", tsid, ++ offsetstr); ++ } ++ if (evlog->env_add != NULL && evlog->env_add[0] != NULL) { ++ sudo_lbuf_append_esc(&lbuf, LBUF_ESC_CNTRL, "ENV=%s", ++ evlog->env_add[0]); ++ for (i = 1; evlog->env_add[i] != NULL; i++) { ++ sudo_lbuf_append_esc(&lbuf, LBUF_ESC_CNTRL, " %s", ++ evlog->env_add[i]); ++ } + } + if (evlog->command != NULL) { +- if (strlcat(line, LL_CMND_STR, len) >= len) +- goto toobig; +- if (strlcat(line, evlog->command, len) >= len) +- goto toobig; ++ sudo_lbuf_append_esc(&lbuf, LBUF_ESC_CNTRL|LBUF_ESC_BLANK, ++ "COMMAND=%s", evlog->command); + if (evlog->argv != NULL && evlog->argv[0] != NULL) { + for (i = 1; evlog->argv[i] != NULL; i++) { +- if (strlcat(line, " ", len) >= len || +- strlcat(line, evlog->argv[i], len) >= len) +- goto toobig; ++ sudo_lbuf_append(&lbuf, " "); ++ if (strchr(evlog->argv[i], ' ') != NULL) { ++ /* Wrap args containing spaces in single quotes. */ ++ sudo_lbuf_append(&lbuf, "'"); ++ sudo_lbuf_append_esc(&lbuf, LBUF_ESC_CNTRL|LBUF_ESC_QUOTE, ++ "%s", evlog->argv[i]); ++ sudo_lbuf_append(&lbuf, "'"); ++ } else { ++ /* Escape quotes here too for consistency. */ ++ sudo_lbuf_append_esc(&lbuf, ++ LBUF_ESC_CNTRL|LBUF_ESC_BLANK|LBUF_ESC_QUOTE, ++ "%s", evlog->argv[i]); ++ } + } + } + if (event_type == EVLOG_EXIT) { + if (evlog->signal_name != NULL) { +- if (strlcat(line, " ; ", len) >= len || +- strlcat(line, LL_SIGNAL_STR, len) >= len || +- strlcat(line, evlog->signal_name, len) >= len) +- goto toobig; ++ sudo_lbuf_append_esc(&lbuf, LBUF_ESC_CNTRL, " ; SIGNAL=%s", ++ evlog->signal_name); + } + if (evlog->exit_value != -1) { +- if (strlcat(line, " ; ", len) >= len || +- strlcat(line, LL_EXIT_STR, len) >= len || +- strlcat(line, exit_str, len) >= len) +- goto toobig; ++ (void)snprintf(exit_str, sizeof(exit_str), "%d", ++ evlog->exit_value); ++ sudo_lbuf_append_esc(&lbuf, LBUF_ESC_CNTRL, " ; EXIT=%s", ++ exit_str); + } + } + } +- +- debug_return_str(line); ++ if (!sudo_lbuf_error(&lbuf)) ++ debug_return_str(lbuf.buf); + oom: +- free(evstr); ++ sudo_lbuf_destroy(&lbuf); + sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); + debug_return_str(NULL); +-toobig: +- free(evstr); +- free(line); +- sudo_warnx(U_("internal error, %s overflow"), __func__); +- debug_return_str(NULL); + } + + static void +--- a/lib/iolog/iolog_json.c ++++ b/lib/iolog/iolog_json.c +@@ -541,45 +541,6 @@ iolog_parse_json_object(struct json_obje + } + } + +- /* Merge cmd and argv as sudoreplay expects. */ +- if (evlog->command != NULL && evlog->argv != NULL && evlog->argv[0] != NULL) { +- size_t len, bufsize = strlen(evlog->command) + 1; +- char *cp, *buf; +- int ac; +- +- /* Skip argv[0], we use evlog->command instead. */ +- for (ac = 1; evlog->argv[ac] != NULL; ac++) +- bufsize += strlen(evlog->argv[ac]) + 1; +- +- if ((buf = malloc(bufsize)) == NULL) { +- sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory")); +- goto done; +- } +- cp = buf; +- +- len = strlcpy(cp, evlog->command, bufsize); +- if (len >= bufsize) +- sudo_fatalx(U_("internal error, %s overflow"), __func__); +- cp += len; +- bufsize -= len; +- +- for (ac = 1; evlog->argv[ac] != NULL; ac++) { +- if (bufsize < 2) +- sudo_fatalx(U_("internal error, %s overflow"), __func__); +- *cp++ = ' '; +- bufsize--; +- +- len = strlcpy(cp, evlog->argv[ac], bufsize); +- if (len >= bufsize) +- sudo_fatalx(U_("internal error, %s overflow"), __func__); +- cp += len; +- bufsize -= len; +- } +- +- free(evlog->command); +- evlog->command = buf; +- } +- + ret = true; + + done: +--- a/lib/util/lbuf.c ++++ b/lib/util/lbuf.c +@@ -88,6 +88,112 @@ sudo_lbuf_expand(struct sudo_lbuf *lbuf, + } + + /* ++ * Escape a character in octal form (#0n) and store it as a string ++ * in buf, which must have at least 6 bytes available. ++ * Returns the length of buf, not counting the terminating NUL byte. ++ */ ++static int ++escape(unsigned char ch, char *buf) ++{ ++ const int len = ch < 0100 ? (ch < 010 ? 3 : 4) : 5; ++ ++ /* Work backwards from the least significant digit to most significant. */ ++ switch (len) { ++ case 5: ++ buf[4] = (ch & 7) + '0'; ++ ch >>= 3; ++ FALLTHROUGH; ++ case 4: ++ buf[3] = (ch & 7) + '0'; ++ ch >>= 3; ++ FALLTHROUGH; ++ case 3: ++ buf[2] = (ch & 7) + '0'; ++ buf[1] = '0'; ++ buf[0] = '#'; ++ break; ++ } ++ buf[len] = '\0'; ++ ++ return len; ++} ++ ++/* ++ * Parse the format and append strings, only %s and %% escapes are supported. ++ * Any non-printable characters are escaped in octal as #0nn. ++ */ ++bool ++sudo_lbuf_append_esc_v1(struct sudo_lbuf *lbuf, int flags, const char *fmt, ...) ++{ ++ unsigned int saved_len = lbuf->len; ++ bool ret = false; ++ const char *s; ++ va_list ap; ++ debug_decl(sudo_lbuf_append_esc, SUDO_DEBUG_UTIL); ++ ++ if (sudo_lbuf_error(lbuf)) ++ debug_return_bool(false); ++ ++#define should_escape(ch) \ ++ ((ISSET(flags, LBUF_ESC_CNTRL) && iscntrl((unsigned char)ch)) || \ ++ (ISSET(flags, LBUF_ESC_BLANK) && isblank((unsigned char)ch))) ++#define should_quote(ch) \ ++ (ISSET(flags, LBUF_ESC_QUOTE) && (ch == '\'' || ch == '\\')) ++ ++ va_start(ap, fmt); ++ while (*fmt != '\0') { ++ if (fmt[0] == '%' && fmt[1] == 's') { ++ if ((s = va_arg(ap, char *)) == NULL) ++ s = "(NULL)"; ++ while (*s != '\0') { ++ if (should_escape(*s)) { ++ if (!sudo_lbuf_expand(lbuf, sizeof("#0177") - 1)) ++ goto done; ++ lbuf->len += escape(*s++, lbuf->buf + lbuf->len); ++ continue; ++ } ++ if (should_quote(*s)) { ++ if (!sudo_lbuf_expand(lbuf, 2)) ++ goto done; ++ lbuf->buf[lbuf->len++] = '\\'; ++ lbuf->buf[lbuf->len++] = *s++; ++ continue; ++ } ++ if (!sudo_lbuf_expand(lbuf, 1)) ++ goto done; ++ lbuf->buf[lbuf->len++] = *s++; ++ } ++ fmt += 2; ++ continue; ++ } ++ if (should_escape(*fmt)) { ++ if (!sudo_lbuf_expand(lbuf, sizeof("#0177") - 1)) ++ goto done; ++ if (*fmt == '\'') { ++ lbuf->buf[lbuf->len++] = '\\'; ++ lbuf->buf[lbuf->len++] = *fmt++; ++ } else { ++ lbuf->len += escape(*fmt++, lbuf->buf + lbuf->len); ++ } ++ continue; ++ } ++ if (!sudo_lbuf_expand(lbuf, 1)) ++ goto done; ++ lbuf->buf[lbuf->len++] = *fmt++; ++ } ++ ret = true; ++ ++done: ++ if (!ret) ++ lbuf->len = saved_len; ++ if (lbuf->size != 0) ++ lbuf->buf[lbuf->len] = '\0'; ++ va_end(ap); ++ ++ debug_return_bool(ret); ++} ++ ++/* + * Parse the format and append strings, only %s and %% escapes are supported. + * Any characters in set are quoted with a backslash. + */ +--- a/lib/util/util.exp.in ++++ b/lib/util/util.exp.in +@@ -98,6 +98,7 @@ sudo_json_get_len_v1 + sudo_json_init_v1 + sudo_json_open_array_v1 + sudo_json_open_object_v1 ++sudo_lbuf_append_esc_v1 + sudo_lbuf_append_quoted_v1 + sudo_lbuf_append_v1 + sudo_lbuf_clearerr_v1 +--- a/plugins/sudoers/sudoreplay.c ++++ b/plugins/sudoers/sudoreplay.c +@@ -62,6 +62,7 @@ + #include "sudo_debug.h" + #include "sudo_event.h" + #include "sudo_eventlog.h" ++#include "sudo_lbuf.h" + #include "sudo_fatal.h" + #include "sudo_gettext.h" + #include "sudo_iolog.h" +@@ -373,6 +374,10 @@ main(int argc, char *argv[]) + if ((evlog = iolog_parse_loginfo(iolog_dir_fd, iolog_dir)) == NULL) + goto done; + printf(_("Replaying sudo session: %s"), evlog->command); ++ if (evlog->argv != NULL && evlog->argv[0] != NULL) { ++ for (i = 1; evlog->argv[i] != NULL; i++) ++ printf(" %s", evlog->argv[i]); ++ } + + /* Setup terminal if appropriate. */ + if (!isatty(STDIN_FILENO) || !isatty(STDOUT_FILENO)) +@@ -1312,11 +1317,57 @@ parse_expr(struct search_node_list *head + debug_return_int(av - argv); + } + ++static char * ++expand_command(struct eventlog *evlog, char **newbuf) ++{ ++ size_t len, bufsize = strlen(evlog->command) + 1; ++ char *cp, *buf; ++ int ac; ++ debug_decl(expand_command, SUDO_DEBUG_UTIL); ++ ++ if (evlog->argv == NULL || evlog->argv[0] == NULL || evlog->argv[1] == NULL) { ++ /* No arguments, we can use the command as-is. */ ++ *newbuf = NULL; ++ debug_return_str(evlog->command); ++ } ++ ++ /* Skip argv[0], we use evlog->command instead. */ ++ for (ac = 1; evlog->argv[ac] != NULL; ac++) ++ bufsize += strlen(evlog->argv[ac]) + 1; ++ ++ if ((buf = malloc(bufsize)) == NULL) ++ sudo_fatalx(U_("%s: %s"), __func__, U_("unable to allocate memory")); ++ cp = buf; ++ ++ len = strlcpy(cp, evlog->command, bufsize); ++ if (len >= bufsize) ++ sudo_fatalx(U_("internal error, %s overflow"), __func__); ++ cp += len; ++ bufsize -= len; ++ ++ for (ac = 1; evlog->argv[ac] != NULL; ac++) { ++ if (bufsize < 2) ++ sudo_fatalx(U_("internal error, %s overflow"), __func__); ++ *cp++ = ' '; ++ bufsize--; ++ ++ len = strlcpy(cp, evlog->argv[ac], bufsize); ++ if (len >= bufsize) ++ sudo_fatalx(U_("internal error, %s overflow"), __func__); ++ cp += len; ++ bufsize -= len; ++ } ++ ++ *newbuf = buf; ++ debug_return_str(buf); ++} ++ + static bool + match_expr(struct search_node_list *head, struct eventlog *evlog, bool last_match) + { + struct search_node *sn; + bool res = false, matched = last_match; ++ char *tofree; + int rc; + debug_decl(match_expr, SUDO_DEBUG_UTIL); + +@@ -1350,13 +1401,15 @@ match_expr(struct search_node_list *head + res = strcmp(sn->u.user, evlog->submituser) == 0; + break; + case ST_PATTERN: +- rc = regexec(&sn->u.cmdre, evlog->command, 0, NULL, 0); ++ rc = regexec(&sn->u.cmdre, expand_command(evlog, &tofree), ++ 0, NULL, 0); + if (rc && rc != REG_NOMATCH) { + char buf[BUFSIZ]; + regerror(rc, &sn->u.cmdre, buf, sizeof(buf)); + sudo_fatalx("%s", buf); + } + res = rc == REG_NOMATCH ? 0 : 1; ++ free(tofree); + break; + case ST_FROMDATE: + res = sudo_timespeccmp(&evlog->submit_time, &sn->u.tstamp, >=); +@@ -1377,12 +1430,13 @@ match_expr(struct search_node_list *head + } + + static int +-list_session(char *log_dir, regex_t *re, const char *user, const char *tty) ++list_session(struct sudo_lbuf *lbuf, char *log_dir, regex_t *re, ++ const char *user, const char *tty) + { + char idbuf[7], *idstr, *cp; + struct eventlog *evlog = NULL; + const char *timestr; +- int ret = -1; ++ int i, ret = -1; + debug_decl(list_session, SUDO_DEBUG_UTIL); + + if ((evlog = iolog_parse_loginfo(-1, log_dir)) == NULL) +@@ -1414,23 +1468,71 @@ list_session(char *log_dir, regex_t *re, + } + /* XXX - print lines + cols? */ + timestr = get_timestr(evlog->submit_time.tv_sec, 1); +- printf("%s : %s : ", timestr ? timestr : "invalid date", evlog->submituser); +- if (evlog->submithost != NULL) +- printf("HOST=%s ; ", evlog->submithost); +- if (evlog->ttyname != NULL) +- printf("TTY=%s ; ", evlog->ttyname); +- if (evlog->runchroot != NULL) +- printf("CHROOT=%s ; ", evlog->runchroot); +- if (evlog->runcwd != NULL || evlog->cwd != NULL) +- printf("CWD=%s ; ", evlog->runcwd ? evlog->runcwd : evlog->cwd); +- printf("USER=%s ; ", evlog->runuser); +- if (evlog->rungroup != NULL) +- printf("GROUP=%s ; ", evlog->rungroup); +- printf("TSID=%s ; COMMAND=%s\n", idstr, evlog->command); ++ sudo_lbuf_append_esc(lbuf, LBUF_ESC_CNTRL, "%s : %s : ", ++ timestr ? timestr : "invalid date", evlog->submituser); ++ if (evlog->submithost != NULL) { ++ sudo_lbuf_append_esc(lbuf, LBUF_ESC_CNTRL, "HOST=%s ; ", ++ evlog->submithost); ++ } ++ if (evlog->ttyname != NULL) { ++ sudo_lbuf_append_esc(lbuf, LBUF_ESC_CNTRL, "TTY=%s ; ", ++ evlog->ttyname); ++ } ++ if (evlog->runchroot != NULL) { ++ sudo_lbuf_append_esc(lbuf, LBUF_ESC_CNTRL, "CHROOT=%s ; ", ++ evlog->runchroot); ++ } ++ if (evlog->runcwd != NULL || evlog->cwd != NULL) { ++ sudo_lbuf_append_esc(lbuf, LBUF_ESC_CNTRL, "CWD=%s ; ", ++ evlog->runcwd ? evlog->runcwd : evlog->cwd); ++ } ++ sudo_lbuf_append_esc(lbuf, LBUF_ESC_CNTRL, "USER=%s ; ", evlog->runuser); ++ if (evlog->rungroup != NULL) { ++ sudo_lbuf_append_esc(lbuf, LBUF_ESC_CNTRL, "GROUP=%s ; ", ++ evlog->rungroup); ++ } ++ sudo_lbuf_append_esc(lbuf, LBUF_ESC_CNTRL, "TSID=%s ; ", idstr); ++ ++ /* ++ * If we have both command and argv from info.json we can escape ++ * blanks in the the command and arguments. If all we have is a ++ * single string containing both the command and arguments we cannot. ++ */ ++ if (evlog->argv != NULL) { ++ /* Command plus argv from the info.json file. */ ++ sudo_lbuf_append_esc(lbuf, LBUF_ESC_CNTRL|LBUF_ESC_BLANK, ++ "COMMAND=%s", evlog->command); ++ if (evlog->argv[0] != NULL) { ++ for (i = 1; evlog->argv[i] != NULL; i++) { ++ sudo_lbuf_append(lbuf, " "); ++ if (strchr(evlog->argv[i], ' ') != NULL) { ++ /* Wrap args containing spaces in single quotes. */ ++ sudo_lbuf_append(lbuf, "'"); ++ sudo_lbuf_append_esc(lbuf, LBUF_ESC_CNTRL|LBUF_ESC_QUOTE, ++ "%s", evlog->argv[i]); ++ sudo_lbuf_append(lbuf, "'"); ++ } else { ++ /* Escape quotes here too for consistency. */ ++ sudo_lbuf_append_esc(lbuf, ++ LBUF_ESC_CNTRL|LBUF_ESC_BLANK|LBUF_ESC_QUOTE, ++ "%s", evlog->argv[i]); ++ } ++ } ++ } ++ } else { ++ /* Single string from the legacy info file. */ ++ sudo_lbuf_append_esc(lbuf, LBUF_ESC_CNTRL, "COMMAND=%s", ++ evlog->command); ++ } + +- ret = 0; ++ if (!sudo_lbuf_error(lbuf)) { ++ puts(lbuf->buf); ++ ret = 0; ++ } + + done: ++ lbuf->error = 0; ++ lbuf->len = 0; + eventlog_free(evlog); + debug_return_int(ret); + } +@@ -1450,6 +1552,7 @@ find_sessions(const char *dir, regex_t * + DIR *d; + struct dirent *dp; + struct stat sb; ++ struct sudo_lbuf lbuf; + size_t sdlen, sessions_len = 0, sessions_size = 0; + unsigned int i; + int len; +@@ -1461,6 +1564,8 @@ find_sessions(const char *dir, regex_t * + #endif + debug_decl(find_sessions, SUDO_DEBUG_UTIL); + ++ sudo_lbuf_init(&lbuf, NULL, 0, NULL, 0); ++ + d = opendir(dir); + if (d == NULL) + sudo_fatal(U_("unable to open %s"), dir); +@@ -1521,7 +1626,7 @@ find_sessions(const char *dir, regex_t * + /* Check for dir with a log file. */ + if (lstat(pathbuf, &sb) == 0 && S_ISREG(sb.st_mode)) { + pathbuf[sdlen + len - 4] = '\0'; +- list_session(pathbuf, re, user, tty); ++ list_session(&lbuf, pathbuf, re, user, tty); + } else { + /* Strip off "/log" and recurse if a non-log dir. */ + pathbuf[sdlen + len - 4] = '\0'; +@@ -1532,6 +1637,7 @@ find_sessions(const char *dir, regex_t * + } + free(sessions); + } ++ sudo_lbuf_destroy(&lbuf); + + debug_return_int(0); + } diff -Nru sudo-1.9.9/debian/patches/CVE-2023-2848x-2.patch sudo-1.9.9/debian/patches/CVE-2023-2848x-2.patch --- sudo-1.9.9/debian/patches/CVE-2023-2848x-2.patch 1970-01-01 00:00:00.000000000 +0000 +++ sudo-1.9.9/debian/patches/CVE-2023-2848x-2.patch 2023-04-03 18:00:44.000000000 +0000 @@ -0,0 +1,22 @@ +Backport of: + +From 12648b4e0a8cf486480442efd52f0e0b6cab6e8b Mon Sep 17 00:00:00 2001 +From: "Todd C. Miller" +Date: Mon, 13 Mar 2023 08:04:32 -0600 +Subject: [PATCH] Add missing " ; " separator between environment variables and + command. This is a regression introduced in sudo 1.9.13. GitHub issue #254. + +--- + lib/eventlog/eventlog.c | 1 + + 1 file changed, 1 insertion(+) + +--- a/lib/eventlog/eventlog.c ++++ b/lib/eventlog/eventlog.c +@@ -185,6 +185,7 @@ new_logline(int event_type, int flags, s + sudo_lbuf_append_esc(&lbuf, LBUF_ESC_CNTRL, " %s", + evlog->env_add[i]); + } ++ sudo_lbuf_append(&lbuf, " ; "); + } + if (evlog->command != NULL) { + sudo_lbuf_append_esc(&lbuf, LBUF_ESC_CNTRL|LBUF_ESC_BLANK, diff -Nru sudo-1.9.9/debian/patches/series sudo-1.9.9/debian/patches/series --- sudo-1.9.9/debian/patches/series 2023-03-01 13:59:27.000000000 +0000 +++ sudo-1.9.9/debian/patches/series 2023-04-03 18:00:31.000000000 +0000 @@ -5,3 +5,5 @@ CVE-2022-33070.patch CVE-2023-22809.patch CVE-2023-27320.patch +CVE-2023-2848x-1.patch +CVE-2023-2848x-2.patch