Skip to content

fix(moduleadmin): render safe-subset HTML in About changelog#119

Merged
mambax7 merged 3 commits into
XOOPS:masterfrom
mambax7:fix/moduleadmin-changelog-safe-html
Jul 5, 2026
Merged

fix(moduleadmin): render safe-subset HTML in About changelog#119
mambax7 merged 3 commits into
XOOPS:masterfrom
mambax7:fix/moduleadmin-changelog-safe-html

Conversation

@mambax7

@mambax7 mambax7 commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

The 2.7.1 hardening pass wrapped every changelog line in htmlspecialchars, so the intentional

//
markup shipped in module changelog.txt files rendered as literal source text on the module About page.

Changelog files ship inside the module and are authored by the module developer, so full escaping broke legitimate formatting for no real gain. Render an allowlist of presentational tags instead, and strip all attributes from the surviving tags, so headings/emphasis/rules display again while a poisoned changelog still cannot inject <script>, event handlers, or href=javascript: vectors.

Module metadata fields keep their existing htmlspecialchars escaping.

Summary by Sourcery

Render module changelog content on the About page using a restricted set of safe HTML tags instead of fully escaping all markup.

Enhancements:

  • Introduce a sanitization helper for changelog lines that strips all attributes while allowing a whitelist of presentational HTML tags for formatting.
  • Update changelog rendering to use the new sanitization helper for both language-specific and docs-based changelog files.

Summary by CodeRabbit

  • Bug Fixes

    • Changelog entries are now displayed more safely while preserving a small set of basic formatting like bold or italics.
    • Unsupported or potentially harmful HTML is shown as plain text instead of being rendered.
  • Tests

    • Added coverage for changelog sanitization to verify safe output across multiple formatting cases.

The 2.7.1 hardening pass wrapped every changelog line in htmlspecialchars,
so the intentional <h5>/<strong>/<hr> markup shipped in module changelog.txt
files rendered as literal source text on the module About page.

Changelog files ship inside the module and are authored by the module
developer, so full escaping broke legitimate formatting for no real gain.
Render an allowlist of presentational tags instead, and strip all attributes
from the surviving tags, so headings/emphasis/rules display again while a
poisoned changelog still cannot inject <script>, event handlers, or
href=javascript: vectors.

Module metadata fields keep their existing htmlspecialchars escaping.
Copilot AI review requested due to automatic review settings July 5, 2026 04:13
@sourcery-ai

sourcery-ai Bot commented Jul 5, 2026

Copy link
Copy Markdown
Reviewer's guide (collapsed on small PRs)

Reviewer's Guide

Render module changelog.txt content on the About page using an allowlisted subset of presentational HTML tags with all attributes stripped, instead of fully escaping lines, so trusted module formatting is preserved without enabling script or event-handler injection.

Sequence diagram for renderAbout changelog sanitization and rendering

sequenceDiagram
    participant ModuleAdmin
    participant Filesystem
    participant PHP_file as file
    participant PHP_array_map as array_map
    participant PHP_changelogLine as changelogLine

    ModuleAdmin->>Filesystem: is_file(changelog.txt)
    alt module_language_file_exists
        ModuleAdmin->>PHP_file: file(changelog_language_path)
    else fallback_to_english_or_docs
        ModuleAdmin->>Filesystem: is_file(english_or_docs_changelog.txt)
        ModuleAdmin->>PHP_file: file(english_or_docs_changelog_path)
    end

    ModuleAdmin->>PHP_array_map: array_map(changelogLine, file_lines)
    PHP_array_map->>PHP_changelogLine: changelogLine(line)
    PHP_changelogLine->>PHP_changelogLine: strip_tags(line, allowedTags)
    PHP_changelogLine->>PHP_changelogLine: preg_replace('<tag_with_attributes>', '<tag_without_attributes>')
    PHP_array_map-->>ModuleAdmin: sanitized_lines
    ModuleAdmin->>ModuleAdmin: implode('<br>', sanitized_lines)
    ModuleAdmin-->>ModuleAdmin: append_to_ret
Loading

File-Level Changes

Change Details Files
Introduce a safe HTML sanitizer for module changelog lines and replace the old htmlspecialchars-based rendering with this sanitizer when building the About page changelog section.
  • Define an allowlist of presentational HTML tags that are permitted in changelog content.
  • Add a local $changelogLine callback that strips disallowed tags and removes all attributes from allowed tags using regex.
  • Update changelog.txt file rendering to use the new $changelogLine mapper instead of the previous $esc escaping function for both language-specific and docs-based changelog locations.
htdocs/Frameworks/moduleclasses/moduleadmin/moduleadmin.php

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: d4a8485e-4b71-4f09-ae52-331a069912a9

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've left some high level feedback:

  • Consider promoting the $allowedTags list and $changelogLine sanitizer closure into a shared helper or constant so the sanitization rules are reusable and centrally maintained across module HTML rendering.
  • The attribute-stripping preg_replace currently rewrites all matching tags; consider tightening the regex to better preserve tag structure (e.g., self-closing or void tags) while still removing attributes for malformed or unusual HTML syntax.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Consider promoting the `$allowedTags` list and `$changelogLine` sanitizer closure into a shared helper or constant so the sanitization rules are reusable and centrally maintained across module HTML rendering.
- The attribute-stripping `preg_replace` currently rewrites all matching tags; consider tightening the regex to better preserve tag structure (e.g., self-closing or void tags) while still removing attributes for malformed or unusual HTML syntax.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@codecov

codecov Bot commented Jul 5, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 18.69%. Comparing base (62d52ff) to head (8fff1c0).
⚠️ Report is 44 commits behind head on master.

Additional details and impacted files
@@             Coverage Diff              @@
##             master     #119      +/-   ##
============================================
+ Coverage     18.13%   18.69%   +0.55%     
- Complexity     7854     8046     +192     
============================================
  Files           666      670       +4     
  Lines         43208    43794     +586     
============================================
+ Hits           7837     8188     +351     
- Misses        35371    35606     +235     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request updates the ModuleAdmin “About” page changelog rendering so module-provided changelog.txt content can display intended formatting again by allowing a restricted set of presentational HTML tags, while stripping attributes from surviving tags to reduce injection risk.

Changes:

  • Introduce an allowlist-based changelog line sanitizer using strip_tags() plus attribute stripping.
  • Switch changelog rendering (language and docs fallback) from full htmlspecialchars escaping to the new sanitizer.

Comment on lines +605 to +609
$changelogLine = static function ($line) use ($allowedTags) {
$line = strip_tags((string) $line, $allowedTags);
// Strip any attributes (onclick=, style=, href=, …) from the surviving tags.
return preg_replace('/<\s*(\/?)\s*([a-zA-Z][a-zA-Z0-9]*)\b[^>]*>/', '<$1$2>', $line);
};

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@htdocs/Frameworks/moduleclasses/moduleadmin/moduleadmin.php`:
- Around line 611-619: Normalize the new changelog lookup block in
moduleadmin.php to PSR-12 spacing by removing unnecessary spaces inside control
structures and function calls. Update the nested if statements and the is_file,
is_readable, implode, array_map, and file calls in this block to match the
project’s standard style, using the surrounding moduleadmin::changelog-related
logic as the reference point.
- Around line 604-609: Add regression tests for the changelog sanitization in
renderAbout() by covering the $changelogLine closure and its handling of allowed
tags, attribute stripping, and disallowed tag removal. Update ModuleAdminTest so
it asserts that safe tags like those in the allowedTags list survive, attributes
such as onclick/style/href are removed, and unsafe tags are stripped, ensuring
the sanitizer behavior in moduleadmin.php does not regress.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: a29003d7-76d1-428d-b17f-51315bb5159a

📥 Commits

Reviewing files that changed from the base of the PR and between 0cfd6d6 and 8e55fb1.

📒 Files selected for processing (1)
  • htdocs/Frameworks/moduleclasses/moduleadmin/moduleadmin.php

Comment thread htdocs/Frameworks/moduleclasses/moduleadmin/moduleadmin.php Outdated
Comment thread htdocs/Frameworks/moduleclasses/moduleadmin/moduleadmin.php
Comment thread htdocs/Frameworks/moduleclasses/moduleadmin/moduleadmin.php Outdated
Comment thread htdocs/Frameworks/moduleclasses/moduleadmin/moduleadmin.php Outdated
@gemini-code-assist

Copy link
Copy Markdown

Warning

Gemini encountered an error creating the review. You can try again by commenting /gemini review.

Address PR review feedback on the About-page changelog rendering.

Replace the strip_tags()-then-attribute-strip closure with an escape-first
approach: HTML-escape the whole line, then re-enable only an explicit
allowlist of attribute-free presentational tags. This preserves literal
angle-bracket text (e.g. "<table>", "requires PHP <= 8.0") and "&" as
visible text instead of dropping it, and enforces the tag allowlist in the
pattern itself rather than relying solely on strip_tags().

Extract the logic into ModuleAdmin::sanitizeChangelogLine() and add
regression tests covering allowed tags, attribute stripping, disallowed
tag handling, and literal-text preservation.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@htdocs/Frameworks/moduleclasses/moduleadmin/moduleadmin.php`:
- Around line 526-537: The sanitization logic in sanitizeChangelogLine is fine,
but this new public method should be typed for safety. Add an explicit parameter
type for the $line argument in sanitizeChangelogLine, and handle the fact that
preg_replace_callback() can return null by normalizing the return value before
exposing it. Keep the allowlist regex and htmlspecialchars flow unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 68befc11-0521-4627-a5a2-975fb6209d1b

📥 Commits

Reviewing files that changed from the base of the PR and between 8e55fb1 and e41938e.

📒 Files selected for processing (2)
  • htdocs/Frameworks/moduleclasses/moduleadmin/moduleadmin.php
  • tests/unit/htdocs/Frameworks/moduleclasses/ModuleAdminTest.php

Comment on lines +526 to +537
public static function sanitizeChangelogLine($line)
{
static $allowed = 'h1|h2|h3|h4|h5|h6|p|br|hr|ul|ol|li|strong|b|em|i|u|code|pre|blockquote|span';

$escaped = htmlspecialchars((string) $line, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');

return preg_replace_callback(
'#&lt;(/?)\s*(' . $allowed . ')\s*(/?)&gt;#i',
static fn($m) => '<' . $m[1] . strtolower($m[2]) . $m[3] . '>',
$escaped
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Sanitizer logic is sound; consider typing this new public method.

The escape-then-re-enable-allowlist approach is correct and safe: attribute-bearing and non-allowlisted tags stay inert escaped text on PHP 8.2–8.5, and the allowlist is enforced by the pattern itself rather than delegated to strip_tags().

As a small type-safety hardening on this new public security-relevant surface, consider declaring the parameter type. Note preg_replace_callback() returns ?string, so guard the return rather than declaring a bare : string.

♻️ Typed signature with null-safe return
-    public static function sanitizeChangelogLine($line)
+    public static function sanitizeChangelogLine(string $line): string
     {
         static $allowed = 'h1|h2|h3|h4|h5|h6|p|br|hr|ul|ol|li|strong|b|em|i|u|code|pre|blockquote|span';

-        $escaped = htmlspecialchars((string) $line, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
+        $escaped = htmlspecialchars($line, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');

-        return preg_replace_callback(
+        return preg_replace_callback(
             '#&lt;(/?)\s*(' . $allowed . ')\s*(/?)&gt;`#i`',
             static fn($m) => '<' . $m[1] . strtolower($m[2]) . $m[3] . '>',
             $escaped
-        );
+        ) ?? $escaped;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public static function sanitizeChangelogLine($line)
{
static $allowed = 'h1|h2|h3|h4|h5|h6|p|br|hr|ul|ol|li|strong|b|em|i|u|code|pre|blockquote|span';
$escaped = htmlspecialchars((string) $line, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
return preg_replace_callback(
'#&lt;(/?)\s*(' . $allowed . ')\s*(/?)&gt;#i',
static fn($m) => '<' . $m[1] . strtolower($m[2]) . $m[3] . '>',
$escaped
);
}
public static function sanitizeChangelogLine(string $line): string
{
static $allowed = 'h1|h2|h3|h4|h5|h6|p|br|hr|ul|ol|li|strong|b|em|i|u|code|pre|blockquote|span';
$escaped = htmlspecialchars($line, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
return preg_replace_callback(
'#&lt;(/?)\s*(' . $allowed . ')\s*(/?)&gt;`#i`',
static fn($m) => '<' . $m[1] . strtolower($m[2]) . $m[3] . '>',
$escaped
) ?? $escaped;
}
🧰 Tools
🪛 PHPMD (2.15.0)

[warning] 534-534: Avoid variables with short names like $m. Configured minimum length is 3. (undefined)

(ShortVariable)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@htdocs/Frameworks/moduleclasses/moduleadmin/moduleadmin.php` around lines 526
- 537, The sanitization logic in sanitizeChangelogLine is fine, but this new
public method should be typed for safety. Add an explicit parameter type for the
$line argument in sanitizeChangelogLine, and handle the fact that
preg_replace_callback() can return null by normalizing the return value before
exposing it. Keep the allowlist regex and htmlspecialchars flow unchanged.

@gitar-bot

gitar-bot Bot commented Jul 5, 2026

Copy link
Copy Markdown
Code Review ✅ Approved 2 resolved / 2 findings

Restores changelog formatting by implementing an HTML allowlist and attribute stripping, effectively replacing overly broad escaping. This addresses previous issues with literal angle-bracket rendering and provides a more robust sanitization approach.

✅ 2 resolved
Edge Case: Literal angle-bracket text in changelog is silently dropped

📄 htdocs/Frameworks/moduleclasses/moduleadmin/moduleadmin.php:604-608
The previous behavior wrapped every changelog line in htmlspecialchars, so a line like Added support for <table> and <div> tags rendered its literal <table>/<div> text intact. With the new strip_tags($line, $allowedTags) approach, any tag-like token that is not in the allowlist (e.g. <table>, <div>, <code x> after attribute strip) is removed entirely rather than escaped, so the surrounding sentence loses content: the example becomes Added support for and tags. This is an inherent tradeoff of switching from escaping to tag-stripping. If preserving arbitrary literal <...> text matters, module authors now have to HTML-escape it themselves in changelog.txt. Consider documenting this behavior change, or escaping non-allowlisted </> instead of dropping them.

Security: Sanitizer relies solely on strip_tags for the tag allowlist

📄 htdocs/Frameworks/moduleclasses/moduleadmin/moduleadmin.php:605-608
The attribute-stripping regex preg_replace('/<\s*(\/?)\s*([a-zA-Z][a-zA-Z0-9]*)\b[^>]*>/', '<$1$2>', $line) preserves ANY tag name it matches — it only removes attributes, it does not enforce the allowlist. All allowlist enforcement depends on strip_tags(). On the supported PHP 8.2–8.5 this is acceptable (modern strip_tags handles the classic <scr<script>ipt> nesting bypasses and the content is authored by the trusted module developer), so this is defense-in-depth rather than an exploitable hole. However, because the regex passes unknown tags through unchanged, a future maintainer who widens/replaces the strip_tags call could unknowingly allow arbitrary tags. Consider having the closure re-validate the surviving tag name against an explicit allowlist array (rejecting anything not in it) so the two layers are not silently coupled to strip_tags alone.

Options

Auto-apply is off → Gitar will not commit updates to this branch.
Display: compact → Showing less information.

Comment with these commands to change the behavior for this request:

Auto-apply Compact
gitar auto-apply:on         
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

@mambax7
mambax7 merged commit 7315c2c into XOOPS:master Jul 5, 2026
14 checks passed
@mambax7
mambax7 deleted the fix/moduleadmin-changelog-safe-html branch July 5, 2026 07:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants