fix: never evaluate caller-supplied selector strings as HTML - #804
Conversation
jQuery builds a detached DOM fragment from any string that looks like markup instead of running it as a CSS selector, so a value such as "<img src=x onerror=...>" passed where a selector is expected ends up executing script rather than matching nothing. Three options took that path: `context` (normalised once for every operation, so it affected create, update and destroy alike), `appendTo`, and the element handed to `$.contextMenu.fromMenu()`. All three are documented as a selector string or a DOM element, never as markup. They now go through a small `resolveSelector()` helper that routes strings through `$(document).find()`, which only ever accepts a selector. Elements, jQuery objects and `document` are passed to `$()` unchanged, so `context` as an Element/jQuery object and the Element and jQuery object `selector` support added in #796 keep working.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4f8c729806
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
|
||
| $.contextMenu('destroy', {context: '#issue-731-container'}); | ||
| $container.find('.issue-731-trigger').trigger($.Event('contextmenu')); | ||
| assert.equal(shown, 1, 'menu was destroyed through a string context'); |
There was a problem hiding this comment.
Fix the context-destroy expectations
When this test destroys with the container as context, the _hasContext branch only removes registrations when $(context).is(o.selector); this container does not match .issue-731-trigger, so the registration remains active. The next trigger increments shown to 2, making this assertion—and the analogous jQuery-object assertion below—deterministically fail in the unit suite.
Useful? React with 👍 / 👎.
…viour Codex pointed out that the destroy assertions in the "supported selector inputs" tests could not fail. It was right about the cause, though not about the outcome: `destroy` treats `context` as the trigger element, so a container context matches no menu and tears nothing down. The assertions still passed because the menu was already open, and a second trigger on an open menu repositions instead of firing `show` again. The context tests now prove scoping instead: a trigger inside the context opens the menu, an identical trigger outside it does not. The destroy tests never open the menu first, so a destroy that silently did nothing is caught. While rewriting them, a raw Element `context` turned out not to scope anything either: an Element has no `length`, so `!o.context.length` sends it down the "no context" branch and it becomes `document`. That is pre-existing and left alone here, the test documents it instead.
|
Thanks, addressed in ebba1e5. Addressed: the destroy assertions in the "supported selector inputs" tests could not fail. The diagnosis was right, the predicted outcome was not: the tests passed on all nine CI jobs, because the menu was already open at that point and a second trigger on an open menu repositions rather than firing The underlying point stands, so those tests were rewritten:
Not addressed, reported instead: while rewriting these, a raw |
|
👍 |
Hardens selector handling so a caller-supplied string is never parsed as HTML.
Problem
jQuery builds a detached DOM fragment from any string that looks like markup, rather than running it as a CSS selector. A value like
<img src=x onerror=...>passed where the plugin expects a selector therefore ends up building an element and running its inline handler instead of simply matching nothing.Three options reached
$()with a raw string:context, normalised once at the top of$.contextMenu(), so it applied equally tocreate,updateanddestroy(thedestroybranch is the one reported in Potentail XSS vulnerability #731)appendTo, used inop.create()elementargument of$.contextMenu.fromMenu()All three are documented as a selector string or a DOM element, never as markup.
Change
A small
resolveSelector()helper routes strings through$(document).find(), which only ever treats its argument as a selector. Anything that is not a string (Element, jQuery object,document) is handed to$()unchanged, so the existing supported inputs keep working, including the Element and jQuery objectselectorsupport added in #796.A markup-looking string now either matches nothing or makes jQuery raise its usual "unrecognized expression" selector error, depending on whether it happens to be valid selector syntax. Neither builds an element.
Tests
test/unit/issue-731-selector-html-injection.test.jscovers each hardened call site with an<img>payload whoseonerrorsets a global flag, asserting that the flag stays unset and that no<img>is added to the document. It also covers the inputs that must keep working:contextas a selector string, as an Element and as a jQuery object, andappendToas a selector string and as an Element.Verified failing before the change and passing after, on jQuery 1, 2, 3 and 4. Acceptance tests and eslint are unchanged.
Not included
item.iconis interpolated into markup for the Font Awesome paths ($('<i class="' + item.icon + '"></i>')). That is a different mechanism andiconmay well be considered trusted config, so it is left alone here.$('<i></i>').addClass('fa ' + item.icon)would be an equivalent, non-parsing alternative if you want it changed.inputLabel()buildslabel[for="' + node.id + '"]. The id comes from the DOM and the string never starts with<, so it is not parsed as HTML, but a quote in an id would still break the selector.$.contextMenu('update', {context: ...})throws for unrelated, pre-existing reasons (op.update()receives the context itself, which has no$menu). Left as is; the test for that path only asserts the string is not evaluated.Closes #731