Advanced Custom Rules

Introduction

You’ve created a simple WAF rule that evaluates one part of a request. How could you create a rule to evaulate multiple parts of a request?

Rules in JSON

All WAF Rules are defined as JSON objects. For complex rules, it can be more efficient to work directly with the JSON format than via the Console rules editor. You can retrieve existing rules in JSON format using the API, CLI or Console using the get-rule-group command. Modify them using your favourite JSON text editor, then reupload them using update-rule-group in the API, CLI or Console.

Defining rules in JSON allows you to use version control as a source of truth to see how, when and why iterations of complex rulesets have changed.

The syntax for defining rules in JSON is provided in the update-rule-group documentation

If you’re ever unsure of the syntax, it can be helpful to create a simple example in the Console visual editor first, then switch to the JSON editor to see the JSON equivalent.

Boolean Logic in Rules

The AND, OR and NOT operators can be used to create more complex rules. This is useful to inspect multiple parts of a request. For example, you could only allow a request if the query string OR the Header contains a certain key/value.

Nested rules can be created using the visual editor. However, they are limited to one level deep.
To create arbitarily nested rules in the console, the JSON editor must be used.
Use the validate action in the console JSON editor to validate the rule

Below is an example of a rule created in the console. This rule will block requests with a query string of length greater than or equal to 0.

This rule will block any request containing a query string.

Console Version of Simple Rule

Here is the same rule defined in JSON

  • Action specifies the action taken by WAF if the rule evaluates to true.
  • VisibilityConfig is used to configure request sampling and CloudWatch metrics.
  • Statement defines the rule expression to be evaluated.
{
  "Name": "example-rule-01",
  "Priority": 0,
  "Action": {
    "Block": {}
  },
  "VisibilityConfig": {
    "SampledRequestsEnabled": true,
    "CloudWatchMetricsEnabled": true,
    "MetricName": "example-rule-01"
  },
  "Statement": {
    "SizeConstraintStatement": {
      "FieldToMatch": {
        "QueryString": {}
      },
      "ComparisonOperator": "GT",
      "Size": "0",
      "TextTransformations": [
        {
          "Type": "NONE",
          "Priority": 0
        }
      ]
    }
  }
}

Example

You’ve been asked by your favourite colleague for some help. They are recieving an attack. They need to block malicious incoming requests without blocking requests from actual customers. The malicious requests contain a body over 100kb, but are missing a header, x-upload-photo: true.

You quickly realise that this isn’t possible to express using the console rule editor. You will need to edit some JSON.

Let’s start with an empty rule:

{
  "Name": "example-rule-02",
  "Priority": 0,
  "Action": {
    "Block": {}
  },
  "VisibilityConfig": {
    "SampledRequestsEnabled": true,
    "CloudWatchMetricsEnabled": true,
    "MetricName": "example-rule-02"
  },
  "Statement": {
    // We will add the rule here
  }
}

There are two cases we need to consider

  1. If the request body is larger than 100kb, block the request
  2. If the request does not contain a header of x-upload-body: true, block the request

We will need to use the OrStatement and NotStatement to express this logic.

{
// fields omitted for brevity
"Statement": {
  "OrStatement": {
    "Statements": [
      {
        // Inspect Body Size here
      },
      {
        "NotStatement": {
           // Inspect Header here
        }
      }
    ]
  }
}

To inspect the size of the body, we will use the SizeConstraintStatement to validate the size of the request body.

"SizeConstraintStatement": {
  "FieldToMatch": {
    "Body": {}
  },
  "ComparisonOperator": "GT",
  "Size": "100",
  "TextTransformations": [
    {
      "Type": "NONE",
      "Priority": 0
    }
  ]
}

To inspect the Headers of the request, use the ByteMatchStatement

"ByteMatchStatement": {
  "FieldToMatch": {
    "SingleHeader": {
      "Name": "x-upload-image"
    }
  },
  "PositionalConstraint": "EXACTLY",
  "SearchString": "true",
  "TextTransformations": [
    {
      "Type": "NONE",
      "Priority": 0
    }
  ]
}

Here is the final rule

{
  "Name": "complex-rule-example",
  "Priority": 0,
  "Action": {
    "Block": {}
  },
  "VisibilityConfig": {
    "SampledRequestsEnabled": true,
    "CloudWatchMetricsEnabled": true,
    "MetricName": "complex-rule-example"
  },
  "Statement": {
    "OrStatement": {
      "Statements": [
        {
          "SizeConstraintStatement": {
            "FieldToMatch": {
              "Body": {}
            },
            "ComparisonOperator": "GT",
            "Size": "100",
            "TextTransformations": [
              {
                "Type": "NONE",
                "Priority": 0
              }
            ]
          }
        },
        {
          "NotStatement": {
            "Statement": {
              "ByteMatchStatement": {
                "FieldToMatch": {
                  "SingleHeader": {
                    "Name": "x-upload-body"
                  }
                },
                "PositionalConstraint": "EXACTLY",
                "SearchString": "true",
                "TextTransformations": [
                  {
                    "Type": "NONE",
                    "Priority": 0
                  }
                ]
              }
            }
          }
        }
      ]
    }
  }
}

Challenge

The milkshake bandits are back attacking your workshop. They’ve changed their attack again! You’ll need to create a new rule to block these requests, whilst allow genuine customers For this challenge, you start with an existing rule.

Select to see your starting rule

Currently it will block any requests that either:

  1. Contain the header x-milkshake: chocolate
  2. Contain the query parameter milkshake=banana

This rule was working, but the attackers have adapted. Now malicious requests contain either

  1. The header x-milkshake: chocolate AND the header x-favourite-topping: nuts
  2. The query parameter milkshake=banana AND the query parameter favourite-topping=sauce

Update the existing rule. Use AndStatement to extend the two existing statements. AndStatement has the following syntax:

"AndStatement": {
  "Statements": [
    # Add your statements here
  ]
}

Test Cases

Test your new rule by creating the following requests using curl.

# Set the JUICESHOP_URL if not already done
JUICESHOP_URL=<Your Juice Shop URL>
# Allowed
curl -H "x-milkshake: chocolate" "${JUICESHOP_URL}"
curl  "${JUICESHOP_URL}?milkshake=banana"

# Blocked
curl -H "x-milkshake: chocolate" -H "x-favourite-topping: nuts" "${JUICESHOP_URL}"
curl  "${JUICESHOP_URL}?milkshake=banana&favourite-topping=sauce"

Blocked requests will give a response like below

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
    <title>ERROR: The request could not be satisfied</title>
  </head>
  <body>
    <h1>403 ERROR</h1>
    <!-- Omitted -->
  </body>
</html>

Answer

Select to see answer

Conclusion

In this section, you learnt about the JSON format for WAF Rules. Complex logic can be defined in rules using the And, Or and Not operators.