From: Willy Tarreau Date: Sat, 15 Aug 2026 18:43:58 +0000 (+0200) Subject: MINOR: ring: add "-s" flag to "show events" to sanitize the output X-Git-Tag: v3.5-dev6~13 X-Git-Url: https://git.kaiwu.me/?a=commitdiff_plain;h=76a739c5deafb7bfe5f5ab341d85e0d66f83c54a;p=haproxy.git MINOR: ring: add "-s" flag to "show events" to sanitize the output "show events" and the other ring dumps (show startup-logs, show errors) emit each ring message verbatim via applet_append_line(). Rings can hold attacker-influenced contents (forwarded syslog lines, log-format output emitted without "+E") that contains ESC, CR, LF or BS. Because the CLI writes the result straight to the user's terminal, such a dump can for example inject ANSI escape sequences and rewrite the screen, BS and visually erase some fields on the screen, or even use UNICODE code points that switch the writing direction from LTR to RTL and conversely. This patch adds an opt-in "-s" flag to "show events", in order to sanitize displayed strings. When set, each event is copied into the thread-local trash chunk and every byte is sanitised before being appended: whitespaces (spaces and tabs) are printed as a space, any other control character (ESC, CR, LF, NUL, ...) as well as non-ASCII as '?', and everything else verbatim. The behaviour is selected by a new RING_WF_SANITIZE watch flag that switches cli_io_handler_show_ring() to a dedicated appender (show_ring_append_escaped()) instead of applet_append_line(). The sanitisation is open-coded rather than reusing existing functions which would stop at NUL. Here we must walk exactly bytes and keep the output length identical to the input so that offsets stay valid. The flag is opt-in on purpose: the primary use of "show events" is a byte-for-byte tail of the log ring piped into grep/awk/parser, or even the dataplane API, and escaping by default would corrupt that. Users who who want a safe output should just pass "-s". Default output is unchanged. Maybe we could change the default in a future version if there is demand. No backport needed though it can be useful if it's easily done. Reported-by: Claude (ANT-2026-ER52H2CR) --- diff --git a/doc/management.txt b/doc/management.txt index e5bf389a1..61b592a63 100644 --- a/doc/management.txt +++ b/doc/management.txt @@ -3185,7 +3185,7 @@ show errors [|] [request|response] error was at byte 23. This is the slash ('/') in header name "header/bizarre", which is not a valid HTTP character for a header name. -show events [] [-w] [-n] [-0] +show events [] [-w] [-n] [-0] [-s] With no option, this lists all known event sinks and their types. With an option, it will dump all available events in the designated sink if it is of type buffer. If option "-w" is passed after the sink name, then once the end @@ -3197,6 +3197,16 @@ show events [] [-w] [-n] [-0] may be used to enable both options at once. By default, all events are delimited by a line feed character ('\n' or 10 or 0x0A). It is possible to change this to the NUL character ('\0' or 0) by passing the "-0" argument. + Option "-s" sanitizes the output, so that binary contents resulting from + captured or decoded data do not alter the terminal's rendering. Whitespace + characters (space and tab) are replaced with a space so that each record + stays on a single line, and the other control characters (0x00 to 0x1F and + 0x7F) as well as non-ASCII chars (0x80 and above) are replaced with a '?'. + The output length is preserved. This way, dumping a ring cannot inject ANSI + escape sequences nor writing direction codes into the operator's terminal, + which is useful when dumping to a terminal, or to a program whose input must + not be interpreted as special (e.g. monitoring scripts whose output is copied + verbatim into reports). show fd [-!plcfbs]* [[]/[] | ] Dump the list of either all open file descriptors or just the one number diff --git a/include/haproxy/ring-t.h b/include/haproxy/ring-t.h index 1d9d5b9dd..26d7c98ea 100644 --- a/include/haproxy/ring-t.h +++ b/include/haproxy/ring-t.h @@ -96,6 +96,7 @@ #define RING_WF_WAIT_MODE 0x00000001 /* wait for new contents */ #define RING_WF_SEEK_NEW 0x00000002 /* seek to new contents */ #define RING_WF_END_ZERO 0x00000004 /* mark end of events with \0 instead of \n */ +#define RING_WF_SANITIZE 0x00000008 /* sanitize control chars characters (e.g. ESC) in dumped events */ /* ring flags */ #define RING_FL_MAPPED 0x00000001 /* mmapped area, must not free() */ diff --git a/src/ring.c b/src/ring.c index ae0ccdfbe..d8e1a8934 100644 --- a/src/ring.c +++ b/src/ring.c @@ -18,6 +18,7 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ +#include #include #include #include @@ -717,6 +718,48 @@ int ring_dispatch_messages(struct ring *ring, void *ctx, size_t *ofs_ptr, size_t return ret; } +/* Same as applet_append_line() but sanitises each event before appending it, + * so that dumping a ring cannot inject control chars (CR, LF, BS, NUL, ANSI + * escapes etc) nor non-ASCII into the operator's terminal, as rings may hold + * contents from untrusted sources (forwarded syslog lines, log-format output + * emitted without "+E"). For every input byte, control chars are replaced + * with '?', and everything else verbatim. This function is selected via + * RING_WF_SANITIZE in flags>. + */ +static ssize_t show_ring_append_sanitized(void *ctx, struct ist v1, struct ist v2, size_t ofs, size_t len, char delim) +{ + struct appctx *appctx = ctx; + char *p; + size_t i; + + if (unlikely(len + 1 > b_size(&trash))) { + /* too large a message to ever fit, let's skip it */ + return -2; + } + + chunk_reset(&trash); + vp_peek_ofs(v1, v2, ofs, trash.area, len); + for (i = 0, p = trash.area; i < len; i++, p++) { + if (isblank((unsigned char)*p)) + *p = ' '; + else if (iscntrl((unsigned char)*p) || (unsigned char)*p >= 0x80) + *p = '?'; + } + /* the delimiter is always emitted, including when it is a NUL byte + * (option "-0"), otherwise records would be glued together. + */ + trash.data += len; + trash.area[trash.data++] = delim; + + if (applet_putchk(appctx, &trash) == -1) { + if (applet_output_data(appctx) > 0) + return -1; // buffer is full + else + return -2; // buffer empty, output will never fit + } + return len; +} + /* This function dumps all events from the ring whose pointer is in into * the appctx's output buffer, and takes from the seek offset into the * buffer's history (0 for oldest known event). It looks at for boolean @@ -736,7 +779,8 @@ int cli_io_handler_show_ring(struct appctx *appctx) MT_LIST_DELETE(&appctx->wait_entry); - ret = ring_dispatch_messages(ring, appctx, &ctx->ofs, &last_ofs, ctx->flags, applet_append_line, + ret = ring_dispatch_messages(ring, appctx, &ctx->ofs, &last_ofs, ctx->flags, + (ctx->flags & RING_WF_SANITIZE) ? show_ring_append_sanitized : applet_append_line, (ctx->flags & RING_WF_END_ZERO) ? 0 : '\n', NULL); if (ret && (ctx->flags & RING_WF_WAIT_MODE)) { diff --git a/src/sink.c b/src/sink.c index e08a8abfb..2ae4e202d 100644 --- a/src/sink.c +++ b/src/sink.c @@ -363,7 +363,7 @@ static int cli_parse_show_events(char **args, char *payload, struct appctx *appc if (!*args[1]) { /* no arg => report the list of supported sink */ - chunk_printf(&trash, "Supported events sinks are listed below. Add -0(zero), -w(wait), -n(new). Any key to stop.\n"); + chunk_printf(&trash, "Supported events sinks are listed below. Add -0(zero), -w(wait), -n(new), -s(anitize). Any key to stop.\n"); list_for_each_entry(sink, &sink_list, sink_list) { chunk_appendf(&trash, " %-10s : type=%s, %u dropped, %s\n", sink->name, @@ -397,6 +397,8 @@ static int cli_parse_show_events(char **args, char *payload, struct appctx *appc ring_flags |= RING_WF_END_ZERO; else if (strcmp(args[arg], "-nw") == 0 || strcmp(args[arg], "-wn") == 0) ring_flags |= RING_WF_WAIT_MODE | RING_WF_SEEK_NEW; + else if (strcmp(args[arg], "-s") == 0) + ring_flags |= RING_WF_SANITIZE; else return cli_err(appctx, "unknown option"); }