head 1.1; access; symbols; locks; strict; comment @# @; 1.1 date 2026.09.25.14.20.39; author wiz; state Exp; branches; next ; commitid TmvT2kz6VnKzH0XG; desc @@ 1.1 log @thttpd: fix a couple CVEs Rename patches, add some patches from Debian/FreeBSD, move MESSAGE to README.pkgsrc. Bump PKGREVISION. From Showta Ishizaki in PR 60761. @ text @$NetBSD$ CVE-2009-4491 -- log injection. thttpd writes the request line, the headers and values derived from them to the log exactly as the client sent them, so a request can place terminal escape sequences into the log. Control characters are written as \xHH instead. This is not only the access log. Every one of these logs a value the client controls, and none of them sanitised it: make_log_entry() the access log and its syslog form: URL, Referer, User-Agent, remote user the "unparsable time" pair the raw If-Modified-Since and If-Range header values check_referrer() the Referer host, the URL and the Referer thirteen more the URL or the expanded filename, in the "goes outside the web tree", "tried to index a directory", "tried to retrieve an auth file", "isn't CGI", opendir, execve and spawn messages thttpd.c the URL in the write-error message httpd_log_escape() is exported because thttpd.c needs it too. It hands back a pointer into a small ring of static buffers so that several values can be escaped in one syslog() call; thttpd is single-threaded, so that is safe. Not in ACME's 2.30 changelog, and not carried by FreeBSD ports, Debian, Fedora, Alpine, MacPorts or Void. --- libhttpd.c.orig +++ libhttpd.c @@@@ -172,6 +172,7 @@@@ static int cgi( httpd_conn* hc ); static int really_start_request( httpd_conn* hc, struct timeval* nowP ); static void make_log_entry( httpd_conn* hc, struct timeval* nowP ); +static char* log_escape( char* dst, size_t dstsize, const char* src ); static int check_referrer( httpd_conn* hc ); static int really_check_referrer( httpd_conn* hc ); static int sockaddr_check( httpd_sockaddr* saP ); @@@@ -1619,7 +1620,7 @@@@ ++nlinks; if ( nlinks > MAX_LINKS ) { - syslog( LOG_ERR, "too many symlinks in %.80s", path ); + syslog( LOG_ERR, "too many symlinks in %.80s", httpd_log_escape( path ) ); return (char*) 0; } lnk[linklen] = '\0'; @@@@ -2173,7 +2174,8 @@@@ cp = &buf[18]; hc->if_modified_since = tdate_parse( cp ); if ( hc->if_modified_since == (time_t) -1 ) - syslog( LOG_DEBUG, "unparsable time: %.80s", cp ); + syslog( LOG_DEBUG, "unparsable time: %.80s", + httpd_log_escape( cp ) ); } else if ( strncasecmp( buf, "Cookie:", 7 ) == 0 ) { @@@@ -2214,7 +2216,8 @@@@ cp = &buf[9]; hc->range_if = tdate_parse( cp ); if ( hc->range_if == (time_t) -1 ) - syslog( LOG_DEBUG, "unparsable time: %.80s", cp ); + syslog( LOG_DEBUG, "unparsable time: %.80s", + httpd_log_escape( cp ) ); } else if ( strncasecmp( buf, "Content-Type:", 13 ) == 0 ) { @@@@ -2380,7 +2383,7 @@@@ { syslog( LOG_NOTICE, "%.80s URL \"%.80s\" goes outside the web tree", - httpd_ntoa( &hc->client_addr ), hc->encodedurl ); + httpd_ntoa( &hc->client_addr ), httpd_log_escape( hc->encodedurl ) ); httpd_send_err( hc, 403, err403title, "", ERROR_FORM( err403form, "The requested URL '%.80s' resolves to a file outside the permitted web server directory tree.\n" ), @@@@ -2734,7 +2737,7 @@@@ dirp = opendir( hc->expnfilename ); if ( dirp == (DIR*) 0 ) { - syslog( LOG_ERR, "opendir %.80s - %m", hc->expnfilename ); + syslog( LOG_ERR, "opendir %.80s - %m", httpd_log_escape( hc->expnfilename ) ); httpd_send_err( hc, 404, err404title, "", err404form, hc->encodedurl ); return -1; } @@@@ -2974,7 +2977,8 @@@@ /* Parent process. */ closedir( dirp ); - syslog( LOG_DEBUG, "spawned indexing process %d for directory '%.200s'", r, hc->expnfilename ); + syslog( LOG_DEBUG, "spawned indexing process %d for directory '%.200s'", + r, httpd_log_escape( hc->expnfilename ) ); #ifdef CGI_TIMELIMIT /* Schedule a kill for the child process, in case it runs too long */ client_data.i = r; @@@@ -3563,7 +3567,7 @@@@ (void) execve( binary, argp, envp ); /* Something went wrong. */ - syslog( LOG_ERR, "execve %.80s - %m", hc->expnfilename ); + syslog( LOG_ERR, "execve %.80s - %m", httpd_log_escape( hc->expnfilename ) ); httpd_send_err( hc, 500, err500title, "", err500form, hc->encodedurl ); httpd_write_response( hc ); _exit( 1 ); @@@@ -3602,7 +3606,8 @@@@ } /* Parent process. */ - syslog( LOG_DEBUG, "spawned CGI process %d for file '%.200s'", r, hc->expnfilename ); + syslog( LOG_DEBUG, "spawned CGI process %d for file '%.200s'", + r, httpd_log_escape( hc->expnfilename ) ); #ifdef CGI_TIMELIMIT /* Schedule a kill for the child process, in case it runs too long */ client_data.i = r; @@@@ -3654,7 +3659,7 @@@@ syslog( LOG_INFO, "%.80s URL \"%.80s\" resolves to a non world-readable file", - httpd_ntoa( &hc->client_addr ), hc->encodedurl ); + httpd_ntoa( &hc->client_addr ), httpd_log_escape( hc->encodedurl ) ); httpd_send_err( hc, 403, err403title, "", ERROR_FORM( err403form, "The requested URL '%.80s' resolves to a file that is not world-readable.\n" ), @@@@ -3709,7 +3714,7 @@@@ syslog( LOG_INFO, "%.80s URL \"%.80s\" tried to index a directory with indexing disabled", - httpd_ntoa( &hc->client_addr ), hc->encodedurl ); + httpd_ntoa( &hc->client_addr ), httpd_log_escape( hc->encodedurl ) ); httpd_send_err( hc, 403, err403title, "", ERROR_FORM( err403form, "The requested URL '%.80s' resolves to a directory that has indexing disabled.\n" ), @@@@ -3729,7 +3734,7 @@@@ #else /* GENERATE_INDEXES */ syslog( LOG_INFO, "%.80s URL \"%.80s\" tried to index a directory", - httpd_ntoa( &hc->client_addr ), hc->encodedurl ); + httpd_ntoa( &hc->client_addr ), httpd_log_escape( hc->encodedurl ) ); httpd_send_err( hc, 403, err403title, "", ERROR_FORM( err403form, "The requested URL '%.80s' is a directory, and directory indexing is disabled on this server.\n" ), @@@@ -3757,7 +3762,7 @@@@ syslog( LOG_INFO, "%.80s URL \"%.80s\" resolves to a non-world-readable index file", - httpd_ntoa( &hc->client_addr ), hc->encodedurl ); + httpd_ntoa( &hc->client_addr ), httpd_log_escape( hc->encodedurl ) ); httpd_send_err( hc, 403, err403title, "", ERROR_FORM( err403form, "The requested URL '%.80s' resolves to an index file that is not world-readable.\n" ), @@@@ -3786,7 +3791,7 @@@@ syslog( LOG_NOTICE, "%.80s URL \"%.80s\" tried to retrieve an auth file", - httpd_ntoa( &hc->client_addr ), hc->encodedurl ); + httpd_ntoa( &hc->client_addr ), httpd_log_escape( hc->encodedurl ) ); httpd_send_err( hc, 403, err403title, "", ERROR_FORM( err403form, "The requested URL '%.80s' is an authorization file, retrieving it is not permitted.\n" ), @@@@ -3801,7 +3806,7 @@@@ syslog( LOG_NOTICE, "%.80s URL \"%.80s\" tried to retrieve an auth file", - httpd_ntoa( &hc->client_addr ), hc->encodedurl ); + httpd_ntoa( &hc->client_addr ), httpd_log_escape( hc->encodedurl ) ); httpd_send_err( hc, 403, err403title, "", ERROR_FORM( err403form, "The requested URL '%.80s' is an authorization file, retrieving it is not permitted.\n" ), @@@@ -3828,7 +3833,7 @@@@ { syslog( LOG_NOTICE, "%.80s URL \"%.80s\" is executable but isn't CGI", - httpd_ntoa( &hc->client_addr ), hc->encodedurl ); + httpd_ntoa( &hc->client_addr ), httpd_log_escape( hc->encodedurl ) ); httpd_send_err( hc, 403, err403title, "", ERROR_FORM( err403form, "The requested URL '%.80s' resolves to a file which is marked executable but is not a CGI file; retrieving it is forbidden.\n" ), @@@@ -3839,7 +3844,7 @@@@ { syslog( LOG_INFO, "%.80s URL \"%.80s\" has pathinfo but isn't CGI", - httpd_ntoa( &hc->client_addr ), hc->encodedurl ); + httpd_ntoa( &hc->client_addr ), httpd_log_escape( hc->encodedurl ) ); httpd_send_err( hc, 403, err403title, "", ERROR_FORM( err403form, "The requested URL '%.80s' resolves to a file plus CGI-style pathinfo, but the file is not a valid CGI file.\n" ), @@@@ -3904,12 +3909,65 @@@@ } +/* Copy src into dst, replacing control characters with \xHH. The request +** line, the headers and anything derived from them reach the log exactly as +** the client sent them, so without this a request can write terminal escape +** sequences into the log. +*/ +static char* +log_escape( char* dst, size_t dstsize, const char* src ) + { + static const char hex[] = "0123456789abcdef"; + size_t i = 0; + unsigned char c; + + if ( src == (const char*) 0 ) + src = ""; + for ( ; *src != '\0' && i + 4 < dstsize; ++src ) + { + c = (unsigned char) *src; + if ( c < 0x20 || c == 0x7f ) + { + dst[i++] = '\\'; + dst[i++] = 'x'; + dst[i++] = hex[c >> 4]; + dst[i++] = hex[c & 0xf]; + } + else + dst[i++] = c; + } + dst[i] = '\0'; + return dst; + } + + +/* The same, for use straight in a syslog() argument list. Returns a pointer +** into a small ring of buffers so that more than one value can be escaped in +** one call. thttpd is single-threaded, so this is safe. +*/ +char* +httpd_log_escape( const char* src ) + { + static char bufs[4][1000]; + static int next = 0; + char* dst; + + dst = bufs[next]; + next = ( next + 1 ) % 4; + return log_escape( dst, sizeof(bufs[0]), src ); + } + + static void make_log_entry( httpd_conn* hc, struct timeval* nowP ) { char* ru; char url[305]; char bytes[40]; + char eurl[305 * 4]; + char eref[200 * 4 + 1]; + char eua[200 * 4 + 1]; + char eru[80 * 4 + 1]; if ( hc->hs->no_log ) return; @@@@ -3922,7 +3980,7 @@@@ /* Format remote user. */ if ( hc->remoteuser[0] != '\0' ) - ru = hc->remoteuser; + ru = log_escape( eru, sizeof(eru), hc->remoteuser ); else ru = "-"; /* If we're vhosting, prepend the hostname to the url. This is @@@@ -3937,6 +3995,9 @@@@ else (void) my_snprintf( url, sizeof(url), "%.200s", hc->encodedurl ); + (void) log_escape( eurl, sizeof(eurl), url ); + (void) log_escape( eref, sizeof(eref), hc->referrer ); + (void) log_escape( eua, sizeof(eua), hc->useragent ); /* Format the bytes. */ if ( hc->bytes_sent >= 0 ) (void) my_snprintf( @@@@ -3985,8 +4046,8 @@@@ (void) fprintf( hc->hs->logfp, "%.80s - %.80s [%s] \"%.80s %.300s %.80s\" %d %s \"%.200s\" \"%.200s\"\n", httpd_ntoa( &hc->client_addr ), ru, date, - httpd_method_str( hc->method ), url, hc->protocol, - hc->status, bytes, hc->referrer, hc->useragent ); + httpd_method_str( hc->method ), eurl, hc->protocol, + hc->status, bytes, eref, eua ); #ifdef FLUSH_LOG_EVERY_TIME (void) fflush( hc->hs->logfp ); #endif @@@@ -3995,8 +4056,8 @@@@ syslog( LOG_INFO, "%.80s - %.80s \"%.80s %.200s %.80s\" %d %s \"%.200s\" \"%.200s\"", httpd_ntoa( &hc->client_addr ), ru, - httpd_method_str( hc->method ), url, hc->protocol, - hc->status, bytes, hc->referrer, hc->useragent ); + httpd_method_str( hc->method ), eurl, hc->protocol, + hc->status, bytes, eref, eua ); } @@@@ -4023,7 +4084,9 @@@@ cp = ""; syslog( LOG_INFO, "%.80s non-local referrer \"%.80s%.80s\" \"%.80s\"", - httpd_ntoa( &hc->client_addr ), cp, hc->encodedurl, hc->referrer ); + httpd_ntoa( &hc->client_addr ), httpd_log_escape( cp ), + httpd_log_escape( hc->encodedurl ), + httpd_log_escape( hc->referrer ) ); httpd_send_err( hc, 403, err403title, "", ERROR_FORM( err403form, "You must supply a local referrer to get URL '%.80s' from this server.\n" ), --- libhttpd.h.orig +++ libhttpd.h @@@@ -263,6 +263,12 @@@@ extern char* httpd_err408form; extern char* httpd_err503title; extern char* httpd_err503form; + +/* Escape control characters in a string for the log, so that a request +** cannot write terminal escape sequences into it. Returns a pointer into +** a small ring of static buffers. +*/ +char* httpd_log_escape( const char* src ); /* Generate a string representation of a method number. */ char* httpd_method_str( int method ); --- thttpd.c.orig +++ thttpd.c @@@@ -1778,7 +1778,8 @@@@ ** And ECONNRESET isn't interesting either. */ if ( errno != EPIPE && errno != EINVAL && errno != ECONNRESET ) - syslog( LOG_ERR, "write - %m sending %.80s", hc->encodedurl ); + syslog( LOG_ERR, "write - %m sending %.80s", + httpd_log_escape( hc->encodedurl ) ); clear_connection( c, tvP ); return; } @