Repository: ember-cli/eslint-plugin-ember (js)

Issue:
New rule: Prevent usage of `htmlSafe`
Any thoughts on adding a rule to prevent usage of `htmlSafe`? `htmlSafe`, when not used properly, can be a vector for XSS and in some codebases it's preferred to prevent it.

ember-template-lint has a [similar rule](https://github.com/ember-template-lint/ember-template-lint/blob/master/docs/rule/no-triple-curlies.md) which prevents usage of triple curlies: `{{{test}}}`. 

I was thinking we could just test for usages of the following? 

```js
import { htmlSafe } from '@ember/string';
```

I'd be happy to put it together if there's interest?

 

Pull request:
Add new rule `no-html-safe`
Closes #1041 

Introduces a `no-html-safe` rule to prevent the usage of the [`htmlSafe` utility](https://api.emberjs.com/ember/release/functions/@ember%2Ftemplate/htmlSafe). 

`htmlSafe` marks a string as safe for unescaped output with Ember templates so you can render it as HTML. While useful this can inadvertently open you up to Cross-site Scripting (XSS) vulnerabilities, especially if the string was generated from user input or some other untrusted source.

Patch:
diff --git a/README.md b/README.md
index 3691607f..ff4b642d 100644
--- a/README.md
+++ b/README.md
@@ -152,6 +152,7 @@ Rules are grouped by category to help you understand their purpose. Each rule ha
 |    | Rule ID | Description |
 |:---|:--------|:------------|
 |  | [named-functions-in-promises](./docs/rules/named-functions-in-promises.md) | enforce usage of named functions in promises |
+|  | [no-html-safe](./docs/rules/no-html-safe.md) | disallow the use of htmlSafe |
 | :white_check_mark: | [no-incorrect-calls-with-inline-anonymous-functions](./docs/rules/no-incorrect-calls-with-inline-anonymous-functions.md) | disallow inline anonymous functions as arguments to `debounce`, `once`, and `scheduleOnce` |
 | :white_check_mark: | [no-invalid-debug-function-arguments](./docs/rules/no-invalid-debug-function-arguments.md) | disallow usages of Ember's `assert()` / `warn()` / `deprecate()` functions that have the arguments passed in the wrong order. |
 |  | [no-restricted-service-injections](./docs/rules/no-restricted-service-injections.md) | disallow injecting certain services under certain paths |
diff --git a/docs/rules/no-html-safe.md b/docs/rules/no-html-safe.md
new file mode 100644
index 00000000..8396b346
--- /dev/null
+++ b/docs/rules/no-html-safe.md
@@ -0,0 +1,113 @@
+# no-html-safe
+
+`htmlSafe` marks a string as safe for unescaped output with Ember templates so you can render it as HTML. `htmlSafe` does **not** perform input sanitization. While useful this can inadvertently open you up to Cross-site Scripting (XSS) vulnerabilities, especially if the string was generated from user input or some other untrusted source. **You should only ever use `htmlSafe` with trusted or sanitized input**.
+
+## Rule Details
+
+This rule prevents importing the `htmlSafe` utillity from `@ember/template` (or `@ember/string` for older Ember versions);
+
+## Examples
+
+Examples of **incorrect** code for this rule:
+
+```js
+import { htmlSafe } from '@ember/template';
+```
+
+```js
+import { htmlSafe } from '@ember/string';
+```
+
+## Alternatives
+
+There are a few alternative strategies to using `htmlSafe`.
+
+### Scenario
+
+In this example we have a property `userContent` which contains a user-generated string of HTML. For example, something like `<h1>Joe Bloggs</h1>`. This is then rendered directly into the template.
+
+``` js
+// components/my-component.js
+import { htmlSafe } from '@ember/template';
+
+class MyComponent extends Component {
+  get myContent() {
+    return htmlSafe(this.args.userContent);
+  }
+}
+```
+
+```hbs
+{{!-- components/my-component.hbs --}}
+{{this.myContent}}
+```
+
+### Alternative 1: Render the HTML yourself
+
+While not as flexible, if you can control the content generated by the user you should only let the user enter plaintext and render the HTML yourself.
+
+```hbs
+{{!-- components/my-component.hbs --}}
+<h1>{{@userContent}}</h1>
+```
+
+### Alternative 2: Define your own trusted sanitizing helper
+
+If you _have_ to render user generated HTML, you should protect yourself by always sanitizing the input first. One strategy is to, rather than have usage of `htmlSafe` proliferated throughout your app, isolate it to a single location and combine it with a sanitization library, e.g. [DOMPurify](https://github.com/cure53/DOMPurify). Then require all HTML strings go through this helper. If this helper is in the same codebase where the `no-html-safe` lint rule is enabled, you can disable the rule for this single location.
+
+``` js
+// app/lib/sanitized-content.js
+import { htmlSafe } from '@ember/template';
+
+export function sanitizedContent(content) {
+  const sanitized = someSanitizationLibrary(content);
+  return htmlSafe(sanitized);
+}
+```
+
+```js
+// app/components/my-component.js
+import { sanitizedContent } from 'my-app/lib/sanitized-content';
+
+class MyComponent extends Component {
+  get myContent() {
+    return sanitizedContent(this.args.userContent);
+  }
+}
+```
+
+```hbs
+{{!-- components/my-component.hbs --}}
+{{this.myContent}}
+```
+
+### Alternative 3: Define your own trusted sanitizing Handlebars helper
+
+This is similar to the previous example but in this case you could create a Handlebars helper which sanitizes your input.
+
+``` js
+// app/helpers/sanitize.js
+import { htmlSafe } from '@ember/template';
+import { helper } from '@ember/component/helper';
+
+function sanitize([content]) {
+  const sanitized = someSanitizationLibrary(content);
+  return htmlSafe(sanitized);
+}
+
+export default helper(substring);
+```
+
+```hbs
+{{!-- components/my-component.hbs --}}
+{{sanitize @userContent}}
+```
+
+## Related Rules
+
+* ember-template-lint has a [no-triple-curlies](https://github.com/ember-template-lint/ember-template-lint/blob/master/docs/rule/no-triple-curlies.md) rule for the template equivalent of this rule.
+
+## References
+
+* [Ember's `htmlSafe` API documentation](https://api.emberjs.com/ember/release/functions/@ember%2Ftemplate/htmlSafe)
+* [MDN's Cross-site Scripting (XSS) documentation](https://developer.mozilla.org/en-US/docs/Glossary/Cross-site_scripting)
diff --git a/lib/index.js b/lib/index.js
index fe586c4f..1d97bc31 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -35,6 +35,7 @@ module.exports = {
     'no-get-with-default': require('./rules/no-get-with-default'),
     'no-get': require('./rules/no-get'),
     'no-global-jquery': require('./rules/no-global-jquery'),
+    'no-html-safe': require('./rules/no-html-safe'),
     'no-incorrect-calls-with-inline-anonymous-functions': require('./rules/no-incorrect-calls-with-inline-anonymous-functions'),
     'no-incorrect-computed-macros': require('./rules/no-incorrect-computed-macros'),
     'no-invalid-debug-function-arguments': require('./rules/no-invalid-debug-function-arguments'),
diff --git a/lib/rules/no-html-safe.js b/lib/rules/no-html-safe.js
new file mode 100644
index 00000000..1ce0b095
--- /dev/null
+++ b/lib/rules/no-html-safe.js
@@ -0,0 +1,39 @@
+'use strict';
+
+const ERROR_MESSAGE = 'Do not use `htmlSafe`.';
+
+//------------------------------------------------------------------------------
+// Rule Definition
+//------------------------------------------------------------------------------
+
+module.exports = {
+  meta: {
+    type: 'suggestion',
+    docs: {
+      description: 'disallow the use of htmlSafe',
+      category: 'Miscellaneous',
+      recommended: false,
+      url:
+        'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/no-html-safe.md',
+    },
+    fixable: null,
+    schema: [],
+  },
+
+  ERROR_MESSAGE,
+
+  create(context) {
+    return {
+      ImportDeclaration(node) {
+        if (node.source.value === '@ember/string' || node.source.value === '@ember/template') {
+          const htmlSafeImport = node.specifiers.find(
+            (s) => (s.imported || s.local).name === 'htmlSafe'
+          );
+          if (htmlSafeImport) {
+            context.report(htmlSafeImport, ERROR_MESSAGE);
+          }
+        }
+      },
+    };
+  },
+};


Test patch:
diff --git a/tests/lib/rules/no-html-safe.js b/tests/lib/rules/no-html-safe.js
new file mode 100644
index 00000000..2fee50e0
--- /dev/null
+++ b/tests/lib/rules/no-html-safe.js
@@ -0,0 +1,45 @@
+//------------------------------------------------------------------------------
+// Requirements
+//------------------------------------------------------------------------------
+
+const rule = require('../../../lib/rules/no-html-safe');
+const RuleTester = require('eslint').RuleTester;
+
+const { ERROR_MESSAGE } = rule;
+
+//------------------------------------------------------------------------------
+// Tests
+//------------------------------------------------------------------------------
+
+const ruleTester = new RuleTester({
+  parserOptions: {
+    ecmaVersion: 6,
+    sourceType: 'module',
+  },
+});
+
+ruleTester.run('no-html-safe', rule, {
+  valid: [
+    "import { ANYTHING } from '@ember/string';",
+    "import { ANYTHING } from '@ember/template';",
+    "import { htmlSafe } from 'something/else';",
+    "import EmberString from '@ember/string';",
+  ],
+  invalid: [
+    {
+      code: "import { htmlSafe } from '@ember/string';",
+      output: null,
+      errors: [{ message: ERROR_MESSAGE, type: 'ImportSpecifier' }],
+    },
+    {
+      code: "import { htmlSafe } from '@ember/template';",
+      output: null,
+      errors: [{ message: ERROR_MESSAGE, type: 'ImportSpecifier' }],
+    },
+    {
+      code: "import { htmlSafe as myCustomNameForHtmlSafe } from '@ember/template';",
+      output: null,
+      errors: [{ message: ERROR_MESSAGE, type: 'ImportSpecifier' }],
+    },
+  ],
+});