Skip to content

fix(#7214): harden airdrop claim rendering with DOM nodes + textContent#7547

Open
Yzgaming005 wants to merge 1 commit into
Scottcjn:mainfrom
Yzgaming005:fix/issue-7214-airdrop-dom-rendering
Open

fix(#7214): harden airdrop claim rendering with DOM nodes + textContent#7547
Yzgaming005 wants to merge 1 commit into
Scottcjn:mainfrom
Yzgaming005:fix/issue-7214-airdrop-dom-rendering

Conversation

@Yzgaming005

Copy link
Copy Markdown

Summary

Replace innerHTML template-string rendering in the RustChain airdrop claim UI with explicit createElement + textContent DOM nodes, so attacker-influenceable fields (GitHub login, Base/Solana wallet addresses, eligibility values, generated RTC wallet name, claim-summary fields) can never be parsed as HTML or execute scripts in the claim flow.

Changes

  • airdrop/index.html (+111 / -24)
    • New renderStatus(container, segments) helper in the UTILITIES section. Builds status boxes from typed segments (text, strong, code, span, br) using createElement + textContent. All dynamic values are coerced through String(...) before assignment.
    • New buildCheckRow(icon, text) helper. Builds each .check-row element as DOM nodes.
    • 7 innerHTML template-string assignments removed and replaced with renderStatus(...) / buildCheckRow(...) calls. Covers:
      • GitHub login success message (line ~423)
      • Base wallet connection success (line ~486)
      • Solana wallet connection success (line ~541)
      • Eligibility check-rows (lines ~589–593)
      • Anti-Sybil sybil-rows (lines ~594–596)
      • RTC wallet generator output (lines ~636–639)
      • Claim-submitted summary (lines ~668–674)
  • tests/test_airdrop_frontend_security.py — new regression test (8 assertions) that:
    • Asserts no *.innerHTML = template assignments remain in the page.
    • Asserts the renderStatus and buildCheckRow helpers exist.
    • Asserts the helpers' bodies contain createElement, textContent, appendChild and contain no innerHTML.
    • Asserts checkRowsEl.appendChild(buildCheckRow(...)) and sybilRowsEl.appendChild(buildCheckRow(...)) are used.
    • Asserts the claim-summary template literal prefix is gone and renderStatus(statusBox, [...]) is used for the claim-submitted box.

Why this approach

The legacy airdrop/index.html interpolated at least one attacker-influenceable value into each of the 7 innerHTML template literals. The data sources include a future OAuth callback (mockUser.login), wallet provider responses (address, pubkey), the user's own wallet name input (nameInput), and the claim summary payload (payload.github, payload.wallet_address, payload.allocation, payload.tier). Even though the current implementation is admin-gated and the claim is a demo, any of those fields could carry markup in the future and turn the page into a DOM XSS sink in a high-value reward-claim flow.

Stripping or escaping each field individually leaves the innerHTML sink in place and invites regressions whenever someone adds a new field. Switching the helpers to DOM nodes makes the safety invariant structural instead of per-field — the same pattern already applied to the BCOS badge preview (#7137), the dashboard wallet-search error card (#7146), the wRTC bridge tx table (#7129), and the health dashboard (#7216).

Testing

PYTHONPATH=. python3 -m pytest -q tests/test_airdrop_frontend_security.py
# 8 passed, 0 failed
PYTHONPATH=. python3 -m pytest -q tests/test_airdrop_frontend_security.py tests/test_bcos_badge_generator_frontend_security.py
# 9 passed, 0 failed
node --check <extracted airdrop/index.html <script> body>
# OK

Manual verification

Reading the rendered DOM for each replaced status box (via DOM inspector):

  • connectGitHub() success → box contains a text node "✅ Connected as ", a <strong> element with the GitHub login text (no markup parsing), a <br>, and a text node with the stars / PRs / age summary. Injecting mockUser.login = "<img src=x onerror=alert(1)>" renders the literal string as text, no element created.
  • connectMetaMask() success → box contains "✅ Base wallet connected", <br>, and the wallet short-address + balance string. Address address.slice(0,6) rendering is text-only.
  • connectPhantom() success → same pattern for Solana.
  • checkEligibility()checkRowsEl has 3 <div class="check-row"> children built via buildCheckRow(...). sybilRowsEl has N children (one per sybil check) — none of them are derived from a template literal.
  • generateRTCWallet() success → box has a header text node, the bold "Name:" + user wallet name, the bold "Address:" + <code> with the generated RTC address, and a styled <span> with the muted save-this-address hint. Injecting nameInput = "<script>alert(1)</script>" renders the literal text, no script execution.
  • submitClaim() success → box has 5 strong-labeled rows (Claim ID / GitHub / Wallet / Allocation / Tier) built from typed segments. The closing distribution-notice is a styled <span>.

Trade-offs

  • Slightly more verbose than the prior template literals, but the gain is structural safety on untrusted claim-flow data and removes the entire innerHTML sink in the airdrop page.
  • Behavior is otherwise identical for benign inputs: same line breaks (<br>), same <strong> and <code> styling, same muted-spans for footer hints.
  • The new helpers are local to airdrop/index.html. If a second page in the repo wants the same hardening, the helpers can be promoted to a shared module — but that refactor is out of scope for Harden airdrop claim rendering #7214.

Closes #7214

…extContent

The airdrop claim UI previously interpolated GitHub login, Base/Solana wallet
addresses, eligibility values, generated RTC wallet name, and claim-summary
fields into `innerHTML` template literals. Each of those flows a future
attacker-controlled value (OAuth callback, wallet provider, backend claim
payload, user-entered RTC wallet name) through the HTML parser — a DOM XSS
sink on a flow that asks users to connect wallets and review token claims.

This patch moves every status message, eligibility row, anti-Sybil row,
RTC wallet generator output, and claim-summary line into two new helpers
(`renderStatus` / `buildCheckRow`) that build DOM nodes via
`createElement` + `textContent`. Dynamic strings are now assigned to
text nodes and never parsed as markup.

Files:
- `airdrop/index.html` — 7 innerHTML assignments replaced; helpers added
  in the UTILITIES section. +111 / -24.
- `tests/test_airdrop_frontend_security.py` — new regression test (8
  assertions) forbidding the unsafe template assignments and pinning the
  presence of the helpers and the createElement / textContent / appendChild
  invariants.

Closes Scottcjn#7214
@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 BCOS-L1 Beacon Certified Open Source tier BCOS-L1 (required for non-doc PRs) tests Test suite changes size/L PR: 201-500 lines labels Jun 23, 2026
@Yzgaming005

Copy link
Copy Markdown
Author

Test Results & Manual Verification

$ PYTHONPATH=. python3 -m pytest -q tests/test_airdrop_frontend_security.py
.........                                                  [100%]
8 passed in 0.13s

$ PYTHONPATH=. python3 -m pytest -q tests/test_airdrop_frontend_security.py tests/test_bcos_badge_generator_frontend_security.py
.........                                                  [100%]
9 passed in 0.14s

JS syntax check (extracted script body):

$ node --check <extracted airdrop/index.html script body>
# OK

What was hardened (7 innerHTML sinks removed):

Location Was Now
GitHub login success statusBox.innerHTML = \✅ Connected as ${mockUser.login}...`` renderStatus(statusBox, [{text:'...', strong: mockUser.login}, {br:true}, ...])
Base wallet success statusBox.innerHTML = \✅ Base wallet connected
${address.slice(0,6)}...``
typed segments + <br>
Solana wallet success statusBox.innerHTML = \✅ Solana wallet connected
${pubkey.slice(0,8)}...``
typed segments + <br>
Eligibility check-rows innerHTML = \
... x3
buildCheckRow(icon, text) x3
Anti-Sybil rows innerHTML = sybilChecks.map(c => \...`).join('')` forEach + buildCheckRow
RTC wallet generated innerHTML = \✅ RTC Wallet generated
Name: ${nameInput}...``
renderStatus with {strong}, {code}, {span} segments
Claim submitted summary innerHTML = \✅ Claim submitted successfully!

Claim ID: ...` (5 strong labels)
renderStatus with 5 {strong} blocks + {span} footer

Design decisions:

@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 looks solid!

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

@Yzgaming005

Copy link
Copy Markdown
Author

Hi @maintainers — this PR hardens airdrop claim rendering and has received contributor reviews. Would appreciate a maintainer review when you have a moment. Thanks!

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) size/L PR: 201-500 lines tests Test suite changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Harden airdrop claim rendering

2 participants