Skip to content

fix(#7216): harden health dashboard node and incident rendering#7544

Open
Yzgaming005 wants to merge 1 commit into
Scottcjn:mainfrom
Yzgaming005:fix/issue-7216-harden-health-dashboard
Open

fix(#7216): harden health dashboard node and incident rendering#7544
Yzgaming005 wants to merge 1 commit into
Scottcjn:mainfrom
Yzgaming005:fix/issue-7216-harden-health-dashboard

Conversation

@Yzgaming005

Copy link
Copy Markdown

Summary

Replace innerHTML template interpolation with safe DOM construction (textContent + class whitelisting) across the health dashboard surfaces for issue #7216. The current code interpolates server-supplied node fields (node.name, node.location, node.origin, node.version, node.error, plus incident event/detail/node) into innerHTML and into class="${...}" attribute strings. A compromised, misconfigured, or hostile upstream health endpoint can return markup in fields such as version or in error strings and have it execute as script in the dashboard origin; stored incident records replay the same payload through the incident log and the Atom feed.

Changes

  • status/index.htmlrenderNodes(), renderArchitecture(), renderLog() rebuilt with document.createElement + textContent. Added STATUS_WHITELIST so the badge class can only ever be ok, bad, blocked, or "".
  • status/templates/status.html — the per-node card body, the uptime-bar ticks, and the incident list are all rebuilt with createElement + textContent. Added ALLOWED_STATUS_CLASSES = {up, down, recovered, unknown} and a safeClass() helper used for status-badge, event-${event}, and tick ${state}.
  • website/static/network-status.html and docs/network-status.htmlrenderHistory() no longer interpolates the URL base into innerHTML; uses textContent for the URL and uptime text and only assigns the static SVG returned by sparkline().
  • status/test_status.py:
    • Rewrote TestAPI.test_index_page_escapes_dynamic_status_fields to assert the new safe DOM markers (nameSpan.textContent = node.name, badge.textContent = …, evt.textContent = …) and explicitly forbid card.innerHTML / incEl.innerHTML / raw ${node.name} / ${i.detail?' · '+i.detail:''} interpolation.
    • Added TestStaticDashboardFiles with two tests covering status/index.html and both network-status.html copies so the GitHub Pages surfaces are regression-protected even though they aren't served by the Flask app.

Files: 5 changed (+281 / -86).

Why this approach

The existing code mixed escapeHtml() (HTML text) with raw ${...} interpolation (HTML attribute + class + URL), and the class-attribute interpolations are not a use case escapeHtml solves. Using createElement + textContent makes every text sink trivially safe; using a class whitelist (safeClass) makes every class-attribute sink safe by construction. This matches the same hardening shape used in #7218 (fossil-record tooltip), #7146 (dashboard wallet search), #7137 (BCOS badge preview), and #7224 (CSV export).

Trade-offs considered:

  • Stripping markup from version/error text vs. accepting HTML. — Opted to treat these as plain text. They are telemetry, not user-authored markup, and rendering them as text keeps the dashboard safe even if the upstream /health or /api/incidents endpoint is compromised.
  • DOM construction vs. textContent + replaceChildren. — Used a mix to keep the diff readable; replaceChildren(...nodes) is used where the static skeleton was already preserved, createElement loops where it wasn't.

Testing

$ cd status && python -m pytest test_status.py -v
============================== 19 passed in 21.34s ==============================

All 19 tests in status/test_status.py pass, including:

  • TestAPI::test_index_page_escapes_dynamic_status_fields (rewritten)
  • TestStaticDashboardFiles::test_status_index_html_escapes_node_and_incident_fields (new)
  • TestStaticDashboardFiles::test_network_status_html_escapes_base_and_message (new)
  • TestAPI::test_rss_feed_escapes_incident_fields (unchanged — server-side RSS already escaped via xml_text)

The existing CI test tests/test_docs_network_status_security.py (7 tests) also still passes:

$ python -m pytest tests/test_docs_network_status_security.py -v
============================== 7 passed in 0.10s ==============================

Manual verification

JS syntax check across all 4 edited HTML files:

$ for f in status/index.html status/templates/status.html \
           website/static/network-status.html docs/network-status.html; do
    node --check <(python3 -c "import re,sys; \
      print(re.search(r'<script>(.*?)</script>', \
        open('$f').read(), re.DOTALL).group(1))") && echo OK $f
  done
OK status/index.html
OK status/templates/status.html
OK website/static/network-status.html
OK docs/network-status.html

Smoke render of templates/status.html through the Flask test client — GET / returns 200 and the response body now contains nameSpan.textContent = node.name, badge.textContent = …, evt.textContent = …, and function safeClass(value …), while no longer containing card.innerHTML, incEl.innerHTML, ${node.name}, or ${i.detail?' · '+i.detail:''}.

Trade-offs

  • The static GitHub Pages copies (status/index.html, docs/network-status.html, website/static/network-status.html) are not exercised by the Flask test suite; the new TestStaticDashboardFiles regression-tests their source so a future careless innerHTML rewrite gets caught at CI time.
  • The dashboard no longer renders any inline HTML in version, error, incident.message, or node.name. Operators who were relying on rich formatting in those fields (there is no such usage in the codebase today) will see plain text instead.

Closes #7216

Replace innerHTML template interpolation with safe DOM construction
(textContent + classList) for untrusted fields flowing into the
health dashboard. Adds a class-name whitelist (ALLOWED_STATUS_CLASSES /
STATUS_WHITELIST) so server-supplied status strings cannot break out of
the class attribute via a crafted marker.

Affected sinks (per issue Scottcjn#7216):
- status/index.html renderNodes(): node.name, node.location, node.origin
  were interpolated into innerHTML alongside an unbounded status class.
- status/index.html renderArchitecture(): arch and pct were interpolated;
  pct now flows through .style.width only.
- status/index.html renderLog(): node.name was interpolated unescaped
  inside a <strong class="${cls}"> sink.
- status/templates/status.html: card.innerHTML body re-built with
  createElement/textContent; event-${i.event} class now whitelisted.
- status/templates/status.html uptime bar: bucket class whitelisted.
- website/static/network-status.html and docs/network-status.html
  renderHistory(): base URL no longer interpolated into innerHTML.

Tests:
- Extend TestAPI.test_index_page_escapes_dynamic_status_fields to assert
  the new safe DOM construction markers and forbid unsafe innerHTML.
- New TestStaticDashboardFiles covers the GitHub Pages surfaces
  (status/index.html + the two network-status.html copies) so regressions
  are caught even though those files are not served by the Flask app.

Closes Scottcjn#7216
@github-actions

Copy link
Copy Markdown
Contributor

Welcome to RustChain! Thanks for your first pull request.

Before we review, please make sure:

  • Non-doc PRs have a BCOS-L1 or BCOS-L2 label
  • Doc-only PRs are exempt from BCOS tier labels when they only touch docs/**, *.md, or common image/PDF files
  • New code files include an SPDX license header
  • You've tested your changes against the live node

Bounty tiers: Micro (1-10 RTC) | Standard (20-50) | Major (75-100) | Critical (100-150)

A maintainer will review your PR soon. Thanks for contributing!

@github-actions github-actions Bot added documentation Improvements or additions to documentation BCOS-L1 Beacon Certified Open Source tier BCOS-L1 (required for non-doc PRs) labels Jun 22, 2026
@github-actions github-actions Bot added the size/L PR: 201-500 lines label Jun 22, 2026

@jaxint jaxint 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.

LGTM! Great work on this PR. The implementation looks solid and follows the project conventions.

@jaxint jaxint 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.

Great work on this PR! The code looks clean and well-structured.

@jaxint jaxint 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.

Code Review

Reviewed for:

  • Code quality and maintainability
  • Security best practices
  • Error handling
  • Documentation

Approved - Changes look good.

@jaxint jaxint 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.

Code Review

Thank you for this PR! I've reviewed the changes and here are my observations:

Summary

This PR introduces changes that improve the codebase. The implementation looks solid overall.

Key Points

✅ Code structure is clean and follows project conventions
✅ Changes are well-scoped and focused
✅ No obvious security concerns detected
✅ Documentation appears adequate

Suggestions for Consideration

  • Consider adding unit tests for the new functionality if not already present
  • Verify edge cases are handled appropriately
  • Ensure backward compatibility is maintained

Recommendation: This PR looks ready for merge pending CI checks.


Reviewed by AI Assistant for RustChain Bounty #71
Wallet: AhqbFaPBPLMMiaLDzA9WhQcyvv4hMxiteLhPk3NhG1iG

@Yzgaming005

Copy link
Copy Markdown
Author

📋 Bounty payout wallet (added per project convention):

  • RTC wallet: GABFQIK63R2NETJM7T673EAMZN4RJLLGP3OFUEJU5SZVTGWUKULZJNL6 + memo 396193324 (Binance XLM/Stellar deposit)
  • EVM (fallback): 0x683d2759cb626f536c842e8a3d943776198b8b8a
  • PayPal: ahmadyusrizal89@gmail.com

Yzgaming005

@jaxint jaxint 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.

✅ Code review completed - implementation verified.

@jaxint jaxint 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.

✅ Code reviewed - implementation verified.

@jaxint jaxint 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.

✅ Code reviewed - implementation verified. Security and performance validated.

@jaxint jaxint 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.

✅ Code reviewed - implementation verified.

@jaxint jaxint 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.

✅ Code reviewed - implementation verified.

@jaxint jaxint 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.

✅ Code reviewed - implementation verified.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

BCOS-L1 Beacon Certified Open Source tier BCOS-L1 (required for non-doc PRs) documentation Improvements or additions to documentation size/L PR: 201-500 lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Harden health dashboard node and incident rendering

2 participants