<div dir="ltr"><div class="gmail-gs" style="margin:0px;min-width:0px;padding:0px 0px 20px;width:auto;font-family:"Google Sans",Roboto,RobotoDraft,Helvetica,Arial,sans-serif;font-size:medium"><div class="gmail-"><div id="gmail-:xo" class="gmail-ii gmail-gt" style="direction:ltr;margin:8px 0px 0px;padding:0px;font-size:0.875rem;overflow-x:hidden"><div id="gmail-:xn" class="gmail-a3s gmail-aiL" style="direction:ltr;font-variant-numeric:normal;font-variant-east-asian:normal;font-variant-alternates:normal;font-size-adjust:none;font-kerning:auto;font-feature-settings:normal;font-stretch:normal;font-size:small;line-height:1.5;font-family:Arial,Helvetica,sans-serif;overflow:auto hidden"><div dir="ltr">Hi BusyBox team,<br><br>I recently sent you an email detailing the HTTP header-injection vulnerability with PoCs;here are the minimal patches. Additionally, this submission fixes an unrelated inline TODO in the FTP code (not included in my previous email): it implements proper handling on the FTP control path.<br><br>Summary<br><br>wget: HTTP header injection via request-target (CR/LF in path)<br><br>Before: If the URL path contains actual CR/LF, the request line is split and the remainder is interpreted as forged headers.<br>After: Such URLs are rejected up front with bad URL; no forged headers are sent.<br><br>wget: FTP control-path hardening for SIZE / RETR<br><br>Before: Path bytes were sent raw on the control channel; 0xff (Telnet IAC) could be misinterpreted, and the source contained a TODO about escaping.<br>After (this patch): Implement the TODO by duplicating 0xff (IAC) for SIZE/RETR so IAC is never emitted as data.<br><br>vi: sanitize names/patterns on the status line and in error messages<br><br>Before: File names and user-supplied strings could carry terminal control sequences (e.g., ANSI escape codes) or other non-printable bytes that would be written to the status line verbatim.<br>After: Use the existing print_literal() consistently before printing those strings (no functional change otherwise).<br><br>What the patches do, concretely<br>wget (shared outbound validation + FTP IAC rule)<br><br>Add die_on_bad_http_bytes(const char *s, int reject_space, const char *what) and call it before protocol branching:<br><br>Rejects CR (0x0D), LF (0x0A), all C0 controls (<0x20), and DEL (0x7F).<br>For the request-target (path/query) only, also rejects space (0x20) to preserve the HTTP/1.1 request-line shape (METHOD SP request-target SP HTTP/1.1).<br><br>Add ftp_sanitize_path(const char *s) and apply it to SIZE and RETR:<br><br>Duplicate 0xff (Telnet IAC) on the FTP control connection (implements the in-code TODO).<br><br>vi<br><br>Route file names/patterns through print_literal() in status/errors (format_edit_status(), status_line_bold_errno(), file_insert() diagnostics, regex error in char_search(), and the :write/:edit/:read/:file outputs).<br><br>Signed-off-by: Takeuchi Yuma<br>---<br>diff --git a/networking/wget.c b/networking/wget.c<br>index ec3767793..0c3d72a66 100644<br>--- a/networking/wget.c<br>+++ b/networking/wget.c<br>@@ -474,6 +474,40 @@ static char* sanitize_string(char *s)<br>        return s;<br> }<br><br>+/* Unified validator:<br>+ * Reject CR/LF and other control characters. Optionally reject SP as well.<br>+ * Use reject_space=1 for the HTTP request-target (path/query), because a space<br>+ * would split the request line and enable header injection.<br>+ * Keep this minimal to avoid code size growth (no %-encoding here).<br>+ */<br>+static void die_on_bad_http_bytes(const char *s, int reject_space, const char *what)<br>+{<br>+       const unsigned char *p = (const unsigned char*)s;<br>+       while (*p) {<br>+               unsigned char c = *p++;<br>+               if (c == '\r' || c == '\n' || c < 0x20 || c == 0x7f || (reject_space && c == ' '))<br>+                       bb_error_msg_and_die("bad %s", what);<br>+       }<br>+}<br>+<br>+#if ENABLE_FEATURE_WGET_FTP<br>+/* FTP path sanitizer:<br>+ * - Disallow CR/LF (would terminate the control command)<br>+ * - Double 0xff (Telnet IAC) per RFC/telnet rules<br>+ * Minimal approach: reject CR/LF, duplicate IAC, no other transformations.<br>+ */<br>+static char *ftp_sanitize_path(const char *s)<br>+{<br>+       const unsigned char *p = (const unsigned char*)s;<br>+       size_t len = 0, iac = 0;<br>+       for (; *p; p++) { if (*p == 0xff) iac++; len++; }<br>+       if (!iac) return xstrdup(s);<br>+       char *out = xmalloc(len + iac + 1), *q = out;<br>+       for (p = (const unsigned char*)s; *p; p++) { *q++ = *p; if (*p == 0xff) *q++ = 0xff; }<br>+       *q = '\0'; return out;<br>+}<br>+#endif<br>+<br> /* Returns '\n' if it was seen, else '\0'. Trims at first '\r' or '\n' */<br> static char fgets_trim_sanitize(FILE *fp, const char *fmt)<br> {<br>@@ -862,12 +896,16 @@ static FILE* prepare_ftp_session(FILE **dfpp, struct host_info *target, len_and_<br>        ftpcmd("TYPE I", NULL, sfp);<br><br>        /* Query file size */<br>-       if (ftpcmd("SIZE ", target->path, sfp) == 213) {<br>-               G.content_len = BB_STRTOOFF(G.wget_buf + 4, NULL, 10);<br>-               if (G.content_len < 0 || errno) {<br>-                       bb_error_msg_and_die("bad SIZE value '%s'", G.wget_buf + 4);<br>+       {<br>+               char *ftp_path = ftp_sanitize_path(target->path);<br>+               int code = ftpcmd("SIZE ", ftp_path, sfp);<br>+               free(ftp_path);<br>+               if (code == 213) {<br>+                       G.content_len = BB_STRTOOFF(G.wget_buf + 4, NULL, 10);<br>+                       if (G.content_len < 0 || errno)<br>+                               bb_error_msg_and_die("bad SIZE value '%s'", G.wget_buf + 4);<br>+                       G.got_clen = 1;<br>                }<br>-               G.got_clen = 1;<br>        }<br><br>        /* Enter passive mode */<br>@@ -904,11 +942,14 @@ static FILE* prepare_ftp_session(FILE **dfpp, struct host_info *target, len_and_<br>                        reset_beg_range_to_zero();<br>        }<br><br>-//TODO: needs ftp-escaping 0xff and '\n' bytes here.<br>-//Or disallow '\n' altogether via sanitize_string() in parse_url().<br>-//But 0xff's are possible in valid utf8 filenames.<br>-       if (ftpcmd("RETR ", target->path, sfp) > 150)<br>-               bb_error_msg_and_die("bad response to %s: %s", "RETR", G.wget_buf);<br>+       /* Apply FTP sanitization when issuing RETR to prevent command injection */<br>+       {<br>+               char *ftp_path = ftp_sanitize_path(target->path);<br>+               int code = ftpcmd("RETR ", ftp_path, sfp);<br>+               free(ftp_path);<br>+               if (code > 150)<br>+                       bb_error_msg_and_die("bad response to %s: %s", "RETR", G.wget_buf);<br>+       }<br><br>        return sfp;<br> }<br>@@ -1177,6 +1218,12 @@ static void download_one_url(const char *url)<br>  establish_session:<br>        /*G.content_len = 0; - redundant, got_clen = 0 is enough */<br>        G.got_clen = 0;<br>+       /* Prevent request-line/header injection.<br>+        * For request-target, also reject space characters.<br>+        */<br>+       die_on_bad_http_bytes(target.path, 1, "URL");<br>+       die_on_bad_http_bytes(target.host, 0, "Host");<br>+<br>        G.chunked = 0;<br>        if (!ENABLE_FEATURE_WGET_FTP<br>         || use_proxy || target.protocol[0] != 'f' /*not ftp[s]*/<br><br><br>Signed-off-by: Takeuchi Yuma<br>---<br>diff --git a/editors/vi.c b/editors/vi.c<br>index 34932f60c..0e941d924 100644<br>--- a/editors/vi.c<br>+++ b/editors/vi.c<br>@@ -556,6 +556,7 @@ static int crashme = 0;<br><br> static void show_status_line(void);    // put a message on the bottom line<br> static void status_line_bold(const char *, ...);<br>+static void print_literal(char *buf, const char *s);<br><br> static void show_help(void)<br> {<br>@@ -1291,19 +1292,29 @@ static int format_edit_status(void)<br>        trunc_at = columns < STATUS_BUFFER_LEN-1 ?<br>                columns : STATUS_BUFFER_LEN-1;<br><br>-       ret = snprintf(status_buffer, trunc_at+1,<br>+       {<br>+               const char *fname_disp;<br>+               char fn_buf[MAX_INPUT_LEN];<br>+               if (current_filename) {<br>+                       print_literal(fn_buf, current_filename);<br>+                       fname_disp = fn_buf;<br>+               } else {<br>+                       fname_disp = "No file";<br>+               }<br>+               ret = snprintf(status_buffer, trunc_at+1,<br> #if ENABLE_FEATURE_VI_READONLY<br>-               "%c %s%s%s %d/%d %d%%",<br>+                       "%c %s%s%s %d/%d %d%%",<br> #else<br>-               "%c %s%s %d/%d %d%%",<br>+                       "%c %s%s %d/%d %d%%",<br> #endif<br>-               cmd_mode_indicator[cmd_mode & 3],<br>-               (current_filename != NULL ? current_filename : "No file"),<br>+                       cmd_mode_indicator[cmd_mode & 3],<br>+                       fname_disp,<br> #if ENABLE_FEATURE_VI_READONLY<br>-               (readonly_mode ? " [Readonly]" : ""),<br>+                       (readonly_mode ? " [Readonly]" : ""),<br> #endif<br>-               (modified_count ? " [Modified]" : ""),<br>-               cur, tot, percent);<br>+                       (modified_count ? " [Modified]" : ""),<br>+                       cur, tot, percent);<br>+       }<br><br>        if (ret >= 0 && ret < trunc_at)<br>                return ret;  // it all fit<br>@@ -1376,7 +1387,13 @@ static void status_line_bold(const char *format, ...)<br> }<br> static void status_line_bold_errno(const char *fn)<br> {<br>-       status_line_bold("'%s' "STRERROR_FMT, fn STRERROR_ERRNO);<br>+       char fnbuf[MAX_INPUT_LEN];<br>+       if (!fn) {<br>+               fnbuf[0] = '\0';<br>+       } else {<br>+               print_literal(fnbuf, fn);<br>+       }<br>+       status_line_bold("'%s' "STRERROR_FMT, fnbuf STRERROR_ERRNO);<br> }<br><br> // copy s to buf, convert unprintable<br>@@ -2003,7 +2020,9 @@ static int file_insert(const char *fn, char *p, int initial)<br>                goto fi;<br>        }<br>        if (!S_ISREG(statbuf.st_mode)) {<br>-               status_line_bold("'%s' is not a regular file", fn);<br>+               char fnbuf[MAX_INPUT_LEN];<br>+               print_literal(fnbuf, fn);<br>+               status_line_bold("'%s' is not a regular file", fnbuf);<br>                goto fi;<br>        }<br>        size = (statbuf.st_size < INT_MAX ? (int)statbuf.st_size : INT_MAX);<br>@@ -2015,7 +2034,12 @@ static int file_insert(const char *fn, char *p, int initial)<br>        } else if (cnt < size) {<br>                // There was a partial read, shrink unused space<br>                p = text_hole_delete(p + cnt, p + size - 1, NO_UNDO);<br>-               status_line_bold("can't read '%s'", fn);<br>+               {<br>+                       char fnbuf[MAX_INPUT_LEN];<br>+                       print_literal(fnbuf, fn);<br>+                       status_line_bold("can't read '%s'",<br>+                                       fnbuf);<br>+               }<br>        }<br> # if ENABLE_FEATURE_VI_UNDO<br>        else {<br>@@ -2401,7 +2425,9 @@ static char *char_search(char *p, const char *pat, int dir_and_range)<br>        preg.not_bol = p != text;<br>        preg.not_eol = p != end - 1;<br>        if (err != NULL) {<br>-               status_line_bold("bad search pattern '%s': %s", pat, err);<br>+               char pbuf[MAX_INPUT_LEN];<br>+               print_literal(pbuf, pat);<br>+               status_line_bold("bad search pattern '%s': %s", pbuf, err);<br>                return p;<br>        }<br><br>@@ -2824,10 +2850,16 @@ static void colon(char *buf)<br>                } else {<br>                        modified_count = 0;<br>                        last_modified_count = -1;<br>-                       status_line("'%s' %uL, %uC",<br>-                               current_filename,<br>+                       {<br>+                               char fnbuf[MAX_INPUT_LEN];<br>+                               const char *disp = current_filename<br>+                                       ? (print_literal(fnbuf, current_filename), fnbuf)<br>+                                       : "No file";<br>+                               status_line("'%s' %uL, %uC",<br>+                                       disp,<br>                                count_lines(text, end - 1), cnt<br>-                       );<br>+                               );<br>+                       }<br>                        if (p[0] == 'x'<br>                         || p[1] == 'q' || p[1] == 'n'<br>                        ) {<br>@@ -2977,6 +3009,7 @@ static void colon(char *buf)<br>                dot_skip_over_ws();<br>        } else if (strncmp(cmd, "edit", i) == 0) {      // Edit a file<br>                int size;<br>+               char fnbuf[MAX_INPUT_LEN];<br><br>                // don't edit, if the current file has been modified<br>                if (modified_count && !useforce) {<br>@@ -3008,16 +3041,21 @@ static void colon(char *buf)<br> # endif<br>                // how many lines in text[]?<br>                li = count_lines(text, end - 1);<br>-               status_line("'%s'%s"<br>+               {<br>+                       const char *disp = args[0]<br>+                               ? (print_literal(fnbuf, fn), fnbuf)<br>+                               : (current_filename ? (print_literal(fnbuf, current_filename), fnbuf) : "No file");<br>+                       status_line("'%s'%s"<br>                        IF_FEATURE_VI_READONLY("%s")<br>                        " %uL, %uC",<br>-                       fn,<br>+                       disp,<br>                        (size < 0 ? " [New file]" : ""),<br>                        IF_FEATURE_VI_READONLY(<br>                                ((readonly_mode) ? " [Readonly]" : ""),<br>                        )<br>                        li, (int)(end - text)<br>-               );<br>+                       );<br>+               }<br>        } else if (strncmp(cmd, "file", i) == 0) {      // what File is this<br>                if (e >= 0) {<br>                        status_line_bold("No address allowed on this command");<br>@@ -3109,6 +3147,7 @@ static void colon(char *buf)<br>                editing = 0;<br>        } else if (strncmp(cmd, "read", i) == 0) {      // read file into text[]<br>                int size, num;<br>+               char fnbuf[MAX_INPUT_LEN];<br><br>                if (args[0]) {<br>                        // the user supplied a file name<br>@@ -3141,13 +3180,18 @@ static void colon(char *buf)<br>                        goto ret;       // nothing was inserted<br>                // how many lines in text[]?<br>                li = count_lines(q, q + size - 1);<br>-               status_line("'%s'"<br>+               {<br>+                       const char *disp = args[0]<br>+                               ? (print_literal(fnbuf, fn), fnbuf)<br>+                               : (current_filename ? (print_literal(fnbuf, current_filename), fnbuf) : "No file");<br>+                       status_line("'%s'"<br>                        IF_FEATURE_VI_READONLY("%s")<br>                        " %uL, %uC",<br>-                       fn,<br>+                       disp,<br>                        IF_FEATURE_VI_READONLY((readonly_mode ? " [Readonly]" : ""),)<br>                        li, size<br>-               );<br>+                       );<br>+               }<br>                dot = find_line(num);<br>        } else if (strncmp(cmd, "rewind", i) == 0) {    // rewind cmd line args<br>                if (modified_count && !useforce) {<br>@@ -3365,7 +3409,10 @@ static void colon(char *buf)<br>                }<br> # if ENABLE_FEATURE_VI_READONLY<br>                else if (readonly_mode && !useforce && fn) {<br>-                       status_line_bold("'%s' is read only", fn);<br>+                       char rb[MAX_INPUT_LEN];<br>+                       print_literal(rb, fn);<br>+                       status_line_bold("'%s' is read only",<br>+                                       rb);<br>                        goto ret;<br>                }<br> # endif<br>@@ -3394,7 +3441,13 @@ static void colon(char *buf)<br>                } else {<br>                        // how many lines written<br>                        li = count_lines(q, q + l - 1);<br>-                       status_line("'%s' %uL, %uC", fn, li, l);<br>+                       {<br>+                               char fnbuf[MAX_INPUT_LEN];<br>+                               const char *disp = fn<br>+                                       ? (print_literal(fnbuf, fn), fnbuf)<br>+                                       : (current_filename ? (print_literal(fnbuf, current_filename), fnbuf) : "No file");<br>+                               status_line("'%s' %uL, %uC", disp, li, l);<br>+                       }<br>                        if (l == size) {<br>                                if (q == text && q + l == end) {<br>                                        modified_count = 0;</div><div class="gmail-yj6qo"></div><div class="gmail-adL"></div></div></div><div class="gmail-WhmR8e" style="clear:both"></div></div></div><br class="gmail-Apple-interchange-newline"></div>