Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[New] add iframe-missing-sandbox rule (migrate rule from TSLint) #2753

Merged
merged 1 commit into from Feb 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Expand Up @@ -9,6 +9,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
* add [`hook-use-state`] rule to enforce symmetric useState hook variable names ([#2921][] @duncanbeevers)
* [`jsx-no-target-blank`]: Improve fixer with option `allowReferrer` ([#3167][] @apepper)
* [`jsx-curly-brace-presence`]: add "propElementValues" config option ([#3191][] @ljharb)
* add [`iframe-missing-sandbox`] rule ([#2753][] @tosmolka @ljharb)

### Fixed
* [`prop-types`], `propTypes`: add support for exported type inference ([#3163][] @vedadeepta)
Expand Down Expand Up @@ -37,6 +38,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
[#3160]: https://github.com/yannickcr/eslint-plugin-react/pull/3160
[#3133]: https://github.com/yannickcr/eslint-plugin-react/pull/3133
[#2921]: https://github.com/yannickcr/eslint-plugin-react/pull/2921
[#2753]: https://github.com/yannickcr/eslint-plugin-react/pull/2753

## [7.28.0] - 2021.12.22

Expand Down Expand Up @@ -3532,6 +3534,7 @@ If you're still not using React 15 you can keep the old behavior by setting the
[`forbid-prop-types`]: docs/rules/forbid-prop-types.md
[`function-component-definition`]: docs/rules/function-component-definition.md
[`hook-use-state`]: docs/rules/hook-use-state.md
[`iframe-missing-sandbox`]: docs/rules/iframe-missing-sandbox.md
[`jsx-boolean-value`]: docs/rules/jsx-boolean-value.md
[`jsx-child-element-spacing`]: docs/rules/jsx-child-element-spacing.md
[`jsx-closing-bracket-location`]: docs/rules/jsx-closing-bracket-location.md
Expand Down
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -132,6 +132,7 @@ Enable the rules that you would like to use.
| | | [react/forbid-prop-types](docs/rules/forbid-prop-types.md) | Forbid certain propTypes |
| | 🔧 | [react/function-component-definition](docs/rules/function-component-definition.md) | Standardize the way function component get defined |
| | | [react/hook-use-state](docs/rules/hook-use-state.md) | Ensure symmetric naming of useState hook value and setter variables |
| | | [react/iframe-missing-sandbox](docs/rules/iframe-missing-sandbox.md) | Enforce sandbox attribute on iframe elements |
| | | [react/no-access-state-in-setstate](docs/rules/no-access-state-in-setstate.md) | Reports when this.state is accessed within setState |
| | | [react/no-adjacent-inline-elements](docs/rules/no-adjacent-inline-elements.md) | Prevent adjacent inline elements not separated by whitespace. |
| | | [react/no-array-index-key](docs/rules/no-array-index-key.md) | Prevent usage of Array index in keys |
Expand Down
40 changes: 40 additions & 0 deletions docs/rules/iframe-missing-sandbox.md
@@ -0,0 +1,40 @@
# Enforce sandbox attribute on iframe elements (react/iframe-missing-sandbox)

The sandbox attribute enables an extra set of restrictions for the content in the iframe. Using sandbox attribute is considered a good security practice.

See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#attr-sandbox

## Rule Details

This rule checks all React iframe elements and verifies that there is sandbox attribute and that it's value is valid. In addition to that it also reports cases where attribute contains `allow-scripts` and `allow-same-origin` at the same time as this combination allows the embedded document to remove the sandbox attribute and bypass the restrictions.

The following patterns are considered warnings:

```jsx
var React = require('react');

var Frame = () => (
<div>
<iframe></iframe>
{React.createElement('iframe')}
</div>
);
```

The following patterns are **not** considered warnings:

```jsx
var React = require('react');

var Frame = <iframe sandbox="allow-popups"/>;
var Frame = () => (
<div>
<iframe sandbox="allow-popups"></iframe>
{React.createElement('iframe', { sandbox: "allow-popups" })}
</div>
);
```

## When not to use

If you don't want to enforce sandbox attribute on iframe elements.
1 change: 1 addition & 0 deletions index.js
Expand Up @@ -17,6 +17,7 @@ const allRules = {
'forbid-prop-types': require('./lib/rules/forbid-prop-types'),
'function-component-definition': require('./lib/rules/function-component-definition'),
'hook-use-state': require('./lib/rules/hook-use-state'),
'iframe-missing-sandbox': require('./lib/rules/iframe-missing-sandbox'),
'jsx-boolean-value': require('./lib/rules/jsx-boolean-value'),
'jsx-child-element-spacing': require('./lib/rules/jsx-child-element-spacing'),
'jsx-closing-bracket-location': require('./lib/rules/jsx-closing-bracket-location'),
Expand Down
142 changes: 142 additions & 0 deletions lib/rules/iframe-missing-sandbox.js
@@ -0,0 +1,142 @@
/**
* @fileoverview TBD
*/

'use strict';

const docsUrl = require('../util/docsUrl');
const isCreateElement = require('../util/isCreateElement');
const report = require('../util/report');

const messages = {
attributeMissing: 'An iframe element is missing a sandbox attribute',
invalidValue: 'An iframe element defines a sandbox attribute with invalid value "{{ value }}"',
invalidCombination: 'An iframe element defines a sandbox attribute with both allow-scripts and allow-same-origin which is invalid',
};

const ALLOWED_VALUES = [
// From https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#attr-sandbox
'',
'allow-downloads-without-user-activation',
'allow-downloads',
'allow-forms',
'allow-modals',
'allow-orientation-lock',
'allow-pointer-lock',
'allow-popups',
'allow-popups-to-escape-sandbox',
'allow-presentation',
'allow-same-origin',
'allow-scripts',
'allow-storage-access-by-user-activation',
'allow-top-navigation',
'allow-top-navigation-by-user-activation',
];

function validateSandboxAttribute(context, node, attribute) {
if (typeof attribute !== 'string') {
// Only string literals are supported for now
return;
}
const values = attribute.split(' ');
let allowScripts = false;
let allowSameOrigin = false;
values.forEach((attributeValue) => {
const trimmedAttributeValue = attributeValue.trim();
if (ALLOWED_VALUES.indexOf(trimmedAttributeValue) === -1) {
report(context, messages.invalidValue, 'invalidValue', {
node,
data: {
value: trimmedAttributeValue,
},
});
}
if (trimmedAttributeValue === 'allow-scripts') {
allowScripts = true;
}
if (trimmedAttributeValue === 'allow-same-origin') {
allowSameOrigin = true;
}
});
if (allowScripts && allowSameOrigin) {
report(context, messages.invalidCombination, 'invalidCombination', {
node,
});
}
}

function checkAttributes(context, node) {
let sandboxAttributeFound = false;
node.attributes.forEach((attribute) => {
if (attribute.type === 'JSXAttribute'
&& attribute.name
&& attribute.name.type === 'JSXIdentifier'
&& attribute.name.name === 'sandbox'
) {
sandboxAttributeFound = true;
if (
attribute.value
&& attribute.value.type === 'Literal'
&& attribute.value.value
) {
validateSandboxAttribute(context, node, attribute.value.value);
}
}
});
if (!sandboxAttributeFound) {
report(context, messages.attributeMissing, 'attributeMissing', {
node,
});
}
}

function checkProps(context, node) {
let sandboxAttributeFound = false;
if (node.arguments.length > 1) {
const props = node.arguments[1];
const sandboxProp = props.properties && props.properties.find((x) => x.type === 'Property' && x.key.name === 'sandbox');
if (sandboxProp) {
sandboxAttributeFound = true;
if (sandboxProp.value && sandboxProp.value.type === 'Literal' && sandboxProp.value.value) {
validateSandboxAttribute(context, node, sandboxProp.value.value);
}
}
}
if (!sandboxAttributeFound) {
report(context, messages.attributeMissing, 'attributeMissing', {
node,
});
}
}

module.exports = {
meta: {
docs: {
description: 'Enforce sandbox attribute on iframe elements',
category: 'Best Practices',
recommended: false,
url: docsUrl('iframe-missing-sandbox'),
},

schema: [],

messages,
},

create(context) {
return {
'JSXOpeningElement[name.name="iframe"]'(node) {
checkAttributes(context, node);
},

CallExpression(node) {
if (isCreateElement(node, context) && node.arguments && node.arguments.length > 0) {
const tag = node.arguments[0];
if (tag.type === 'Literal' && tag.value === 'iframe') {
checkProps(context, node);
}
}
},
};
},
};
124 changes: 124 additions & 0 deletions tests/lib/rules/iframe-missing-sandbox.js
@@ -0,0 +1,124 @@
/**
* @fileoverview TBD
*/

'use strict';

// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------

const RuleTester = require('eslint').RuleTester;
const rule = require('../../../lib/rules/iframe-missing-sandbox');

const parsers = require('../../helpers/parsers');

const parserOptions = {
ecmaVersion: 2018,
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
};

// ------------------------------------------------------------------------------
// Tests
// ------------------------------------------------------------------------------

const ruleTester = new RuleTester({ parserOptions });
ruleTester.run('iframe-missing-sandbox', rule, {
valid: parsers.all([
{ code: '<div sandbox="__unknown__" />;' },

{ code: '<iframe sandbox="" />;' },
{ code: '<iframe sandbox={""} />' },
{ code: 'React.createElement("iframe", { sandbox: "" });' },

{ code: '<iframe src="foo.htm" sandbox></iframe>' },
{ code: 'React.createElement("iframe", { src: "foo.htm", sandbox: true })' },

{ code: '<iframe src="foo.htm" sandbox sandbox></iframe>' },

{ code: '<iframe sandbox="allow-forms"></iframe>' },
{ code: '<iframe sandbox="allow-modals"></iframe>' },
{ code: '<iframe sandbox="allow-orientation-lock"></iframe>' },
{ code: '<iframe sandbox="allow-pointer-lock"></iframe>' },
{ code: '<iframe sandbox="allow-popups"></iframe>' },
{ code: '<iframe sandbox="allow-popups-to-escape-sandbox"></iframe>' },
{ code: '<iframe sandbox="allow-presentation"></iframe>' },
{ code: '<iframe sandbox="allow-same-origin"></iframe>' },
{ code: '<iframe sandbox="allow-scripts"></iframe>' },
{ code: '<iframe sandbox="allow-top-navigation"></iframe>' },
{ code: '<iframe sandbox="allow-top-navigation-by-user-activation"></iframe>' },
{ code: '<iframe sandbox="allow-forms allow-modals"></iframe>' },
{ code: '<iframe sandbox="allow-popups allow-popups-to-escape-sandbox allow-pointer-lock allow-same-origin allow-top-navigation"></iframe>' },
{ code: 'React.createElement("iframe", { sandbox: "allow-forms" })' },
{ code: 'React.createElement("iframe", { sandbox: "allow-modals" })' },
{ code: 'React.createElement("iframe", { sandbox: "allow-orientation-lock" })' },
{ code: 'React.createElement("iframe", { sandbox: "allow-pointer-lock" })' },
{ code: 'React.createElement("iframe", { sandbox: "allow-popups" })' },
{ code: 'React.createElement("iframe", { sandbox: "allow-popups-to-escape-sandbox" })' },
{ code: 'React.createElement("iframe", { sandbox: "allow-presentation" })' },
{ code: 'React.createElement("iframe", { sandbox: "allow-same-origin" })' },
{ code: 'React.createElement("iframe", { sandbox: "allow-scripts" })' },
{ code: 'React.createElement("iframe", { sandbox: "allow-top-navigation" })' },
{ code: 'React.createElement("iframe", { sandbox: "allow-top-navigation-by-user-activation" })' },
{ code: 'React.createElement("iframe", { sandbox: "allow-forms allow-modals" })' },
{ code: 'React.createElement("iframe", { sandbox: "allow-popups allow-popups-to-escape-sandbox allow-pointer-lock allow-same-origin allow-top-navigation" })' },
]),
invalid: parsers.all([
{
code: '<iframe></iframe>;',
errors: [{ messageId: 'attributeMissing' }],
},
{
code: '<iframe/>;',
errors: [{ messageId: 'attributeMissing' }],
},
{
code: 'React.createElement("iframe");',
errors: [{ messageId: 'attributeMissing' }],
},
{
code: 'React.createElement("iframe", {});',
errors: [{ messageId: 'attributeMissing' }],
},
{
code: 'React.createElement("iframe", null);',
errors: [{ messageId: 'attributeMissing' }],
},

{
code: '<iframe sandbox="__unknown__"></iframe>',
errors: [{ messageId: 'invalidValue', data: { value: '__unknown__' } }],
},
{
code: 'React.createElement("iframe", { sandbox: "__unknown__" })',
errors: [{ messageId: 'invalidValue', data: { value: '__unknown__' } }],
},

{
code: '<iframe sandbox="allow-popups __unknown__"/>',
errors: [{ messageId: 'invalidValue', data: { value: '__unknown__' } }],
},
{
code: '<iframe sandbox="__unknown__ allow-popups"/>',
errors: [{ messageId: 'invalidValue', data: { value: '__unknown__' } }],
},
{
code: '<iframe sandbox=" allow-forms __unknown__ allow-popups __unknown__ "/>',
errors: [
{ messageId: 'invalidValue', data: { value: '__unknown__' } },
{ messageId: 'invalidValue', data: { value: '__unknown__' } },
],
},
{
code: '<iframe sandbox="allow-scripts allow-same-origin"></iframe>;',
errors: [{ messageId: 'invalidCombination' }],
},
{
code: '<iframe sandbox="allow-same-origin allow-scripts"/>;',
errors: [{ messageId: 'invalidCombination' }],
},
]),
});