Skip to content

CEL (Common Expression Language)

While you can use direct field matchers during rule building, more advanced scenarios might require using CEL expressions.

Common Expression Language (CEL) has been designed by Google to be a safe, expressive, and efficient way to express conditional logic. This page contains a list of CEL fields and functions that are available for you to use - some of them are default features of the CEL, some of them are specific to rescaled WAF.

Upon startup, all CEL expressions are being compiled. If a compilation error occurs, the startup will fail. Pre-compiling expressions during startup not only allow to catch errors early, but also to avoid unnecessary runtime overhead.

Using Expressions in Rules

The expression field during rule building accepts three forms.

Single Expression

A plain string is evaluated as-is:

- name: block-empty-ua
  action: DENY
  expression: 'userAgent == ""'

Multiple Expressions - All Must Match (AND)

When using the all key, every expression must evaluate to true:

- name: deny-post-to-upload
  action: DENY
  expression:
    all:
      - 'method == "POST"'
      - 'path == "/upload"'

Internally the expressions are joined with &&: ( method == "POST" ) && ( path == "/upload" )

This forms a logical AND of all provided expressions.

Multiple Expressions - Any Must Match (OR)

When using the any key, at least one provided expression must evaluate to true:

- name: challenge-suspicious-countries
  action: CHALLENGE
  expression:
    any:
      - 'geoCountry == "CN"'
      - 'geoCountry == "RU"'
      - 'geoCountry == "KP"'

Internally the expressions are joined with ||: ( geoCountry == "CN" ) || ( geoCountry == "RU" ) || ( geoCountry == "KP" )

This forms a logical OR of all provided expressions.

You cannot combine all and any in the same expression field. If you need both, nest the logic in a single expression string using && and ||.

Variables

Core Request Variables

Always available on every request.

Variable Type Description
remoteAddress string Client IP address (from the configured client IP header).
host string HTTP Host header value.
method string HTTP method (GET, POST, PUT, etc.).
path string Request path without the query string.
userAgent string User-Agent header value. Empty string when absent.
contentLength int Content-Length header parsed as integer. 0 when absent.
headers map(string, string) All HTTP headers, normalized to lowercase names
query map(string, string) Parsed query parameters. Multi-valued parameters are comma-joined into a single string.

Examples

method == "POST"
path.startsWith("/api/v2/")
contentLength > 1048576

Working with headers

As string operations in CEL are case-sensitive, all header names are normalized to lowercase. When writing CEL expressions against headers, make sure that you use lowercase names throughout all your rules. The content of a header will not be touched or modified in any way.

// Check if a header exists.
"authorization" in headers

// Match a header value.
headers["content-type"] == "application/json"

// Partial match.
headers["accept"].contains("text/html")

// Number of headers.
headers.size() > 0

Working with query

// Check if a parameter exists.
"token" in query

// Match a parameter value.
query["format"] == "json"

// Partial match on a comma-joined multi-value parameter.
// Given ?tag=foo&tag=bar, query["tag"] is "foo,bar".
query["tag"].contains("foo")

GeoIP Variables

Available when both features.geoip.enabled and features.geoip.policy_usage.enabled are true. Referencing these variables when GeoIP is disabled causes a compilation error at startup.

Variable Type Description Example value
geoCountry string ISO 3166-1 alpha-2 country code. "US", "DE"
geoCountryName string English country name. "United States", "Germany"
geoCity string City name in English. "San Francisco"
geoContinent string Two-letter continent code. "NA", "EU", "AS"

Examples

geoCountry == "US"
geoContinent == "EU" && geoCity != ""
geoCountryName.contains("Korea")

ASN Variables

Available when both features.asn.enabled and features.asn.policy_usage.enabled are true. Referencing these variables when ASN is disabled causes a compilation error at startup.

Variable Type Description Example value
asnNumber int Autonomous System Number. 15169 (Google)
asnOrg string Registered organization name. "GOOGLE"

Examples

// Match a specific ASN (Google).
asnNumber == 15169
// Match by organization name substring.
asnOrg.contains("AMAZON")
// Combine ASN and GeoIP data.
asnNumber == 13335 && geoCountry != "US"

Custom Functions

Aside from the standard CEL functionality, rescaled WAF supports a number of custom functions that are useful for implementing complex logic.

missingHeader(headers, name) -> bool

Returns true if the named header is absent from the request.

Parameter Type Description
headers map(string, string) The headers variable.
name string Header name to check (case-sensitive, use lowercase).
// Block requests without a User-Agent header.
missingHeader(headers, "user-agent")
// Require Accept header on API paths (combine with all mode).
// all:
//   - 'path.startsWith("/api/")'
//   - 'missingHeader(headers, "accept")'
// Inverse: check that a header IS present.
!missingHeader(headers, "authorization")

randInt(n) -> int

Returns a random integer in the range [0, n). Useful for probabilistic sampling or gradual rollouts.

Parameter Type Description
n int Upper bound (exclusive). Must be > 0.
// Challenge 10% of matching requests.
randInt(100) < 10
// 50/50 coin flip.
randInt(2) == 0
// Log roughly 1 in 1000 requests for debugging.
randInt(1000) == 0

regexSafe(s) -> string

Escapes regex metacharacters in s so it can be safely interpolated into a regex pattern. Escapes: \ . : * ? - [ ] ( ) + { } | ^ $

Parameter Type Description
s string String to escape.
regexSafe("file.txt")    // "file\.txt"
regexSafe("(test)")      // "\(test\)"
regexSafe("a|b")         // "a\|b"

ip_list(name) -> IPList

Returns an IP list container for use with the in operator. The named list must be defined in the ip_lists configuration section. Available only when at least one IP list is configured.

Parameter Type Description
name string Name of a configured IP list.

The returned container supports CIDR range matching — if the list contains 10.0.0.0/8, any address in that range matches.

// Check if client IP is in the "tor-exits" list.
remoteAddress in ip_list("tor-exits")
// Combine with other conditions.
remoteAddress in ip_list("rfc1918") && path.startsWith("/internal/")
// Negation: not in a trusted list.
!(remoteAddress in ip_list("office-ips"))

Referencing a non-existent list name causes a runtime error. Always make sure the list name matches a key in ip_lists.

String Extension Functions

The CEL environment includes the Google CEL Strings extension, providing these methods on string values. All indices are zero-based.

charAt(index) -> string

Returns the character at the given position.

path.charAt(0) == "/"

indexOf(substring) -> int / indexOf(substring, offset) -> int

Returns the index of the first occurrence, or -1 if not found. The optional offset starts the search from that position.

userAgent.indexOf("bot") >= 0
// Search starting from position 5.
path.indexOf("/", 1)

lastIndexOf(substring) -> int / lastIndexOf(substring, offset) -> int

Returns the index of the last occurrence, or -1 if not found.

path.lastIndexOf("/")

lowerAscii() -> string

Converts ASCII characters to lowercase. Prefer this for case-insensitive ASCII comparisons.

userAgent.lowerAscii().contains("curl")

upperAscii() -> string

Converts ASCII characters to uppercase.

method.upperAscii() == "POST"

replace(target, replacement) -> string / replace(target, replacement, count) -> string

Replaces occurrences of target with replacement. With count, limits the number of replacements.

path.replace("//", "/")

split(separator) -> list(string) / split(separator, limit) -> list(string)

Splits the string by separator. With limit, stops after that many parts.

path.split("/").size() > 3
// Split query value "a,b,c" into at most 2 parts: ["a", "b,c"].
query["tags"].split(",", 2)

substring(start) -> string / substring(start, end) -> string

Returns a substring from start (inclusive) to end (exclusive) or end of string.

// Strip leading slash.
path.substring(1)
// First 4 characters.
path.substring(0, 4) == "/api"

trim() -> string

Removes leading and trailing whitespace.

headers["x-custom"].trim() != ""

reverse() -> string

Returns the string with characters in reverse order.

path.reverse().startsWith("lmth.")

join() -> string / join(separator) -> string

Joins a list(string) into a single string.

["hello", "world"].join(" ") == "hello world"

format(args) -> string

Printf-style formatting. Supports %s, %d, %f, %e, %b, %x, %o.

"status: %d".format([200])

quote() -> string

Escapes special characters to make the string safe to print.

strings.quote(userAgent)

Built-in String Methods (CEL Standard)

These are part of CEL itself, not the extension:

// Substring containment.
userAgent.contains("bot")

// Prefix check.
path.startsWith("/api/")

// Suffix check.
path.endsWith(".json")

// RE2 regex match.
userAgent.matches("(?i)bot|crawl|spider")

// String length.
userAgent.size() > 0
// Or equivalently:
size(userAgent) > 0

Operators

Comparison

Operator Description Example
== Equal method == "GET"
!= Not equal path != "/"
< Less than contentLength < 1024
<= Less than or equal asnNumber <= 100
> Greater than contentLength > 0
>= Greater than or equal headers.size() >= 5
in Membership "accept" in headers

Logical

Operator Description Example
&& AND (short-circuit) method == "POST" && path == "/login"
\|\| OR (short-circuit) method == "PUT" \|\| method == "DELETE"
! NOT !missingHeader(headers, "accept")

Arithmetic

Operator Description Example
+ Add / string concat contentLength + 0
- Subtract asnNumber - 1
* Multiply randInt(10) * 2
/ Divide contentLength / 1024
% Modulo randInt(100) % 10

Ternary

condition ? value_if_true : value_if_false
// Not directly useful for boolean rule matching, but valid in sub-expressions:
(method == "POST" ? contentLength : 0) > 1024

Common Patterns

Block Requests Missing Common Headers

- name: deny-missing-headers
  action: DENY
  expression:
    all:
      - 'missingHeader(headers, "user-agent")'
      - 'missingHeader(headers, "accept")'

Challenge Known Scanner User-Agents

- name: challenge-scanners
  action: CHALLENGE
  expression: 'userAgent.matches("(?i)(sqlmap|nikto|nmap|masscan)")'
  challenge:
    algorithm: pow
    difficulty: 8

Weight Suspicious Requests (Auto-Challenge)

- name: weigh-no-accept
  action: WEIGH
  weight: 15
  expression: 'missingHeader(headers, "accept")'

- name: weigh-curl
  action: WEIGH
  weight: 20
  expression: 'userAgent.lowerAscii().contains("curl")'

- name: weigh-empty-ua
  action: WEIGH
  weight: 25
  expression: 'userAgent == ""'

HIT Score Sensitive File Access Attempts

- name: hit-dotfile-probe
  action: HIT
  amount: 5
  expression: 'path.matches("\\.(env|git|bak|sql|config)$")'

- name: hit-admin-probe
  action: HIT
  amount: 3
  expression: 'path.matches("(?i)/(admin|wp-admin|phpmyadmin)")'

Log Requests from Specific Networks

- name: log-datacenter-traffic
  action: LOG
  expression:
    any:
      - 'asnOrg.contains("AMAZON")'
      - 'asnOrg.contains("GOOGLE")'
      - 'asnOrg.contains("MICROSOFT")'

Allow Health Checks from Internal Networks

- name: allow-internal-health
  action: ALLOW
  expression:
    all:
      - 'remoteAddress in ip_list("rfc1918")'
      - 'path == "/healthz"'

Probabilistic Sampling

- name: challenge-sample
  action: CHALLENGE
  expression: 'randInt(100) < 5'
  challenge:
    algorithm: metarefresh
    difficulty: 1

Complex Multi-Condition Rule

When you need both AND and OR logic, write a single expression:

- name: deny-suspicious-api-post
  action: DENY
  expression: >-
    method == "POST"
    && path.startsWith("/api/")
    && (missingHeader(headers, "content-type")
        || missingHeader(headers, "authorization"))

Geo-Fencing with ASN Exception

- name: allow-known-cdn
  action: ALLOW
  expression: 'asnNumber == 13335'

- name: deny-geo-blocked
  action: DENY
  expression:
    any:
      - 'geoCountry == "KP"'
      - 'geoCountry == "IR"'

Error Handling

Compilation Errors (Startup)

CEL expressions are compiled and type-checked when rescaled-waf starts. Errors prevent startup entirely, with a clear message:

policy: compilation failed:
rule[3] "my-rule": CEL compilation failed: undeclared reference to 'geoCountry'

Common causes:

  • Referencing geoCountry/geoCity/geoContinent/geoCountryName when GeoIP policy usage is disabled.
  • Referencing asnNumber/asnOrg when ASN policy usage is disabled.
  • Type mismatch (e.g. method == 123 — comparing string to int).
  • Syntax errors in the expression string.
  • Referencing an IP list name that does not exist in ip_lists.

Runtime Behavior

  • Missing map keys: Accessing a header or query parameter that does not exist (e.g. headers["x-missing"]) produces a CEL error value, which causes the rule to be skipped. Use "key" in headers or missingHeader() to safely check first.
  • Invalid IP in ip_list(): If remoteAddress is not a valid IP address, the in check returns false (no error).
  • Invalid regex in matches(): Malformed regex patterns cause the expression evaluation to fail; the rule is skipped with a warning log.
  • Scope match errors: Any expression evaluation error is logged at warn level and the rule is skipped — it does not block the request or crash the server.