[MEDIUM] http: capture invalid requests/responses even if accepted
It's useful to be able to accept an invalid header name in a request
or response but still be able to monitor further such errors. Now,
when an invalid request/response is received and accepted due to
an "accept-invalid-http-{request|response}" option, the invalid
request will be captured for later analysis with "show errors" on
the stats socket.
diff --git a/doc/configuration.txt b/doc/configuration.txt
index 68f946d..32a2b7d 100644
--- a/doc/configuration.txt
+++ b/doc/configuration.txt
@@ -592,6 +592,10 @@
monitor-net X X X -
monitor-uri X X X -
[no] option abortonclose X - X X
+[no] option accept-invalid-
+ http-request X X X -
+[no] option accept-invalid-
+ http-response X - X X
[no] option allbackups X - X X
[no] option checkcache X - X X
[no] option clitcpka X X X -
@@ -1806,6 +1810,72 @@
See also : "timeout queue" and server's "maxconn" and "maxqueue" parameters
+option accept-invalid-http-request
+no option accept-invalid-http-request
+ Enable or disable relaxing of HTTP request parsing
+ May be used in sections : defaults | frontend | listen | backend
+ yes | yes | yes | no
+ Arguments : none
+
+ By default, HAProxy complies with RFC2616 in terms of message parsing. This
+ means that invalid characters in header names are not permitted and cause an
+ error to be returned to the client. This is the desired behaviour as such
+ forbidden characters are essentially used to build attacks exploiting server
+ weaknesses, and bypass security filtering. Sometimes, a buggy browser or
+ server will emit invalid header names for whatever reason (configuration,
+ implementation) and the issue will not be immediately fixed. In such a case,
+ it is possible to relax HAProxy's header name parser to accept any character
+ even if that does not make sense, by specifying this option.
+
+ This option should never be enabled by default as it hides application bugs
+ and open security breaches. It should only be deployed after a problem has
+ been confirmed.
+
+ When this option is enabled, erroneous header names will still be accepted in
+ requests, but the complete request will be captured in order to permit later
+ analysis using the "show errors" request on the UNIX stats socket. Doing this
+ also helps confirming that the issue has been solved.
+
+ If this option has been enabled in a "defaults" section, it can be disabled
+ in a specific instance by prepending the "no" keyword before it.
+
+ See also : "option accept-invalid-http-response" and "show errors" on the
+ stats socket.
+
+
+option accept-invalid-http-response
+no option accept-invalid-http-response
+ Enable or disable relaxing of HTTP response parsing
+ May be used in sections : defaults | frontend | listen | backend
+ yes | no | yes | yes
+ Arguments : none
+
+ By default, HAProxy complies with RFC2616 in terms of message parsing. This
+ means that invalid characters in header names are not permitted and cause an
+ error to be returned to the client. This is the desired behaviour as such
+ forbidden characters are essentially used to build attacks exploiting server
+ weaknesses, and bypass security filtering. Sometimes, a buggy browser or
+ server will emit invalid header names for whatever reason (configuration,
+ implementation) and the issue will not be immediately fixed. In such a case,
+ it is possible to relax HAProxy's header name parser to accept any character
+ even if that does not make sense, by specifying this option.
+
+ This option should never be enabled by default as it hides application bugs
+ and open security breaches. It should only be deployed after a problem has
+ been confirmed.
+
+ When this option is enabled, erroneous header names will still be accepted in
+ responses, but the complete response will be captured in order to permit
+ later analysis using the "show errors" request on the UNIX stats socket.
+ Doing this also helps confirming that the issue has been solved.
+
+ If this option has been enabled in a "defaults" section, it can be disabled
+ in a specific instance by prepending the "no" keyword before it.
+
+ See also : "option accept-invalid-http-request" and "show errors" on the
+ stats socket.
+
+
option allbackups
no option allbackups
Use either all backup servers at a time or only the first one
diff --git a/include/proto/proto_http.h b/include/proto/proto_http.h
index 41f1922..175eb04 100644
--- a/include/proto/proto_http.h
+++ b/include/proto/proto_http.h
@@ -86,6 +86,9 @@
void http_sess_log(struct session *s);
void perform_http_redirect(struct session *s, struct stream_interface *si);
void http_return_srv_error(struct session *s, struct stream_interface *si);
+void http_capture_bad_message(struct error_snapshot *es, struct session *s,
+ struct buffer *buf, struct http_msg *msg,
+ struct proxy *other_end);
#endif /* _PROTO_PROTO_HTTP_H */
diff --git a/src/proto_http.c b/src/proto_http.c
index a77ab79..3325b31 100644
--- a/src/proto_http.c
+++ b/src/proto_http.c
@@ -1614,6 +1614,8 @@
/* 2: have we encountered a read error ? */
else if (req->flags & BF_READ_ERROR) {
/* we cannot return any message on error */
+ if (msg->err_pos >= 0)
+ http_capture_bad_message(&s->fe->invalid_req, s, req, msg, s->fe);
msg->msg_state = HTTP_MSG_ERROR;
req->analysers = 0;
s->fe->failed_req++;
@@ -1627,6 +1629,8 @@
/* 3: has the read timeout expired ? */
else if (req->flags & BF_READ_TIMEOUT || tick_is_expired(req->analyse_exp, now_ms)) {
/* read timeout : give up with an error message. */
+ if (msg->err_pos >= 0)
+ http_capture_bad_message(&s->fe->invalid_req, s, req, msg, s->fe);
txn->status = 408;
stream_int_retnclose(req->prod, error_message(s, HTTP_ERR_408));
msg->msg_state = HTTP_MSG_ERROR;
@@ -1641,6 +1645,8 @@
/* 4: have we encountered a close ? */
else if (req->flags & BF_SHUTR) {
+ if (msg->err_pos >= 0)
+ http_capture_bad_message(&s->fe->invalid_req, s, req, msg, s->fe);
txn->status = 400;
stream_int_retnclose(req->prod, error_message(s, HTTP_ERR_400));
msg->msg_state = HTTP_MSG_ERROR;
@@ -1672,6 +1678,9 @@
* of each header's length, so we can parse them quickly. *
****************************************************************/
+ if (msg->err_pos >= 0)
+ http_capture_bad_message(&s->fe->invalid_req, s, req, msg, s->fe);
+
req->analysers &= ~AN_REQ_HTTP_HDR;
req->analyse_exp = TICK_ETERNITY;
@@ -2331,21 +2340,13 @@
return 1;
return_bad_req: /* let's centralize all bad requests */
- if (unlikely(msg->msg_state == HTTP_MSG_ERROR)) {
+ if (unlikely(msg->msg_state == HTTP_MSG_ERROR) || msg->err_pos >= 0) {
/* we detected a parsing error. We want to archive this request
* in the dedicated proxy area for later troubleshooting.
*/
- struct error_snapshot *es = &s->fe->invalid_req;
- int maxlen = MIN(req->r - req->data + msg->som, sizeof(es->buf));
- memcpy(es->buf, req->data + msg->som, maxlen);
- es->pos = req->lr - req->data + msg->som;
- es->len = req->r - req->data + msg->som;
- es->when = date; // user-visible date
- es->sid = s->uniq_id;
- es->srv = s->srv;
- es->oe = s->be;
- es->src = s->cli_addr;
+ http_capture_bad_message(&s->fe->invalid_req, s, req, msg, s->fe);
}
+
txn->req.msg_state = HTTP_MSG_ERROR;
txn->status = 400;
req->analysers = 0;
@@ -2570,18 +2571,10 @@
/* we detected a parsing error. We want to archive this response
* in the dedicated proxy area for later troubleshooting.
*/
- struct error_snapshot *es = &t->be->invalid_rep;
- int maxlen = MIN(rep->r - rep->data + msg->som, sizeof(es->buf));
- memcpy(es->buf, rep->data + msg->som, maxlen);
- es->pos = rep->lr - rep->data + msg->som;
- es->len = rep->r - rep->data + msg->som;
- es->when = date; // user-visible date
- es->sid = t->uniq_id;
- es->srv = t->srv;
- es->oe = t->fe;
- es->src = t->cli_addr;
-
hdr_response_bad:
+ if (msg->msg_state == HTTP_MSG_ERROR || msg->err_pos >= 0)
+ http_capture_bad_message(&t->be->invalid_rep, t, rep, msg, t->fe);
+
buffer_shutr_now(rep);
buffer_shutw_now(req);
if (t->srv)
@@ -2603,6 +2596,8 @@
}
/* read error */
else if (rep->flags & BF_READ_ERROR) {
+ if (msg->err_pos >= 0)
+ http_capture_bad_message(&t->be->invalid_rep, t, rep, msg, t->fe);
buffer_shutr_now(rep);
buffer_shutw_now(req);
if (t->srv)
@@ -2619,6 +2614,8 @@
}
/* read timeout : return a 504 to the client. */
else if (rep->flags & BF_READ_TIMEOUT) {
+ if (msg->err_pos >= 0)
+ http_capture_bad_message(&t->be->invalid_rep, t, rep, msg, t->fe);
buffer_shutr_now(rep);
buffer_shutw_now(req);
if (t->srv)
@@ -2635,6 +2632,8 @@
}
/* close from server */
else if (rep->flags & BF_SHUTR) {
+ if (msg->err_pos >= 0)
+ http_capture_bad_message(&t->be->invalid_rep, t, rep, msg, t->fe);
buffer_shutw_now(req);
if (t->srv)
t->srv->failed_resp++;
@@ -2650,6 +2649,8 @@
}
/* write error to client (we don't send any message then) */
else if (rep->flags & BF_WRITE_ERROR) {
+ if (msg->err_pos >= 0)
+ http_capture_bad_message(&t->be->invalid_rep, t, rep, msg, t->fe);
buffer_shutr_now(rep);
t->be->failed_resp++;
rep->analysers = 0;
@@ -2670,6 +2671,9 @@
* of each header's length, so we can parse them quickly. *
****************************************************************/
+ if (msg->err_pos >= 0)
+ http_capture_bad_message(&t->be->invalid_rep, t, rep, msg, t->fe);
+
rep->analysers &= ~AN_RTR_HTTP_HDR;
/* ensure we keep this pointer to the beginning of the message */
@@ -4381,6 +4385,27 @@
return 1;
}
+/*
+ * Capture a bad request or response and archive it in the proxy's structure.
+ */
+void http_capture_bad_message(struct error_snapshot *es, struct session *s,
+ struct buffer *buf, struct http_msg *msg,
+ struct proxy *other_end)
+{
+ int maxlen = MIN(buf->r - buf->data + msg->som, sizeof(es->buf));
+
+ memcpy(es->buf, buf->data + msg->som, maxlen);
+ if (msg->err_pos >= 0)
+ es->pos = msg->err_pos + msg->som;
+ else
+ es->pos = buf->lr - buf->data + msg->som;
+ es->len = buf->r - buf->data + msg->som;
+ es->when = date; // user-visible date
+ es->sid = s->uniq_id;
+ es->srv = s->srv;
+ es->oe = other_end;
+ es->src = s->cli_addr;
+}
/*
* Print a debug line with a header