Control characters, as defined by RFC5234, are 0x00 to 0x1F and 0x7F.
They're not easy to insert in a config by definition and difficult to
match. Let's add a sample converter which takes a binary input and
returns a boolean indicating if any such character is found within a
possibly configurable class.
By default with no argument, it checks the range above except TAB (0x9)
which is common. With argument "any", it checks them all. With argument
"http", it only checks the strictly forbidden HTTP ones (CR, LF, NUL)
in headers. Otherwise it takes a mask made of the bits corresponding
to each character, with bit 32 corresponding to character 0x7F.
It can be used to detect anomalies, e.g. by logging or dropping when
any such character is found.
field(index,delimiters[,count]) string string
fix_is_valid binary boolean
fix_tag_value(tag) binary binary
+has_ctl([mask]) binary boolean
hex binary string
hex2i binary integer
hmac(algorithm,key) binary binary
tcp-request content set-var(txn.foo) req.payload(0,0),fix_tag_value(35)
tcp-request content set-var(txn.bar) req.payload(0,0),fix_tag_value(MsgType)
+has_ctl([mask])
+ Checks the input binary sample for control characters as defined by the mask
+ argument. The mask is a 33-bit number (either decimal or hexadecimal prefixed
+ by "0x"), which has one bit set for each character to be detected in the 0x00
+ to 0x1F range, and bit 32 set to match the DEL character (0x7F). When no mask
+ is specified, the converter will use value 0x1FFFFFDFF, matching all control
+ characters except TAB (0x09), which is commonly used in HTTP headers. The
+ special mask "any" corresponds to 0x1FFFFFFFF which will match all control
+ characters, TAB included. The special mask "http" corresponds to 0x2401 and
+ will only cause the control characterss forbidden in HTTP header values to be
+ matched, which are CR (0x0D), LF (0x0A) and NUL (0x00).
+
+ Examples:
+ # reject presence of DEL, CR, LF, NUL characters in the referer header
+ http-request deny if { req.fhdr(referer),has_ctl(0x100002401) }
+ # reject presence of any control char but tab in any HTTP header value
+ http-request deny if { req.hdr(),has_ctl }
+
hex
Converts a binary input sample to a hex string containing two hex digits per
input byte. It is used to log or transfer hex dumps of some binary input data
--- /dev/null
+varnishtest "has_ctl converter Test"
+
+feature ignore_unknown_macro
+
+# The test payloads are passed base64-encoded in the "data" header and decoded
+# so that control chars (including NUL) can safely transit over HTTP.
+
+server s1 {
+ rxreq
+ txresp
+} -repeat 8 -start
+
+haproxy h1 -conf {
+ global
+ .if feature(THREAD)
+ thread-groups 1
+ .endif
+
+ defaults
+ mode http
+ timeout connect "${HAPROXY_TEST_TIMEOUT-5s}"
+ timeout client "${HAPROXY_TEST_TIMEOUT-5s}"
+ timeout server "${HAPROXY_TEST_TIMEOUT-5s}"
+
+ frontend fe
+ bind "fd@${fe}"
+
+ http-request set-var(txn.in) req.hdr(data),b64dec
+
+ # no arg and empty arg both mean 00-1F and 7F except tab
+ http-response set-header ctl-def "%[var(txn.in),has_ctl]"
+ http-response set-header ctl-empty "%[var(txn.in),has_ctl()]"
+ # all of 00-1F and 7F, tab included
+ http-response set-header ctl-any "%[var(txn.in),has_ctl(any)]"
+ # only the chars strictly forbidden in HTTP: NUL, LF, CR
+ http-response set-header ctl-http "%[var(txn.in),has_ctl(http)]"
+ # explicit mask, here only tab (bit 9)
+ http-response set-header ctl-mask "%[var(txn.in),has_ctl(0x200)]"
+
+ default_backend be
+
+ backend be
+ server s1 ${s1_addr}:${s1_port}
+} -start
+
+client c1 -connect ${h1_fe_sock} {
+ # "hello world": no control char at all
+ txreq -url "/" -hdr "Data: aGVsbG8gd29ybGQ="
+ rxresp
+ expect resp.status == 200
+ expect resp.http.ctl-def == "0"
+ expect resp.http.ctl-empty == "0"
+ expect resp.http.ctl-any == "0"
+ expect resp.http.ctl-http == "0"
+ expect resp.http.ctl-mask == "0"
+
+ # "a<tab>b": only matched by "any" and by the explicit tab mask
+ txreq -url "/" -hdr "Data: YQli"
+ rxresp
+ expect resp.status == 200
+ expect resp.http.ctl-def == "0"
+ expect resp.http.ctl-empty == "0"
+ expect resp.http.ctl-any == "1"
+ expect resp.http.ctl-http == "0"
+ expect resp.http.ctl-mask == "1"
+
+ # "a<cr>b": matched everywhere except by the tab mask
+ txreq -url "/" -hdr "Data: YQ1i"
+ rxresp
+ expect resp.status == 200
+ expect resp.http.ctl-def == "1"
+ expect resp.http.ctl-empty == "1"
+ expect resp.http.ctl-any == "1"
+ expect resp.http.ctl-http == "1"
+ expect resp.http.ctl-mask == "0"
+
+ # "a<lf>b"
+ txreq -url "/" -hdr "Data: YQpi"
+ rxresp
+ expect resp.status == 200
+ expect resp.http.ctl-def == "1"
+ expect resp.http.ctl-empty == "1"
+ expect resp.http.ctl-any == "1"
+ expect resp.http.ctl-http == "1"
+ expect resp.http.ctl-mask == "0"
+
+ # "a<nul>b"
+ txreq -url "/" -hdr "Data: YQBi"
+ rxresp
+ expect resp.status == 200
+ expect resp.http.ctl-def == "1"
+ expect resp.http.ctl-empty == "1"
+ expect resp.http.ctl-any == "1"
+ expect resp.http.ctl-http == "1"
+ expect resp.http.ctl-mask == "0"
+
+ # "a<soh>b": a control char but not one forbidden in HTTP
+ txreq -url "/" -hdr "Data: YQFi"
+ rxresp
+ expect resp.status == 200
+ expect resp.http.ctl-def == "1"
+ expect resp.http.ctl-empty == "1"
+ expect resp.http.ctl-any == "1"
+ expect resp.http.ctl-http == "0"
+ expect resp.http.ctl-mask == "0"
+
+ # "a<esc>b"
+ txreq -url "/" -hdr "Data: YRti"
+ rxresp
+ expect resp.status == 200
+ expect resp.http.ctl-def == "1"
+ expect resp.http.ctl-empty == "1"
+ expect resp.http.ctl-any == "1"
+ expect resp.http.ctl-http == "0"
+ expect resp.http.ctl-mask == "0"
+
+ # "a<del>b": 0x7f is the 33rd bit of the mask
+ txreq -url "/" -hdr "Data: YX9i"
+ rxresp
+ expect resp.status == 200
+ expect resp.http.ctl-def == "1"
+ expect resp.http.ctl-empty == "1"
+ expect resp.http.ctl-any == "1"
+ expect resp.http.ctl-http == "0"
+ expect resp.http.ctl-mask == "0"
+} -run
+
+# a non-numeric, non-keyword argument must be rejected
+haproxy h2 -conf-BAD {} {
+ global
+ .if feature(THREAD)
+ thread-groups 1
+ .endif
+
+ defaults
+ mode http
+ timeout connect "${HAPROXY_TEST_TIMEOUT-5s}"
+ timeout client "${HAPROXY_TEST_TIMEOUT-5s}"
+ timeout server "${HAPROXY_TEST_TIMEOUT-5s}"
+
+ frontend fe
+ bind "fd@${fe2}"
+ http-request set-var(txn.ctl) req.hdr(data),has_ctl(bogus)
+}
+
+# a mask with bits above the 33rd must be rejected
+haproxy h3 -conf-BAD {} {
+ global
+ .if feature(THREAD)
+ thread-groups 1
+ .endif
+
+ defaults
+ mode http
+ timeout connect "${HAPROXY_TEST_TIMEOUT-5s}"
+ timeout client "${HAPROXY_TEST_TIMEOUT-5s}"
+ timeout server "${HAPROXY_TEST_TIMEOUT-5s}"
+
+ frontend fe
+ bind "fd@${fe3}"
+ http-request set-var(txn.ctl) req.hdr(data),has_ctl(0x400000000)
+}
return 1;
}
+/* check and/or preset the optional argument of has_ctl() */
+static int sample_conv_hasctl_check(struct arg *args, struct sample_conv *conv,
+ const char *file, int line, char **err)
+{
+ char *endarg;
+ long long arg_int;
+
+ if (args[0].type != ARGT_STR || !*args[0].data.str.area) {
+ /* default to match any ctl char \x00-\x1f and \x7f, except tab (0x09) */
+ args[0].data.str.area = "0x1FFFFFDFF";
+ }
+ else if (strcmp(args[0].data.str.area, "any") == 0) {
+ /* any means any, thus \x00-\x1f and \x7f */
+ args[0].data.str.area = "0x1FFFFFFFF";
+ }
+ else if (strcmp(args[0].data.str.area, "http") == 0) {
+ /* default HTTP control chars: CR, LF, NUL */
+ args[0].data.str.area = "0x2401";
+ }
+
+
+ arg_int = strtoll(args[0].data.str.area, &endarg, 0);
+ if (*endarg) {
+ memprintf(err,
+ "cannot parse value '%s', problem at position %d; expecting an integer value (decimal or hex with '0x' prefix).\n",
+ args[0].data.str.area,
+ (int)(endarg - args[0].data.str.area));
+ return 0;
+ }
+
+ if (arg_int & ~0x1FFFFFFFFLL) {
+ memprintf(err, "unsupported excess bits 0x%llx in argument value %s",
+ arg_int & ~0x1FFFFFFFFLL,
+ args[0].data.str.area);
+ return 0;
+ }
+
+ args[0].type = ARGT_SINT;
+ args[0].data.sint = arg_int;
+ return 1;
+}
+
+/* Checks an input sample (binary or string) for control chars among those set
+ * in the mask in args[0], where one bit corresponds to one char for the first
+ * 32, and the 33th corresponds to 0x7F. This is used to detect some forbidden
+ * chars in header values. The result is a boolean indicating if any such char
+ * was found.
+ */
+static int sample_conv_hasctl(const struct arg *args, struct sample *smp, void *private)
+{
+ const char *end = smp->data.u.str.area + smp->data.u.str.data;
+ long long mask = args[0].data.sint;
+ unsigned char c;
+ const char *p;
+
+ for (p = smp->data.u.str.area; p < end; p++) {
+ c = *p;
+ if ((c < 0x20 && (mask & (1ULL << c))) ||
+ (c == 0x7f && (mask & (1ULL << 32))))
+ break;
+ }
+
+ smp->data.type = SMP_T_BOOL;
+ smp->data.u.sint = p < end; // true if found
+ smp->flags &= ~SMP_F_CONST;
+ return 1;
+}
+
static int sample_conv_bin2hex(const struct arg *arg_p, struct sample *smp, void *private)
{
struct buffer *trash = get_best_trash_chunk(&smp->data.u.str, smp->data.u.str.data*2);
{ "be2dec", sample_conv_be2dec, ARG3(1,STR,SINT,SINT), sample_conv_2dec_check, SMP_T_BIN, SMP_T_STR },
{ "le2dec", sample_conv_le2dec, ARG3(1,STR,SINT,SINT), sample_conv_2dec_check, SMP_T_BIN, SMP_T_STR },
{ "be2hex", sample_conv_be2hex, ARG3(1,STR,SINT,SINT), sample_conv_be2hex_check, SMP_T_BIN, SMP_T_STR },
+ { "has_ctl", sample_conv_hasctl, ARG1(0,STR), sample_conv_hasctl_check, SMP_T_BIN, SMP_T_BOOL },
{ "hex", sample_conv_bin2hex, 0, NULL, SMP_T_BIN, SMP_T_STR },
{ "hex2i", sample_conv_hex2int, 0, NULL, SMP_T_STR, SMP_T_SINT },
{ "ipmask", sample_conv_ipmask, ARG2(1,MSK4,MSK6), NULL, SMP_T_ADDR, SMP_T_ADDR },