Hi,
in various situations, including directory listings, progress and
diagnostic messages, the sftp(1) and scp(1) programs print untrusted
strings to the local terminal, often strings that were received
from a remote system over the wire, often containing whatever random
filenames on the remote system may contain, or in general whatever
the remote server sends. That can screw up terminal state, for
example hang the local terminal, change key bindings, change fonts,
encodings, windows titles and many other terminal properties. With
xterm(1), i did not find any vulnerabilities that could execute
arbitrary code injected remotely. It is unclear what other terminals
and terminal emulators may do.
Here are a few examples of file names you can use for testing:
touch `printf "bel\a."`
touch `printf "esc\033[4munder"`
touch `printf "wt\033]0;rogue_title\a."`
Try listing the directory containing such files in sftp(1) interactive
mode remotely or downloading them with sftp(1) or scp(1), in
particular using wildcards. Typically, filename display silently
lacks some of the bytes contained in the filename, but the terminal
executes control codes instead (like beeping, changing fonts,
changing titles, or whatever the controls do).
The following patch sanitizes such strings before sending them to the
terminal. I'm posting it here for wider review because deraadt@
suggested that public feedback is needed, in particular from people
who know a bit about multibyte character handling.
Note that the problems are not related to the following areas:
- Portable OpenSSH. They also affect OpenBSD-current to OpenBSD-current
connections.
- Line editing with libedit / editline(3). They also affect
non-interactive mode.
- Multibyte characters. They also occur when all relevant locales
are set to C/POSIX and only seven-bit bytes are sent.
- Character display widths. The patch below does not attempt
to fix output columnation but only output corruption involving
terminal control codes. While some of the new functions are
already instrumented for width calculation (it doesn't complicate
the code since wcwidth(3) validates printability of a character
just like iswprint(3) does and isn't any harder to use), the
calling code doesn't use that information yet, except in the
progressmeter, where is it easily used without complicating the
code.
Some general remarks:
* The function names in utf8.c are composed from "mbs" for "multibyte
string" and standard printf(3) function names, and they follow the
spirit of the standard functions as closely as they can while still
providing the additional sanitation required. See the comments in
utf8.c for details. The general style of the functions resembles
the style we developed for our ls(1) and similar programs, but as
we already experienced, not two such programs needed exactly the
same helper functions so far, but all have subtly different needs.
* For a few of the strings printed with the new sanitizing functions,
it may not be possible that they contain remote data. But even
local data may pose risks, for example when root copies around
user home directories for backup or migration purposes. So i'd
prefer to err to the side of caution.
* At first, i intended to do this in two steps: First implement
safe handling of US-ASCII, then in a second step add multibyte
character handling. But unfortunately, that doesn't work.
US-ASCII sanitation would break display of filenames containing
multibyte characters, and even though there isn't much explicit
code in the programs to handle such characters yet, they partially
work because most of the code just didn't care in the past what
bytes in file names represented and just passed everything through
verbatim, which is exactly where the problems come from, but
OpenSSH developers pointed out to me that some users may rely
on that, so they don't want to break the existing partial multibyte
support, not even to improve ASCII terminal sanitation. So,
unfortunately, introducing the first bits of explicit multibyte
support and introducing the first bits of terminal sanitation
must be done in a single, rather big step.
* Even this patch isn't perfect yet. It still allows printing
'\r' and '\n' to the terminal at too many places, for example
when remote filenames contain these characters. But the patch
is already big, so fixing that should probably be kept seperate.
* As noted in my recent undeadly posting, protecting the terminal
is only possible if either (a) the terminal is UTF-8 *and* the
remote side is either UTF-8 or US-ASCII, or (b) the *user*
makes sure that the local locale(1) agrees with the remote one.
If the remote side uses a non-UTF-8 and non-ASCII locale *or*
you don't set a matching locale(1) before starting sftp(1) or
scp(1), these programs can still screw up your terminal even
with this patch, and i don't think there is any way to protect
you from that.
* On non-OpenBSD systems, users may legitimately set both the
remote and the local locale(1) to the same encoding that is
neither UTF-8 nor US-ASCII, in the worst case a state-dependent
SHIFT encoding, and i do want to protect users doing that.
However, in state dependent locales, one must NEVER attempt to
recover from encoding errors because those invalidate state
information, so to continue parsing would be utterly unsafe.
Given an arbitrary locale on an arbitrary operating system,
figuring out whether it is safely possible to recover from a
particular error would be very complicated and fragile, which
would be inadequate for security software like OpenSSH.
Consequently, whenever encoding errors or non-printable characters
are encountered in any non-UTF-8, non-ASCII locale, the code
immediately aborts parsing and truncates the string to be displayed
at the point of the first error.
Some comments on specific hunks of the patch:
* progressmeter.c @@ -117,14 +119,14 @@ char buf
The larger buffer will always be large enough on OpenBSD. On
systems with state-dependent encodings, if strings switch state
back and forth a lot, the buffer may get exhausted before the
display width limit is reached, so it is important to always
check the buffer size before adding more bytes to prevent buffer
overruns.
* progressmeter.c @@ -155,17 +157,16 @@ buf[0]
buf[0] = '\r' must be set up front. mbssnprintf() cannot accept
'\r' in the mode with a width limit because that character does
not have a width.
* progressmeter.c @@ -173,18 +174,18 @@ and subsequent
win_size can no longer be used for overflow checks.
Using sizeof(buf) seems safer anyway.
* scp.c @@ -362,6 +364,8 @@ setlocale(LC_CTYPE, "");
Adding #include <locale.h> and setlocale(LC_CTYPE, "") is
necessary. Running in POSIX mode when the terminal is set
to an non-UTF-8 multibyte locale is NOT safe.
Feedback is welcome, and so are OKs.
Ingo
Index: progressmeter.c
===================================================================
RCS file: /cvs/src/usr.bin/ssh/progressmeter.c,v
retrieving revision 1.42
diff -u -p -r1.42 progressmeter.c
--- progressmeter.c 2 Mar 2016 22:42:40 -0000 1.42
+++ progressmeter.c 15 Mar 2016 15:47:37 -0000
@@ -29,6 +29,7 @@
#include <errno.h>
#include <signal.h>
+#include <stdarg.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
@@ -37,6 +38,7 @@
#include "progressmeter.h"
#include "atomicio.h"
#include "misc.h"
+#include "utf8.h"
#define DEFAULT_WINSIZE 80
#define MAX_WINSIZE 512
@@ -117,14 +119,14 @@ format_size(char *buf, int size, off_t b
void
refresh_progress_meter(void)
{
- char buf[MAX_WINSIZE + 1];
+ char buf[MAX_WINSIZE * 4 + 1];
off_t transferred;
double elapsed, now;
int percent;
off_t bytes_left;
int cur_speed;
int hours, minutes, seconds;
- int i, len;
+ size_t i;
int file_len;
transferred = *counter - (cur_pos ? cur_pos : start_pos);
@@ -155,17 +157,16 @@ refresh_progress_meter(void)
bytes_per_second = cur_speed;
/* filename */
- buf[0] = '\0';
+ buf[0] = '\r';
+ buf[1] = '\0';
file_len = win_size - 35;
if (file_len > 0) {
- len = snprintf(buf, file_len + 1, "\r%s", file);
- if (len < 0)
- len = 0;
- if (len >= file_len + 1)
- len = file_len;
- for (i = len; i < file_len; i++)
- buf[i] = ' ';
- buf[file_len] = '\0';
+ (void) mbssnprintf(buf + 1, sizeof(buf) - 1 - 35,
+ &file_len, "%s", file);
+ i = strlen(buf);
+ while (file_len++ < win_size - 35 && i + 1 < sizeof(buf))
+ buf[i++] = ' ';
+ buf[i] = '\0';
}
/* percent of transfer done */
@@ -173,18 +174,18 @@ refresh_progress_meter(void)
percent = ((float)cur_pos / end_pos) * 100;
else
percent = 100;
- snprintf(buf + strlen(buf), win_size - strlen(buf),
+ snprintf(buf + strlen(buf), sizeof(buf) - strlen(buf),
" %3d%% ", percent);
/* amount transferred */
- format_size(buf + strlen(buf), win_size - strlen(buf),
+ format_size(buf + strlen(buf), sizeof(buf) - strlen(buf),
cur_pos);
- strlcat(buf, " ", win_size);
+ strlcat(buf, " ", sizeof(buf));
/* bandwidth usage */
- format_rate(buf + strlen(buf), win_size - strlen(buf),
+ format_rate(buf + strlen(buf), sizeof(buf) - strlen(buf),
(off_t)bytes_per_second);
- strlcat(buf, "/s ", win_size);
+ strlcat(buf, "/s ", sizeof(buf));
/* ETA */
if (!transferred)
@@ -193,9 +194,9 @@ refresh_progress_meter(void)
stalled = 0;
if (stalled >= STALL_TIME)
- strlcat(buf, "- stalled -", win_size);
+ strlcat(buf, "- stalled -", sizeof(buf));
else if (bytes_per_second == 0 && bytes_left)
- strlcat(buf, " --:-- ETA", win_size);
+ strlcat(buf, " --:-- ETA", sizeof(buf));
else {
if (bytes_left > 0)
seconds = bytes_left / bytes_per_second;
@@ -208,19 +209,19 @@ refresh_progress_meter(void)
seconds -= minutes * 60;
if (hours != 0)
- snprintf(buf + strlen(buf), win_size - strlen(buf),
+ snprintf(buf + strlen(buf), sizeof(buf) - strlen(buf),
"%d:%02d:%02d", hours, minutes, seconds);
else
- snprintf(buf + strlen(buf), win_size - strlen(buf),
+ snprintf(buf + strlen(buf), sizeof(buf) - strlen(buf),
" %02d:%02d", minutes, seconds);
if (bytes_left > 0)
- strlcat(buf, " ETA", win_size);
+ strlcat(buf, " ETA", sizeof(buf));
else
- strlcat(buf, " ", win_size);
+ strlcat(buf, " ", sizeof(buf));
}
- atomicio(vwrite, STDOUT_FILENO, buf, win_size - 1);
+ atomicio(vwrite, STDOUT_FILENO, buf, strlen(buf));
last_update = now;
}
Index: scp.c
===================================================================
RCS file: /cvs/src/usr.bin/ssh/scp.c,v
retrieving revision 1.185
diff -u -p -r1.185 scp.c
--- scp.c 2 Mar 2016 22:43:52 -0000 1.185
+++ scp.c 15 Mar 2016 15:47:38 -0000
@@ -83,6 +83,7 @@
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
+#include <locale.h>
#include <pwd.h>
#include <signal.h>
#include <stdarg.h>
@@ -100,6 +101,7 @@
#include "log.h"
#include "misc.h"
#include "progressmeter.h"
+#include "utf8.h"
#define COPY_BUFLEN 16384
@@ -175,7 +177,7 @@ do_local_cmd(arglist *a)
if (verbose_mode) {
fprintf(stderr, "Executing:");
for (i = 0; i < a->num; i++)
- fprintf(stderr, " %s", a->list[i]);
+ mbsfprintf(stderr, " %s", a->list[i]);
fprintf(stderr, "\n");
}
if ((pid = fork()) == -1)
@@ -216,7 +218,7 @@ do_cmd(char *host, char *remuser, char *
int pin[2], pout[2], reserved[2];
if (verbose_mode)
- fprintf(stderr,
+ mbsfprintf(stderr,
"Executing: program %s host %s, user %s, command %s\n",
ssh_program, host,
remuser ? remuser : "(unspecified)", cmd);
@@ -291,7 +293,7 @@ do_cmd2(char *host, char *remuser, char
int status;
if (verbose_mode)
- fprintf(stderr,
+ mbsfprintf(stderr,
"Executing: 2nd program %s host %s, user %s, command %s\n",
ssh_program, host,
remuser ? remuser : "(unspecified)", cmd);
@@ -362,6 +364,8 @@ main(int argc, char **argv)
/* Ensure that fds 0, 1 and 2 are open or directed to /dev/null */
sanitise_stdfd();
+ setlocale(LC_CTYPE, "");
+
/* Copy argv, because we modify it */
newargv = xcalloc(MAX(argc + 1, 1), sizeof(*newargv));
for (n = 0; n < argc; n++)
@@ -789,9 +793,8 @@ syserr: run_err("%s: %s", name, strerr
snprintf(buf, sizeof buf, "C%04o %lld %s\n",
(u_int) (stb.st_mode & FILEMODEMASK),
(long long)stb.st_size, last);
- if (verbose_mode) {
- fprintf(stderr, "Sending file modes: %s", buf);
- }
+ if (verbose_mode)
+ mbsfprintf(stderr, "Sending file modes: %s", buf);
(void) atomicio(vwrite, remout, buf, strlen(buf));
if (response() < 0)
goto next;
@@ -868,7 +871,7 @@ rsource(char *name, struct stat *statp)
(void) snprintf(path, sizeof path, "D%04o %d %.1024s\n",
(u_int) (statp->st_mode & FILEMODEMASK), 0, last);
if (verbose_mode)
- fprintf(stderr, "Entering directory: %s", path);
+ mbsfprintf(stderr, "Entering directory: %s", path);
(void) atomicio(vwrite, remout, path, strlen(path));
if (response() < 0) {
closedir(dirp);
@@ -908,7 +911,7 @@ sink(int argc, char **argv)
off_t size, statbytes;
unsigned long long ull;
int setimes, targisdir, wrerrno = 0;
- char ch, *cp, *np, *targ, *why, *vect[1], buf[2048];
+ char ch, *cp, *np, *targ, *why, *vect[1], buf[2048], visbuf[2048];
struct timeval tv[2];
#define atime tv[0]
@@ -943,12 +946,15 @@ sink(int argc, char **argv)
} while (cp < &buf[sizeof(buf) - 1] && ch != '\n');
*cp = 0;
if (verbose_mode)
- fprintf(stderr, "Sink: %s", buf);
+ mbsfprintf(stderr, "Sink: %s", buf);
if (buf[0] == '\01' || buf[0] == '\02') {
- if (iamremote == 0)
+ if (iamremote == 0) {
+ (void) mbssnprintf(visbuf, sizeof(visbuf),
+ NULL, "%s", buf + 1);
(void) atomicio(vwrite, STDERR_FILENO,
- buf + 1, strlen(buf + 1));
+ visbuf, strlen(visbuf));
+ }
if (buf[0] == '\02')
exit(1);
++errs;
@@ -1183,7 +1189,7 @@ screwup:
int
response(void)
{
- char ch, *cp, resp, rbuf[2048];
+ char ch, *cp, resp, rbuf[2048], visbuf[2048];
if (atomicio(read, remin, &resp, sizeof(resp)) != sizeof(resp))
lostconn(0);
@@ -1203,8 +1209,13 @@ response(void)
*cp++ = ch;
} while (cp < &rbuf[sizeof(rbuf) - 1] && ch != '\n');
- if (!iamremote)
- (void) atomicio(vwrite, STDERR_FILENO, rbuf, cp - rbuf);
+ if (!iamremote) {
+ cp[-1] = '\0';
+ (void) mbssnprintf(visbuf, sizeof(visbuf),
+ NULL, "%s\n", rbuf);
+ (void) atomicio(vwrite, STDERR_FILENO,
+ visbuf, strlen(visbuf));
+ }
++errs;
if (resp == 1)
return (-1);
@@ -1242,7 +1253,7 @@ run_err(const char *fmt,...)
if (!iamremote) {
va_start(ap, fmt);
- vfprintf(stderr, fmt, ap);
+ mbsvfprintf(stderr, fmt, ap);
va_end(ap);
fprintf(stderr, "\n");
}
@@ -1288,7 +1299,7 @@ okname(char *cp0)
} while (*++cp);
return (1);
-bad: fprintf(stderr, "%s: invalid user name\n", cp0);
+bad: mbsfprintf(stderr, "%s: invalid user name\n", cp0);
return (0);
}
Index: sftp-client.c
===================================================================
RCS file: /cvs/src/usr.bin/ssh/sftp-client.c,v
retrieving revision 1.121
diff -u -p -r1.121 sftp-client.c
--- sftp-client.c 11 Feb 2016 02:21:34 -0000 1.121
+++ sftp-client.c 15 Mar 2016 15:47:38 -0000
@@ -46,6 +46,7 @@
#include "atomicio.h"
#include "progressmeter.h"
#include "misc.h"
+#include "utf8.h"
#include "sftp.h"
#include "sftp-common.h"
@@ -604,7 +605,7 @@ do_lsreaddir(struct sftp_conn *conn, con
}
if (print_flag)
- printf("%s\n", longname);
+ mbsprintf("%s\n", longname);
/*
* Directory entries should never contain '/'
@@ -1450,7 +1451,7 @@ download_dir_internal(struct sftp_conn *
return -1;
}
if (print_flag)
- printf("Retrieving %s\n", src);
+ mbsprintf("Retrieving %s\n", src);
if (dirattrib->flags & SSH2_FILEXFER_ATTR_PERMISSIONS)
mode = dirattrib->perm & 01777;
@@ -1782,7 +1783,7 @@ upload_dir_internal(struct sftp_conn *co
return -1;
}
if (print_flag)
- printf("Entering %s\n", src);
+ mbsprintf("Entering %s\n", src);
attrib_clear(&a);
stat_to_attrib(&sb, &a);
Index: sftp.c
===================================================================
RCS file: /cvs/src/usr.bin/ssh/sftp.c,v
retrieving revision 1.172
diff -u -p -r1.172 sftp.c
--- sftp.c 15 Feb 2016 09:47:49 -0000 1.172
+++ sftp.c 15 Mar 2016 15:47:39 -0000
@@ -31,6 +31,7 @@
#include <libgen.h>
#include <locale.h>
#include <signal.h>
+#include <stdarg.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
@@ -43,6 +44,7 @@
#include "log.h"
#include "pathnames.h"
#include "misc.h"
+#include "utf8.h"
#include "sftp.h"
#include "ssherr.h"
@@ -622,9 +624,11 @@ process_get(struct sftp_conn *conn, char
resume |= global_aflag;
if (!quiet && resume)
- printf("Resuming %s to %s\n", g.gl_pathv[i], abs_dst);
+ mbsprintf("Resuming %s to %s\n",
+ g.gl_pathv[i], abs_dst);
else if (!quiet && !resume)
- printf("Fetching %s to %s\n", g.gl_pathv[i], abs_dst);
+ mbsprintf("Fetching %s to %s\n",
+ g.gl_pathv[i], abs_dst);
if (pathname_is_dir(g.gl_pathv[i]) && (rflag || global_rflag)) {
if (download_dir(conn, g.gl_pathv[i], abs_dst, NULL,
pflag || global_pflag, 1, resume,
@@ -713,10 +717,11 @@ process_put(struct sftp_conn *conn, char
resume |= global_aflag;
if (!quiet && resume)
- printf("Resuming upload of %s to %s\n", g.gl_pathv[i],
- abs_dst);
+ mbsprintf("Resuming upload of %s to %s\n",
+ g.gl_pathv[i], abs_dst);
else if (!quiet && !resume)
- printf("Uploading %s to %s\n", g.gl_pathv[i], abs_dst);
+ mbsprintf("Uploading %s to %s\n",
+ g.gl_pathv[i], abs_dst);
if (pathname_is_dir(g.gl_pathv[i]) && (rflag || global_rflag)) {
if (upload_dir(conn, g.gl_pathv[i], abs_dst,
pflag || global_pflag, 1, resume,
@@ -817,12 +822,12 @@ do_ls_dir(struct sftp_conn *conn, char *
attrib_to_stat(&d[n]->a, &sb);
lname = ls_file(fname, &sb, 1,
(lflag & LS_SI_UNITS));
- printf("%s\n", lname);
+ mbsprintf("%s\n", lname);
free(lname);
} else
- printf("%s\n", d[n]->longname);
+ mbsprintf("%s\n", d[n]->longname);
} else {
- printf("%-*s", colspace, fname);
+ mbsprintf("%-*s", colspace, fname);
if (c >= columns) {
printf("\n");
c = 1;
@@ -903,10 +908,10 @@ do_globbed_ls(struct sftp_conn *conn, ch
}
lname = ls_file(fname, g.gl_statv[i], 1,
(lflag & LS_SI_UNITS));
- printf("%s\n", lname);
+ mbsprintf("%s\n", lname);
free(lname);
} else {
- printf("%-*s", colspace, fname);
+ mbsprintf("%-*s", colspace, fname);
if (c >= columns) {
printf("\n");
c = 1;
@@ -1434,7 +1439,7 @@ parse_dispatch_command(struct sftp_conn
remote_glob(conn, path1, GLOB_NOCHECK, NULL, &g);
for (i = 0; g.gl_pathv[i] && !interrupted; i++) {
if (!quiet)
- printf("Removing %s\n", g.gl_pathv[i]);
+ mbsprintf("Removing %s\n", g.gl_pathv[i]);
err = do_rm(conn, g.gl_pathv[i]);
if (err != 0 && err_abort)
break;
@@ -1534,7 +1539,8 @@ parse_dispatch_command(struct sftp_conn
remote_glob(conn, path1, GLOB_NOCHECK, NULL, &g);
for (i = 0; g.gl_pathv[i] && !interrupted; i++) {
if (!quiet)
- printf("Changing mode on %s\n", g.gl_pathv[i]);
+ mbsprintf("Changing mode on %s\n",
+ g.gl_pathv[i]);
err = do_setstat(conn, g.gl_pathv[i], &a);
if (err != 0 && err_abort)
break;
@@ -1564,12 +1570,12 @@ parse_dispatch_command(struct sftp_conn
aa->flags &= SSH2_FILEXFER_ATTR_UIDGID;
if (cmdnum == I_CHOWN) {
if (!quiet)
- printf("Changing owner on %s\n",
+ mbsprintf("Changing owner on %s\n",
g.gl_pathv[i]);
aa->uid = n_arg;
} else {
if (!quiet)
- printf("Changing group on %s\n",
+ mbsprintf("Changing group on %s\n",
g.gl_pathv[i]);
aa->gid = n_arg;
}
@@ -1579,7 +1585,7 @@ parse_dispatch_command(struct sftp_conn
}
break;
case I_PWD:
- printf("Remote working directory: %s\n", *pwd);
+ mbsprintf("Remote working directory: %s\n", *pwd);
break;
case I_LPWD:
if (!getcwd(path_buf, sizeof(path_buf))) {
@@ -1587,7 +1593,7 @@ parse_dispatch_command(struct sftp_conn
err = -1;
break;
}
- printf("Local working directory: %s\n", path_buf);
+ mbsprintf("Local working directory: %s\n", path_buf);
break;
case I_QUIT:
/* Processed below */
@@ -1655,7 +1661,7 @@ complete_display(char **list, u_int len)
for (y = 0; list[y]; y++) {
llen = strlen(list[y]);
tmp = llen > len ? list[y] + len : "";
- printf("%-*s", colspace, tmp);
+ mbsprintf("%-*s", colspace, tmp);
if (m >= columns) {
printf("\n");
m = 1;
@@ -2036,7 +2042,7 @@ interactive_loop(struct sftp_conn *conn,
if (remote_is_dir(conn, dir) && file2 == NULL) {
if (!quiet)
- printf("Changing to: %s\n", dir);
+ mbsprintf("Changing to: %s\n", dir);
snprintf(cmd, sizeof cmd, "cd \"%s\"", dir);
if (parse_dispatch_command(conn, cmd,
&remote_path, 1) != 0) {
@@ -2082,7 +2088,7 @@ interactive_loop(struct sftp_conn *conn,
break;
}
if (!interactive) { /* Echo command */
- printf("sftp> %s", cmd);
+ mbsprintf("sftp> %s", cmd);
if (strlen(cmd) > 0 &&
cmd[strlen(cmd) - 1] != '\n')
printf("\n");
Index: utf8.c
===================================================================
RCS file: utf8.c
diff -N utf8.c
--- /dev/null 1 Jan 1970 00:00:00 -0000
+++ utf8.c 15 Mar 2016 15:47:39 -0000
@@ -0,0 +1,163 @@
+/* $OpenBSD$ */
+/*
+ * Copyright (c) 2016 Ingo Schwarze <[email protected]>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+/*
+ * Utility functions for multibyte-character handling,
+ * in particular to sanitize untrusted strings for terminal output.
+ */
+
+#include <sys/types.h>
+#include <langinfo.h>
+#include <limits.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "utf8.h"
+
+static int dangerous_locale(void);
+static int mbsvsnprintf(char *, size_t, int *, const char *, va_list);
+
+
+/*
+ * For US-ASCII and UTF-8 encodings, we can safely recover from
+ * encoding errors and from non-printable characters. For any
+ * other encodings, err to the side of caution and abort parsing:
+ * For state-dependent encodings, recovery is impossible.
+ * For arbitrary encodings, replacement of non-printable
+ * characters would be non-trivial and too fragile.
+ */
+
+static int
+dangerous_locale(void) {
+ char *loc;
+
+ loc = nl_langinfo(CODESET);
+ return strcmp(loc, "US-ASCII") && strcmp(loc, "UTF-8");
+}
+
+/*
+ * The following two functions limit the number of bytes written,
+ * including the terminating '\0', to sz. Unless wp is NULL,
+ * they limit the number of display columns occupied to *wp.
+ * Whichever is reached first terminates the output string.
+ * To stay close to the standard interfaces, they return the number of
+ * non-NUL bytes that would have been written if both were unlimited.
+ * If wp is NULL, newline, carriage return, and tab are allowed;
+ * otherwise, the actual number of columns occupied by what was
+ * written is returned in *wp.
+ */
+
+static int
+mbsvsnprintf(char *str, size_t sz, int *wp, const char *fmt, va_list ap)
+{
+ char *sp;
+ wchar_t wc;
+ int len, width, total_width, max_width, ret;
+
+ if ((ret = vsnprintf(str, sz, fmt, ap)) <= 0)
+ *str = '\0';
+
+ total_width = 0;
+ max_width = wp == NULL ? INT_MAX : *wp;
+ for (sp = str; *sp != '\0'; sp += len) {
+ if ((len = mbtowc(&wc, sp, MB_CUR_MAX)) == -1) {
+ (void)mbtowc(NULL, NULL, MB_CUR_MAX);
+ if (dangerous_locale()) {
+ *sp = '\0';
+ break;
+ }
+ *sp = '?';
+ len = 1;
+ width = 1;
+ } else if (wp == NULL &&
+ (wc == L'\n' || wc == L'\r' || wc == L'\t')) {
+ width = 0;
+ } else if ((width = wcwidth(wc)) == -1) {
+ if (dangerous_locale()) {
+ *sp = '\0';
+ break;
+ }
+ memset(sp, '?', len);
+ width = len;
+ }
+ if (total_width + width > max_width) {
+ *sp = '\0';
+ break;
+ }
+ total_width += width;
+ }
+ if (wp != NULL)
+ *wp = total_width;
+ return ret;
+}
+
+int
+mbssnprintf(char *str, size_t sz, int *wp, const char *fmt, ...)
+{
+ va_list ap;
+ int ret;
+
+ va_start(ap, fmt);
+ ret = mbsvsnprintf(str, sz, wp, fmt, ap);
+ va_end(ap);
+ return ret;
+}
+
+/*
+ * To stay close to the standard interfaces, the following functions
+ * return the number of non-NUL bytes written.
+ * Truncation detection is not needed and not supported.
+ */
+
+int
+mbsvfprintf(FILE *stream, const char *fmt, va_list ap)
+{
+ char msgbuf[2048];
+ int ret;
+
+ ret = mbsvsnprintf(msgbuf, sizeof(msgbuf), NULL, fmt, ap);
+ if (ret >= sizeof(msgbuf))
+ ret = sizeof(msgbuf) - 1;
+ fputs(msgbuf, stream);
+ return ret;
+}
+
+int
+mbsfprintf(FILE *stream, const char *fmt, ...)
+{
+ va_list ap;
+ int ret;
+
+ va_start(ap, fmt);
+ ret = mbsvfprintf(stream, fmt, ap);
+ va_end(ap);
+ return ret;
+}
+
+int
+mbsprintf(const char *fmt, ...)
+{
+ va_list ap;
+ int ret;
+
+ va_start(ap, fmt);
+ ret = mbsvfprintf(stdout, fmt, ap);
+ va_end(ap);
+ return ret;
+}
Index: utf8.h
===================================================================
RCS file: utf8.h
diff -N utf8.h
--- /dev/null 1 Jan 1970 00:00:00 -0000
+++ utf8.h 15 Mar 2016 15:47:39 -0000
@@ -0,0 +1,24 @@
+/* $OpenBSD$ */
+/*
+ * Copyright (c) 2016 Ingo Schwarze <[email protected]>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+int mbsprintf(const char *, ...)
+ __attribute__((format(printf, 1, 2)));
+int mbsfprintf(FILE *, const char *, ...)
+ __attribute__((format(printf, 2, 3)));
+int mbsvfprintf(FILE *, const char *, va_list);
+int mbssnprintf(char *, size_t, int *, const char *, ...)
+ __attribute__((format(printf, 4, 5)));
Index: lib/Makefile
===================================================================
RCS file: /cvs/src/usr.bin/ssh/lib/Makefile,v
retrieving revision 1.84
diff -u -p -r1.84 Makefile
--- lib/Makefile 14 Jan 2016 16:17:40 -0000 1.84
+++ lib/Makefile 15 Mar 2016 15:47:39 -0000
@@ -28,7 +28,7 @@ SRCS= ${LIB_SRCS} \
cleanup.c compat.c crc32.c deattack.c fatal.c \
hostfile.c log.c match.c nchan.c packet.c opacket.c readpass.c \
ttymodes.c xmalloc.c atomicio.c \
- key.c dispatch.c kex.c mac.c uidswap.c uuencode.c misc.c \
+ key.c dispatch.c kex.c mac.c uidswap.c uuencode.c misc.c utf8.c \
msg.c progressmeter.c dns.c \
monitor_fdpass.c addrmatch.c \
smult_curve25519_ref.c \