diff --git a/src/http_auth.c b/src/http_auth.c index 8dfe9a6b..7031fa28 100644 --- a/src/http_auth.c +++ b/src/http_auth.c @@ -887,6 +887,7 @@ int http_auth_digest_check(server *srv, connection *con, mod_auth_plugin_data *p *(dkv[i].ptr) = c + dkv[i].key_len; c += strlen(c) - 1; } + break; } } } @@ -981,6 +982,22 @@ int http_auth_digest_check(server *srv, connection *con, mod_auth_plugin_data *p buffer_free(password); + /* detect if attacker is attempting to reuse valid digest for one uri + * on a different request uri. Might also happen if intermediate proxy + * altered client request line. (Altered request would not result in + * the same digest as that calculated by the client.) */ + { + const size_t ulen = strlen(uri); + const size_t rlen = buffer_string_length(con->request.uri); + if (!buffer_is_equal_string(con->request.uri, uri, ulen) + && !(rlen < ulen && 0 == memcmp(con->request.uri->ptr, uri, rlen) && uri[rlen] == '?')) { + log_error_write(srv, __FILE__, __LINE__, "sbssss", + "digest: auth failed: uri mismatch (", con->request.uri, "!=", uri, "), IP:", inet_ntop_cache_get_ip(srv, &(con->dst_addr))); + buffer_free(b); + return -1; + } + } + if (algorithm && strcasecmp(algorithm, "md5-sess") == 0) { li_MD5_Init(&Md5Ctx); @@ -1054,6 +1071,28 @@ int http_auth_digest_check(server *srv, connection *con, mod_auth_plugin_data *p return 0; } + /* check age of nonce. Note that rand() is used in nonce generation + * in http_auth_digest_generate_nonce(). If that were replaced + * with nanosecond time, then nonce secret would remain unique enough + * for the purposes of Digest auth, and would be reproducible (and + * verifiable) if nanoseconds were inclued with seconds as part of the + * nonce "timestamp:secret". Since that is not done, timestamp in + * nonce could theoretically be modified and still produce same md5sum, + * but that is highly unlikely within a 10 min (moving) window of valid + * time relative to current time (now) */ + { + time_t ts = 0; + const unsigned char * const nonce_uns = (unsigned char *)nonce; + for (i = 0; i < 8 && light_isxdigit(nonce_uns[i]); ++i) { + ts = (ts << 4) + hex2int(nonce_uns[i]); + } + if (i != 8 || nonce[8] != ':' + || ts > srv->cur_ts || srv->cur_ts - ts > 600) { /*(10 mins)*/ + buffer_free(b); + return -2; /* nonce is stale; have client regenerate digest */ + } /*(future: might send nextnonce when expiration is imminent)*/ + } + /* remember the username */ buffer_copy_string(p->auth_user, username); diff --git a/src/mod_auth.c b/src/mod_auth.c index cfadba4b..d4520ad6 100644 --- a/src/mod_auth.c +++ b/src/mod_auth.c @@ -287,7 +287,7 @@ static handler_t mod_auth_uri_handler(server *srv, connection *con, void *p_d) { } } - if (!auth_satisfied) { + if (1 != auth_satisfied) { /*(0 or -2)*/ data_string *method, *realm; method = (data_string *)array_get_element(req, "method"); realm = (data_string *)array_get_element(req, "realm"); @@ -311,8 +311,13 @@ static handler_t mod_auth_uri_handler(server *srv, connection *con, void *p_d) { buffer_copy_string_len(p->tmp_buf, CONST_STR_LEN("Digest realm=\"")); buffer_append_string_buffer(p->tmp_buf, realm->value); buffer_append_string_len(p->tmp_buf, CONST_STR_LEN("\", charset=\"UTF-8\", nonce=\"")); + buffer_append_uint_hex(p->tmp_buf, (uintmax_t)srv->cur_ts); + buffer_append_string_len(p->tmp_buf, CONST_STR_LEN(":")); buffer_append_string(p->tmp_buf, hh); buffer_append_string_len(p->tmp_buf, CONST_STR_LEN("\", qop=\"auth\"")); + if (-2 == auth_satisfied) { + buffer_append_string_len(p->tmp_buf, CONST_STR_LEN(", stale=true")); + } response_header_insert(srv, con, CONST_STR_LEN("WWW-Authenticate"), CONST_BUF_LEN(p->tmp_buf)); } else { diff --git a/tests/mod-auth.t b/tests/mod-auth.t index cc03aa8a..ba76b040 100755 --- a/tests/mod-auth.t +++ b/tests/mod-auth.t @@ -8,7 +8,7 @@ BEGIN { use strict; use IO::Socket; -use Test::More tests => 19; +use Test::More tests => 20; use LightyTest; my $tf = LightyTest->new(); @@ -133,6 +133,9 @@ EOF $t->{RESPONSE} = [ { 'HTTP-Protocol' => 'HTTP/1.0', 'HTTP-Status' => 401 } ]; ok($tf->handle_http($t) == 0, 'Digest-Auth: missing qop, no crash'); +# (Note: test case is invalid; mismatch between request line and uri="..." +# is not what is intended to be tested here, but that is what is invalid) +# https://redmine.lighttpd.net/issues/477 ## this should not crash $t->{REQUEST} = ( <{RESPONSE} = [ { 'HTTP-Protocol' => 'HTTP/1.0', 'HTTP-Status' => 401 } ]; ok($tf->handle_http($t) == 0, 'Basic-Auth: Invalid Base64'); - $t->{REQUEST} = ( <{RESPONSE} = [ { 'HTTP-Protocol' => 'HTTP/1.0', 'HTTP-Status' => 401 } ]; +$t->{RESPONSE} = [ { 'HTTP-Protocol' => 'HTTP/1.0', 'HTTP-Status' => 400 } ]; ok($tf->handle_http($t) == 0, 'Digest-Auth: md5-sess + missing cnonce'); -$t->{REQUEST} = ( <{REQUEST} = ( <{RESPONSE} = [ { 'HTTP-Protocol' => 'HTTP/1.0', 'HTTP-Status' => 401 } ]; -ok($tf->handle_http($t) == 0, 'Digest-Auth: trailing WS'); + ); +$t->{RESPONSE} = [ { 'HTTP-Protocol' => 'HTTP/1.0', 'HTTP-Status' => 401, 'WWW-Authenticate' => '/, stale=true$/' } ]; +ok($tf->handle_http($t) == 0, 'Digest-Auth: stale nonce'); + +$t->{REQUEST} = ( <{RESPONSE} = [ { 'HTTP-Protocol' => 'HTTP/1.0', 'HTTP-Status' => 401, 'WWW-Authenticate' => '/, stale=true$/' } ]; +ok($tf->handle_http($t) == 0, 'Digest-Auth: trailing WS, stale nonce');