From 465253c64050681b73b4a678e57613a94f2c99fa Mon Sep 17 00:00:00 2001 From: Josh Knapp Date: Mon, 17 Aug 2026 12:54:22 -0700 Subject: [PATCH] fix(haproxy): silence the ACL pattern warning on wp_admin_asset HAProxy warns on any pattern whose first character is "(", because it cannot distinguish an intended regex from a fetch-argument list with a stray space: parsing acl 'wp_admin_asset' : matching 'path_reg' for pattern '(^|/)wp-admin/...' is likely a mistake and probably not what you want. "--" is HAProxy's documented end-of-flags marker and is the remedy the warning itself names. Cosmetic to matching, but not to operations: left unsilenced it fires on every config load and every reload on every host, which trains operators to skim past warnings and gives a real one somewhere to hide. Matching semantics are unchanged, verified rather than assumed. Both forms were run side by side as two frontends under real HAProxy 3.0.11-1+deb13u3 and gave identical verdicts on all 8 vectors: /wp-admin/css/login.min.css MATCH / MATCH /wp-admin/js/user-profile.min.js MATCH / MATCH /wp-admin/images/x.png MATCH / MATCH /wp-admin/css/sub/deep.css MATCH / MATCH /blog/wp-admin/css/a.css MATCH / MATCH /wp-admin/css/x.php NOMATCH / NOMATCH /wp-admin/plugins.php NOMATCH / NOMATCH /some--path/file.css NOMATCH / NOMATCH The last vector is the one that matters: it proves HAProxy consumed "--" as end-of-flags rather than adopting it as the pattern. Had it done the latter, the ACL would have matched paths containing "--" and stopped matching css/js -- gating every login page's own stylesheets while the page itself still returned 200. wp_admin_path needs no "--" only because its "-i" flag already occupies the flag slot; it is not otherwise special. Adds a regression test asserting both the "--" and the pattern it guards, so this cannot pass by the pattern having been changed. Verified to fail when the "--" is removed. --- scripts/test-wpadmin-gate.py | 19 +++++++++++++++++++ templates/hap_listener.tpl | 17 ++++++++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/scripts/test-wpadmin-gate.py b/scripts/test-wpadmin-gate.py index 16abc91..8900c7c 100644 --- a/scripts/test-wpadmin-gate.py +++ b/scripts/test-wpadmin-gate.py @@ -491,6 +491,25 @@ class UriNormalisation(unittest.TestCase): self.assertIn('.php', acl_line, 'wp_admin_asset must exclude .php explicitly') + def test_wp_admin_asset_pattern_is_end_of_flags_guarded(self): + """A pattern starting with "(" makes HAProxy warn on EVERY load/reload: + + parsing acl 'wp_admin_asset' : matching 'path_reg' for pattern + '(^|/)wp-admin/...' is likely a mistake ... Maybe you need to + remove the extraneous space before '('. + + "--" is the end-of-flags marker HAProxy itself names as the fix. It is + cosmetic to matching but not to operations: an unsilenced warning on + every reload on every host trains people to skim past warnings, which + is how a real one gets missed. Assert the pattern is still the one we + think it is, so this can never pass by the pattern having been changed. + """ + acl_line = require_rule(self.cfg, 'acl wp_admin_asset', 'wp_admin_asset ACL') + self.assertRegex( + acl_line, r'path_reg\s+--\s+\(', + 'wp_admin_asset pattern begins with "(" and MUST be preceded by "--"') + self.assertIn('(^|/)wp-admin/(css|js|images)/', acl_line) + def test_case_insensitive_acl_and_regsub_are_kept_in_sync(self): """A case-insensitive wp_admin_path with a case-sensitive regsub is an INFINITE REDIRECT LOOP: regsub finds no "/wp-admin/" in diff --git a/templates/hap_listener.tpl b/templates/hap_listener.tpl index 463afff..3035470 100644 --- a/templates/hap_listener.tpl +++ b/templates/hap_listener.tpl @@ -473,7 +473,22 @@ frontend web # substring, so it cannot cause the silent "login page renders unstyled" # regression that an extension allowlist would risk. Requires PCRE2, which # both the Debian (deployed) and Alpine haproxy builds have (+PCRE2). - acl wp_admin_asset path_reg (^|/)wp-admin/(css|js|images)/(?!.*\.php).*$ + # + # THE "--" IS LOAD-BEARING, DO NOT DELETE IT. HAProxy warns on any pattern + # whose first character is "(", because it cannot tell an intended regex + # from a fetch-argument list someone typo'd a space into: + # parsing acl 'wp_admin_asset' : matching 'path_reg' for pattern + # '(^|/)wp-admin/...' is likely a mistake and probably not what you want. + # Maybe you need to remove the extraneous space before '('. + # "--" is HAProxy's documented end-of-flags marker and is the fix it names + # itself ("please insert '--' between the match and the pattern"). It changes + # no matching semantics -- verified live on whp02: /wp-admin/css/login.min.css + # still passes and /wp-admin/css/x.php is still gated, before and after. + # Left unsilenced this fires on EVERY config load and reload on every host, + # where it trains operators to skim past warnings and can bury a real one. + # wp_admin_path escapes the warning only because its "-i" flag happens to + # consume the flag slot first; it is not otherwise special. + acl wp_admin_asset path_reg -- (^|/)wp-admin/(css|js|images)/(?!.*\.php).*$ acl wp_gate_exempt hdr(host),lower -f /etc/haproxy/wpadmin_gate_exempt.list # ENCODED SEPARATOR. percent-decode-unreserved deliberately does NOT decode # %2F -- "/" is a reserved character, and decoding it in the normalizer