From b0b21ec4e561b06376192b3b24a04a590997cbbe Mon Sep 17 00:00:00 2001 From: Felipe Zipitria Date: Mon, 7 Sep 2026 16:55:05 -0300 Subject: [PATCH 1/4] fix: render CI badges larger in the generated README table GitHub's actions badge.svg renders at its native ~20px height, which reads as illegibly small next to the table's other columns. Since the badge is a real SVG, render it via an tag with an explicit height instead of plain markdown image syntax, so it scales up cleanly. --- README.md | 48 ++++++++++++++++++------------------ scripts/generate_registry.py | 3 ++- 2 files changed, 26 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index e21bd05..3a36849 100644 --- a/README.md +++ b/README.md @@ -12,35 +12,35 @@ maps the regular CRS IDs from 900K for each rule to the range 9,900,000 - 9,999, | *Plugin Name* | *Rule ID Range* | *Repository* | *Type* | *Status* | *CI* | *License* | |---|---|---|---|---|---|---| -| template | 9,500,000 - 9,500,999 | [coreruleset/template-plugin](https://github.com/coreruleset/template-plugin) | official | ✅ tested | ![Integration tests](https://github.com/coreruleset/template-plugin/actions/workflows/integration.yml/badge.svg) | Apache-2.0 | +| template | 9,500,000 - 9,500,999 | [coreruleset/template-plugin](https://github.com/coreruleset/template-plugin) | official | ✅ tested | Integration tests | Apache-2.0 | | auto-decoding | 9,501,000 - 9,501,999 | [coreruleset/auto-decoding-plugin](https://github.com/coreruleset/auto-decoding-plugin) | official | untested | | Apache-2.0 | | antivirus | 9,502,000 - 9,502,999 | [coreruleset/antivirus-plugin](https://github.com/coreruleset/antivirus-plugin) | official | being tested | | Apache-2.0 | -| body-decompress | 9,503,000 - 9,503,999 | [coreruleset/body-decompress-plugin](https://github.com/coreruleset/body-decompress-plugin) | official | being tested | ![Integration tests](https://github.com/coreruleset/body-decompress-plugin/actions/workflows/integration.yml/badge.svg) | Apache-2.0 | -| fake-bot | 9,504,000 - 9,504,999 | [coreruleset/fake-bot-plugin](https://github.com/coreruleset/fake-bot-plugin) | official | ✅ tested | ![Integration tests](https://github.com/coreruleset/fake-bot-plugin/actions/workflows/integration.yml/badge.svg) | Apache-2.0 | -| google-oauth2 | 9,505,000 - 9,505,999 | [coreruleset/google-oauth2-plugin](https://github.com/coreruleset/google-oauth2-plugin) | official | ✅ tested | ![Integration tests](https://github.com/coreruleset/google-oauth2-plugin/actions/workflows/integration.yml/badge.svg) | Apache-2.0 | -| drupal-rule-exclusions | 9,506,000 - 9,506,999 | [coreruleset/drupal-rule-exclusions-plugin](https://github.com/coreruleset/drupal-rule-exclusions-plugin) | official | ✅ tested | ![Integration tests](https://github.com/coreruleset/drupal-rule-exclusions-plugin/actions/workflows/integration.yml/badge.svg) | Apache-2.0 | -| wordpress-rule-exclusions | 9,507,000 - 9,507,999 | [coreruleset/wordpress-rule-exclusions-plugin](https://github.com/coreruleset/wordpress-rule-exclusions-plugin) | official | ✅ tested | ![Integration tests](https://github.com/coreruleset/wordpress-rule-exclusions-plugin/actions/workflows/integration.yml/badge.svg) | Apache-2.0 | -| nextcloud-rule-exclusions | 9,508,000 - 9,508,999 | [coreruleset/nextcloud-rule-exclusions-plugin](https://github.com/coreruleset/nextcloud-rule-exclusions-plugin) | official | ✅ tested | ![Integration tests](https://github.com/coreruleset/nextcloud-rule-exclusions-plugin/actions/workflows/integration.yml/badge.svg) | Apache-2.0 | -| dokuwiki-rule-exclusions | 9,509,000 - 9,509,999 | [coreruleset/dokuwiki-rule-exclusions-plugin](https://github.com/coreruleset/dokuwiki-rule-exclusions-plugin) | official | ✅ tested | ![Integration tests](https://github.com/coreruleset/dokuwiki-rule-exclusions-plugin/actions/workflows/integration.yml/badge.svg) | Apache-2.0 | -| cpanel-rule-exclusions | 9,510,000 - 9,510,999 | [coreruleset/cpanel-rule-exclusions-plugin](https://github.com/coreruleset/cpanel-rule-exclusions-plugin) | official | ✅ tested | ![Integration tests](https://github.com/coreruleset/cpanel-rule-exclusions-plugin/actions/workflows/integration.yml/badge.svg) | Apache-2.0 | -| xenforo-rule-exclusions | 9,511,000 - 9,511,999 | [coreruleset/xenforo-rule-exclusions-plugin](https://github.com/coreruleset/xenforo-rule-exclusions-plugin) | official | ✅ tested | ![Integration tests](https://github.com/coreruleset/xenforo-rule-exclusions-plugin/actions/workflows/integration.yml/badge.svg) | Apache-2.0 | -| phpbb-rule-exclusions | 9,512,000 - 9,512,999 | [coreruleset/phpbb-rule-exclusions-plugin](https://github.com/coreruleset/phpbb-rule-exclusions-plugin) | official | ✅ tested | ![Integration tests](https://github.com/coreruleset/phpbb-rule-exclusions-plugin/actions/workflows/integration.yml/badge.svg) | Apache-2.0 | -| phpmyadmin-rule-exclusions | 9,513,000 - 9,513,999 | [coreruleset/phpmyadmin-rule-exclusions-plugin](https://github.com/coreruleset/phpmyadmin-rule-exclusions-plugin) | official | ✅ tested | ![Integration tests](https://github.com/coreruleset/phpmyadmin-rule-exclusions-plugin/actions/workflows/integration.yml/badge.svg) | Apache-2.0 | +| body-decompress | 9,503,000 - 9,503,999 | [coreruleset/body-decompress-plugin](https://github.com/coreruleset/body-decompress-plugin) | official | being tested | Integration tests | Apache-2.0 | +| fake-bot | 9,504,000 - 9,504,999 | [coreruleset/fake-bot-plugin](https://github.com/coreruleset/fake-bot-plugin) | official | ✅ tested | Integration tests | Apache-2.0 | +| google-oauth2 | 9,505,000 - 9,505,999 | [coreruleset/google-oauth2-plugin](https://github.com/coreruleset/google-oauth2-plugin) | official | ✅ tested | Integration tests | Apache-2.0 | +| drupal-rule-exclusions | 9,506,000 - 9,506,999 | [coreruleset/drupal-rule-exclusions-plugin](https://github.com/coreruleset/drupal-rule-exclusions-plugin) | official | ✅ tested | Integration tests | Apache-2.0 | +| wordpress-rule-exclusions | 9,507,000 - 9,507,999 | [coreruleset/wordpress-rule-exclusions-plugin](https://github.com/coreruleset/wordpress-rule-exclusions-plugin) | official | ✅ tested | Integration tests | Apache-2.0 | +| nextcloud-rule-exclusions | 9,508,000 - 9,508,999 | [coreruleset/nextcloud-rule-exclusions-plugin](https://github.com/coreruleset/nextcloud-rule-exclusions-plugin) | official | ✅ tested | Integration tests | Apache-2.0 | +| dokuwiki-rule-exclusions | 9,509,000 - 9,509,999 | [coreruleset/dokuwiki-rule-exclusions-plugin](https://github.com/coreruleset/dokuwiki-rule-exclusions-plugin) | official | ✅ tested | Integration tests | Apache-2.0 | +| cpanel-rule-exclusions | 9,510,000 - 9,510,999 | [coreruleset/cpanel-rule-exclusions-plugin](https://github.com/coreruleset/cpanel-rule-exclusions-plugin) | official | ✅ tested | Integration tests | Apache-2.0 | +| xenforo-rule-exclusions | 9,511,000 - 9,511,999 | [coreruleset/xenforo-rule-exclusions-plugin](https://github.com/coreruleset/xenforo-rule-exclusions-plugin) | official | ✅ tested | Integration tests | Apache-2.0 | +| phpbb-rule-exclusions | 9,512,000 - 9,512,999 | [coreruleset/phpbb-rule-exclusions-plugin](https://github.com/coreruleset/phpbb-rule-exclusions-plugin) | official | ✅ tested | Integration tests | Apache-2.0 | +| phpmyadmin-rule-exclusions | 9,513,000 - 9,513,999 | [coreruleset/phpmyadmin-rule-exclusions-plugin](https://github.com/coreruleset/phpmyadmin-rule-exclusions-plugin) | official | ✅ tested | Integration tests | Apache-2.0 | | dos-protection-modsecurity | 9,514,000 - 9,514,999 | [coreruleset/dos-protection-plugin-modsecurity](https://github.com/coreruleset/dos-protection-plugin-modsecurity) | official | untested | | Apache-2.0 | -| machine-learning-integration-plugin | 9,516,000 - 9,516,999 | [coreruleset/machine-learning-integration-plugin](https://github.com/coreruleset/machine-learning-integration-plugin) | official | draft | ![Integration tests](https://github.com/coreruleset/machine-learning-integration-plugin/actions/workflows/integration.yml/badge.svg) | Apache-2.0 | -| performance-plugin | 9,517,000 - 9,517,999 | [coreruleset/performance-plugin](https://github.com/coreruleset/performance-plugin) | official | draft (Private) | ![Integration tests](https://github.com/coreruleset/performance-plugin/actions/workflows/integration.yml/badge.svg) | Apache-2.0 | -| ghost-rule-exclusions | 9,518,000 - 9,518,999 | [coreruleset/ghost-rule-exclusions-plugin](https://github.com/coreruleset/ghost-rule-exclusions-plugin) | official | draft (Private) | ![Integration tests](https://github.com/coreruleset/ghost-rule-exclusions-plugin/actions/workflows/integration.yml/badge.svg) | Apache-2.0 | -| roundcube-rule-exclusions-plugin | 9,519,000 - 9,519,999 | [EsadCetiner/roundcube-rule-exclusions-plugin](https://github.com/EsadCetiner/roundcube-rule-exclusions-plugin) | 3rd-party | ✅ tested | ![Integration tests](https://github.com/EsadCetiner/roundcube-rule-exclusions-plugin/actions/workflows/integration.yml/badge.svg) | GPL-2.0 | -| sogo-rule-exclusions-plugin | 9,520,000 - 9,520,999 | [EsadCetiner/sogo-rule-exclusions-plugin](https://github.com/EsadCetiner/sogo-rule-exclusions-plugin) | 3rd-party | ✅ tested | ![Integration tests](https://github.com/EsadCetiner/sogo-rule-exclusions-plugin/actions/workflows/integration.yml/badge.svg) | GPL-2.0 | -| iredadmin-rule-exclusions-plugin | 9,521,000 - 9,521,999 | [EsadCetiner/iredadmin-rule-exclusions-plugin](https://github.com/EsadCetiner/iredadmin-rule-exclusions-plugin) | 3rd-party | ✅ tested | ![Integration tests](https://github.com/EsadCetiner/iredadmin-rule-exclusions-plugin/actions/workflows/integration.yml/badge.svg) | GPL-2.0 | -| wordpress-hardening-plugin | 9,522,000 - 9,522,999 | [eilandert/wordpress-hardening-plugin](https://github.com/eilandert/wordpress-hardening-plugin) | 3rd-party | ✅ tested | ![Integration tests](https://github.com/eilandert/wordpress-hardening-plugin/actions/workflows/integration.yml/badge.svg) | Apache-2.0 | +| machine-learning-integration-plugin | 9,516,000 - 9,516,999 | [coreruleset/machine-learning-integration-plugin](https://github.com/coreruleset/machine-learning-integration-plugin) | official | draft | Integration tests | Apache-2.0 | +| performance-plugin | 9,517,000 - 9,517,999 | [coreruleset/performance-plugin](https://github.com/coreruleset/performance-plugin) | official | draft (Private) | Integration tests | Apache-2.0 | +| ghost-rule-exclusions | 9,518,000 - 9,518,999 | [coreruleset/ghost-rule-exclusions-plugin](https://github.com/coreruleset/ghost-rule-exclusions-plugin) | official | draft (Private) | Integration tests | Apache-2.0 | +| roundcube-rule-exclusions-plugin | 9,519,000 - 9,519,999 | [EsadCetiner/roundcube-rule-exclusions-plugin](https://github.com/EsadCetiner/roundcube-rule-exclusions-plugin) | 3rd-party | ✅ tested | Integration tests | GPL-2.0 | +| sogo-rule-exclusions-plugin | 9,520,000 - 9,520,999 | [EsadCetiner/sogo-rule-exclusions-plugin](https://github.com/EsadCetiner/sogo-rule-exclusions-plugin) | 3rd-party | ✅ tested | Integration tests | GPL-2.0 | +| iredadmin-rule-exclusions-plugin | 9,521,000 - 9,521,999 | [EsadCetiner/iredadmin-rule-exclusions-plugin](https://github.com/EsadCetiner/iredadmin-rule-exclusions-plugin) | 3rd-party | ✅ tested | Integration tests | GPL-2.0 | +| wordpress-hardening-plugin | 9,522,000 - 9,522,999 | [eilandert/wordpress-hardening-plugin](https://github.com/eilandert/wordpress-hardening-plugin) | 3rd-party | ✅ tested | Integration tests | Apache-2.0 | | database-logging-plugin | 9,523,000 - 9,523,999 | [coreruleset/database-logging-plugin](https://github.com/coreruleset/database-logging-plugin) | official | untested | | Apache-2.0 | -| referer-hardening-plugin | 9,524,000 - 9,524,999 | [coreruleset/referer-hardening-plugin](https://github.com/coreruleset/referer-hardening-plugin) | official | ✅ tested | ![Integration tests](https://github.com/coreruleset/referer-hardening-plugin/actions/workflows/integration.yml/badge.svg) | Apache-2.0 | -| false-positive-report-plugin | 9,525,000 - 9,525,999 | [coreruleset/false-positive-report-plugin](https://github.com/coreruleset/false-positive-report-plugin) | official | ✅ tested | ![Integration tests](https://github.com/coreruleset/false-positive-report-plugin/actions/workflows/integration.yml/badge.svg) | Apache-2.0 | -| traffic-observation-plugin | 9,526,000 - 9,526,999 | [coreruleset/traffic-observation-plugin](https://github.com/coreruleset/traffic-observation-plugin) | official | untested | ![Integration tests](https://github.com/coreruleset/traffic-observation-plugin/actions/workflows/integration.yml/badge.svg) | Apache-2.0 | +| referer-hardening-plugin | 9,524,000 - 9,524,999 | [coreruleset/referer-hardening-plugin](https://github.com/coreruleset/referer-hardening-plugin) | official | ✅ tested | Integration tests | Apache-2.0 | +| false-positive-report-plugin | 9,525,000 - 9,525,999 | [coreruleset/false-positive-report-plugin](https://github.com/coreruleset/false-positive-report-plugin) | official | ✅ tested | Integration tests | Apache-2.0 | +| traffic-observation-plugin | 9,526,000 - 9,526,999 | [coreruleset/traffic-observation-plugin](https://github.com/coreruleset/traffic-observation-plugin) | official | untested | Integration tests | Apache-2.0 | | netnea-crs-upgrading-plugin | 9,527,000 - 9,527,999 | [netnea/netnea-crs-upgrading-plugin](https://github.com/netnea/netnea-crs-upgrading-plugin) | 3rd-party | untested | | GPL-3.0 | -| plausible-rule-exclusions-plugin | 9,528,000 - 9,528,999 | [EsadCetiner/plausible-rule-exclusions-plugin](https://github.com/EsadCetiner/plausible-rule-exclusions-plugin) | 3rd-party | ✅ tested | ![Integration tests](https://github.com/EsadCetiner/plausible-rule-exclusions-plugin/actions/workflows/integration.yml/badge.svg) | GPL-2.0 | -| vimbadmin-crs-plugin | 9,529,000 - 9,529,999 | [eilandert/vimbadmin-crs-plugin](https://github.com/eilandert/vimbadmin-crs-plugin) | 3rd-party | ✅ tested | ![Integration tests](https://github.com/eilandert/vimbadmin-crs-plugin/actions/workflows/integration.yml/badge.svg) | Apache-2.0 | +| plausible-rule-exclusions-plugin | 9,528,000 - 9,528,999 | [EsadCetiner/plausible-rule-exclusions-plugin](https://github.com/EsadCetiner/plausible-rule-exclusions-plugin) | 3rd-party | ✅ tested | Integration tests | GPL-2.0 | +| vimbadmin-crs-plugin | 9,529,000 - 9,529,999 | [eilandert/vimbadmin-crs-plugin](https://github.com/eilandert/vimbadmin-crs-plugin) | 3rd-party | ✅ tested | Integration tests | Apache-2.0 | | incubator | 9,900,000 - 9,999,999 | [coreruleset/incubator-plugin](https://github.com/coreruleset/incubator-plugin) | official | being tested | | Apache-2.0 | diff --git a/scripts/generate_registry.py b/scripts/generate_registry.py index bc2ae20..43d9a9a 100755 --- a/scripts/generate_registry.py +++ b/scripts/generate_registry.py @@ -50,7 +50,8 @@ def format_status(plugin: dict) -> str: def format_ci(plugin: dict) -> str: if not plugin.get("ci"): return "" - return f"![Integration tests]({plugin['repository'].rstrip('/')}/actions/workflows/integration.yml/badge.svg)" + url = f"{plugin['repository'].rstrip('/')}/actions/workflows/integration.yml/badge.svg" + return f'Integration tests' def render_row(plugin: dict) -> str: From 98d9d66431f60525732d1311ad9db08124003d43 Mon Sep 17 00:00:00 2001 From: Felipe Zipitria Date: Mon, 7 Sep 2026 18:08:36 -0300 Subject: [PATCH 2/4] fix: escape CI badge URL before interpolating into HTML registry-schema.json's repository pattern ([^/]+) allows characters like '"' in the owner/repo segments, so a crafted repository value could break out of the attribute in the generated README.md. Escape the URL with html.escape(quote=True) before interpolation. Addresses a CodeRabbit review comment on this PR. --- scripts/generate_registry.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/generate_registry.py b/scripts/generate_registry.py index 43d9a9a..864aa99 100755 --- a/scripts/generate_registry.py +++ b/scripts/generate_registry.py @@ -12,6 +12,7 @@ import json import re import sys +from html import escape from pathlib import Path import yaml @@ -51,7 +52,7 @@ def format_ci(plugin: dict) -> str: if not plugin.get("ci"): return "" url = f"{plugin['repository'].rstrip('/')}/actions/workflows/integration.yml/badge.svg" - return f'Integration tests' + return f'Integration tests' def render_row(plugin: dict) -> str: From d3709d99d5e38cdaeb3230d1b49f8ee4d484865e Mon Sep 17 00:00:00 2001 From: Felipe Zipitria Date: Mon, 7 Sep 2026 18:21:40 -0300 Subject: [PATCH 3/4] fix: escape repository field, avoid regex-replacement backreferences Two more injection/robustness issues in the same family as the CodeRabbit-flagged badge src escaping: - format_repository() dropped the repository field unescaped into a markdown link. CommonMark treats raw sequences in link text as HTML passthrough, so a crafted repository value injected HTML into the rendered README the same way the badge src did. Escape both the link text and the URL. - render_readme() passed the generated table to pattern.sub() as a replacement *string*, which makes Python's re module interpret literal backslash-digit sequences (e.g. "\1") as backreferences and crash generation (re.error: invalid group reference). Use a replacement function instead, which is inserted literally. Also tightens registry-schema.json's repository pattern to GitHub's own owner/repo charset (alphanumerics, ., _, -), closing this class of problem at the source rather than only at each render call site. Adds regression tests for all three (badge escaping, link escaping, regex-backreference safety) plus a schema fixture for the tightened repository pattern. --- registry-schema.json | 4 +- scripts/generate_registry.py | 4 +- .../invalid/bad-repository-special-chars.yaml | 8 +++ tests/registry/test_validate.py | 51 +++++++++++++++++-- 4 files changed, 59 insertions(+), 8 deletions(-) create mode 100644 tests/registry/invalid/bad-repository-special-chars.yaml diff --git a/registry-schema.json b/registry-schema.json index 1517f0b..be0da94 100644 --- a/registry-schema.json +++ b/registry-schema.json @@ -53,8 +53,8 @@ }, "repository": { "type": "string", - "pattern": "^https://github\\.com/[^/]+/[^/]+/?$", - "description": "URL of the plugin source repository (currently only GitHub repositories are supported)" + "pattern": "^https://github\\.com/[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?/[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?/?$", + "description": "URL of the plugin source repository (currently only GitHub repositories are supported); owner and repo segments are restricted to GitHub's own charset so this value can never break out of the markdown/HTML it's rendered into" }, "type": { "type": "string", diff --git a/scripts/generate_registry.py b/scripts/generate_registry.py index 864aa99..00234d9 100755 --- a/scripts/generate_registry.py +++ b/scripts/generate_registry.py @@ -37,7 +37,7 @@ def format_range(rule_id_range: dict) -> str: def format_repository(repository: str) -> str: slug = repository.removeprefix("https://github.com/").rstrip("/") - return f"[{slug}]({repository})" + return f"[{escape(slug, quote=True)}]({escape(repository, quote=True)})" def format_status(plugin: dict) -> str: @@ -107,7 +107,7 @@ def render_readme(registry: dict) -> None: pattern = re.compile(re.escape(BEGIN_MARKER) + r".*?" + re.escape(END_MARKER), re.DOTALL) if not pattern.search(readme): raise SystemExit(f"README.md is missing {BEGIN_MARKER} / {END_MARKER} markers") - README.write_text(pattern.sub(table, readme)) + README.write_text(pattern.sub(lambda _match: table, readme)) def render_json(registry: dict) -> None: diff --git a/tests/registry/invalid/bad-repository-special-chars.yaml b/tests/registry/invalid/bad-repository-special-chars.yaml new file mode 100644 index 0000000..0a34c36 --- /dev/null +++ b/tests/registry/invalid/bad-repository-special-chars.yaml @@ -0,0 +1,8 @@ +schema_version: 1 +plugins: + - name: example + rule_id_range: {start: 9990000, end: 9990999} + repository: 'https://github.com/foo/bar">' + type: official + status: tested + license: Apache-2.0 diff --git a/tests/registry/test_validate.py b/tests/registry/test_validate.py index 604e53b..3ac7345 100755 --- a/tests/registry/test_validate.py +++ b/tests/registry/test_validate.py @@ -3,15 +3,17 @@ # requires-python = ">=3.11" # dependencies = ["pyyaml==6.0.3"] # /// -"""Self-check for the semantic validation in scripts/generate_registry.py. +"""Self-check for scripts/generate_registry.py: semantic validation and rendering safety. check-jsonschema only catches structural errors; duplicate names and overlapping rule ID ranges are cross-item constraints it cannot express, -so they are asserted here instead. +so they are asserted here instead. This also covers rendering-safety +regressions (HTML/markdown escaping, regex-replacement backreferences) +that a schema alone can't guarantee against. """ import importlib.util -import sys +import re from pathlib import Path ROOT = Path(__file__).resolve().parent.parent.parent @@ -26,7 +28,7 @@ def load(path: Path) -> dict: return yaml.safe_load(path.read_text()) -def main() -> None: +def check_semantic_validation() -> None: registry = load(ROOT / "registry.yaml") assert generate_registry.validate(registry) == [], "registry.yaml must be semantically valid" @@ -38,6 +40,47 @@ def main() -> None: errors = generate_registry.validate(overlapping) assert any("overlapping rule ID ranges" in e for e in errors), errors + +def check_ci_badge_escaping() -> None: + # registry-schema.json's pattern rules this repository value out today, but + # the escaping is defense in depth, independent of the schema. + malicious = {"ci": True, "repository": 'https://github.com/foo/bar" onerror="alert(1)'} + badge = generate_registry.format_ci(malicious) + # The tag has exactly three literal attributes (alt=".." src=".." height="..") -> 6 quotes. + # A leaked, unescaped quote from the malicious value would add 2 more. + assert badge.count('"') == 6, badge + assert """ in badge, badge + + +def check_repository_link_escaping() -> None: + malicious = "https://github.com/foo/bar" + rendered = generate_registry.format_repository(malicious) + assert "