From 4a600dabd5e2799bf0c3048859ee4f00808b7d89 Mon Sep 17 00:00:00 2001
From: Glenn Strauss <gstrauss@gluelogic.com>
Date: Sat, 6 Feb 2021 08:29:41 -0500
Subject: [PATCH] [mod_auth] close HTTP/2 connection after bad pass

mitigation slows down brute force password attacks

x-ref:
  "Possible feature: authentication brute force hardening"
  https://redmine.lighttpd.net/boards/3/topics/8885

Signed-off-by: Glenn Strauss <gstrauss@gluelogic.com>
---
 src/connections.c   | 22 +++++++++++++++++++++-
 src/mod_accesslog.c |  2 +-
 src/mod_auth.c      |  6 +++---
 src/reqpool.c       |  1 +
 src/request.h       |  2 +-
 src/response.c      |  4 ++--
 6 files changed, 29 insertions(+), 8 deletions(-)

--- a/src/connections.c
+++ b/src/connections.c
@@ -228,7 +228,7 @@ static void connection_handle_response_e
 		}
 	}
 
-        if (r->keep_alive) {
+        if (r->keep_alive > 0) {
 		request_reset(r);
 		config_reset_config(r);
 		con->is_readable = 1; /* potentially trigger optimistic read */
@@ -1265,6 +1265,19 @@ connection_set_fdevent_interest (request
 }
 
 
+__attribute_cold__
+static void
+connection_request_end_h2 (request_st * const h2r, connection * const con)
+{
+    if (h2r->keep_alive >= 0) {
+        h2r->keep_alive = -1;
+        h2_send_goaway(con, H2_E_NO_ERROR);
+    }
+    else /*(abort connection upon second request to close h2 connection)*/
+        h2_send_goaway(con, H2_E_ENHANCE_YOUR_CALM);
+}
+
+
 static void
 connection_state_machine_h2 (request_st * const h2r, connection * const con)
 {
@@ -1359,8 +1372,15 @@ connection_state_machine_h2 (request_st
                     && !chunkqueue_is_empty(con->read_queue))
                     resched |= 1;
                 h2_send_end_stream(r, con);
+                const int alive = r->keep_alive;
                 h2_retire_stream(r, con);/*r invalidated;removed from h2c->r[]*/
                 --i;/* adjust loop i; h2c->rused was modified to retire r */
+                /*(special-case: allow *stream* to set r->keep_alive = -1 to
+                 * trigger goaway on h2 connection, e.g. after mod_auth failure
+                 * in attempt to mitigate brute force attacks by forcing a
+                 * reconnect and (somewhat) slowing down retries)*/
+                if (alive < 0)
+                    connection_request_end_h2(h2r, con);
             }
         }
     }
--- a/src/mod_accesslog.c
+++ b/src/mod_accesslog.c
@@ -1108,7 +1108,7 @@ static int log_access_record (const requ
 				break;
 			case FORMAT_CONNECTION_STATUS:
 				if (r->state == CON_STATE_RESPONSE_END) {
-					if (0 == r->keep_alive) {
+					if (r->keep_alive <= 0) {
 						buffer_append_string_len(b, CONST_STR_LEN("-"));
 					} else {
 						buffer_append_string_len(b, CONST_STR_LEN("+"));
--- a/src/mod_auth.c
+++ b/src/mod_auth.c
@@ -828,7 +828,7 @@ static handler_t mod_auth_check_basic(re
 		log_error(r->conf.errh, __FILE__, __LINE__,
 		  "password doesn't match for %s username: %s IP: %s",
 		  r->uri.path.ptr, username->ptr, r->con->dst_addr_buf->ptr);
-		r->keep_alive = 0; /*(disable keep-alive if bad password)*/
+		r->keep_alive = -1; /*(disable keep-alive if bad password)*/
 		rc = HANDLER_UNSET;
 		break;
 	}
@@ -1461,7 +1461,7 @@ static handler_t mod_auth_check_digest(r
 		return HANDLER_FINISHED;
 	case HANDLER_ERROR:
 	default:
-		r->keep_alive = 0; /*(disable keep-alive if unknown user)*/
+		r->keep_alive = -1; /*(disable keep-alive if unknown user)*/
 		buffer_free(b);
 		return mod_auth_send_401_unauthorized_digest(r, require, 0);
 	}
@@ -1482,7 +1482,7 @@ static handler_t mod_auth_check_digest(r
 		log_error(r->conf.errh, __FILE__, __LINE__,
 		  "digest: auth failed for %s: wrong password, IP: %s",
 		  username, r->con->dst_addr_buf->ptr);
-		r->keep_alive = 0; /*(disable keep-alive if bad password)*/
+		r->keep_alive = -1; /*(disable keep-alive if bad password)*/
 
 		buffer_free(b);
 		return mod_auth_send_401_unauthorized_digest(r, require, 0);
--- a/src/reqpool.c
+++ b/src/reqpool.c
@@ -58,6 +58,7 @@ request_reset (request_st * const r)
     http_response_reset(r);
 
     r->loops_per_request = 0;
+    r->keep_alive = 0;
 
     r->h2state = 0; /* H2_STATE_IDLE */
     r->h2id = 0;
--- a/src/request.h
+++ b/src/request.h
@@ -175,7 +175,7 @@ struct request_st {
     char resp_header_repeated;
 
     char loops_per_request;  /* catch endless loops in a single request */
-    char keep_alive; /* only request.c can enable it, all other just disable */
+    int8_t keep_alive; /* only request.c can enable it, all other just disable */
     char async_callback;
 
     buffer *tmp_buf;                    /* shared; same as srv->tmp_buf */
--- a/src/response.c
+++ b/src/response.c
@@ -103,9 +103,9 @@ http_response_write_header (request_st *
 	if (light_btst(r->resp_htags, HTTP_HEADER_UPGRADE)
 	    && r->http_version == HTTP_VERSION_1_1) {
 		http_header_response_set(r, HTTP_HEADER_CONNECTION, CONST_STR_LEN("Connection"), CONST_STR_LEN("upgrade"));
-	} else if (0 == r->keep_alive) {
+	} else if (r->keep_alive <= 0) {
 		http_header_response_set(r, HTTP_HEADER_CONNECTION, CONST_STR_LEN("Connection"), CONST_STR_LEN("close"));
-	} else if (r->http_version == HTTP_VERSION_1_0) {/*(&& r->keep_alive != 0)*/
+	} else if (r->http_version == HTTP_VERSION_1_0) {/*(&& r->keep_alive > 0)*/
 		http_header_response_set(r, HTTP_HEADER_CONNECTION, CONST_STR_LEN("Connection"), CONST_STR_LEN("keep-alive"));
 	}
 
