Hi BusyBox team,

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.

Summary

wget: HTTP header injection via request-target (CR/LF in path)

Before: If the URL path contains actual CR/LF, the request line is split
and the remainder is interpreted as forged headers.
After: Such URLs are rejected up front with bad URL; no forged headers are
sent.

wget: FTP control-path hardening for SIZE / RETR

Before: Path bytes were sent raw on the control channel; 0xff (Telnet IAC)
could be misinterpreted, and the source contained a TODO about escaping.
After (this patch): Implement the TODO by duplicating 0xff (IAC) for
SIZE/RETR so IAC is never emitted as data.

vi: sanitize names/patterns on the status line and in error messages

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.
After: Use the existing print_literal() consistently before printing those
strings (no functional change otherwise).

What the patches do, concretely
wget (shared outbound validation + FTP IAC rule)

Add die_on_bad_http_bytes(const char *s, int reject_space, const char
*what) and call it before protocol branching:

Rejects CR (0x0D), LF (0x0A), all C0 controls (<0x20), and DEL (0x7F).
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).

Add ftp_sanitize_path(const char *s) and apply it to SIZE and RETR:

Duplicate 0xff (Telnet IAC) on the FTP control connection (implements the
in-code TODO).

vi

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).

Signed-off-by: Takeuchi Yuma
---
diff --git a/networking/wget.c b/networking/wget.c
index ec3767793..0c3d72a66 100644
--- a/networking/wget.c
+++ b/networking/wget.c
@@ -474,6 +474,40 @@ static char* sanitize_string(char *s)
        return s;
 }

+/* Unified validator:
+ * Reject CR/LF and other control characters. Optionally reject SP as well.
+ * Use reject_space=1 for the HTTP request-target (path/query), because a
space
+ * would split the request line and enable header injection.
+ * Keep this minimal to avoid code size growth (no %-encoding here).
+ */
+static void die_on_bad_http_bytes(const char *s, int reject_space, const
char *what)
+{
+       const unsigned char *p = (const unsigned char*)s;
+       while (*p) {
+               unsigned char c = *p++;
+               if (c == '\r' || c == '\n' || c < 0x20 || c == 0x7f ||
(reject_space && c == ' '))
+                       bb_error_msg_and_die("bad %s", what);
+       }
+}
+
+#if ENABLE_FEATURE_WGET_FTP
+/* FTP path sanitizer:
+ * - Disallow CR/LF (would terminate the control command)
+ * - Double 0xff (Telnet IAC) per RFC/telnet rules
+ * Minimal approach: reject CR/LF, duplicate IAC, no other transformations.
+ */
+static char *ftp_sanitize_path(const char *s)
+{
+       const unsigned char *p = (const unsigned char*)s;
+       size_t len = 0, iac = 0;
+       for (; *p; p++) { if (*p == 0xff) iac++; len++; }
+       if (!iac) return xstrdup(s);
+       char *out = xmalloc(len + iac + 1), *q = out;
+       for (p = (const unsigned char*)s; *p; p++) { *q++ = *p; if (*p ==
0xff) *q++ = 0xff; }
+       *q = '\0'; return out;
+}
+#endif
+
 /* Returns '\n' if it was seen, else '\0'. Trims at first '\r' or '\n' */
 static char fgets_trim_sanitize(FILE *fp, const char *fmt)
 {
@@ -862,12 +896,16 @@ static FILE* prepare_ftp_session(FILE **dfpp, struct
host_info *target, len_and_
        ftpcmd("TYPE I", NULL, sfp);

        /* Query file size */
-       if (ftpcmd("SIZE ", target->path, sfp) == 213) {
-               G.content_len = BB_STRTOOFF(G.wget_buf + 4, NULL, 10);
-               if (G.content_len < 0 || errno) {
-                       bb_error_msg_and_die("bad SIZE value '%s'",
G.wget_buf + 4);
+       {
+               char *ftp_path = ftp_sanitize_path(target->path);
+               int code = ftpcmd("SIZE ", ftp_path, sfp);
+               free(ftp_path);
+               if (code == 213) {
+                       G.content_len = BB_STRTOOFF(G.wget_buf + 4, NULL,
10);
+                       if (G.content_len < 0 || errno)
+                               bb_error_msg_and_die("bad SIZE value '%s'",
G.wget_buf + 4);
+                       G.got_clen = 1;
                }
-               G.got_clen = 1;
        }

        /* Enter passive mode */
@@ -904,11 +942,14 @@ static FILE* prepare_ftp_session(FILE **dfpp, struct
host_info *target, len_and_
                        reset_beg_range_to_zero();
        }

-//TODO: needs ftp-escaping 0xff and '\n' bytes here.
-//Or disallow '\n' altogether via sanitize_string() in parse_url().
-//But 0xff's are possible in valid utf8 filenames.
-       if (ftpcmd("RETR ", target->path, sfp) > 150)
-               bb_error_msg_and_die("bad response to %s: %s", "RETR",
G.wget_buf);
+       /* Apply FTP sanitization when issuing RETR to prevent command
injection */
+       {
+               char *ftp_path = ftp_sanitize_path(target->path);
+               int code = ftpcmd("RETR ", ftp_path, sfp);
+               free(ftp_path);
+               if (code > 150)
+                       bb_error_msg_and_die("bad response to %s: %s",
"RETR", G.wget_buf);
+       }

        return sfp;
 }
@@ -1177,6 +1218,12 @@ static void download_one_url(const char *url)
  establish_session:
        /*G.content_len = 0; - redundant, got_clen = 0 is enough */
        G.got_clen = 0;
+       /* Prevent request-line/header injection.
+        * For request-target, also reject space characters.
+        */
+       die_on_bad_http_bytes(target.path, 1, "URL");
+       die_on_bad_http_bytes(target.host, 0, "Host");
+
        G.chunked = 0;
        if (!ENABLE_FEATURE_WGET_FTP
         || use_proxy || target.protocol[0] != 'f' /*not ftp[s]*/


Signed-off-by: Takeuchi Yuma
---
diff --git a/editors/vi.c b/editors/vi.c
index 34932f60c..0e941d924 100644
--- a/editors/vi.c
+++ b/editors/vi.c
@@ -556,6 +556,7 @@ static int crashme = 0;

 static void show_status_line(void);    // put a message on the bottom line
 static void status_line_bold(const char *, ...);
+static void print_literal(char *buf, const char *s);

 static void show_help(void)
 {
@@ -1291,19 +1292,29 @@ static int format_edit_status(void)
        trunc_at = columns < STATUS_BUFFER_LEN-1 ?
                columns : STATUS_BUFFER_LEN-1;

-       ret = snprintf(status_buffer, trunc_at+1,
+       {
+               const char *fname_disp;
+               char fn_buf[MAX_INPUT_LEN];
+               if (current_filename) {
+                       print_literal(fn_buf, current_filename);
+                       fname_disp = fn_buf;
+               } else {
+                       fname_disp = "No file";
+               }
+               ret = snprintf(status_buffer, trunc_at+1,
 #if ENABLE_FEATURE_VI_READONLY
-               "%c %s%s%s %d/%d %d%%",
+                       "%c %s%s%s %d/%d %d%%",
 #else
-               "%c %s%s %d/%d %d%%",
+                       "%c %s%s %d/%d %d%%",
 #endif
-               cmd_mode_indicator[cmd_mode & 3],
-               (current_filename != NULL ? current_filename : "No file"),
+                       cmd_mode_indicator[cmd_mode & 3],
+                       fname_disp,
 #if ENABLE_FEATURE_VI_READONLY
-               (readonly_mode ? " [Readonly]" : ""),
+                       (readonly_mode ? " [Readonly]" : ""),
 #endif
-               (modified_count ? " [Modified]" : ""),
-               cur, tot, percent);
+                       (modified_count ? " [Modified]" : ""),
+                       cur, tot, percent);
+       }

        if (ret >= 0 && ret < trunc_at)
                return ret;  // it all fit
@@ -1376,7 +1387,13 @@ static void status_line_bold(const char *format, ...)
 }
 static void status_line_bold_errno(const char *fn)
 {
-       status_line_bold("'%s' "STRERROR_FMT, fn STRERROR_ERRNO);
+       char fnbuf[MAX_INPUT_LEN];
+       if (!fn) {
+               fnbuf[0] = '\0';
+       } else {
+               print_literal(fnbuf, fn);
+       }
+       status_line_bold("'%s' "STRERROR_FMT, fnbuf STRERROR_ERRNO);
 }

 // copy s to buf, convert unprintable
@@ -2003,7 +2020,9 @@ static int file_insert(const char *fn, char *p, int
initial)
                goto fi;
        }
        if (!S_ISREG(statbuf.st_mode)) {
-               status_line_bold("'%s' is not a regular file", fn);
+               char fnbuf[MAX_INPUT_LEN];
+               print_literal(fnbuf, fn);
+               status_line_bold("'%s' is not a regular file", fnbuf);
                goto fi;
        }
        size = (statbuf.st_size < INT_MAX ? (int)statbuf.st_size : INT_MAX);
@@ -2015,7 +2034,12 @@ static int file_insert(const char *fn, char *p, int
initial)
        } else if (cnt < size) {
                // There was a partial read, shrink unused space
                p = text_hole_delete(p + cnt, p + size - 1, NO_UNDO);
-               status_line_bold("can't read '%s'", fn);
+               {
+                       char fnbuf[MAX_INPUT_LEN];
+                       print_literal(fnbuf, fn);
+                       status_line_bold("can't read '%s'",
+                                       fnbuf);
+               }
        }
 # if ENABLE_FEATURE_VI_UNDO
        else {
@@ -2401,7 +2425,9 @@ static char *char_search(char *p, const char *pat,
int dir_and_range)
        preg.not_bol = p != text;
        preg.not_eol = p != end - 1;
        if (err != NULL) {
-               status_line_bold("bad search pattern '%s': %s", pat, err);
+               char pbuf[MAX_INPUT_LEN];
+               print_literal(pbuf, pat);
+               status_line_bold("bad search pattern '%s': %s", pbuf, err);
                return p;
        }

@@ -2824,10 +2850,16 @@ static void colon(char *buf)
                } else {
                        modified_count = 0;
                        last_modified_count = -1;
-                       status_line("'%s' %uL, %uC",
-                               current_filename,
+                       {
+                               char fnbuf[MAX_INPUT_LEN];
+                               const char *disp = current_filename
+                                       ? (print_literal(fnbuf,
current_filename), fnbuf)
+                                       : "No file";
+                               status_line("'%s' %uL, %uC",
+                                       disp,
                                count_lines(text, end - 1), cnt
-                       );
+                               );
+                       }
                        if (p[0] == 'x'
                         || p[1] == 'q' || p[1] == 'n'
                        ) {
@@ -2977,6 +3009,7 @@ static void colon(char *buf)
                dot_skip_over_ws();
        } else if (strncmp(cmd, "edit", i) == 0) {      // Edit a file
                int size;
+               char fnbuf[MAX_INPUT_LEN];

                // don't edit, if the current file has been modified
                if (modified_count && !useforce) {
@@ -3008,16 +3041,21 @@ static void colon(char *buf)
 # endif
                // how many lines in text[]?
                li = count_lines(text, end - 1);
-               status_line("'%s'%s"
+               {
+                       const char *disp = args[0]
+                               ? (print_literal(fnbuf, fn), fnbuf)
+                               : (current_filename ? (print_literal(fnbuf,
current_filename), fnbuf) : "No file");
+                       status_line("'%s'%s"
                        IF_FEATURE_VI_READONLY("%s")
                        " %uL, %uC",
-                       fn,
+                       disp,
                        (size < 0 ? " [New file]" : ""),
                        IF_FEATURE_VI_READONLY(
                                ((readonly_mode) ? " [Readonly]" : ""),
                        )
                        li, (int)(end - text)
-               );
+                       );
+               }
        } else if (strncmp(cmd, "file", i) == 0) {      // what File is this
                if (e >= 0) {
                        status_line_bold("No address allowed on this
command");
@@ -3109,6 +3147,7 @@ static void colon(char *buf)
                editing = 0;
        } else if (strncmp(cmd, "read", i) == 0) {      // read file into
text[]
                int size, num;
+               char fnbuf[MAX_INPUT_LEN];

                if (args[0]) {
                        // the user supplied a file name
@@ -3141,13 +3180,18 @@ static void colon(char *buf)
                        goto ret;       // nothing was inserted
                // how many lines in text[]?
                li = count_lines(q, q + size - 1);
-               status_line("'%s'"
+               {
+                       const char *disp = args[0]
+                               ? (print_literal(fnbuf, fn), fnbuf)
+                               : (current_filename ? (print_literal(fnbuf,
current_filename), fnbuf) : "No file");
+                       status_line("'%s'"
                        IF_FEATURE_VI_READONLY("%s")
                        " %uL, %uC",
-                       fn,
+                       disp,
                        IF_FEATURE_VI_READONLY((readonly_mode ? "
[Readonly]" : ""),)
                        li, size
-               );
+                       );
+               }
                dot = find_line(num);
        } else if (strncmp(cmd, "rewind", i) == 0) {    // rewind cmd line
args
                if (modified_count && !useforce) {
@@ -3365,7 +3409,10 @@ static void colon(char *buf)
                }
 # if ENABLE_FEATURE_VI_READONLY
                else if (readonly_mode && !useforce && fn) {
-                       status_line_bold("'%s' is read only", fn);
+                       char rb[MAX_INPUT_LEN];
+                       print_literal(rb, fn);
+                       status_line_bold("'%s' is read only",
+                                       rb);
                        goto ret;
                }
 # endif
@@ -3394,7 +3441,13 @@ static void colon(char *buf)
                } else {
                        // how many lines written
                        li = count_lines(q, q + l - 1);
-                       status_line("'%s' %uL, %uC", fn, li, l);
+                       {
+                               char fnbuf[MAX_INPUT_LEN];
+                               const char *disp = fn
+                                       ? (print_literal(fnbuf, fn), fnbuf)
+                                       : (current_filename ?
(print_literal(fnbuf, current_filename), fnbuf) : "No file");
+                               status_line("'%s' %uL, %uC", disp, li, l);
+                       }
                        if (l == size) {
                                if (q == text && q + l == end) {
                                        modified_count = 0;
_______________________________________________
busybox mailing list
[email protected]
https://lists.busybox.net/mailman/listinfo/busybox

Reply via email to