Skip to content
01. Lexical structure

01. Lexical structure

SecLang configuration is line-oriented text. This file defines how lines become directives; 02-grammar.md defines what a directive line may contain.

Lines and directives

Status: Core

Syntax. A configuration file is a sequence of lines separated by LF or CRLF. After line continuation (below) each logical line is empty, a comment, or one directive. A directive is a name followed by zero or more whitespace-separated arguments.

Semantics. Engines MUST accept both LF and CRLF line endings. Leading and trailing whitespace on a line is ignored. Empty lines are ignored. The character encoding of a configuration file is bytes; engines MUST NOT reject non-ASCII bytes inside quoted arguments (operator parameters routinely contain them).

Divergence notes. None known.

Tests. tests/engine/lexical/crlf-line-endings.yaml

Comments

Status: Core

Syntax. A logical line whose first non-blank character is # is a comment.

Semantics. Comment lines are discarded before any directive is parsed. There are no trailing comments: a # anywhere after the first non-blank character is data and reaches the directive unchanged. Because line continuation is applied first, a comment line ending in \ continues onto the next physical line and the whole logical line is the comment; engines MUST NOT parse the continued text as a directive (ADR-0013).

Divergence notes. ModSecurity v2 (Apache joins continuation lines before testing for #) and libmodsecurity v3 (src/parser/seclang-scanner.ll enters a comment state on # SecRule … \) behave as specified. Coraza 3.8.1 drops the comment line before testing for a trailing backslash (internal/seclang/parser.go, the # check precedes the continuation check), so the continued text is parsed as a new directive. A commented-out multi-line rule therefore comes back to life in Coraza. See ADR-0013.

Tests. tests/engine/lexical/comment-lines.yaml, tests/engine/lexical/comment-with-trailing-backslash.yaml

Line continuation

Status: Core

Syntax. A physical line whose last non-blank character is \ continues onto the next physical line.

Semantics. The backslash and the line break are removed and the two pieces are joined with no inserted character. Whitespace before the backslash and leading whitespace on the continuation line are kept as written; rule authors rely on this to indent continued lines inside a quoted action list. Continuation may repeat over any number of lines. OWASP CRS v4 contains over seven thousand continued lines, so this is the most exercised lexical feature in practice.

Divergence notes. None known for directive lines. For comment lines see #comments and ADR-0013.

Tests. tests/engine/lexical/continuation-lines.yaml

Quoting and escapes

Status: Core

Syntax. A directive argument is either a bare token, which extends to the next whitespace, or a string delimited by double quotes ". Inside double quotes, \" yields a literal double quote without ending the argument. A backslash before any other character is passed through to the directive unchanged, with one exception that is not specified: the pair \\, which Apache collapses to a single backslash before ModSecurity v2 sees it while libmodsecurity v3 and Coraza keep both characters. Portable rules therefore never write \\; OWASP CRS writes a literal backslash in regular expressions as \x5c for exactly this reason (its CONTRIBUTING.md).

Semantics. The quotes are delimiters, not part of the argument. An argument that contains whitespace, " or begins with @ or ! SHOULD be quoted. Single quotes are not argument delimiters at this level; they delimit values inside an action list and are defined in 02-grammar.md#action-list.

Divergence notes. Coraza turns \" into " and keeps \\ (verified by adapters/coraza). ModSecurity v2 receives its arguments from Apache’s configuration reader (server/util.c, substring_conf), which collapses both \" and \\. libmodsecurity v3 lexes operator arguments verbatim (src/parser/seclang-scanner.ll, FREE_TEXT_DOUBLE_QUOTE_MACRO_EXPANSION, appended with appendText) and never removes the backslash of \", so @streq say \"hi\" now compares against the backslashes too; quoted-arguments.yaml is predicted to fail there (compat/known-gaps.md). Engines also differ in how they parse \" inside the action list after macro expansion; a libmodsecurity v3 fix for an escaped quote following a macro landed in 2026 and that case is tested in 02-grammar.md.

Tests. tests/engine/lexical/quoted-arguments.yaml

Include

Status: Core

Syntax. Include PATH

Semantics. The file at PATH is parsed in place, as if its lines replaced the Include line. If PATH contains *, it is a glob pattern; every matching file is included in lexicographic order. Engines MUST support absolute paths and * globs. A non-glob PATH that does not exist MUST be a configuration error. The outcome of a glob that matches nothing is not specified (see the divergence note); portable configurations only use globs that match at least one file. Included files may themselves Include; an engine MAY cap the total number of included files, and the cap MUST be at least 100.

The directory against which a relative PATH is resolved is not specified: see the divergence note. Portable configurations use absolute paths or paths relative to a directory the deployment controls.

Divergence notes. ModSecurity v2 uses the Apache Include directive, which resolves relative paths against ServerRoot. libmodsecurity v3 and Coraza resolve them against the directory of the including file (Coraza: internal/seclang/parser.go, which also caps includes at 100 files via maxIncludeRecursion). Coraza only treats PATH as a glob when it contains *; Apache also expands ? and [...]. Only * is Core. An empty glob is a configuration error in ModSecurity v2 (Apache 2.4 Include: “No matches for the wildcard”; IncludeOptional is the lenient form) and libmodsecurity v3 (seclang-scanner.ll, “Not able to open file”), and only a warning in Coraza (parser.go, “empty glob result”).

Tests. tests/engine/lexical/include-relative-file.yaml

Name matching

Status: Core

Syntax. Directive names (SecRule), operator names (@contains), action names (deny, t:), transformation names (lowercase) and ctl: option names (ruleEngine) are identifiers made of ASCII letters and digits.

Semantics. Engines MUST match all of these identifiers case-insensitively. secrule, SecRule and SECRULE denote the same directive; @CONTAINS and @contains the same operator; T:LOWERCASE and t:lowercase the same transformation. The canonical spelling used in this specification is the mixed-case form from the ModSecurity reference manual; engines SHOULD emit that spelling in logs and error messages.

Variable and collection names (ARGS, TX) and collection keys are not covered by this section; their case rules are defined with the variables (ADR-0008, Phase 3).

Divergence notes. ModSecurity v2 and v3 already behave this way. Coraza 3.8.1 matches directive, action and transformation names case-insensitively but operator names case-sensitively. See ADR-0002.

Tests. tests/engine/lexical/case-insensitive-names.yaml