Skip to content

Commit 30a2985

Browse files
committed
First attempt on iframe-missing-sandbox rule
1 parent d8741de commit 30a2985

File tree

5 files changed

+217
-0
lines changed

5 files changed

+217
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ Enable the rules that you would like to use.
109109
* [react/forbid-foreign-prop-types](docs/rules/forbid-foreign-prop-types.md): Forbid using another component's propTypes
110110
* [react/forbid-prop-types](docs/rules/forbid-prop-types.md): Forbid certain propTypes
111111
* [react/function-component-definition](docs/rules/function-component-definition.md): Standardize the way function component get defined (fixable)
112+
* [react/iframe-missing-sandbox](docs/rules/iframe-missing-sandbox.md): Enforce sandbox attribute on iframe elements
112113
* [react/no-access-state-in-setstate](docs/rules/no-access-state-in-setstate.md): Reports when this.state is accessed within setState
113114
* [react/no-adjacent-inline-elements](docs/rules/no-adjacent-inline-elements.md): Prevent adjacent inline elements not separated by whitespace.
114115
* [react/no-array-index-key](docs/rules/no-array-index-key.md): Prevent usage of Array index in keys
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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://www.w3schools.com/tags/att_iframe_sandbox.asp
6+
7+
## Rule Details
8+
9+
This rule checks all JSX 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 = <iframe></iframe>;
17+
```
18+
19+
The following patterns are **not** considered warnings:
20+
21+
```jsx
22+
var React = require('react');
23+
24+
var Frame = <iframe sandbox="allow-popups"/>;
25+
```
26+
27+
## When not to use
28+
29+
If you don't want to enforce sandbox attribute on iframe elements.

index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const allRules = {
1616
'forbid-foreign-prop-types': require('./lib/rules/forbid-foreign-prop-types'),
1717
'forbid-prop-types': require('./lib/rules/forbid-prop-types'),
1818
'function-component-definition': require('./lib/rules/function-component-definition'),
19+
'iframe-missing-sandbox': require('./lib/rules/iframe-missing-sandbox'),
1920
'jsx-boolean-value': require('./lib/rules/jsx-boolean-value'),
2021
'jsx-child-element-spacing': require('./lib/rules/jsx-child-element-spacing'),
2122
'jsx-closing-bracket-location': require('./lib/rules/jsx-closing-bracket-location'),
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/**
2+
* @fileoverview TBD
3+
*/
4+
5+
'use strict';
6+
7+
const docsUrl = require('../util/docsUrl');
8+
9+
module.exports = {
10+
meta: {
11+
docs: {
12+
description: 'Enforce sandbox attribute on iframe elements',
13+
category: 'Best Practices',
14+
recommended: false,
15+
url: docsUrl('iframe-missing-sandbox')
16+
},
17+
18+
schema: [],
19+
20+
messages: {
21+
attributeMissing: 'An iframe element is missing a sandbox attribute',
22+
invalidValue: 'An iframe element defines a sandbox attribute with invalid value "{{ value }}"',
23+
invalidCombination: 'An iframe element defines a sandbox attribute with both allow-scripts and allow-same-origin which is invalid'
24+
}
25+
},
26+
27+
create(context) {
28+
const ALLOWED_VALUES = [
29+
// From https://www.w3schools.com/tags/att_iframe_sandbox.asp
30+
'',
31+
'allow-forms',
32+
'allow-modals',
33+
'allow-orientation-lock',
34+
'allow-pointer-lock',
35+
'allow-popups',
36+
'allow-popups-to-escape-sandbox',
37+
'allow-presentation',
38+
'allow-same-origin',
39+
'allow-scripts',
40+
'allow-top-navigation',
41+
'allow-top-navigation-by-user-activation'
42+
];
43+
44+
function validateSandboxAttribute(node, attribute) {
45+
const values = attribute.value.value.split(' ');
46+
let allowScripts = false;
47+
let allowSameOrigin = false;
48+
values.forEach((attributeValue) => {
49+
const trimmedAttributeValue = attributeValue.trim();
50+
if (ALLOWED_VALUES.indexOf(trimmedAttributeValue) === -1) {
51+
context.report({
52+
node,
53+
messageId: 'invalidValue',
54+
data: {
55+
value: trimmedAttributeValue
56+
}
57+
});
58+
}
59+
if (trimmedAttributeValue === 'allow-scripts') {
60+
allowScripts = true;
61+
}
62+
if (trimmedAttributeValue === 'allow-same-origin') {
63+
allowSameOrigin = true;
64+
}
65+
});
66+
if (allowScripts && allowSameOrigin) {
67+
context.report({
68+
node,
69+
messageId: 'invalidCombination'
70+
});
71+
}
72+
}
73+
74+
return {
75+
'JSXOpeningElement[name.name="iframe"]'(node) {
76+
let sandboxAttributeFound = false;
77+
node.attributes.forEach((attribute) => {
78+
if (attribute.type === 'JSXAttribute'
79+
&& attribute.name
80+
&& attribute.name.type === 'JSXIdentifier'
81+
&& attribute.name.name === 'sandbox'
82+
) {
83+
sandboxAttributeFound = true;
84+
if (
85+
attribute.value
86+
&& attribute.value.type === 'Literal'
87+
&& attribute.value.value
88+
) {
89+
// Only string literals are supported for now
90+
validateSandboxAttribute(node, attribute);
91+
}
92+
}
93+
});
94+
if (!sandboxAttributeFound) {
95+
context.report({
96+
node,
97+
messageId: 'attributeMissing'
98+
});
99+
}
100+
}
101+
};
102+
}
103+
};
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
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 parserOptions = {
15+
ecmaVersion: 2018,
16+
sourceType: 'module',
17+
ecmaFeatures: {
18+
jsx: true
19+
}
20+
};
21+
22+
// ------------------------------------------------------------------------------
23+
// Tests
24+
// ------------------------------------------------------------------------------
25+
26+
const ruleTester = new RuleTester({parserOptions});
27+
ruleTester.run('iframe-missing-sandbox', rule, {
28+
valid: [
29+
{code: '<div sandbox="__unknown__" />;'},
30+
{code: '<iframe sandbox="" />;'},
31+
{code: '<iframe src="foo.htm" sandbox></iframe>'},
32+
{code: '<iframe src="foo.htm" sandbox sandbox></iframe>'},
33+
{code: '<iframe sandbox={""} />'},
34+
{code: '<iframe sandbox="allow-forms"></iframe>'},
35+
{code: '<iframe sandbox="allow-modals"></iframe>'},
36+
{code: '<iframe sandbox="allow-orientation-lock"></iframe>'},
37+
{code: '<iframe sandbox="allow-pointer-lock"></iframe>'},
38+
{code: '<iframe sandbox="allow-popups"></iframe>'},
39+
{code: '<iframe sandbox="allow-popups-to-escape-sandbox"></iframe>'},
40+
{code: '<iframe sandbox="allow-presentation"></iframe>'},
41+
{code: '<iframe sandbox="allow-same-origin"></iframe>'},
42+
{code: '<iframe sandbox="allow-scripts"></iframe>'},
43+
{code: '<iframe sandbox="allow-top-navigation"></iframe>'},
44+
{code: '<iframe sandbox="allow-top-navigation-by-user-activation"></iframe>'},
45+
{code: '<iframe sandbox="allow-forms allow-modals"></iframe>'},
46+
{code: '<iframe sandbox="allow-popups allow-popups-to-escape-sandbox allow-pointer-lock allow-same-origin allow-top-navigation"></iframe>'}
47+
],
48+
invalid: [{
49+
code: '<iframe></iframe>;',
50+
errors: [{messageId: 'attributeMissing'}]
51+
},
52+
{
53+
code: '<iframe/>;',
54+
errors: [{messageId: 'attributeMissing'}]
55+
},
56+
{
57+
code: '<iframe sandbox="__unknown__"></iframe>',
58+
errors: [{messageId: 'invalidValue', data: {value: '__unknown__'}}]
59+
},
60+
{
61+
code: '<iframe sandbox="allow-popups __unknown__"/>',
62+
errors: [{messageId: 'invalidValue', data: {value: '__unknown__'}}]
63+
},
64+
{
65+
code: '<iframe sandbox="__unknown__ allow-popups"/>',
66+
errors: [{messageId: 'invalidValue', data: {value: '__unknown__'}}]
67+
},
68+
{
69+
code: '<iframe sandbox=" allow-forms __unknown__ allow-popups __unknown__ "/>',
70+
errors: [
71+
{messageId: 'invalidValue', data: {value: '__unknown__'}},
72+
{messageId: 'invalidValue', data: {value: '__unknown__'}}
73+
]
74+
},
75+
{
76+
code: '<iframe sandbox="allow-scripts allow-same-origin"></iframe>;',
77+
errors: [{messageId: 'invalidCombination'}]
78+
},
79+
{
80+
code: '<iframe sandbox="allow-same-origin allow-scripts"/>;',
81+
errors: [{messageId: 'invalidCombination'}]
82+
}]
83+
});

0 commit comments

Comments
 (0)