eslint-plugin-no-unsanitized
Advanced tools
Comparing version 4.0.2 to 4.1.0
@@ -7,6 +7,7 @@ # Customization | ||
This will be used to check code like here: | ||
```js | ||
const greeting_template = `<p>Hello World!</p>`; | ||
// ... lots of other code in between ... | ||
someElemenet.innerHTML = greeting_template; | ||
someElement.innerHTML = greeting_template; | ||
``` | ||
@@ -23,18 +24,19 @@ | ||
## Customization Examples | ||
You can customize the way this rule works in various ways. | ||
* Add to the list of properties or functions to be checked for potentially | ||
dangers variable input | ||
* Add to the list of allowed escaping functions to mitigate security concerns | ||
* Besides adding to the list, you may override the defaults and provide an exhaustive list yourself | ||
- Add to the list of properties or functions to be checked for potentially | ||
dangers variable input | ||
- Add to the list of allowed escaping functions to mitigate security concerns | ||
- Besides adding to the list, you may override the defaults and provide an exhaustive list yourself | ||
### Disallow the `html` function by specifically checking input for the first function parameter | ||
```json | ||
{ | ||
"rules": { | ||
"rules": { | ||
"no-unsanitized/method": [ | ||
"error", | ||
{}, | ||
{ | ||
}, | ||
{ | ||
"html": { | ||
@@ -45,3 +47,3 @@ "properties": [0] | ||
] | ||
} | ||
} | ||
} | ||
@@ -87,8 +89,8 @@ ``` | ||
### Override list of escaping functions for property assignments only | ||
### Override list of escaping functions for property assignments only | ||
TBD | ||
#### More | ||
* See [our rule schema definition](SCHEMA.md). | ||
- See [our rule schema definition](https://github.com/mozilla/eslint-plugin-no-unsanitized/blob/main/SCHEMA.md). |
# Fixing Rule Violations | ||
The default configuration will allow your code to pass if it ensures | ||
that all user input is properly escaped. | ||
Using the [sanitizer.js](https://github.com/fxos-eng/sanitizer) | ||
Using the [sanitizer.js](https://github.com/fxos-eng/sanitizer) | ||
library your code would look like this: | ||
@@ -9,9 +10,13 @@ | ||
// example for no-unsanitized/property | ||
foo.innerHTML = Sanitizer.escapeHTML`<a href="${link}">click</a>` | ||
foo.innerHTML = Sanitizer.escapeHTML`<a href="${link}">click</a>`; | ||
// example for no-unsanitized/method | ||
node.insertAdjacentHTML('afterend', Sanitizer.escapeHTML`<a href="${link}">click</a>`); | ||
node.insertAdjacentHTML( | ||
"afterend", | ||
Sanitizer.escapeHTML`<a href="${link}">click</a>` | ||
); | ||
``` | ||
## Wrapping & Unwrapping | ||
If you need to generate your HTML somewhere else and e.g. cache it, | ||
@@ -26,7 +31,8 @@ you won't be able to run `escapeHTML` on a string that knows no | ||
Example: | ||
```js | ||
// create the HTML object for later usage | ||
function storeGreeting(username) { | ||
var safeHTML = Sanitizer.createSafeHTML`<p>Hello ${username}</p>`; | ||
cache.store('greeting', safeHTML) | ||
var safeHTML = Sanitizer.createSafeHTML`<p>Hello ${username}</p>`; | ||
cache.store("greeting", safeHTML); | ||
} | ||
@@ -36,4 +42,4 @@ | ||
function useGreeting(domNode) { | ||
var htmlObj = cache.retrieve('greeting'); | ||
domNode.innerHTML = Sanitizer.unwrapSafeHTML(htmlObj); | ||
var htmlObj = cache.retrieve("greeting"); | ||
domNode.innerHTML = Sanitizer.unwrapSafeHTML(htmlObj); | ||
} | ||
@@ -43,4 +49,5 @@ ``` | ||
# That still does not solve my problem | ||
It might very well be the case that there's a bug in our linter rule. | ||
[Please file an issue.](https://github.com/mozilla/eslint-plugin-no-unsanitized/issues/new) |
# method | ||
The *method* rule in *eslint-plugin-no-unsanitized* perform basic security | ||
The _method_ rule in _eslint-plugin-no-unsanitized_ perform basic security | ||
checks for function calls. The idea of these checks is to ensure that certain insecure | ||
@@ -8,2 +9,3 @@ coding patterns are avoided in your codebase. We encourage developers | ||
## Unsafe call to insertAdjacentHTML, document.write or document.writeln | ||
This error message suggests that you are using an unsafe coding | ||
@@ -16,4 +18,5 @@ pattern. Please do not simply call functions like `insertAdjacentHTML` with a | ||
### Further Reading | ||
* Advanced guidance on [Fixing rule violations](fixing-violations.md) | ||
* This rule has some [customization](customization.md) options that allow you | ||
to add or remove functions that should not be called | ||
- Advanced guidance on [Fixing rule violations](fixing-violations.md) | ||
- This rule has some [customization](customization.md) options that allow you | ||
to add or remove functions that should not be called |
# property | ||
The *property* rule in *eslint-plugin-no-unsanitized* perform basic security | ||
The _property_ rule in _eslint-plugin-no-unsanitized_ perform basic security | ||
checks for property assignments. The idea of these checks is to ensure that | ||
@@ -8,3 +9,4 @@ certain insecure coding patterns are avoided in your codebase. We encourage | ||
## Unsafe assignment to innerHTML or outerHTML | ||
## Unsafe assignment to innerHTML or outerHTML | ||
This error message suggests that you are using an unsafe coding | ||
@@ -17,4 +19,5 @@ pattern. Please do not simply assign variables to `innertHTML`, | ||
### Further Reading | ||
* Advanced guidance on [Fixing rule violations](fixing-violations.md) | ||
* This rule has some [customization](customization.md) options that allow you | ||
to add or remove functions that should not be called | ||
- Advanced guidance on [Fixing rule violations](fixing-violations.md) | ||
- This rule has some [customization](customization.md) options that allow you | ||
to add or remove functions that should not be called |
80
index.js
@@ -1,54 +0,36 @@ | ||
module.exports = { | ||
const { readFileSync } = require("fs"); | ||
const data = readFileSync("./package.json"); | ||
const packageJSON = JSON.parse(data); | ||
const plugin = { | ||
meta: { | ||
name: "eslint-plugin-no-unsanitized", | ||
version: packageJSON.version, | ||
}, | ||
rules: { | ||
"property": require("./lib/rules/property"), | ||
"method": require("./lib/rules/method") | ||
property: require("./lib/rules/property"), | ||
method: require("./lib/rules/method"), | ||
}, | ||
configs: { | ||
DOM: { | ||
plugins: ["no-unsanitized"], | ||
rules: { | ||
"no-unsanitized/property": [ | ||
"error", | ||
{ | ||
}, | ||
{ | ||
configs: {}, | ||
}; | ||
// Check unsafe assignment to innerHTML | ||
innerHTML: {}, | ||
const rules = { | ||
"no-unsanitized/property": "error", | ||
"no-unsanitized/method": "error", | ||
}; | ||
// Check unsafe assignment to outerHTML | ||
outerHTML: {}, | ||
} | ||
], | ||
"no-unsanitized/method": [ | ||
"error", | ||
{ | ||
}, | ||
{ | ||
Object.assign(plugin.configs, { | ||
"recommended-legacy": { | ||
plugins: ["no-unsanitized"], | ||
rules, | ||
}, | ||
recommended: [ | ||
{ | ||
plugins: { "no-unsanitized": plugin }, | ||
rules, | ||
}, | ||
], | ||
}); | ||
// check second parameter to .insertAdjacentHTML() | ||
insertAdjacentHTML: { | ||
properties: [1] | ||
}, | ||
// check first parameter to .write(), as long as the preceeding object matches the regex "document" | ||
write: { | ||
objectMatches: [ | ||
"document" | ||
], | ||
properties: [0] | ||
}, | ||
// check first parameter to .writeLn(), as long as the preceeding object matches the regex "document" | ||
writeln: { | ||
objectMatches: [ | ||
"document" | ||
], | ||
properties: [0] | ||
} | ||
} | ||
] | ||
} | ||
} | ||
} | ||
}; | ||
module.exports = plugin; |
/** | ||
* @fileoverview ESLint helpers for checking sanitization | ||
* @file ESLint helpers for checking sanitization | ||
* @author Frederik Braun et al. | ||
@@ -12,9 +12,26 @@ * @copyright 2015-2017 Mozilla Corporation. All rights reserved. | ||
/** Change this to class RuleHelper when <4.2.6 is no longer an issue | ||
/** | ||
* Gets the scope for a node taking account of where the scope function | ||
* is available (supports node versions earlier than 8.37.0). | ||
* | ||
* @constructor | ||
* @param {Object} context ESLint configuration context | ||
* @param {Object} defaultRuleChecks Default rules to merge with | ||
* @param {object} context | ||
* The context passed from ESLint. | ||
* @param {object} node | ||
* The node to get the scope for. | ||
* @returns {Function} | ||
* The getScope function object. | ||
*/ | ||
function getScope(context, node) { | ||
return context.sourceCode?.getScope | ||
? context.sourceCode.getScope(node) | ||
: context.getScope(node); | ||
} | ||
/** | ||
* Change this to class RuleHelper when <4.2.6 is no longer an issue | ||
* | ||
* @class | ||
* @param {object} context ESLint configuration context | ||
* @param {object} defaultRuleChecks Default rules to merge with | ||
* this.context | ||
* | ||
*/ | ||
@@ -33,9 +50,8 @@ function RuleHelper(context, defaultRuleChecks) { | ||
* | ||
* @param {Object} expression Checks whether this node is an allowed expression. | ||
* @param {Object} escapeObject contains keys "methods" and "taggedTemplates" which are arrays of strings | ||
* @param {object} expression Checks whether this node is an allowed expression. | ||
* @param {object} escapeObject contains keys "methods" and "taggedTemplates" which are arrays of strings | ||
* of matching escaping function names. | ||
* @param {Object} details Additional linter violation state information, in case this function was called | ||
* @param {object} details Additional linter violation state information, in case this function was called | ||
* recursively. | ||
* @returns {boolean} Returns whether the expression is allowed. | ||
* | ||
*/ | ||
@@ -54,6 +70,5 @@ allowedExpression(expression, escapeObject, details) { | ||
switch(expression.type) { | ||
case"Literal": | ||
/* surely, someone could have an evil literal in there, but that"s malice | ||
switch (expression.type) { | ||
case "Literal": | ||
/* surely, someone could have an evil literal in there, but that"s malice | ||
we can just check for unsafe coding practice, not outright malice | ||
@@ -63,41 +78,72 @@ example literal "<script>eval(location.hash.slice(1)</script>" | ||
*/ | ||
allowed = true; | ||
break; | ||
case "TemplateElement": | ||
// Raw text from a template | ||
allowed = true; | ||
break; | ||
case "TemplateLiteral": | ||
// check only the ${..} expressions | ||
allowed = this.allowedExpression(expression.expressions, escapeObject, details); | ||
break; | ||
case "TaggedTemplateExpression": | ||
allowed = this.isAllowedCallExpression(expression.tag, escapeObject.taggedTemplates || VALID_ESCAPERS); | ||
break; | ||
case "CallExpression": | ||
allowed = this.isAllowedCallExpression(expression.callee, escapeObject.methods || VALID_UNWRAPPERS); | ||
break; | ||
case "BinaryExpression": | ||
allowed = ((this.allowedExpression(expression.left, escapeObject, details)) | ||
&& (this.allowedExpression(expression.right, escapeObject, details))); | ||
break; | ||
case "TSAsExpression": | ||
// TSAsExpressions contain the raw javascript value in 'expression' | ||
allowed = this.allowedExpression(expression.expression, escapeObject, details); | ||
break; | ||
case "TypeCastExpression": | ||
allowed = this.allowedExpression(expression.expression, escapeObject, details); | ||
break; | ||
case "Identifier": | ||
allowed = this.isAllowedIdentifier(expression, escapeObject, details); | ||
break; | ||
default: | ||
// everything that doesn't match is considered unsafe: | ||
allowed = false; | ||
break; | ||
allowed = true; | ||
break; | ||
case "TemplateElement": | ||
// Raw text from a template | ||
allowed = true; | ||
break; | ||
case "TemplateLiteral": | ||
// check only the ${..} expressions | ||
allowed = this.allowedExpression( | ||
expression.expressions, | ||
escapeObject, | ||
details | ||
); | ||
break; | ||
case "TaggedTemplateExpression": | ||
allowed = this.isAllowedCallExpression( | ||
expression.tag, | ||
escapeObject.taggedTemplates || VALID_ESCAPERS | ||
); | ||
break; | ||
case "CallExpression": | ||
allowed = this.isAllowedCallExpression( | ||
expression.callee, | ||
escapeObject.methods || VALID_UNWRAPPERS | ||
); | ||
break; | ||
case "BinaryExpression": | ||
allowed = | ||
this.allowedExpression( | ||
expression.left, | ||
escapeObject, | ||
details | ||
) && | ||
this.allowedExpression( | ||
expression.right, | ||
escapeObject, | ||
details | ||
); | ||
break; | ||
case "TSAsExpression": | ||
// TSAsExpressions contain the raw javascript value in 'expression' | ||
allowed = this.allowedExpression( | ||
expression.expression, | ||
escapeObject, | ||
details | ||
); | ||
break; | ||
case "TypeCastExpression": | ||
allowed = this.allowedExpression( | ||
expression.expression, | ||
escapeObject, | ||
details | ||
); | ||
break; | ||
case "Identifier": | ||
allowed = this.isAllowedIdentifier( | ||
expression, | ||
escapeObject, | ||
details | ||
); | ||
break; | ||
default: | ||
// everything that doesn't match is considered unsafe: | ||
allowed = false; | ||
break; | ||
} | ||
if (Array.isArray(expression)) { | ||
allowed = expression.every((e) => this.allowedExpression(e, escapeObject, details)); | ||
allowed = expression.every((e) => | ||
this.allowedExpression(e, escapeObject, details) | ||
); | ||
} | ||
@@ -114,6 +160,6 @@ return allowed; | ||
* | ||
* @param {Object} expression Identifier expression | ||
* @param {Object} escapeObject contains keys "methods" and "taggedTemplates" which are arrays of strings | ||
* @param {object} expression Identifier expression | ||
* @param {object} escapeObject contains keys "methods" and "taggedTemplates" which are arrays of strings | ||
* of matching escaping function names. | ||
* @param {Object} details Additional linter violation state information, in case this function was called | ||
* @param {object} details Additional linter violation state information, in case this function was called | ||
* recursively. | ||
@@ -123,3 +169,2 @@ * @returns {boolean} Returns whether the Identifier is deemed safe. | ||
isAllowedIdentifier(expression, escapeObject, details) { | ||
// respect the custom config property `variableTracing`: | ||
@@ -131,12 +176,14 @@ if (!this.ruleChecks["variableTracing"]) { | ||
// find declared variables and see which are literals | ||
const scope = this.context.getScope(expression); | ||
const variableInfo = scope.set.get(expression.name); | ||
const scope = getScope(this.context, expression); | ||
const variableInfo = scope.variableScope.set.get(expression.name); | ||
let allowed = false; | ||
// If we can't get info on the variable, we just can't allow it | ||
if (!variableInfo || | ||
if ( | ||
!variableInfo || | ||
!variableInfo.defs || | ||
variableInfo.defs.length == 0 || | ||
!variableInfo.references || | ||
variableInfo.references.length == 0) { | ||
variableInfo.references.length == 0 | ||
) { | ||
// FIXME Fix/Adjust towards a helpful message here and update tests accordingly. | ||
@@ -154,5 +201,8 @@ // details.message = `Variable ${expression.name} considered unsafe: variable initialization not found`; | ||
// ArrowFunctionExpression, FunctionDeclaration or FunctionExpression | ||
const {line, column} = def.node.loc.start; | ||
if ((def.node.type === "FunctionDeclaration") || (def.node.type == "ArrowFunctionExpression") || (def.node.type === "FunctionExpression")) | ||
{ | ||
const { line, column } = def.node.loc.start; | ||
if ( | ||
def.node.type === "FunctionDeclaration" || | ||
def.node.type == "ArrowFunctionExpression" || | ||
def.node.type === "FunctionExpression" | ||
) { | ||
details.message = `Variable '${expression.name}' declared as function parameter, which is considered unsafe. '${def.node.type}' at ${line}:${column}`; | ||
@@ -165,3 +215,3 @@ } else { | ||
} | ||
if ((def.kind !== "let") && (def.kind !== "const")) { | ||
if (def.kind !== "let" && def.kind !== "const") { | ||
// We do not allow for identifiers declared with "var", as they can be overridden in a | ||
@@ -177,7 +227,10 @@ // way that is hard for us to follow (e.g., assignments to globalThis[theirNameAsString]). | ||
// When the variable is only declared but not initialized, `init` is `null`. | ||
if (varInitAs && !this.allowedExpression(varInitAs, escapeObject, details)) { | ||
if ( | ||
varInitAs && | ||
!this.allowedExpression(varInitAs, escapeObject, details) | ||
) { | ||
// if one variable definition is considered unsafe, all are. | ||
// NB: order of definition is unclear. See issue #168. | ||
if (!details.message) { | ||
const {line, column} = varInitAs.loc.start; | ||
const { line, column } = varInitAs.loc.start; | ||
details.message = `Variable '${expression.name}' initialized with unsafe value at ${line}:${column}`; | ||
@@ -199,3 +252,6 @@ } | ||
// then we should consider it safe. | ||
if (variableInfo.references.filter(ref => ref.isWrite()).length === 0) { | ||
if ( | ||
variableInfo.references.filter((ref) => ref.isWrite()) | ||
.length === 0 | ||
) { | ||
allWritingRefsAllowed = true; | ||
@@ -212,5 +268,11 @@ } | ||
// due to dynamic behavior if-conditions and such | ||
if (!this.allowedExpression(writeExpr, escapeObject, details)) { | ||
if ( | ||
!this.allowedExpression( | ||
writeExpr, | ||
escapeObject, | ||
details | ||
) | ||
) { | ||
if (!details.message) { | ||
const {line, column} = writeExpr.loc.start; | ||
const { line, column } = writeExpr.loc.start; | ||
details.message = `Variable '${expression.name}' reassigned with unsafe value at ${line}:${column}`; | ||
@@ -234,3 +296,3 @@ } | ||
* | ||
* @param {Object} callee Function that is being called expression.tag | ||
* @param {object} callee Function that is being called expression.tag | ||
* or expression.callee | ||
@@ -253,4 +315,4 @@ * @param {Array} allowedSanitizers List of valid wrapping expressions | ||
* | ||
* @param {Object} node A callable expression to be simplified | ||
* @returns {Object} Method and (if applicable) object name | ||
* @param {object} node A callable expression to be simplified | ||
* @returns {object} Method and (if applicable) object name | ||
*/ | ||
@@ -261,26 +323,34 @@ normalizeMethodCall(node) { | ||
switch (node.type) { | ||
case "Identifier": | ||
methodName = node.name; | ||
break; | ||
case "MemberExpression": | ||
methodName = node.property.name; | ||
objectName = node.object.name || this.context.getSource(node.object); | ||
break; | ||
case "ConditionalExpression": | ||
case "CallExpression": | ||
case "ArrowFunctionExpression": | ||
methodName = ""; | ||
break; | ||
case "AssignmentExpression": | ||
methodName = this.normalizeMethodCall(node.right); | ||
break; | ||
case "Import": | ||
methodName = "import"; | ||
break; | ||
default: | ||
this.reportUnsupported(node, "Unexpected callable", `unexpected ${node.type} in normalizeMethodCall`); | ||
case "Identifier": | ||
methodName = node.name; | ||
break; | ||
case "MemberExpression": | ||
methodName = node.property.name; | ||
objectName = | ||
node.object.name || | ||
(this.context.sourceCode?.getText | ||
? this.context.sourceCode.getText(node.object) | ||
: this.context.getSource(node.object)); | ||
break; | ||
case "ConditionalExpression": | ||
case "CallExpression": | ||
case "ArrowFunctionExpression": | ||
methodName = ""; | ||
break; | ||
case "AssignmentExpression": | ||
methodName = this.normalizeMethodCall(node.right); | ||
break; | ||
case "Import": | ||
methodName = "import"; | ||
break; | ||
default: | ||
this.reportUnsupported( | ||
node, | ||
"Unexpected callable", | ||
`unexpected ${node.type} in normalizeMethodCall` | ||
); | ||
} | ||
return { | ||
objectName, | ||
methodName | ||
methodName, | ||
}; | ||
@@ -292,4 +362,4 @@ }, | ||
* | ||
* @param {Object} node A callable expression | ||
* @returns {String} A nice name to expression call | ||
* @param {object} node A callable expression | ||
* @returns {string} A nice name to expression call | ||
*/ | ||
@@ -311,6 +381,7 @@ getCodeName(node) { | ||
* Checks if there are objectMatches we need to apply | ||
* @param {Object} node Call expression node | ||
* @param {Object} objectMatches Strings that are checked as regex to | ||
* | ||
* @param {object} node Call expression node | ||
* @param {object} objectMatches Strings that are checked as regex to | ||
* match an object name | ||
* @returns {Boolean} Returns whether to run checks expression | ||
* @returns {boolean} Returns whether to run checks expression | ||
*/ | ||
@@ -322,4 +393,7 @@ shouldCheckMethodCall(node, objectMatches) { | ||
// Allow methods named "import": | ||
if (normalizedMethodCall.methodName === "import" | ||
&& node.callee && node.callee.type === "MemberExpression") { | ||
if ( | ||
normalizedMethodCall.methodName === "import" && | ||
node.callee && | ||
node.callee.type === "MemberExpression" | ||
) { | ||
return false; | ||
@@ -340,3 +414,6 @@ } | ||
// If we do have object filters and the call is a function then it should not be checked | ||
if ("objectName" in normalizedMethodCall && normalizedMethodCall.objectName) { | ||
if ( | ||
"objectName" in normalizedMethodCall && | ||
normalizedMethodCall.objectName | ||
) { | ||
for (const objectMatch of objectMatches) { | ||
@@ -358,4 +435,5 @@ const match = new RegExp(objectMatch, "gi"); | ||
* Algorithm used to decide on merging ruleChecks with this.context | ||
* @param {Object} defaultRuleChecks Object containing default rules | ||
* @returns {Object} The merged ruleChecks | ||
* | ||
* @param {object} defaultRuleChecks Object containing default rules | ||
* @returns {object} The merged ruleChecks | ||
*/ | ||
@@ -367,5 +445,11 @@ combineRuleChecks(defaultRuleChecks) { | ||
if (!("defaultDisable" in parentRuleChecks) | ||
|| !parentRuleChecks.defaultDisable) { | ||
childRuleChecks = Object.assign({}, defaultRuleChecks, childRuleChecks); | ||
if ( | ||
!("defaultDisable" in parentRuleChecks) || | ||
!parentRuleChecks.defaultDisable | ||
) { | ||
childRuleChecks = Object.assign( | ||
{}, | ||
defaultRuleChecks, | ||
childRuleChecks | ||
); | ||
} | ||
@@ -376,3 +460,4 @@ | ||
if ("variableTracing" in parentRuleChecks) { | ||
ruleCheckOutput["variableTracing"] = !!parentRuleChecks["variableTracing"]; | ||
ruleCheckOutput["variableTracing"] = | ||
!!parentRuleChecks["variableTracing"]; | ||
} | ||
@@ -384,12 +469,20 @@ | ||
const ruleCheck = Object.assign( | ||
"defaultDisable" in parentRuleChecks ? {} : | ||
{ | ||
escape: { | ||
taggedTemplates: ["Sanitizer.escapeHTML", "escapeHTML"], | ||
methods: ["Sanitizer.unwrapSafeHTML", "unwrapSafeHTML"] | ||
} | ||
}, | ||
"defaultDisable" in parentRuleChecks | ||
? {} | ||
: { | ||
escape: { | ||
taggedTemplates: [ | ||
"Sanitizer.escapeHTML", | ||
"escapeHTML", | ||
], | ||
methods: [ | ||
"Sanitizer.unwrapSafeHTML", | ||
"unwrapSafeHTML", | ||
], | ||
}, | ||
}, | ||
defaultRuleChecks[ruleCheckKey], | ||
parentRuleChecks, | ||
childRuleChecks[ruleCheckKey]); | ||
childRuleChecks[ruleCheckKey] | ||
); | ||
ruleCheckOutput[ruleCheckKey] = ruleCheck; | ||
@@ -403,3 +496,4 @@ }); | ||
* Runs the checks against a CallExpression | ||
* @param {Object} node Call expression node | ||
* | ||
* @param {object} node Call expression node | ||
* @returns {undefined} Does not return | ||
@@ -414,3 +508,6 @@ */ | ||
if (!Array.isArray(ruleCheck.properties)) { | ||
this.context.report(node, `Method check requires properties array in eslint rule ${methodName}`); | ||
this.context.report( | ||
node, | ||
`Method check requires properties array in eslint rule ${methodName}` | ||
); | ||
return; | ||
@@ -426,11 +523,19 @@ } | ||
const details = {}; | ||
if (this.shouldCheckMethodCall(node, ruleCheck.objectMatches) | ||
&& !this.allowedExpression(argument, ruleCheck.escape, details)) { | ||
if ( | ||
this.shouldCheckMethodCall(node, ruleCheck.objectMatches) && | ||
!this.allowedExpression(argument, ruleCheck.escape, details) | ||
) { | ||
// Include the additional details if available (e.g. name of a disallowed variable | ||
// and the position of the expression that made it disallowed). | ||
if (details.message) { | ||
this.context.report(node, `Unsafe call to ${this.getCodeName(node.callee)} for argument ${propertyId} (${details.message})`); | ||
this.context.report( | ||
node, | ||
`Unsafe call to ${this.getCodeName(node.callee)} for argument ${propertyId} (${details.message})` | ||
); | ||
return; | ||
} | ||
this.context.report(node, `Unsafe call to ${this.getCodeName(node.callee)} for argument ${propertyId}`); | ||
this.context.report( | ||
node, | ||
`Unsafe call to ${this.getCodeName(node.callee)} for argument ${propertyId}` | ||
); | ||
} | ||
@@ -443,17 +548,31 @@ }); | ||
* Runs the checks against an assignment expression | ||
* @param {Object} node Assignment expression node | ||
* | ||
* @param {object} node Assignment expression node | ||
* @returns {undefined} Does not return | ||
*/ | ||
checkProperty(node) { | ||
if (Object.prototype.hasOwnProperty.call(this.ruleChecks, node.left.property.name)) { | ||
if ( | ||
Object.prototype.hasOwnProperty.call( | ||
this.ruleChecks, | ||
node.left.property.name | ||
) | ||
) { | ||
const ruleCheck = this.ruleChecks[node.left.property.name]; | ||
const details = {}; | ||
if (!this.allowedExpression(node.right, ruleCheck.escape, details)) { | ||
if ( | ||
!this.allowedExpression(node.right, ruleCheck.escape, details) | ||
) { | ||
// Include the additional details if available (e.g. name of a disallowed variable | ||
// and the position of the expression that made it disallowed). | ||
if (details.message) { | ||
this.context.report(node, `Unsafe assignment to ${node.left.property.name} (${details.message})`); | ||
this.context.report( | ||
node, | ||
`Unsafe assignment to ${node.left.property.name} (${details.message})` | ||
); | ||
return; | ||
} | ||
this.context.report(node, `Unsafe assignment to ${node.left.property.name}`); | ||
this.context.report( | ||
node, | ||
`Unsafe assignment to ${node.left.property.name}` | ||
); | ||
} | ||
@@ -465,7 +584,9 @@ } | ||
const bugPath = `https://github.com/mozilla/eslint-plugin-no-unsanitized/issues/new?title=${encodeURIComponent(errorTitle)}`; | ||
this.context.report(node, `Error in no-unsanitized: ${reason}. Please report a minimal code snippet to the developers at ${bugPath}`); | ||
} | ||
this.context.report( | ||
node, | ||
`Error in no-unsanitized: ${reason}. Please report a minimal code snippet to the developers at ${bugPath}` | ||
); | ||
}, | ||
}; | ||
module.exports = RuleHelper; |
/** | ||
* @fileoverview ESLint rule to disallow unsanitized method calls | ||
* @file ESLint rule to disallow unsanitized method calls | ||
* @author Frederik Braun et al. | ||
@@ -14,14 +14,11 @@ * @copyright 2015-2017 Mozilla Corporation. All rights reserved. | ||
const defaultRuleChecks = { | ||
// check second parameter to .insertAdjacentHTML() | ||
insertAdjacentHTML: { | ||
properties: [1] | ||
properties: [1], | ||
}, | ||
// check first parameter of import() | ||
import: { | ||
properties: [0] | ||
properties: [0], | ||
}, | ||
@@ -31,3 +28,3 @@ | ||
createContextualFragment: { | ||
properties: [0] | ||
properties: [0], | ||
}, | ||
@@ -37,6 +34,4 @@ | ||
write: { | ||
objectMatches: [ | ||
"document" | ||
], | ||
properties: [0] | ||
objectMatches: ["document"], | ||
properties: [0], | ||
}, | ||
@@ -46,7 +41,10 @@ | ||
writeln: { | ||
objectMatches: [ | ||
"document" | ||
], | ||
properties: [0] | ||
} | ||
objectMatches: ["document"], | ||
properties: [0], | ||
}, | ||
// check first parameter to `setHTMLUnsafe()` | ||
setHTMLUnsafe: { | ||
properties: [0], | ||
}, | ||
}; | ||
@@ -56,8 +54,12 @@ | ||
* On newer parsers, `import(foo)` gets parsed as a keyword. | ||
* @param {Object} ruleHelper a RuleHelper instance | ||
* @param {Object} importExpr The ImportExpression we triggered on | ||
* | ||
* @param {object} ruleHelper a RuleHelper instance | ||
* @param {object} importExpr The ImportExpression we triggered on | ||
* @returns {undefined} Does not return | ||
*/ | ||
function checkImport(ruleHelper, importExpr) { | ||
const fakeCall = {callee: {type: "Import"}, arguments: [importExpr.source]}; | ||
const fakeCall = { | ||
callee: { type: "Import" }, | ||
arguments: [importExpr.source], | ||
}; | ||
Object.assign(fakeCall, importExpr); | ||
@@ -69,86 +71,91 @@ ruleHelper.checkMethod(fakeCall); | ||
* Run ruleHelper.checkMethod for all but irrelevant callees (FunctionExpression, etc.) | ||
* @param {Object} ruleHelper a RuleHelper instance | ||
* @param {Object} callExpr The CallExpression we triggered on | ||
* @param {Object} node The callee node | ||
* | ||
* @param {object} ruleHelper a RuleHelper instance | ||
* @param {object} callExpr The CallExpression we triggered on | ||
* @param {object} node The callee node | ||
* @returns {undefined} Does not return | ||
*/ | ||
function checkCallExpression(ruleHelper, callExpr, node) { | ||
switch(node.type) { | ||
case "Identifier": | ||
case "MemberExpression": | ||
if (callExpr.arguments && callExpr.arguments.length > 0) { | ||
ruleHelper.checkMethod(callExpr); | ||
switch (node.type) { | ||
case "Identifier": | ||
case "MemberExpression": | ||
if (callExpr.arguments && callExpr.arguments.length > 0) { | ||
ruleHelper.checkMethod(callExpr); | ||
} | ||
break; | ||
case "TSNonNullExpression": { | ||
const newCallExpr = Object.assign({}, callExpr); | ||
newCallExpr.callee = node.expression; | ||
checkCallExpression(ruleHelper, newCallExpr, node.expression); | ||
break; | ||
} | ||
break; | ||
case "TSNonNullExpression": { | ||
const newCallExpr = Object.assign({}, callExpr); | ||
newCallExpr.callee = node.expression; | ||
checkCallExpression(ruleHelper, newCallExpr, node.expression); | ||
break; | ||
} | ||
case "TaggedTemplateExpression": { | ||
const newCallExpr = Object.assign({}, callExpr); | ||
newCallExpr.callee = node.tag; | ||
const expressions = node.quasi.expressions; | ||
const strings = node.quasi.quasis; | ||
newCallExpr.arguments = [strings, ...expressions]; | ||
checkCallExpression(ruleHelper, newCallExpr, node.tag); | ||
break; | ||
} | ||
case "TaggedTemplateExpression": { | ||
const newCallExpr = Object.assign({}, callExpr); | ||
newCallExpr.callee = node.tag; | ||
const expressions = node.quasi.expressions; | ||
const strings = node.quasi.quasis; | ||
newCallExpr.arguments = [strings, ...expressions]; | ||
checkCallExpression(ruleHelper, newCallExpr, node.tag); | ||
break; | ||
} | ||
case "TypeCastExpression": { | ||
const newCallExpr = Object.assign({}, callExpr); | ||
newCallExpr.callee = node.expression; | ||
checkCallExpression(ruleHelper, newCallExpr, node.expression); | ||
break; | ||
} | ||
case "AssignmentExpression": | ||
if (node.right.type === "MemberExpression") { | ||
case "TypeCastExpression": { | ||
const newCallExpr = Object.assign({}, callExpr); | ||
newCallExpr.callee = node.right; | ||
checkCallExpression(ruleHelper, newCallExpr, node.right); | ||
newCallExpr.callee = node.expression; | ||
checkCallExpression(ruleHelper, newCallExpr, node.expression); | ||
break; | ||
} | ||
checkCallExpression(ruleHelper, callExpr, node.right); | ||
break; | ||
case "Import": | ||
ruleHelper.checkMethod(callExpr); | ||
break; | ||
case "AssignmentExpression": | ||
if (node.right.type === "MemberExpression") { | ||
const newCallExpr = Object.assign({}, callExpr); | ||
newCallExpr.callee = node.right; | ||
checkCallExpression(ruleHelper, newCallExpr, node.right); | ||
break; | ||
} | ||
checkCallExpression(ruleHelper, callExpr, node.right); | ||
break; | ||
case "SequenceExpression": { | ||
// the return value of a SequenceExpression is the last expression. | ||
// So, we create a new mock CallExpression with the actually called | ||
// ... expression as the callee node and pass it to checkMethod() | ||
case "Import": | ||
ruleHelper.checkMethod(callExpr); | ||
break; | ||
const newCallExpr = Object.assign({}, callExpr); | ||
const idx = node.expressions.length - 1; | ||
const called = node.expressions[idx]; | ||
newCallExpr.callee = called; | ||
ruleHelper.checkMethod(newCallExpr); | ||
break; | ||
} | ||
case "SequenceExpression": { | ||
// the return value of a SequenceExpression is the last expression. | ||
// So, we create a new mock CallExpression with the actually called | ||
// ... expression as the callee node and pass it to checkMethod() | ||
case "TSAsExpression": | ||
break; | ||
const newCallExpr = Object.assign({}, callExpr); | ||
const idx = node.expressions.length - 1; | ||
const called = node.expressions[idx]; | ||
newCallExpr.callee = called; | ||
ruleHelper.checkMethod(newCallExpr); | ||
break; | ||
} | ||
// those are fine: | ||
case "LogicalExpression": // Should we scan these? issue #62. | ||
case "ConditionalExpression": | ||
case "ArrowFunctionExpression": | ||
case "FunctionExpression": | ||
case "Super": | ||
case "CallExpression": | ||
case "ThisExpression": | ||
case "NewExpression": | ||
case "TSTypeAssertion": | ||
case "AwaitExpression": // see issue #122 | ||
break; | ||
case "TSAsExpression": | ||
break; | ||
// If we don't cater for this expression throw an error | ||
default: | ||
ruleHelper.reportUnsupported(node, "Unexpected Callee", `Unsupported Callee of type ${node.type} for CallExpression`); | ||
// those are fine: | ||
case "LogicalExpression": // Should we scan these? issue #62. | ||
case "ConditionalExpression": | ||
case "ArrowFunctionExpression": | ||
case "FunctionExpression": | ||
case "Super": | ||
case "CallExpression": | ||
case "ThisExpression": | ||
case "NewExpression": | ||
case "TSTypeAssertion": | ||
case "AwaitExpression": // see issue #122 | ||
break; | ||
// If we don't cater for this expression throw an error | ||
default: | ||
ruleHelper.reportUnsupported( | ||
node, | ||
"Unexpected Callee", | ||
`Unsupported Callee of type ${node.type} for CallExpression` | ||
); | ||
} | ||
@@ -163,10 +170,38 @@ } | ||
category: "possible-errors", | ||
url: "https://github.com/mozilla/eslint-plugin-no-unsanitized/tree/master/docs/rules/method.md" | ||
url: "https://github.com/mozilla/eslint-plugin-no-unsanitized/tree/master/docs/rules/method.md", | ||
}, | ||
/* schema statement TBD until we have options | ||
schema: [ | ||
{ | ||
type: array | ||
} | ||
]*/ | ||
type: "object", | ||
properties: { | ||
defaultDisable: { | ||
type: "boolean", | ||
}, | ||
escape: { | ||
type: "object", | ||
properties: { | ||
taggedTemplates: { | ||
type: "array", | ||
items: [{ type: "string" }], | ||
}, | ||
methods: { | ||
type: "array", | ||
items: [{ type: "string" }], | ||
}, | ||
}, | ||
}, | ||
objectMatches: { | ||
type: "array", | ||
}, | ||
properties: { | ||
type: "array", | ||
}, | ||
variableTracing: { type: "boolean" }, | ||
}, | ||
additionalProperties: false, | ||
}, | ||
{ | ||
type: "object", | ||
}, | ||
], | ||
}, | ||
@@ -195,3 +230,3 @@ create(context) { | ||
}; | ||
} | ||
}, | ||
}; |
/** | ||
* @fileoverview ESLint rule to disallow unsanitized property assignment | ||
* @file ESLint rule to disallow unsanitized property assignment | ||
* @author Frederik Braun et al. | ||
@@ -14,12 +14,8 @@ * @copyright 2015-2017 Mozilla Corporation. All rights reserved. | ||
const defaultRuleChecks = { | ||
// Check unsafe assignment to innerHTML | ||
innerHTML: { | ||
}, | ||
innerHTML: {}, | ||
// Check unsafe assignment to outerHTML | ||
outerHTML: { | ||
} | ||
outerHTML: {}, | ||
}; | ||
@@ -31,12 +27,32 @@ | ||
docs: { | ||
description: "ESLint rule to disallow unsanitized property assignment", | ||
description: | ||
"ESLint rule to disallow unsanitized property assignment", | ||
category: "possible-errors", | ||
url: "https://github.com/mozilla/eslint-plugin-no-unsanitized/tree/master/docs/rules/property.md" | ||
url: "https://github.com/mozilla/eslint-plugin-no-unsanitized/tree/master/docs/rules/property.md", | ||
}, | ||
/* schema statement TBD until we have options | ||
schema: [ | ||
{ | ||
type: array | ||
} | ||
]*/ | ||
type: "object", | ||
properties: { | ||
escape: { | ||
type: "object", | ||
properties: { | ||
taggedTemplates: { | ||
type: "array", | ||
items: [{ type: "string" }], | ||
}, | ||
methods: { | ||
type: "array", | ||
items: [{ type: "string" }], | ||
}, | ||
}, | ||
}, | ||
variableTracing: { type: "boolean" }, | ||
}, | ||
additionalProperties: false, | ||
}, | ||
{ | ||
type: "object", | ||
}, | ||
], | ||
}, | ||
@@ -50,3 +66,15 @@ create(context) { | ||
// - https://github.com/estree/estree/blob/master/es2016.md#assignmentoperator | ||
const PERMITTED_OPERATORS = ["-=", "*=", "/=", "%=", "<<=", ">>=", ">>>=", "|=", "^=", "&=", "**="]; | ||
const PERMITTED_OPERATORS = [ | ||
"-=", | ||
"*=", | ||
"/=", | ||
"%=", | ||
"<<=", | ||
">>=", | ||
">>>=", | ||
"|=", | ||
"^=", | ||
"&=", | ||
"**=", | ||
]; | ||
@@ -63,4 +91,11 @@ // operators to match against, such as X.innerHTML += foo | ||
if (PERMITTED_OPERATORS.indexOf(node.operator) === -1) { | ||
if (CHECK_REQUIRED_OPERATORS.indexOf(node.operator) === -1) { | ||
ruleHelper.reportUnsupported(node, "Unexpected Assignment", `Unsupported Operator ${encodeURIComponent(node.operator)} for AssignmentExpression`); | ||
if ( | ||
CHECK_REQUIRED_OPERATORS.indexOf(node.operator) === | ||
-1 | ||
) { | ||
ruleHelper.reportUnsupported( | ||
node, | ||
"Unexpected Assignment", | ||
`Unsupported Operator ${encodeURIComponent(node.operator)} for AssignmentExpression` | ||
); | ||
} | ||
@@ -70,5 +105,5 @@ ruleHelper.checkProperty(node); | ||
} | ||
} | ||
}, | ||
}; | ||
} | ||
}, | ||
}; |
{ | ||
"name": "eslint-plugin-no-unsanitized", | ||
"description": "ESLint rule to disallow unsanitized code", | ||
"version": "4.0.2", | ||
"version": "4.1.0", | ||
"author": { | ||
@@ -14,13 +14,15 @@ "name": "Frederik Braun et al." | ||
"@babel/eslint-parser": "^7.15.8", | ||
"@babel/plugin-proposal-logical-assignment-operators": "^7.11.0", | ||
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.10.4", | ||
"@babel/plugin-syntax-flow": "^7.16.0", | ||
"@typescript-eslint/parser": "^5.2.0", | ||
"eslint": "^8.1.0", | ||
"@typescript-eslint/parser": "^8.0.0", | ||
"eslint": "9.0.0 || ^8.57.0", | ||
"eslint-config-prettier": "9.1.0", | ||
"eslint-plugin-jsdoc": "48.2.3", | ||
"globals": "^15.0.0", | ||
"mocha": "^9.2.0", | ||
"nyc": "^15.1.0", | ||
"typescript": "^3.9.7" | ||
"prettier": "3.2.5", | ||
"typescript": ">=4.7.4" | ||
}, | ||
"peerDependencies": { | ||
"eslint": "^6 || ^7 || ^8" | ||
"eslint": "^8 || ^9" | ||
}, | ||
@@ -45,3 +47,3 @@ "homepage": "https://github.com/mozilla/eslint-plugin-no-unsanitized/", | ||
"test": "nyc mocha tests/rules/", | ||
"lint": "eslint ." | ||
"lint": "eslint . && prettier --check ." | ||
}, | ||
@@ -56,3 +58,4 @@ "files": [ | ||
"lib" | ||
] | ||
], | ||
"dependencies": {} | ||
} |
[![Build Status](https://travis-ci.org/mozilla/eslint-plugin-no-unsanitized.svg?branch=master)](https://travis-ci.org/mozilla/eslint-plugin-no-unsanitized) | ||
# Disallow unsanitized code (no-unsanitized) | ||
@@ -19,3 +20,4 @@ | ||
## method | ||
The *method* rule disallows certain function calls. | ||
The _method_ rule disallows certain function calls. | ||
E.g., `document.write()` or `insertAdjacentHTML()`. | ||
@@ -25,7 +27,7 @@ See [docs/rules/method.md](docs/rules/method.md) for more. | ||
## property | ||
The *property* rule disallows certain assignment expressions, e.g., to `innerHTML`. | ||
The _property_ rule disallows certain assignment expressions, e.g., to `innerHTML`. | ||
See [docs/rules/property.md](docs/rules/property.md) for more. | ||
## Examples | ||
@@ -37,3 +39,3 @@ | ||
foo.innerHTML = input.value; | ||
bar.innerHTML = "<a href='"+url+"'>About</a>"; | ||
bar.innerHTML = "<a href='" + url + "'>About</a>"; | ||
``` | ||
@@ -49,9 +51,7 @@ | ||
# Install | ||
With **yarn** or **npm**: | ||
``` | ||
```bash | ||
$ yarn add -D eslint-plugin-no-unsanitized | ||
@@ -63,7 +63,34 @@ $ npm install --save-dev eslint-plugin-no-unsanitized | ||
### Flat config | ||
```js | ||
import nounsanitized from "eslint-plugin-no-unsanitized"; | ||
export default config = [nounsanitized.configs.recommended]; | ||
``` | ||
or | ||
```js | ||
import nounsanitized from "eslint-plugin-no-unsanitized"; | ||
export default config = [ | ||
{ | ||
files: ["**/*.js"], | ||
plugins: { nounsanitized }, | ||
rules: { | ||
"no-unsanitized/method": "error", | ||
"no-unsanitized/property": "error", | ||
}, | ||
}, | ||
]; | ||
``` | ||
### eslintrc | ||
In your `.eslintrc.json` file enable this rule with the following: | ||
``` | ||
```json | ||
{ | ||
"extends": ["plugin:no-unsanitized/DOM"] | ||
"extends": ["plugin:no-unsanitized/recommended-legacy"] | ||
} | ||
@@ -73,3 +100,4 @@ ``` | ||
Or: | ||
``` | ||
```json | ||
{ | ||
@@ -85,2 +113,3 @@ "plugins": ["no-unsanitized"], | ||
# Documentation | ||
See [docs/](docs/). |
@@ -9,3 +9,5 @@ # Security Policy | ||
## Our Threat Model | ||
### What is considered a vulnerability? | ||
We assume that a project which makes use of this plugin to apply | ||
@@ -15,3 +17,3 @@ a reasonable amount of scrutiny to all patches that are accepted. | ||
judgement and sensitivity to security issues will vary between | ||
projects and reviewers. | ||
projects and reviewers. | ||
@@ -25,2 +27,3 @@ JavaScript static analysis is always very limited. On top we are | ||
Here's a a couple of examples which we do **not** consider a bug in the linter: | ||
```js | ||
@@ -34,4 +37,4 @@ foo['inner'+'HTML'] = evil; // no way to resolve concatenation | ||
## Reporting a Vulnerability | ||
## Reporting a Vulnerability | ||
### Bug Bounty / Vulnerability Rewards | ||
@@ -50,10 +53,11 @@ | ||
### Sneaking your *obfuscated* code past this linter | ||
### Sneaking your _obfuscated_ code past this linter | ||
Hey, did you read "What is considered a vulnerability", above? | ||
### Sneaking your innocent looking code past the linter | ||
If what you found constitues in a code pattern that eslint should complain about, but doesn't, | ||
[please file a private bug Bugzilla using this form][bugzilla-enter-private-issue] (requires creating an account). | ||
[bugzilla-enter-private-issue]: https://bugzilla.mozilla.org/enter_bug.cgi?assigned_to=nobody%40mozilla.org&bug_ignored=0&bug_severity=--&bug_status=UNCONFIRMED&bug_type=defect&cf_fission_milestone=---&cf_fx_iteration=---&cf_fx_points=---&cf_root_cause=---&cf_status_firefox77=---&cf_status_firefox78=---&cf_status_firefox79=---&cf_status_firefox_esr68=---&cf_status_firefox_esr78=---&cf_status_thunderbird_esr60=---&cf_status_thunderbird_esr68=---&cf_tracking_firefox77=---&cf_tracking_firefox78=---&cf_tracking_firefox79=---&cf_tracking_firefox_esr68=---&cf_tracking_firefox_esr78=---&cf_tracking_firefox_relnote=---&cf_tracking_thunderbird_esr60=---&cf_tracking_thunderbird_esr68=---&component=Lint%20and%20Formatting&contenttypemethod=list&contenttypeselection=text%2Fplain&defined_groups=1&filed_via=standard_form&flag_type-203=X&flag_type-37=X&flag_type-4=X&flag_type-41=X&flag_type-607=X&flag_type-721=X&flag_type-737=X&flag_type-787=X&flag_type-799=X&flag_type-800=X&flag_type-803=X&flag_type-846=X&flag_type-855=X&flag_type-864=X&flag_type-913=X&flag_type-930=X&flag_type-936=X&flag_type-941=X&flag_type-945=X&form_name=enter_bug&groups=firefox-core-security&maketemplate=Remember%20values%20as%20bookmarkable%20template&op_sys=Unspecified&priority=--&product=Firefox%20Build%20System&rep_platform=Unspecified&status_whiteboard=%5Beslint-plugin-no-unsanitized%5D&target_milestone=---&version=unspecified |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
65505
858
109
12
13
1