From 6f3e6b029b622bca019960d0901481e727c8df28 Mon Sep 17 00:00:00 2001 From: Masaori Koshiba Date: Mon, 14 Sep 2026 15:30:17 +0900 Subject: [PATCH 1/3] hrw4u: enforce sandbox policy on value functions The sandbox `functions` deny/warn list was only consulted for statement functions, so an entry naming a function used as a value in an expression was accepted by the loader and then silently ignored. `access()`, which reaches the filesystem, was among them. The split between the two resolvers is an implementation detail; the grammar has one `functionCall` rule and the policy YAML exposes one `functions` category, so both now go through the same check. --- doc/admin-guide/configuration/hrw4u.en.rst | 12 ++++++++++-- tools/hrw4u/schema/sandbox.schema.json | 11 +++++++++-- tools/hrw4u/src/symbols.py | 2 ++ .../tests/data/sandbox/denied-value-function.ast.txt | 1 + .../data/sandbox/denied-value-function.error.txt | 3 +++ .../data/sandbox/denied-value-function.input.txt | 7 +++++++ .../data/sandbox/denied-value-function.sandbox.yaml | 7 +++++++ .../tests/data/sandbox/warned-value-function.ast.txt | 1 + .../data/sandbox/warned-value-function.input.txt | 5 +++++ .../data/sandbox/warned-value-function.output.txt | 3 +++ .../data/sandbox/warned-value-function.sandbox.yaml | 6 ++++++ .../data/sandbox/warned-value-function.warning.txt | 2 ++ 12 files changed, 56 insertions(+), 4 deletions(-) create mode 100644 tools/hrw4u/tests/data/sandbox/denied-value-function.ast.txt create mode 100644 tools/hrw4u/tests/data/sandbox/denied-value-function.error.txt create mode 100644 tools/hrw4u/tests/data/sandbox/denied-value-function.input.txt create mode 100644 tools/hrw4u/tests/data/sandbox/denied-value-function.sandbox.yaml create mode 100644 tools/hrw4u/tests/data/sandbox/warned-value-function.ast.txt create mode 100644 tools/hrw4u/tests/data/sandbox/warned-value-function.input.txt create mode 100644 tools/hrw4u/tests/data/sandbox/warned-value-function.output.txt create mode 100644 tools/hrw4u/tests/data/sandbox/warned-value-function.sandbox.yaml create mode 100644 tools/hrw4u/tests/data/sandbox/warned-value-function.warning.txt diff --git a/doc/admin-guide/configuration/hrw4u.en.rst b/doc/admin-guide/configuration/hrw4u.en.rst index b6953df6415..9a28458438e 100644 --- a/doc/admin-guide/configuration/hrw4u.en.rst +++ b/doc/admin-guide/configuration/hrw4u.en.rst @@ -739,16 +739,22 @@ the body is not validated. Functions --------- -The ``functions`` list accepts any of the statement-function names used in -HRW4U source. The complete set of deniable functions is: +The ``functions`` list accepts any of the function names used in HRW4U source, +both statement functions and the functions that produce a value in an +expression. The complete set of deniable functions is: ====================== ============================================= Function Description ====================== ============================================= +``access`` File accessibility check ``add-header`` Add a header (``+=`` operator equivalent) +``cache`` Cache lookup result status +``cidr`` Masked client IP address match ``counter`` Increment an ATS statistics counter +``internal`` Internally generated transaction check ``keep_query`` Keep only specified query parameters ``no-op`` Explicit no-op statement +``random`` Random number in the given range ``remove_query`` Remove specified query parameters ``run-plugin`` Invoke an external remap plugin ``set-body-from`` Set response body from a URL @@ -757,6 +763,8 @@ Function Description ``set-plugin-cntl`` Set a plugin control flag ``set-redirect`` Issue an HTTP redirect response ``skip-remap`` Skip remap processing (open proxy) +``ssn-txn-count`` Transaction count on server connection +``txn-count`` Transaction count on client connection ====================== ============================================= Conditions and Operators diff --git a/tools/hrw4u/schema/sandbox.schema.json b/tools/hrw4u/schema/sandbox.schema.json index 927e2d8e66d..9b7734d70d8 100644 --- a/tools/hrw4u/schema/sandbox.schema.json +++ b/tools/hrw4u/schema/sandbox.schema.json @@ -53,14 +53,19 @@ }, "functions": { "type": "array", - "description": "Statement function names.", + "description": "Function names, both statement functions and functions used as values in expressions.", "items": { "type": "string", "enum": [ + "access", "add-header", + "cache", + "cidr", "counter", + "internal", "keep_query", "no-op", + "random", "remove_query", "run-plugin", "set-body", @@ -70,7 +75,9 @@ "set-debug", "set-plugin-cntl", "set-redirect", - "skip-remap" + "skip-remap", + "ssn-txn-count", + "txn-count" ] }, "uniqueItems": true diff --git a/tools/hrw4u/src/symbols.py b/tools/hrw4u/src/symbols.py index a57c6f27041..3dc66fee983 100644 --- a/tools/hrw4u/src/symbols.py +++ b/tools/hrw4u/src/symbols.py @@ -181,6 +181,8 @@ def resolve_condition(self, name: str, section: SectionType | None = None) -> tu def resolve_function(self, func_name: str, args: list[str], strip_quotes: bool = False) -> str: with self.debug_context("resolve_function", func_name, args): + self._collect_warning(self._sandbox.check_function(func_name)) + if params := self._lookup_function_cached(func_name): tag = params.target validator = params.validate diff --git a/tools/hrw4u/tests/data/sandbox/denied-value-function.ast.txt b/tools/hrw4u/tests/data/sandbox/denied-value-function.ast.txt new file mode 100644 index 00000000000..619ebb87be3 --- /dev/null +++ b/tools/hrw4u/tests/data/sandbox/denied-value-function.ast.txt @@ -0,0 +1 @@ +(program (programItem (section REMAP { (sectionBody (conditional (ifStatement if (condition (expression (term (factor (functionCall access ( (argumentList (value "/tmp/hrw4u-probe")) )))))) (block { (blockItem (statement inbound.req.X-Probe = (value "yes") ;)) })))) (sectionBody (statement inbound.req.X-Txn-Count = (value "{txn-count()}") ;)) })) ) diff --git a/tools/hrw4u/tests/data/sandbox/denied-value-function.error.txt b/tools/hrw4u/tests/data/sandbox/denied-value-function.error.txt new file mode 100644 index 00000000000..3cf46508ffe --- /dev/null +++ b/tools/hrw4u/tests/data/sandbox/denied-value-function.error.txt @@ -0,0 +1,3 @@ +'access' is denied by sandbox policy (function) +'txn-count' is denied by sandbox policy (function) +Feature denied by sandbox policy. Contact platform team. diff --git a/tools/hrw4u/tests/data/sandbox/denied-value-function.input.txt b/tools/hrw4u/tests/data/sandbox/denied-value-function.input.txt new file mode 100644 index 00000000000..2f88b18ddfb --- /dev/null +++ b/tools/hrw4u/tests/data/sandbox/denied-value-function.input.txt @@ -0,0 +1,7 @@ +REMAP { + if access("/tmp/hrw4u-probe") { + inbound.req.X-Probe = "yes"; + } + + inbound.req.X-Txn-Count = "{txn-count()}"; +} diff --git a/tools/hrw4u/tests/data/sandbox/denied-value-function.sandbox.yaml b/tools/hrw4u/tests/data/sandbox/denied-value-function.sandbox.yaml new file mode 100644 index 00000000000..923045b668a --- /dev/null +++ b/tools/hrw4u/tests/data/sandbox/denied-value-function.sandbox.yaml @@ -0,0 +1,7 @@ +sandbox: + message: "Feature denied by sandbox policy. Contact platform team." + + deny: + functions: + - access + - txn-count diff --git a/tools/hrw4u/tests/data/sandbox/warned-value-function.ast.txt b/tools/hrw4u/tests/data/sandbox/warned-value-function.ast.txt new file mode 100644 index 00000000000..eeab10ba62e --- /dev/null +++ b/tools/hrw4u/tests/data/sandbox/warned-value-function.ast.txt @@ -0,0 +1 @@ +(program (programItem (section REMAP { (sectionBody (conditional (ifStatement if (condition (expression (term (factor (comparison (comparable (functionCall cidr ( (argumentList (value 24) , (value 48)) ))) == (value "10.0.0.0")))))) (block { (blockItem (statement inbound.req.X-Net = (value "matched") ;)) })))) })) ) diff --git a/tools/hrw4u/tests/data/sandbox/warned-value-function.input.txt b/tools/hrw4u/tests/data/sandbox/warned-value-function.input.txt new file mode 100644 index 00000000000..30650f52988 --- /dev/null +++ b/tools/hrw4u/tests/data/sandbox/warned-value-function.input.txt @@ -0,0 +1,5 @@ +REMAP { + if cidr(24, 48) == "10.0.0.0" { + inbound.req.X-Net = "matched"; + } +} diff --git a/tools/hrw4u/tests/data/sandbox/warned-value-function.output.txt b/tools/hrw4u/tests/data/sandbox/warned-value-function.output.txt new file mode 100644 index 00000000000..feeedd3db29 --- /dev/null +++ b/tools/hrw4u/tests/data/sandbox/warned-value-function.output.txt @@ -0,0 +1,3 @@ +cond %{REMAP_PSEUDO_HOOK} [AND] +cond %{CIDR:24,48} ="10.0.0.0" + set-header X-Net "matched" diff --git a/tools/hrw4u/tests/data/sandbox/warned-value-function.sandbox.yaml b/tools/hrw4u/tests/data/sandbox/warned-value-function.sandbox.yaml new file mode 100644 index 00000000000..3709f31dc49 --- /dev/null +++ b/tools/hrw4u/tests/data/sandbox/warned-value-function.sandbox.yaml @@ -0,0 +1,6 @@ +sandbox: + message: "This feature will be denied in a future release. Contact platform team." + + warn: + functions: + - cidr diff --git a/tools/hrw4u/tests/data/sandbox/warned-value-function.warning.txt b/tools/hrw4u/tests/data/sandbox/warned-value-function.warning.txt new file mode 100644 index 00000000000..9c1a45ce251 --- /dev/null +++ b/tools/hrw4u/tests/data/sandbox/warned-value-function.warning.txt @@ -0,0 +1,2 @@ +'cidr' is warned by sandbox policy (function) +This feature will be denied in a future release. Contact platform team. From e67b8c197c64b0233194ba9df74fd5b213874b3f Mon Sep 17 00:00:00 2001 From: Masaori Koshiba Date: Mon, 14 Sep 2026 16:09:18 +0900 Subject: [PATCH 2/3] doc: list set-body and set-body-from-file with the other functions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both are statement functions in their own right — `set-body("x")` and `set-body-from-file("/p")` compile — and the schema enum already accepts them, but neither the sandbox function table nor the operator mapping table listed them all. Both tables now match STATEMENT_FUNCTION_MAP. --- doc/admin-guide/configuration/hrw4u.en.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/admin-guide/configuration/hrw4u.en.rst b/doc/admin-guide/configuration/hrw4u.en.rst index 9a28458438e..e59d4d706eb 100644 --- a/doc/admin-guide/configuration/hrw4u.en.rst +++ b/doc/admin-guide/configuration/hrw4u.en.rst @@ -347,6 +347,7 @@ rm-destination QUERY ... [I] keep_query("foo,bar") Keep only specif run-plugin foo.so "args" run-plugin("foo.so", "arg1", ...) Run an external remap plugin set-body "foo" inbound.resp.body = "foo" Set the response body set-body-from "\https://..." set-body-from("\https://...") Set the response body from a URL +set-body-from-file "/tmp/b" set-body-from-file("/tmp/b") Set the response body from a local file set-config 12 set-config("name", 17) Set a configuration variable to a value set-conn-dscp 8 inbound.conn.dscp = 8 Set the DSCP value for the connection set-conn-mark 17 inbound.conn.mark = 17 Set the MARK value for the connection @@ -757,7 +758,9 @@ Function Description ``random`` Random number in the given range ``remove_query`` Remove specified query parameters ``run-plugin`` Invoke an external remap plugin +``set-body`` Set the response body ``set-body-from`` Set response body from a URL +``set-body-from-file`` Set response body from a local file ``set-config`` Override an ATS configuration variable ``set-debug`` Enable per-transaction ATS debug logging ``set-plugin-cntl`` Set a plugin control flag From b5d44c346a4864d96c03adc2e9a1298e9f9e836c Mon Sep 17 00:00:00 2001 From: Masaori Koshiba Date: Tue, 22 Sep 2026 21:17:37 +0900 Subject: [PATCH 3/3] hrw4u: enforce the sandbox policy inside string interpolation `_substitute_strings` handled a `SandboxDenialError` like any symbol error, so a function or condition reached only through `"{...}"` was reported without its policy message. It is now reported as a denial, and the rest of the string is still checked so every denial in it is listed. --- doc/admin-guide/configuration/hrw4u.en.rst | 6 ++++-- tools/hrw4u/src/visitor.py | 6 ++++++ .../data/sandbox/denied-interpolated-function.ast.txt | 1 + .../data/sandbox/denied-interpolated-function.error.txt | 2 ++ .../data/sandbox/denied-interpolated-function.input.txt | 3 +++ .../sandbox/denied-interpolated-function.sandbox.yaml | 6 ++++++ .../data/sandbox/denied-interpolated-multiple.error.txt | 3 +++ .../data/sandbox/denied-interpolated-multiple.input.txt | 3 +++ .../sandbox/denied-interpolated-multiple.sandbox.yaml | 8 ++++++++ 9 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 tools/hrw4u/tests/data/sandbox/denied-interpolated-function.ast.txt create mode 100644 tools/hrw4u/tests/data/sandbox/denied-interpolated-function.error.txt create mode 100644 tools/hrw4u/tests/data/sandbox/denied-interpolated-function.input.txt create mode 100644 tools/hrw4u/tests/data/sandbox/denied-interpolated-function.sandbox.yaml create mode 100644 tools/hrw4u/tests/data/sandbox/denied-interpolated-multiple.error.txt create mode 100644 tools/hrw4u/tests/data/sandbox/denied-interpolated-multiple.input.txt create mode 100644 tools/hrw4u/tests/data/sandbox/denied-interpolated-multiple.sandbox.yaml diff --git a/doc/admin-guide/configuration/hrw4u.en.rst b/doc/admin-guide/configuration/hrw4u.en.rst index e59d4d706eb..8ba355c47d0 100644 --- a/doc/admin-guide/configuration/hrw4u.en.rst +++ b/doc/admin-guide/configuration/hrw4u.en.rst @@ -742,7 +742,8 @@ Functions The ``functions`` list accepts any of the function names used in HRW4U source, both statement functions and the functions that produce a value in an -expression. The complete set of deniable functions is: +expression, including a call written inside a string interpolation such as +``"{txn-count()}"``. The complete set of deniable functions is: ====================== ============================================= Function Description @@ -775,7 +776,8 @@ Conditions and Operators The ``conditions`` and ``operators`` lists use the same dot-notation keys shown in the `Conditions`_ and `Operators`_ tables above (e.g. ``inbound.req.``, -``geo.``, ``outbound.conn.``). +``geo.``, ``outbound.conn.``). A condition read inside a string interpolation, +such as ``"{inbound.method}"``, is checked the same way. Entries ending with ``.`` use **prefix matching** — ``geo.`` denies all ``geo.*`` lookups (``geo.city``, ``geo.ASN``, etc.). Entries without a trailing diff --git a/tools/hrw4u/src/visitor.py b/tools/hrw4u/src/visitor.py index 23c6d6bd439..d430bd878b5 100644 --- a/tools/hrw4u/src/visitor.py +++ b/tools/hrw4u/src/visitor.py @@ -230,6 +230,12 @@ def repl(m: re.Match) -> str: self.debug(f"substitute: {{{var_name}}} -> {replacement}") return replacement raise SymbolResolutionError(m.group(0), "Unrecognized substitution format") + except SandboxDenialError as e: + # Report it as a denial so the sandbox message is set. + e.add_note(f"String interpolation context: {s[:50]}...") + with self.trap(ctx): + raise + return f"{{ERROR: {e}}}" except Exception as e: error = hrw4u_error(self.filename, ctx, f"symbol error in {{}}: {e}") if hasattr(error, 'add_note'): diff --git a/tools/hrw4u/tests/data/sandbox/denied-interpolated-function.ast.txt b/tools/hrw4u/tests/data/sandbox/denied-interpolated-function.ast.txt new file mode 100644 index 00000000000..4fea90bbb49 --- /dev/null +++ b/tools/hrw4u/tests/data/sandbox/denied-interpolated-function.ast.txt @@ -0,0 +1 @@ +(program (programItem (section REMAP { (sectionBody (statement inbound.req.X-Txn-Count = (value "{txn-count()}") ;)) })) ) diff --git a/tools/hrw4u/tests/data/sandbox/denied-interpolated-function.error.txt b/tools/hrw4u/tests/data/sandbox/denied-interpolated-function.error.txt new file mode 100644 index 00000000000..a04ed897602 --- /dev/null +++ b/tools/hrw4u/tests/data/sandbox/denied-interpolated-function.error.txt @@ -0,0 +1,2 @@ +'txn-count' is denied by sandbox policy (function) +Feature denied by sandbox policy. Contact platform team. diff --git a/tools/hrw4u/tests/data/sandbox/denied-interpolated-function.input.txt b/tools/hrw4u/tests/data/sandbox/denied-interpolated-function.input.txt new file mode 100644 index 00000000000..0dbdc3c1df6 --- /dev/null +++ b/tools/hrw4u/tests/data/sandbox/denied-interpolated-function.input.txt @@ -0,0 +1,3 @@ +REMAP { + inbound.req.X-Txn-Count = "{txn-count()}"; +} diff --git a/tools/hrw4u/tests/data/sandbox/denied-interpolated-function.sandbox.yaml b/tools/hrw4u/tests/data/sandbox/denied-interpolated-function.sandbox.yaml new file mode 100644 index 00000000000..3f47292938c --- /dev/null +++ b/tools/hrw4u/tests/data/sandbox/denied-interpolated-function.sandbox.yaml @@ -0,0 +1,6 @@ +sandbox: + message: "Feature denied by sandbox policy. Contact platform team." + + deny: + functions: + - txn-count diff --git a/tools/hrw4u/tests/data/sandbox/denied-interpolated-multiple.error.txt b/tools/hrw4u/tests/data/sandbox/denied-interpolated-multiple.error.txt new file mode 100644 index 00000000000..c6fb75255b8 --- /dev/null +++ b/tools/hrw4u/tests/data/sandbox/denied-interpolated-multiple.error.txt @@ -0,0 +1,3 @@ +'txn-count' is denied by sandbox policy (function) +'inbound.method' is denied by sandbox policy (condition) +Feature denied by sandbox policy. Contact platform team. diff --git a/tools/hrw4u/tests/data/sandbox/denied-interpolated-multiple.input.txt b/tools/hrw4u/tests/data/sandbox/denied-interpolated-multiple.input.txt new file mode 100644 index 00000000000..276205d5959 --- /dev/null +++ b/tools/hrw4u/tests/data/sandbox/denied-interpolated-multiple.input.txt @@ -0,0 +1,3 @@ +REMAP { + inbound.req.X-Info = "{txn-count()} {inbound.method}"; +} diff --git a/tools/hrw4u/tests/data/sandbox/denied-interpolated-multiple.sandbox.yaml b/tools/hrw4u/tests/data/sandbox/denied-interpolated-multiple.sandbox.yaml new file mode 100644 index 00000000000..25a9492cf3e --- /dev/null +++ b/tools/hrw4u/tests/data/sandbox/denied-interpolated-multiple.sandbox.yaml @@ -0,0 +1,8 @@ +sandbox: + message: "Feature denied by sandbox policy. Contact platform team." + + deny: + conditions: + - inbound.method + functions: + - txn-count