HRW: Adds a NORM normalization modifier for strings - #13587
Conversation
There was a problem hiding this comment.
Pull request overview
This PR introduces a new [NORM] modifier to header_rewrite / hrw4u that normalizes string values (currently percent-decoding) before they are compared or expanded, addressing evasion of string/regex conditions via percent-encoding.
Changes:
- Add
[NORM]to header_rewrite condition modifiers and apply normalization via a non-virtualCondition::append_value()wrapper so it also covers%{...}expansions used in operator values. - Extend hrw4u/u4wrh parsing, symbol conversion, and LSP documentation to support
NORMboth on conditions and on interpolated values ({... with NORM}/%{... [NORM]}). - Add hrw4u golden data and header_rewrite AuTest replay coverage for condition matching and value expansion behavior.
Reviewed changes
Copilot reviewed 27 out of 27 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| tools/hrw4u/tests/data/ops/norm-expansion.output.txt | New expected output for NORM in interpolated expansions. |
| tools/hrw4u/tests/data/ops/norm-expansion.input.txt | New hrw4u input exercising NORM in interpolated expansions. |
| tools/hrw4u/tests/data/ops/norm_bad_value_mod.fail.input.txt | Negative test: disallow non-value modifiers in interpolation context. |
| tools/hrw4u/tests/data/ops/norm_bad_value_mod.fail.error.txt | Expected error output for invalid interpolation modifier. |
| tools/hrw4u/tests/data/conds/norm-modifier.output.txt | New expected output for NORM on conditions. |
| tools/hrw4u/tests/data/conds/norm-modifier.input.txt | New hrw4u input exercising NORM on conditions. |
| tools/hrw4u/src/visitor.py | Parse and validate value modifiers in {...} substitutions; emit %{... [NORM]} when applicable. |
| tools/hrw4u/src/states.py | Add NORM and track it in condition state; define allowed VALUE_MODIFIERS set. |
| tools/hrw4u/src/sandbox.py | Allow NORM in sandbox modifier validation. |
| tools/hrw4u/src/lsp/documentation.py | Add LSP hover/docs entry describing NORM. |
| tools/hrw4u/src/interning.py | Intern NORM modifier token. |
| tools/hrw4u/src/hrw_symbols.py | Preserve/translate trailing %{... [MODS]} into with clauses during reverse symbol conversion. |
| tools/hrw4u/src/common.py | Add helpers to split/apply interpolation and percent modifiers. |
| tools/hrw4u/schema/sandbox.schema.json | Add NORM to the sandbox schema enum. |
| tools/hrw4u/grammar/u4wrh.g4 | Extend u4wrh grammar to parse NORM as a modifier. |
| tests/gold_tests/pluginTest/header_rewrite/rules/norm_modifier.conf | New header_rewrite rules validating NORM on conditions and value expansions. |
| tests/gold_tests/pluginTest/header_rewrite/rules/norm_block.conf | New header_rewrite rule exercising NORM for blocklist-style regex. |
| tests/gold_tests/pluginTest/header_rewrite/header_rewrite_bundle.replay.yaml | Add replay sessions to validate NORM behavior end-to-end. |
| plugins/header_rewrite/value.cc | Strip trailing [MODS] from condition_factory input while still parsing full token for modifiers. |
| plugins/header_rewrite/parser.cc | Treat %{...} as a single token even when it contains whitespace (e.g. %{... [NORM]}). |
| plugins/header_rewrite/matcher.h | Add MOD_NORM flag definition. |
| plugins/header_rewrite/conditions.h | Refactor condition implementations to override do_append_value() instead of append_value(). |
| plugins/header_rewrite/conditions.cc | Update implementations to match do_append_value() refactor. |
| plugins/header_rewrite/condition.h | Add non-virtual append_value() wrapper applying value modifiers; introduce do_append_value() and normalize(). |
| plugins/header_rewrite/condition.cc | Implement percent-decoding normalization and parse NORM modifier. |
| doc/admin-guide/plugins/header_rewrite.en.rst | Document [NORM] semantics and usage in both conditions and %{...} expansions. |
| doc/admin-guide/configuration/hrw4u.en.rst | Document NORM in hrw4u modifier list and interpolated-value usage. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 27 out of 27 changed files in this pull request and generated no new comments.
Suppressed comments (1)
plugins/header_rewrite/condition.cc:158
- Condition::initialize() calls parse_matcher_op(p.get_arg()) unconditionally, but Parser::preprocess() leaves get_arg() empty for %{} expansions used in operator values (e.g. when Value::set_value() parses "CLIENT-URL:PATH [NORM]" as an operator line). Accessing arg[0] in parse_matcher_op() is undefined behavior when the string is empty, and can lead to mis-parsing or crashes depending on the STL implementation.
if (p.consume_mod("NORM")) {
_mods |= CondModifiers::MOD_NORM;
}
if (p.consume_mod("L")) {
_mods |= CondModifiers::MOD_L;
}
_cond_op = parse_matcher_op(p.get_arg());
p.validate_mods();
A client can evade a string or regex condition by escaping any
character of the pattern, so [NORM] normalizes the condition's value
before it is matched. Normalization is percent-decoding for now, but
the name leaves room for more. Applying it in a non-virtual
append_value() wrapper means it also covers regexes and %{} expansions
inside operator values, neither of which the matcher-level modifiers
(PRE/SUF/MID/EXT) reach.
Teaching the tokenizer that %{} spans whitespace is what lets the
mods be written inside the braces, as in %{CLIENT-URL:PATH [NORM]}.
Conditions spell it with the existing "with" clause. Interpolated
values reuse the same word rather than inventing a second syntax for
the same concept, so "{inbound.url.path with NORM}" compiles to
%{CLIENT-URL:PATH [NORM]}, and both forms round-trip through u4wrh.
NORM is the only modifier accepted on a value, since the others just
change how a condition compares and would silently do nothing there.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 28 out of 28 changed files in this pull request and generated no new comments.
Suppressed comments (1)
plugins/header_rewrite/condition.cc:162
Condition::initialize()callsparse_matcher_op(p.get_arg()), butParser::get_arg()can be an empty string (e.g. value expansions parsed fromValue::set_value()likeCLIENT-URL:PATH [NORM]have no matcher operand).parse_matcher_op()unconditionally readsarg[0], which is undefined behavior when the arg is empty and can lead to crashes.
if (p.consume_mod("L")) {
_mods |= CondModifiers::MOD_L;
}
_cond_op = parse_matcher_op(p.get_arg());
p.validate_mods();
}
A client can evade a string or regex condition by escaping any character of the pattern, so [NORM] normalizes the condition's value before it is matched. Normalization is percent-decoding for now, but the name leaves room for more.
Applying it in a non-virtual append_value() wrapper means it also covers regexes and %{} expansions inside operator values, neither of which the matcher-level modifiers (PRE/SUF/MID/EXT) reach.