Check unsafe method calls made through bind() - #296
Open
owevertonguedes wants to merge 1 commit into
Open
Conversation
The method rule looked at the callee of a CallExpression and treated
another CallExpression as fine, so a sink reached through
Function.prototype.bind was never checked:
document.body.insertAdjacentHTML.bind(document.body)("afterend", evil)
Rewrite such a call into the direct call it performs and check that
one instead, the same way the SequenceExpression case builds a mock
node. Arguments given to bind() are prepended to the ones of the
eventual call, so partial application is checked too.
The rewrite only happens when the bound function is an identifier, a
member expression or another bind(), so that binding an expression the
rule cannot name stays as quiet as calling it directly would be. A
spread in the thisArg position hides how many arguments bind()
prepends, so those calls are left alone.
Fixes mozilla#115
Author
|
@mozfreddyb this follows the approach you sketched on the issue: the bound call is rewritten into the direct call it performs, and that mock node is checked instead. CI has not run yet, it is sitting on workflow approval since this is my first contribution here. Whenever you get a chance to release it. Two parts go slightly beyond the three cases you listed, and both are separable if you would rather keep the change minimal: merging the arguments that |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #115
The bug
checkCallExpression()treated aCallExpressioncallee as always fine, so a sink reached throughFunction.prototype.bindwas never checked:The fix
Following the approach suggested in the issue, a bound call is rewritten into the direct call it actually performs, and that mock node is checked instead, the same way the
SequenceExpressioncase already builds one.Two details beyond the three examples in the issue:
bind()prepends its own arguments, all butthisArg, to the ones of the eventual call, soinsertAdjacentHTML.bind(el, "afterend")(evil)putsevilat argument 1 and is now reported. Without merging the arguments, that shape would be a silent bypass.bind(). Recursing into anything else made the rule newly report benign code such as(class {}).bind(null)()as an unsupported callee, and made(foo.insertAdjacentHTML`x${evil}`).bind(null)()report twice. Both now behave exactly as they did before this patch.A spread in the
thisArgposition hides how many argumentsbind()prepends, so those calls are left alone rather than guessed at, in the spirit of #214.Verification
npm test(193 passing, up from 180) andnpm run lintare clean.The property I was really after is that a bound call gets the same verdict as its direct equivalent, so I checked it by brute force rather than by eye: 972 pairs, generated by splitting every argument list of literals, identifiers, escaped templates and spreads across every possible
bind()/call boundary, comparinginsertAdjacentHTML(ARGS)against everyinsertAdjacentHTML.bind(el, PREFIX)(REST)that performs it. Zero divergences.I also diffed the patched rule against the current
mainrule on exotic callee shapes (class and object expressions,yield, tagged templates, sequence expressions, chained binds, optional chaining, and the TypeScript and Flow parser configurations used in the test file) and confirmed no input changed verdict except the bound calls this patch is meant to catch.Nine tests were added. The five invalid ones all fail against
main; on the valid side,getInserter()('afterend', evil)and the spreadthisArgcase fail if the corresponding guard is removed. The remaining valid cases are the ones listed in the issue.