Skip to content

Escape profile badge custom labels#4865

Open
SimoneMariaRomeo wants to merge 1 commit into
Scottcjn:mainfrom
SimoneMariaRomeo:codex/profile-badge-html-escape
Open

Escape profile badge custom labels#4865
SimoneMariaRomeo wants to merge 1 commit into
Scottcjn:mainfrom
SimoneMariaRomeo:codex/profile-badge-html-escape

Conversation

@SimoneMariaRomeo
Copy link
Copy Markdown

Summary

  • Encodes the Shields.io label segment with safe="" so custom labels cannot alter the badge URL path.
  • Escapes generated HTML attributes and Markdown alt text that include user-controlled badge labels.
  • Replaces the badge preview innerHTML sink with DOM-created image nodes while keeping preview_html safe for existing API consumers.
  • Adds regression coverage for quote/script/slash payloads and the DOM preview path.

Fixes #4815.

Validation

  • python -m pytest tests\test_profile_badge_generator_security.py -q -> 2 passed
  • python -m py_compile profile_badge_generator.py tests\test_profile_badge_generator_security.py -> passed
  • git diff --check -- profile_badge_generator.py tests\test_profile_badge_generator_security.py -> passed
  • python tools\bcos_spdx_check.py --base-ref origin/main -> OK
  • python -m ruff check profile_badge_generator.py tests\test_profile_badge_generator_security.py --select E9,F821,F811 --output-format=concise -> unavailable locally: No module named ruff

@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/M PR: 51-200 lines labels May 12, 2026
Copy link
Copy Markdown

@saim256 saim256 left a comment

Choose a reason for hiding this comment

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

Approving current head e217036.

The #4815 badge-label injection path is addressed in the scoped areas: the Shields label segment is URL-encoded with safe='', generated HTML attributes are escaped with quote handling, Markdown alt text escapes bracket/control characters, and the browser preview no longer assigns the returned preview_html through innerHTML. The regression tests cover the quote/script/slash payload and the DOM preview path.

Validation performed:

  • python -m pytest tests\test_profile_badge_generator_security.py -q -> 2 passed
  • python -m py_compile profile_badge_generator.py tests\test_profile_badge_generator_security.py -> passed
  • python tools\bcos_spdx_check.py --base-ref origin/main -> BCOS SPDX check: OK
  • git diff --check origin/main...HEAD -- profile_badge_generator.py tests/test_profile_badge_generator_security.py -> passed
  • python -m ruff check profile_badge_generator.py tests\test_profile_badge_generator_security.py --select E9,F821,F811 --output-format=concise -> All checks passed

Scope note: profile_badge_generator.py still has an existing app.run(debug=True) dev entrypoint, but that is not introduced by this PR and is already being handled under the separate Flask debug issue/PR track.

Copy link
Copy Markdown
Contributor

@jaxint jaxint left a comment

Choose a reason for hiding this comment

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

LGTM! Thanks for contributing. Approved.

Copy link
Copy Markdown
Contributor

@jaxint jaxint left a comment

Choose a reason for hiding this comment

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

LGTM! Thanks for contributing. Approved.

Copy link
Copy Markdown
Contributor

@godd-ctrl godd-ctrl left a comment

Choose a reason for hiding this comment

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

Approved current head e217036.

The profile badge fix escapes the generated HTML/preview markup, avoids assigning returned preview_html through innerHTML, percent-encodes the shield label path segment, and keeps generated Markdown alt text from breaking out of the image syntax. The added regression tests cover the custom-message injection path and DOM preview behavior.

Validation performed:

  • git diff --name-status origin/main...HEAD -> profile_badge_generator.py modified, tests/test_profile_badge_generator_security.py added
  • git diff --check origin/main...HEAD -> passed
  • python tools\bcos_spdx_check.py --base-ref origin/main -> passed
  • python -m py_compile profile_badge_generator.py tests\test_profile_badge_generator_security.py -> passed
  • python -m pytest tests\test_profile_badge_generator_security.py -q -> 2 passed
  • Additional local smoke with hostile labels including quote/onerror, script tag, newline, and URL punctuation -> passed

No blocking issues found in the reviewed diff.

Copy link
Copy Markdown
Contributor

@jaxint jaxint left a comment

Choose a reason for hiding this comment

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

LGTM! Thanks for contributing. Approved.

@508704820
Copy link
Copy Markdown
Contributor

Code Review: Escape profile badge custom labels

Summary

This PR fixes an HTML injection vulnerability in the BCOS badge generator. User-controlled input (custom_message, username) was being directly interpolated into HTML without escaping, allowing XSS attacks.

Positive Observations ✅

  1. Critical XSS fix: innerHTML = data.preview_html replaced with safe DOM manipulation (replaceChildren + createElement)
  2. Proper output encoding: html_utils.escape() for HTML attributes, urllib.parse.quote(label, safe='') for URLs, escape_markdown_alt() for markdown
  3. text_field() helper: Centralized input sanitization — handles None, strips whitespace, coerces to string
  4. Safe defaults: get_json(silent=True) or {} prevents crashes on malformed JSON
  5. Markdown escaping: Covers \, [, ], newlines — prevents markdown injection in alt text

Issues Found 🔍

1. escape_markdown_alt() incomplete (LOW)

Markdown has many more special characters that could cause issues: (, ), <, >, etc. For badge alt text specifically this is probably fine, but if this function is reused elsewhere it could be insufficient.

2. text_field() doesn't limit length (LOW)

No maximum length check on custom_message. A malicious user could submit a 10MB string. Consider adding a length limit (e.g., 100 chars for custom_message).

3. Badge type not validated against allowlist (MEDIUM)

badge_type is sanitized but not validated against badge_colors keys. An arbitrary badge_type will silently get color='blue'. This is safe but might confuse users. Consider validating against the known types.

Verdict

Good security fix that properly addresses the HTML injection vulnerability. The multi-layer encoding approach (HTML, URL, Markdown) is correct. After adding a length limit on custom_message, this is ready to merge.

Review quality: Security-focused review with actionable feedback

Copy link
Copy Markdown
Contributor

@jaxint jaxint left a comment

Choose a reason for hiding this comment

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

Great contribution! LGTM.

Copy link
Copy Markdown

@loganoe loganoe left a comment

Choose a reason for hiding this comment

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

Security review for Scottcjn/rustchain-bounties#73 on current head e217036f3a6c8b45aa838ef48c6f649ac7c73163.

The badge-label injection fix covers the relevant output contexts: the Shields label path segment is percent-encoded with safe='', generated HTML attributes are escaped with quote handling, Markdown alt text no longer keeps bracket/control breakouts, and the preview JavaScript now creates an image node instead of assigning API-provided markup through innerHTML.

Validation performed:

  • PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 uv run --no-project --with pytest --with flask python -m pytest tests/test_profile_badge_generator_security.py -q -> 2 passed
  • python3 -m py_compile profile_badge_generator.py tests/test_profile_badge_generator_security.py -> passed
  • git diff --check origin/main...HEAD -- profile_badge_generator.py tests/test_profile_badge_generator_security.py -> passed
  • python3 tools/bcos_spdx_check.py --base-ref origin/main -> OK
  • Hostile-label API smoke with quote/onerror/script/slash payload confirmed the badge URL is percent-encoded and preview HTML uses escaped attributes rather than raw injected markup.

No blocking issues found in this diff.

Copy link
Copy Markdown
Contributor

@508704820 508704820 left a comment

Choose a reason for hiding this comment

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

Code Review: Escape Profile Badge Custom Labels

Summary

Fixes XSS vulnerability in the profile badge generator. Previously used innerHTML with unsanitized data; now uses DOM manipulation (replaceChildren + createElement) and server-side HTML escaping via html.escape().

What Works Well

  1. XSS fix: innerHTML with user data → safe DOM manipulation
  2. Server-side escaping: html_utils.escape() on custom_label
  3. DOM API: replaceChildren() + createElement('img') instead of innerHTML
  4. Defense in depth: Both client-side and server-side sanitization

Issues Found

1. Low — alt_text not escaped server-side
data.alt_text || 'RustChain Badge' is used directly. If alt_text contains special characters, it could cause issues. Consider escaping on the server side.

2. Nit — html_utils import name
import html as html_utils — standard library, but the alias is unusual. import html is more conventional since Python 3.

Verdict: Approve

Important XSS fix. Defense-in-depth approach with both client and server sanitization.

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/M PR: 51-200 lines tests Test suite changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[SECURITY] Profile badge custom message can inject preview HTML

6 participants