fix(packages): escape HTML special chars in serializeAttributes to prevent XSS#1670
fix(packages): escape HTML special chars in serializeAttributes to prevent XSS#1670Jerricho93 wants to merge 4 commits into
Conversation
|
@Jerricho93 is attempting to deploy a commit to the Mux Team on Vercel. A member of the Team first needs to authorize it. |
👷 Deploy request for vjs10-site pending review.Visit the deploys page to approve it
|
| if (value === '') html += ` ${key}`; | ||
| else html += ` ${key}="${value}"`; | ||
| else html += ` ${key}="${escapeAttributeValue(value)}"`; |
| // Ampersand must be escaped first to avoid double-encoding the entities below. | ||
| function escapeAttributeValue(value: string): string { | ||
| return value.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); | ||
| } |
There was a problem hiding this comment.
can we move this to its own file and name it more generic escapeHtml(). it's pretty common as utility.
https://github.com/videojs/v10/blob/440145e9de980fda317b11ae2b7ee3abdc7e3ff7/packages/utils/src/string/escape-html.ts#L10
| @@ -27,7 +27,7 @@ export class BackgroundVideoSkinElement extends ReactiveElement { | |||
|
|
|||
| if (!this.shadowRoot) { | |||
| this.attachShadow((this.constructor as typeof BackgroundVideoSkinElement).shadowRootOptions); | |||
| this.shadowRoot!.innerHTML = getTemplateHTML(namedNodeMapToObject(this.attributes)); | |||
There was a problem hiding this comment.
The _attrs parameter is not being used here. If we were to accept attrs in getTemplateHTML without escaping, it would introduce the same issues we're fixing with this PR. If there is a plan in the future to add this (by removing _ to attrs), we can reintroduce it and add the escape logic now. Does it makes sense? Otherwise, at this point in time in the code, this is effectively dead code
luwes
left a comment
There was a problem hiding this comment.
would like some small changes but looks good overall!
…event XSS Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…n in e2e XSS tests Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
530412d to
449c141
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 449c141. Configure here.
…ideo Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|

Summary
serializeAttributesinterpolated attribute values directly into HTML strings without encoding. Because the result is assigned toshadowRoot.innerHTMLat construction time, and Shadow DOM does not sandbox script execution, any attribute value containing"(e.g. a user-controlledposter,src, orcrossorigin) could break out of the attribute and inject event handlers or<script>siblings into the shadow root.Changes per sink:
utils: add privateescapeAttributeValuethat encodes&,<,>,"(ampersand first to prevent double-encoding); apply inserializeAttributeshtml/BackgroundVideo: remove the local duplicateserializeAttributes; import the shared util andpickagainst the existing attribute allowlisthtml/define/background: remove dead_attrsparameter (was passed to a template that never interpolated it)icons/scripts/build: inlineesc()in the generatedrenderIconfunction body to close the codegen sinksite/build-ejected-skins: extend the partial localescapeAttributeValueto also cover<and>Testing
packages/utils/src/dom/tests/attributes.test.ts: 9 cases covering all four chars, boolean branch, ampersand-first ordering regression, andnamedNodeMapToObjectraw-value preservationpackages/html/src/media/background-video/tests/background-video.test.ts: quote breakout, angle-bracket injection, non-allowlisted attribute filtering, boolean attrs, safe value round-trippackages/core/.../custom-media-element.test.ts:describe('XSS prevention')with 4 casesapps/e2e/tests/xss-prevention.spec.ts: 3 Playwright tests exercising the construction-time path viacontainer.innerHTMLValidation
Known issue (follow-up)
CustomMediaElementhas a pre-existing attribute routing inconsistency:getAttrsFromPropsuses.toLowerCase()to buildobservedAttributes(e.g.crossorigin,controlslist,disableremoteplayback), while#defineuseskebabCaseto keymediaHostAttrToProp(e.g.cross-origin,controls-list,disable-remote-playback). Because these never match, multi-word attributes are never added to the disallowed set and always flow throughserializeAttributesrather thanattributeChangedCallback. This means post-constructionsetAttributecalls for those attributes do not sync to the media host setter. This fix makesserializeAttributessafe, closing the injection vector — but the routing inconsistency itself should be tracked and corrected separately.Closes #1562
Note
Medium Risk
Security fix on construction-time shadow HTML for media elements; behavior change is limited to escaping special characters in serialized attributes, with broad test coverage but many call sites rely on the shared helper.
Overview
Closes an XSS path where attribute values were interpolated into
shadowRoot.innerHTMLwithout encoding, so craftedposter,src,crossorigin, etc. could break out of quotes and inject markup or handlers in custom media shadow roots.serializeAttributesnow runs values through a sharedescapeHtmlhelper (&,<,>,", with&first).BackgroundVideodrops its local serializer and uses the shared util with an explicit video-attribute allowlist;background-video-skinstops passing unused attrs into its template. IconrenderIconcodegen and ejected-skinmedia-iconHTML apply the same escaping for dynamic attribute strings.Coverage adds unit tests for
escapeHtml/serializeAttributes, XSS cases for CustomMediaElement and BackgroundVideo, and Playwright checks onhls-videoconstruction viainnerHTML.Reviewed by Cursor Bugbot for commit d222690. Bugbot is set up for automated code reviews on this repo. Configure here.