MEDIUM: http-rules: Rely on http reply for http deny/tarpit rules

"http-request deny", "http-request tarpit" and "http-response deny" rules now
use the same syntax than http return rules and internally rely on the http
replies. The behaviour is not the same when no argument is specified (or only
the status code). For http replies, a dummy response is produced, with no
payload. For old deny/tarpit rules, the proxy's error messages are used. Thus,
to be compatible with existing configuration, the "default-errorfiles" parameter
is implied. For instance :

  http-request deny deny_status 404

is now an alias of

  http-request deny status 404 default-errorfiles
This commit is contained in:
Christopher Faulet 2020-05-13 17:56:56 +02:00
parent 0e2ad61315
commit 5cb513abeb
7 changed files with 176 additions and 165 deletions

View File

@ -4991,18 +4991,22 @@ http-request del-map(<file-name>) <key fmt> [ { if | unless } <condition> ]
It takes one argument: "file name" It is the equivalent of the "del map"
command from the stats socket, but can be triggered by an HTTP request.
http-request deny [deny_status <status>] [ { errorfile | errorfiles } <err> ]
[ { if | unless } <condition> ]
http-request deny [deny_status <status>] [ { if | unless } <condition> ]
http-request deny [ { status | deny_status } <code>] [content-type <type>]
[ { default-errorfiles | errorfile <file> | errorfiles <name> |
file <file> | lf-file <file> | string <str> | lf-string <fmt> } ]
[ hdr <name> <fmt> ]*
[ { if | unless } <condition> ]
This stops the evaluation of the rules and immediately rejects the request
and emits an HTTP 403 error, or optionally the status code specified as an
argument to "deny_status". The list of permitted status codes is limited to
those that can be overridden by the "errorfile" directive. A specific error
message may be specified. It may be an error file, using the "errorfile"
keyword followed by the file containing the full HTTP response. It may also
be an error from an http-errors section, using the "errorfiles" keyword
followed by the section name.
This stops the evaluation of the rules and immediately rejects the request.
By default an HTTP 403 error is returned. But the response may be customized
using same syntax than "http-request return" rules. Thus, see "http-request
return" for details. For compatiblity purpose, when no argument is defined,
or only "deny_status", the argument "default-errorfiles" is implied. It means
"http-request deny [deny_status <status>]" is an alias of
"http-request deny [status <status>] default-errorfiles".
No further "http-request" rules are evaluated.
See also "http-request return".
http-request disable-l7-retry [ { if | unless } <condition> ]
This disables any attempt to retry the request if it fails for any other
@ -5576,26 +5580,32 @@ http-request strict-mode { on | off }
the frontend, the default mode is restored when HAProxy starts the backend
rules evaluation.
http-request tarpit [deny_status <status>] [ { errorfile | errorfiles } <err> ]
[ { if | unless } <condition> ]
http-request tarpit [deny_status <status>] [ { if | unless } <condition> ]
http-request tarpit [ { status | deny_status } <code>] [content-type <type>]
[ { default-errorfiles | errorfile <file> | errorfiles <name> |
file <file> | lf-file <file> | string <str> | lf-string <fmt> } ]
[ hdr <name> <fmt> ]*
[ { if | unless } <condition> ]
This stops the evaluation of the rules and immediately blocks the request
without responding for a delay specified by "timeout tarpit" or
"timeout connect" if the former is not set. After that delay, if the client
is still connected, an HTTP error 500 (or optionally the status code
specified as an argument to "deny_status") is returned so that the client
does not suspect it has been tarpitted. Logs will report the flags "PT".
The goal of the tarpit rule is to slow down robots during an attack when
they're limited on the number of concurrent requests. It can be very
efficient against very dumb robots, and will significantly reduce the load
on firewalls compared to a "deny" rule. But when facing "correctly"
developed robots, it can make things worse by forcing haproxy and the front
firewall to support insane number of concurrent connections. A specific error
message may be specified. It may be an error file, using the "errorfile"
keyword followed by the file containing the full HTTP response. It may also
be an error from an http-errors section, using the "errorfiles" keyword
followed by the section name.
See also the "silent-drop" action.
is still connected, a response is returned so that the client does not
suspect it has been tarpitted. Logs will report the flags "PT". The goal of
the tarpit rule is to slow down robots during an attack when they're limited
on the number of concurrent requests. It can be very efficient against very
dumb robots, and will significantly reduce the load on firewalls compared to
a "deny" rule. But when facing "correctly" developed robots, it can make
things worse by forcing haproxy and the front firewall to support insane
number of concurrent connections. By default an HTTP error 500 is returned.
But the response may be customized using same syntax than
"http-request return" rules. Thus, see "http-request return" for details.
For compatiblity purpose, when no argument is defined, or only "deny_status",
the argument "default-errorfiles" is implied. It means
"http-request tarpit [deny_status <status>]" is an alias of
"http-request tarpit [status <status>] default-errorfiles".
No further "http-request" rules are evaluated.
See also "http-request return" and "http-request silent-drop".
http-request track-sc0 <key> [table <table>] [ { if | unless } <condition> ]
http-request track-sc1 <key> [table <table>] [ { if | unless } <condition> ]
@ -5779,18 +5789,22 @@ http-response del-map(<file-name>) <key fmt> [ { if | unless } <condition> ]
It takes one argument: "file name" It is the equivalent of the "del map"
command from the stats socket, but can be triggered by an HTTP response.
http-response deny [deny_status <status>] [ { errorfile | errorfiles } <err> ]
[ { if | unless } <condition> ]
http-response deny [deny_status <status>] [ { if | unless } <condition> ]
http-response deny [ { status | deny_status } <code>] [content-type <type>]
[ { default-errorfiles | errorfile <file> | errorfiles <name> |
file <file> | lf-file <file> | string <str> | lf-string <fmt> } ]
[ hdr <name> <fmt> ]*
[ { if | unless } <condition> ]
This stops the evaluation of the rules and immediately rejects the response
and emits an HTTP 502 error, or optionally the status code specified as an
argument to "deny_status". The list of permitted status codes is limited to
those that can be overridden by the "errorfile" directive. A specific error
message may be specified. It may be an error file, using the "errorfile"
keyword followed by the file containing the full HTTP response. It may also
be an error from an http-errors section, using the "errorfiles" keyword
followed by the section name.
This stops the evaluation of the rules and immediately rejects the response.
By default an HTTP 502 error is returned. But the response may be customized
using same syntax than "http-response return" rules. Thus, see
"http-response return" for details. For compatiblity purpose, when no
argument is defined, or only "deny_status", the argument "default-errorfiles"
is implied. It means "http-response deny [deny_status <status>]" is an alias
of "http-response deny [status <status>] default-errorfiles".
No further "http-response" rules are evaluated.
See also "http-response return".
http-response redirect <rule> [ { if | unless } <condition> ]

View File

@ -125,11 +125,7 @@ struct act_rule {
struct list fmt; /* log-format compatible expression */
struct my_regex *re; /* used by replace-header/value/uri/path */
} http; /* args used by some HTTP rules */
struct {
int status; /* status code */
struct buffer *errmsg; /* HTTP error message, may be NULL */
} http_deny; /* args used by HTTP deny rules */
struct http_reply *http_reply; /* HTTP response to be used by return rules */
struct http_reply *http_reply; /* HTTP response to be used by return/deny/tarpit rules */
struct redirect_rule *redir; /* redirect rule or "http-request redirect" */
struct {
char *ref; /* MAP or ACL file name to update */

View File

@ -26,6 +26,7 @@
#include <common/http.h>
#include <types/channel.h>
#include <types/http_htx.h>
/* These are the flags that are found in txn->flags */
@ -174,6 +175,7 @@ struct http_txn {
/* 1 unused byte here */
short status; /* HTTP status from the server, negative if from proxy */
struct buffer *errmsg; /* custom HTTP error message to use as reply */
struct http_reply *http_reply; /* The HTTP reply to use as reply */
char cache_hash[20]; /* Store the cache hash */
char *uri; /* first line if log needed, NULL otherwise */

View File

@ -0,0 +1 @@
The path "%[path]" is forbidden

View File

@ -27,6 +27,9 @@ haproxy h1 -conf {
http-request deny deny_status 500 errorfile /dev/null if { path /500-1 }
http-request deny deny_status 500 errorfiles errors-1 if { path /500-2 }
http-request deny status 500 hdr x-err-info "path=%[path]" content-type "text/plain" string "Internal Error" if { path /int-err }
http-request deny status 403 hdr x-err-info "path=%[path]" content-type "text/plain" lf-file ${testdir}/errors/lf-403.txt if { path /forbidden }
} -start
client c1r1 -connect ${h1_fe1_sock} {
@ -55,3 +58,20 @@ client c1r5 -connect ${h1_fe1_sock} {
txreq -req GET -url /500-2
expect_close
} -run
client c1r6 -connect ${h1_fe1_sock} {
txreq -req GET -url /int-err
rxresp
expect resp.status == 500
expect resp.http.x-err-info == "path=/int-err"
expect resp.http.content-type == "text/plain"
expect resp.http.content-length == 14
expect resp.body == "Internal Error"
} -run
client c1r7 -connect ${h1_fe1_sock} {
txreq -req GET -url /forbidden
rxresp
expect resp.status == 403
expect resp.http.x-err-info == "path=/forbidden"
expect resp.http.content-type == "text/plain"
expect resp.body == "The path \"/forbidden\" is forbidden\n"
} -run

View File

@ -58,6 +58,30 @@ static void release_http_action(struct act_rule *rule)
}
}
/* Release memory allocated by HTTP actions relying on an http reply. Concretly,
* it releases <.arg.http_reply>
*/
static void release_act_http_reply(struct act_rule *rule)
{
release_http_reply(rule->arg.http_reply);
rule->arg.http_reply = NULL;
}
/* Check function for HTTP actions relying on an http reply. The function
* returns 1 in success case, otherwise, it returns 0 and err is filled.
*/
static int check_act_http_reply(struct act_rule *rule, struct proxy *px, char **err)
{
struct http_reply *reply = rule->arg.http_reply;
if (!http_check_http_reply(reply, px, err)) {
release_act_http_reply(rule);
return 0;
}
return 1;
}
/* This function executes one of the set-{method,path,query,uri} actions. It
* builds a string in the trash from the specified format string. It finds
@ -797,119 +821,66 @@ static enum act_parse_ret parse_http_allow(const char **args, int *orig_arg, str
return ACT_RET_PRS_OK;
}
/* Check an "http-request deny" action when an http-errors section is referenced.
*
* The function returns 1 in success case, otherwise, it returns 0 and err is
* filled.
*/
static int check_http_deny_action(struct act_rule *rule, struct proxy *px, char **err)
{
struct http_errors *http_errs;
int status = (intptr_t)(rule->arg.act.p[0]);
int ret = 1;
list_for_each_entry(http_errs, &http_errors_list, list) {
if (strcmp(http_errs->id, (char *)rule->arg.act.p[1]) == 0) {
free(rule->arg.act.p[1]);
rule->arg.http_deny.status = status;
rule->arg.http_deny.errmsg = http_errs->errmsg[http_get_status_idx(status)];
if (!rule->arg.http_deny.errmsg)
ha_warning("Proxy '%s': status '%d' referenced by http deny rule "
"not declared in http-errors section '%s'.\n",
px->id, status, http_errs->id);
break;
}
}
if (&http_errs->list == &http_errors_list) {
memprintf(err, "unknown http-errors section '%s' referenced by http deny rule",
(char *)rule->arg.act.p[1]);
free(rule->arg.act.p[1]);
ret = 0;
}
return ret;
}
/* Parse "deny" or "tarpit" actions for a request rule or "deny" action for a
* response rule. It may take optional arguments to define the status code, the
* error file or the http-errors section to use. It returns ACT_RET_PRS_OK on
* success, ACT_RET_PRS_ERR on error.
* response rule. It returns ACT_RET_PRS_OK on success, ACT_RET_PRS_ERR on
* error. It relies on http_parse_http_reply() to set
* <.arg.http_reply>.
*/
static enum act_parse_ret parse_http_deny(const char **args, int *orig_arg, struct proxy *px,
struct act_rule *rule, char **err)
{
int default_status, status, hc, cur_arg;
int default_status;
int cur_arg, arg = 0;
cur_arg = *orig_arg;
if (rule->from == ACT_F_HTTP_REQ) {
if (!strcmp(args[cur_arg-1], "tarpit")) {
rule->action = ACT_HTTP_REQ_TARPIT;
default_status = status = 500;
default_status = 500;
}
else {
rule->action = ACT_ACTION_DENY;
default_status = status = 403;
default_status = 403;
}
}
else {
rule->action = ACT_ACTION_DENY;
default_status = status = 502;
default_status = 502;
}
/* If no args or only a deny_status specified, fallback on the legacy
* mode and use default error files despite the fact that
* default-errorfiles is not used. Otherwise, parse an http reply.
*/
/* Prepare parsing of log-format strings */
px->conf.args.ctx = ((rule->from == ACT_F_HTTP_REQ) ? ARGC_HRQ : ARGC_HRS);
if (!*(args[cur_arg])) {
rule->arg.http_reply = http_parse_http_reply((const char *[]){"default-errorfiles", ""}, &arg, px, default_status, err);
goto end;
}
rule->flags |= ACT_FLAG_FINAL;
if (strcmp(args[cur_arg], "deny_status") == 0) {
cur_arg++;
if (!*args[cur_arg]) {
memprintf(err, "'%s' expects <status_code> as argument", args[cur_arg-1]);
return ACT_RET_PRS_ERR;
}
status = atol(args[cur_arg]);
cur_arg++;
for (hc = 0; hc < HTTP_ERR_SIZE; hc++) {
if (http_err_codes[hc] == status)
break;
}
if (hc >= HTTP_ERR_SIZE) {
memprintf(err, "status code '%d' not handled, using default code '%d'",
status, default_status);
status = default_status;
hc = http_get_status_idx(status);
if (!*(args[cur_arg+2]) ||
(strcmp(args[cur_arg+2], "errorfile") != 0 && strcmp(args[cur_arg+2], "errorfiles") != 0)) {
rule->arg.http_reply = http_parse_http_reply((const char *[]){"status", args[cur_arg+1], "default-errorfiles", ""},
&arg, px, default_status, err);
*orig_arg += 2;
goto end;
}
args[cur_arg] += 5; /* skip "deny_" for the parsing */
}
if (strcmp(args[cur_arg], "errorfile") == 0) {
cur_arg++;
if (!*args[cur_arg]) {
memprintf(err, "'%s' expects <file> as argument", args[cur_arg-1]);
return ACT_RET_PRS_ERR;
}
rule->arg.http_reply = http_parse_http_reply(args, orig_arg, px, default_status, err);
rule->arg.http_deny.errmsg = http_load_errorfile(args[cur_arg], err);
if (!rule->arg.http_deny.errmsg)
return ACT_RET_PRS_ERR;
cur_arg++;
}
else if (strcmp(args[cur_arg], "errorfiles") == 0) {
cur_arg++;
if (!*args[cur_arg]) {
memprintf(err, "'%s' expects <http_errors_name> as argument", args[cur_arg-1]);
return ACT_RET_PRS_ERR;
}
/* Must be resolved during the config validity check */
rule->arg.act.p[0] = (void *)((intptr_t)status);
rule->arg.act.p[1] = strdup(args[cur_arg]);
rule->check_ptr = check_http_deny_action;
cur_arg++;
goto out;
}
end:
if (!rule->arg.http_reply)
return ACT_RET_PRS_ERR;
rule->arg.http_deny.status = status;
out:
*orig_arg = cur_arg;
rule->flags |= ACT_FLAG_FINAL;
rule->check_ptr = check_act_http_reply;
rule->release_ptr = release_act_http_reply;
return ACT_RET_PRS_OK;
}
@ -1802,13 +1773,6 @@ static enum act_parse_ret parse_http_strict_mode(const char **args, int *orig_ar
return ACT_RET_PRS_OK;
}
/* Release <.arg.http_reply> */
static void release_http_return(struct act_rule *rule)
{
release_http_reply(rule->arg.http_reply);
rule->arg.http_reply = NULL;
}
/* This function executes a return action. It builds an HTX message from an
* errorfile, an raw file or a log-format string, depending on <.action>
* value. On success, it returns ACT_RET_ABRT. If an error occurs ACT_RET_ERR is
@ -1839,20 +1803,6 @@ static enum act_return http_action_return(struct act_rule *rule, struct proxy *p
return ACT_RET_ABRT;
}
/* Check an "http-request return" action. The function returns 1 in success
* case, otherwise, it returns 0 and err is filled.
*/
static int check_http_return_action(struct act_rule *rule, struct proxy *px, char **err)
{
struct http_reply *reply = rule->arg.http_reply;
if (!http_check_http_reply(reply, px, err)) {
release_http_return(rule);
return 0;
}
return 1;
}
/* Parse a "return" action. It returns ACT_RET_PRS_OK on success,
* ACT_RET_PRS_ERR on error. It relies on http_parse_http_reply() to set
* <.arg.http_reply>.
@ -1868,9 +1818,9 @@ static enum act_parse_ret parse_http_return(const char **args, int *orig_arg, st
rule->flags |= ACT_FLAG_FINAL;
rule->action = ACT_CUSTOM;
rule->check_ptr = check_http_return_action;
rule->check_ptr = check_act_http_reply;
rule->action_ptr = http_action_return;
rule->release_ptr = release_http_return;
rule->release_ptr = release_act_http_reply;
return ACT_RET_PRS_OK;
}

View File

@ -672,6 +672,12 @@ int http_process_req_common(struct stream *s, struct channel *req, int an_bit, s
_HA_ATOMIC_ADD(&s->be->be_counters.denied_req, 1);
if (sess->listener->counters)
_HA_ATOMIC_ADD(&sess->listener->counters->denied_req, 1);
if (txn->http_reply) {
if (http_reply_message(s, txn->http_reply) == -1)
goto return_int_err;
goto return_prx_cond;
}
goto return_prx_err;
return_int_err:
@ -1002,8 +1008,28 @@ int http_process_tarpit(struct stream *s, struct channel *req, int an_bit)
*/
s->logs.t_queue = tv_ms_elapsed(&s->logs.tv_accept, &now);
http_reply_and_close(s, txn->status, (!(req->flags & CF_READ_ERROR) ? http_error_message(s) : NULL));
if (req->flags & CF_READ_ERROR) {
http_reply_and_close(s, txn->status, NULL);
goto end;
}
if (txn->http_reply) {
if (!http_reply_message(s, txn->http_reply))
goto end;
txn->status = 500;
if (!(s->flags & SF_ERR_MASK))
s->flags |= SF_ERR_INTERNAL;
_HA_ATOMIC_ADD(&s->sess->fe->fe_counters.internal_errors, 1);
if (s->flags & SF_BE_ASSIGNED)
_HA_ATOMIC_ADD(&s->be->be_counters.internal_errors, 1);
if (s->sess->listener->counters)
_HA_ATOMIC_ADD(&s->sess->listener->counters->internal_errors, 1);
}
http_reply_and_close(s, txn->status, http_error_message(s));
end:
req->analysers &= AN_REQ_FLT_END;
req->analyse_exp = TICK_ETERNITY;
@ -2159,6 +2185,12 @@ int http_process_res_common(struct stream *s, struct channel *rep, int an_bit, s
_HA_ATOMIC_ADD(&sess->listener->counters->denied_resp, 1);
if (objt_server(s->target))
_HA_ATOMIC_ADD(&__objt_server(s->target)->counters.denied_resp, 1);
if (txn->http_reply) {
if (http_reply_message(s, txn->http_reply) == -1)
goto return_int_err;
goto return_prx_cond;
}
goto return_prx_err;
return_int_err:
@ -2907,17 +2939,15 @@ static enum rule_result http_req_get_intercept_rule(struct proxy *px, struct lis
goto end;
case ACT_ACTION_DENY:
txn->status = rule->arg.http_deny.status;
if (rule->arg.http_deny.errmsg)
txn->errmsg = rule->arg.http_deny.errmsg;
txn->status = rule->arg.http_reply->status;
txn->http_reply = rule->arg.http_reply;
rule_ret = HTTP_RULE_RES_DENY;
goto end;
case ACT_HTTP_REQ_TARPIT:
txn->flags |= TX_CLTARPIT;
txn->status = rule->arg.http_deny.status;
if (rule->arg.http_deny.errmsg)
txn->errmsg = rule->arg.http_deny.errmsg;
txn->status = rule->arg.http_reply->status;
txn->http_reply = rule->arg.http_reply;
rule_ret = HTTP_RULE_RES_DENY;
goto end;
@ -3085,9 +3115,8 @@ resume_execution:
goto end;
case ACT_ACTION_DENY:
txn->status = rule->arg.http_deny.status;
if (rule->arg.http_deny.errmsg)
txn->errmsg = rule->arg.http_deny.errmsg;
txn->status = rule->arg.http_reply->status;
txn->http_reply = rule->arg.http_reply;
rule_ret = HTTP_RULE_RES_DENY;
goto end;
@ -4688,9 +4717,7 @@ int http_reply_message(struct stream *s, struct http_reply *reply)
/* get default error message */
errmsg = http_error_message(s);
}
if (b_is_null(errmsg))
goto leave;
if (!channel_htx_copy_msg(res, htx, errmsg))
if (!b_is_null(errmsg) && !channel_htx_copy_msg(res, htx, errmsg))
goto fail;
}
else {
@ -5178,6 +5205,7 @@ void http_init_txn(struct stream *s)
: 0);
txn->status = -1;
txn->errmsg = NULL;
txn->http_reply = NULL;
write_u32(txn->cache_hash, 0);
txn->cookie_first_date = 0;