Skip to content

Commit d9ccfe4

Browse files
tosmolkaljharb
andcommitted
[New] add iframe-missing-sandbox rule
Co-authored-by: Tobias Smolka <tosmolka@microsoft.com> Co-authored-by: Jordan Harband <ljharb@gmail.com>
1 parent 36482a3 commit d9ccfe4

File tree

6 files changed

+311
-0
lines changed

6 files changed

+311
-0
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
99
* add [`hook-use-state`] rule to enforce symmetric useState hook variable names ([#2921][] @duncanbeevers)
1010
* [`jsx-no-target-blank`]: Improve fixer with option `allowReferrer` ([#3167][] @apepper)
1111
* [`jsx-curly-brace-presence`]: add "propElementValues" config option ([#3191][] @ljharb)
12+
* add [`iframe-missing-sandbox`] rule ([#2753][] @tosmolka @ljharb)
1213

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

4143
## [7.28.0] - 2021.12.22
4244

@@ -3532,6 +3534,7 @@ If you're still not using React 15 you can keep the old behavior by setting the
35323534
[`forbid-prop-types`]: docs/rules/forbid-prop-types.md
35333535
[`function-component-definition`]: docs/rules/function-component-definition.md
35343536
[`hook-use-state`]: docs/rules/hook-use-state.md
3537+
[`iframe-missing-sandbox`]: docs/rules/iframe-missing-sandbox.md
35353538
[`jsx-boolean-value`]: docs/rules/jsx-boolean-value.md
35363539
[`jsx-child-element-spacing`]: docs/rules/jsx-child-element-spacing.md
35373540
[`jsx-closing-bracket-location`]: docs/rules/jsx-closing-bracket-location.md

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ Enable the rules that you would like to use.
132132
| | | [react/forbid-prop-types](docs/rules/forbid-prop-types.md) | Forbid certain propTypes |
133133
| | 🔧 | [react/function-component-definition](docs/rules/function-component-definition.md) | Standardize the way function component get defined |
134134
| | | [react/hook-use-state](docs/rules/hook-use-state.md) | Ensure symmetric naming of useState hook value and setter variables |
135+
| | | [react/iframe-missing-sandbox](docs/rules/iframe-missing-sandbox.md) | Enforce sandbox attribute on iframe elements |
135136
| | | [react/no-access-state-in-setstate](docs/rules/no-access-state-in-setstate.md) | Reports when this.state is accessed within setState |
136137
| | | [react/no-adjacent-inline-elements](docs/rules/no-adjacent-inline-elements.md) | Prevent adjacent inline elements not separated by whitespace. |
137138
| | | [react/no-array-index-key](docs/rules/no-array-index-key.md) | Prevent usage of Array index in keys |

docs/rules/iframe-missing-sandbox.md

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Enforce sandbox attribute on iframe elements (react/iframe-missing-sandbox)
2+
3+
The sandbox attribute enables an extra set of restrictions for the content in the iframe. Using sandbox attribute is considered a good security practice.
4+
5+
See https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#attr-sandbox
6+
7+
## Rule Details
8+
9+
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.
10+
11+
The following patterns are considered warnings:
12+
13+
```jsx
14+
var React = require('react');
15+
16+
var Frame = () => (
17+
<div>
18+
<iframe></iframe>
19+
{React.createElement('iframe')}
20+
</div>
21+
);
22+
```
23+
24+
The following patterns are **not** considered warnings:
25+
26+
```jsx
27+
var React = require('react');
28+
29+
var Frame = <iframe sandbox="allow-popups"/>;
30+
var Frame = () => (
31+
<div>
32+
<iframe sandbox="allow-popups"></iframe>
33+
{React.createElement('iframe', { sandbox: "allow-popups" })}
34+
</div>
35+
);
36+
```
37+
38+
## When not to use
39+
40+
If you don't want to enforce sandbox attribute on iframe elements.

index.js

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const allRules = {
1717
'forbid-prop-types': require('./lib/rules/forbid-prop-types'),
1818
'function-component-definition': require('./lib/rules/function-component-definition'),
1919
'hook-use-state': require('./lib/rules/hook-use-state'),
20+
'iframe-missing-sandbox': require('./lib/rules/iframe-missing-sandbox'),
2021
'jsx-boolean-value': require('./lib/rules/jsx-boolean-value'),
2122
'jsx-child-element-spacing': require('./lib/rules/jsx-child-element-spacing'),
2223
'jsx-closing-bracket-location': require('./lib/rules/jsx-closing-bracket-location'),

lib/rules/iframe-missing-sandbox.js

+142
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
/**
2+
* @fileoverview TBD
3+
*/
4+
5+
'use strict';
6+
7+
const docsUrl = require('../util/docsUrl');
8+
const isCreateElement = require('../util/isCreateElement');
9+
const report = require('../util/report');
10+
11+
const messages = {
12+
attributeMissing: 'An iframe element is missing a sandbox attribute',
13+
invalidValue: 'An iframe element defines a sandbox attribute with invalid value "{{ value }}"',
14+
invalidCombination: 'An iframe element defines a sandbox attribute with both allow-scripts and allow-same-origin which is invalid',
15+
};
16+
17+
const ALLOWED_VALUES = [
18+
// From https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#attr-sandbox
19+
'',
20+
'allow-downloads-without-user-activation',
21+
'allow-downloads',
22+
'allow-forms',
23+
'allow-modals',
24+
'allow-orientation-lock',
25+
'allow-pointer-lock',
26+
'allow-popups',
27+
'allow-popups-to-escape-sandbox',
28+
'allow-presentation',
29+
'allow-same-origin',
30+
'allow-scripts',
31+
'allow-storage-access-by-user-activation',
32+
'allow-top-navigation',
33+
'allow-top-navigation-by-user-activation',
34+
];
35+
36+
function validateSandboxAttribute(context, node, attribute) {
37+
if (typeof attribute !== 'string') {
38+
// Only string literals are supported for now
39+
return;
40+
}
41+
const values = attribute.split(' ');
42+
let allowScripts = false;
43+
let allowSameOrigin = false;
44+
values.forEach((attributeValue) => {
45+
const trimmedAttributeValue = attributeValue.trim();
46+
if (ALLOWED_VALUES.indexOf(trimmedAttributeValue) === -1) {
47+
report(context, messages.invalidValue, 'invalidValue', {
48+
node,
49+
data: {
50+
value: trimmedAttributeValue,
51+
},
52+
});
53+
}
54+
if (trimmedAttributeValue === 'allow-scripts') {
55+
allowScripts = true;
56+
}
57+
if (trimmedAttributeValue === 'allow-same-origin') {
58+
allowSameOrigin = true;
59+
}
60+
});
61+
if (allowScripts && allowSameOrigin) {
62+
report(context, messages.invalidCombination, 'invalidCombination', {
63+
node,
64+
});
65+
}
66+
}
67+
68+
function checkAttributes(context, node) {
69+
let sandboxAttributeFound = false;
70+
node.attributes.forEach((attribute) => {
71+
if (attribute.type === 'JSXAttribute'
72+
&& attribute.name
73+
&& attribute.name.type === 'JSXIdentifier'
74+
&& attribute.name.name === 'sandbox'
75+
) {
76+
sandboxAttributeFound = true;
77+
if (
78+
attribute.value
79+
&& attribute.value.type === 'Literal'
80+
&& attribute.value.value
81+
) {
82+
validateSandboxAttribute(context, node, attribute.value.value);
83+
}
84+
}
85+
});
86+
if (!sandboxAttributeFound) {
87+
report(context, messages.attributeMissing, 'attributeMissing', {
88+
node,
89+
});
90+
}
91+
}
92+
93+
function checkProps(context, node) {
94+
let sandboxAttributeFound = false;
95+
if (node.arguments.length > 1) {
96+
const props = node.arguments[1];
97+
const sandboxProp = props.properties && props.properties.find((x) => x.type === 'Property' && x.key.name === 'sandbox');
98+
if (sandboxProp) {
99+
sandboxAttributeFound = true;
100+
if (sandboxProp.value && sandboxProp.value.type === 'Literal' && sandboxProp.value.value) {
101+
validateSandboxAttribute(context, node, sandboxProp.value.value);
102+
}
103+
}
104+
}
105+
if (!sandboxAttributeFound) {
106+
report(context, messages.attributeMissing, 'attributeMissing', {
107+
node,
108+
});
109+
}
110+
}
111+
112+
module.exports = {
113+
meta: {
114+
docs: {
115+
description: 'Enforce sandbox attribute on iframe elements',
116+
category: 'Best Practices',
117+
recommended: false,
118+
url: docsUrl('iframe-missing-sandbox'),
119+
},
120+
121+
schema: [],
122+
123+
messages,
124+
},
125+
126+
create(context) {
127+
return {
128+
'JSXOpeningElement[name.name="iframe"]'(node) {
129+
checkAttributes(context, node);
130+
},
131+
132+
CallExpression(node) {
133+
if (isCreateElement(node, context) && node.arguments && node.arguments.length > 0) {
134+
const tag = node.arguments[0];
135+
if (tag.type === 'Literal' && tag.value === 'iframe') {
136+
checkProps(context, node);
137+
}
138+
}
139+
},
140+
};
141+
},
142+
};
+124
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
/**
2+
* @fileoverview TBD
3+
*/
4+
5+
'use strict';
6+
7+
// ------------------------------------------------------------------------------
8+
// Requirements
9+
// ------------------------------------------------------------------------------
10+
11+
const RuleTester = require('eslint').RuleTester;
12+
const rule = require('../../../lib/rules/iframe-missing-sandbox');
13+
14+
const parsers = require('../../helpers/parsers');
15+
16+
const parserOptions = {
17+
ecmaVersion: 2018,
18+
sourceType: 'module',
19+
ecmaFeatures: {
20+
jsx: true,
21+
},
22+
};
23+
24+
// ------------------------------------------------------------------------------
25+
// Tests
26+
// ------------------------------------------------------------------------------
27+
28+
const ruleTester = new RuleTester({ parserOptions });
29+
ruleTester.run('iframe-missing-sandbox', rule, {
30+
valid: parsers.all([
31+
{ code: '<div sandbox="__unknown__" />;' },
32+
33+
{ code: '<iframe sandbox="" />;' },
34+
{ code: '<iframe sandbox={""} />' },
35+
{ code: 'React.createElement("iframe", { sandbox: "" });' },
36+
37+
{ code: '<iframe src="foo.htm" sandbox></iframe>' },
38+
{ code: 'React.createElement("iframe", { src: "foo.htm", sandbox: true })' },
39+
40+
{ code: '<iframe src="foo.htm" sandbox sandbox></iframe>' },
41+
42+
{ code: '<iframe sandbox="allow-forms"></iframe>' },
43+
{ code: '<iframe sandbox="allow-modals"></iframe>' },
44+
{ code: '<iframe sandbox="allow-orientation-lock"></iframe>' },
45+
{ code: '<iframe sandbox="allow-pointer-lock"></iframe>' },
46+
{ code: '<iframe sandbox="allow-popups"></iframe>' },
47+
{ code: '<iframe sandbox="allow-popups-to-escape-sandbox"></iframe>' },
48+
{ code: '<iframe sandbox="allow-presentation"></iframe>' },
49+
{ code: '<iframe sandbox="allow-same-origin"></iframe>' },
50+
{ code: '<iframe sandbox="allow-scripts"></iframe>' },
51+
{ code: '<iframe sandbox="allow-top-navigation"></iframe>' },
52+
{ code: '<iframe sandbox="allow-top-navigation-by-user-activation"></iframe>' },
53+
{ code: '<iframe sandbox="allow-forms allow-modals"></iframe>' },
54+
{ code: '<iframe sandbox="allow-popups allow-popups-to-escape-sandbox allow-pointer-lock allow-same-origin allow-top-navigation"></iframe>' },
55+
{ code: 'React.createElement("iframe", { sandbox: "allow-forms" })' },
56+
{ code: 'React.createElement("iframe", { sandbox: "allow-modals" })' },
57+
{ code: 'React.createElement("iframe", { sandbox: "allow-orientation-lock" })' },
58+
{ code: 'React.createElement("iframe", { sandbox: "allow-pointer-lock" })' },
59+
{ code: 'React.createElement("iframe", { sandbox: "allow-popups" })' },
60+
{ code: 'React.createElement("iframe", { sandbox: "allow-popups-to-escape-sandbox" })' },
61+
{ code: 'React.createElement("iframe", { sandbox: "allow-presentation" })' },
62+
{ code: 'React.createElement("iframe", { sandbox: "allow-same-origin" })' },
63+
{ code: 'React.createElement("iframe", { sandbox: "allow-scripts" })' },
64+
{ code: 'React.createElement("iframe", { sandbox: "allow-top-navigation" })' },
65+
{ code: 'React.createElement("iframe", { sandbox: "allow-top-navigation-by-user-activation" })' },
66+
{ code: 'React.createElement("iframe", { sandbox: "allow-forms allow-modals" })' },
67+
{ code: 'React.createElement("iframe", { sandbox: "allow-popups allow-popups-to-escape-sandbox allow-pointer-lock allow-same-origin allow-top-navigation" })' },
68+
]),
69+
invalid: parsers.all([
70+
{
71+
code: '<iframe></iframe>;',
72+
errors: [{ messageId: 'attributeMissing' }],
73+
},
74+
{
75+
code: '<iframe/>;',
76+
errors: [{ messageId: 'attributeMissing' }],
77+
},
78+
{
79+
code: 'React.createElement("iframe");',
80+
errors: [{ messageId: 'attributeMissing' }],
81+
},
82+
{
83+
code: 'React.createElement("iframe", {});',
84+
errors: [{ messageId: 'attributeMissing' }],
85+
},
86+
{
87+
code: 'React.createElement("iframe", null);',
88+
errors: [{ messageId: 'attributeMissing' }],
89+
},
90+
91+
{
92+
code: '<iframe sandbox="__unknown__"></iframe>',
93+
errors: [{ messageId: 'invalidValue', data: { value: '__unknown__' } }],
94+
},
95+
{
96+
code: 'React.createElement("iframe", { sandbox: "__unknown__" })',
97+
errors: [{ messageId: 'invalidValue', data: { value: '__unknown__' } }],
98+
},
99+
100+
{
101+
code: '<iframe sandbox="allow-popups __unknown__"/>',
102+
errors: [{ messageId: 'invalidValue', data: { value: '__unknown__' } }],
103+
},
104+
{
105+
code: '<iframe sandbox="__unknown__ allow-popups"/>',
106+
errors: [{ messageId: 'invalidValue', data: { value: '__unknown__' } }],
107+
},
108+
{
109+
code: '<iframe sandbox=" allow-forms __unknown__ allow-popups __unknown__ "/>',
110+
errors: [
111+
{ messageId: 'invalidValue', data: { value: '__unknown__' } },
112+
{ messageId: 'invalidValue', data: { value: '__unknown__' } },
113+
],
114+
},
115+
{
116+
code: '<iframe sandbox="allow-scripts allow-same-origin"></iframe>;',
117+
errors: [{ messageId: 'invalidCombination' }],
118+
},
119+
{
120+
code: '<iframe sandbox="allow-same-origin allow-scripts"/>;',
121+
errors: [{ messageId: 'invalidCombination' }],
122+
},
123+
]),
124+
});

0 commit comments

Comments
 (0)