Skip to content

Commit 454874a

Browse files
radu2147DonIsaac
andauthored
feat(linter): Implement react/iframe-missing-sandbox (#6383)
#1022 --------- Co-authored-by: Don Isaac <donald.isaac@gmail.com>
1 parent a9544ae commit 454874a

File tree

3 files changed

+328
-0
lines changed

3 files changed

+328
-0
lines changed

crates/oxc_linter/src/rules.rs

+2
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,7 @@ mod jest {
234234
mod react {
235235
pub mod button_has_type;
236236
pub mod checked_requires_onchange_or_readonly;
237+
pub mod iframe_missing_sandbox;
237238
pub mod jsx_boolean_value;
238239
pub mod jsx_curly_brace_presence;
239240
pub mod jsx_key;
@@ -771,6 +772,7 @@ oxc_macros::declare_all_lint_rules! {
771772
promise::valid_params,
772773
react::button_has_type,
773774
react::checked_requires_onchange_or_readonly,
775+
react::iframe_missing_sandbox,
774776
react::jsx_boolean_value,
775777
react::jsx_curly_brace_presence,
776778
react::jsx_key,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
use oxc_ast::ast::{
2+
Argument, Expression, JSXAttributeItem, JSXAttributeValue, JSXElementName, ObjectProperty,
3+
ObjectPropertyKind, StringLiteral,
4+
};
5+
use oxc_ast::AstKind;
6+
use oxc_diagnostics::OxcDiagnostic;
7+
use oxc_macros::declare_oxc_lint;
8+
use oxc_span::Span;
9+
use phf::{phf_set, Set};
10+
11+
use crate::utils::{get_prop_value, has_jsx_prop_ignore_case, is_create_element_call};
12+
use crate::{context::LintContext, rule::Rule, AstNode};
13+
14+
fn missing_sandbox_prop(span: Span) -> OxcDiagnostic {
15+
OxcDiagnostic::warn("An iframe element is missing a sandbox attribute")
16+
.with_help("Add a `sandbox` attribute to the `iframe` element.")
17+
.with_label(span)
18+
}
19+
20+
fn invalid_sandbox_prop(span: Span, value: &str) -> OxcDiagnostic {
21+
OxcDiagnostic::warn(format!("An iframe element defines a sandbox attribute with invalid value: {value}"))
22+
.with_help("Check this link for the valid values of `sandbox` attribute: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#sandbox.")
23+
.with_label(span)
24+
}
25+
26+
fn invalid_sandbox_combination_prop(span: Span) -> OxcDiagnostic {
27+
OxcDiagnostic::warn("An `iframe` element defines a sandbox attribute with both allow-scripts and allow-same-origin which is invalid")
28+
.with_help("Remove `allow-scripts` or `allow-same-origin`.")
29+
.with_label(span)
30+
}
31+
32+
const ALLOWED_VALUES: Set<&'static str> = phf_set! {
33+
"",
34+
"allow-downloads-without-user-activation",
35+
"allow-downloads",
36+
"allow-forms",
37+
"allow-modals",
38+
"allow-orientation-lock",
39+
"allow-pointer-lock",
40+
"allow-popups",
41+
"allow-popups-to-escape-sandbox",
42+
"allow-presentation",
43+
"allow-same-origin",
44+
"allow-scripts",
45+
"allow-storage-access-by-user-activation",
46+
"allow-top-navigation",
47+
"allow-top-navigation-by-user-activation"
48+
};
49+
50+
#[derive(Debug, Default, Clone)]
51+
pub struct IframeMissingSandbox;
52+
53+
declare_oxc_lint!(
54+
/// ### What it does
55+
///
56+
/// Enforce sandbox attribute on iframe elements
57+
///
58+
/// ### Why is this bad?
59+
///
60+
/// The sandbox attribute enables an extra set of restrictions for the content in the iframe. Using sandbox attribute is considered a good security practice.
61+
/// To learn more about sandboxing, see [MDN's documentation on the `sandbox` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#sandbox).
62+
63+
///
64+
/// 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.
65+
66+
/// ### Examples
67+
///
68+
/// Examples of **incorrect** code for this rule:
69+
/// ```jsx
70+
/// <iframe/>;
71+
/// <iframe sandbox="invalid-value" />;
72+
/// <iframe sandbox="allow-same-origin allow-scripts"/>;
73+
/// ```
74+
///
75+
/// Examples of **correct** code for this rule:
76+
/// ```jsx
77+
/// <iframe sandbox="" />;
78+
/// <iframe sandbox="allow-origin" />;
79+
/// ```
80+
IframeMissingSandbox,
81+
correctness,
82+
pending
83+
);
84+
85+
impl Rule for IframeMissingSandbox {
86+
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
87+
match node.kind() {
88+
AstKind::JSXOpeningElement(jsx_el) => {
89+
let JSXElementName::Identifier(identifier) = &jsx_el.name else {
90+
return;
91+
};
92+
93+
if identifier.name != "iframe" {
94+
return;
95+
}
96+
97+
has_jsx_prop_ignore_case(jsx_el, "sandbox").map_or_else(
98+
|| {
99+
ctx.diagnostic(missing_sandbox_prop(identifier.span));
100+
},
101+
|sandbox_prop| {
102+
validate_sandbox_attribute(sandbox_prop, ctx);
103+
},
104+
);
105+
}
106+
AstKind::CallExpression(call_expr) => {
107+
if is_create_element_call(call_expr) {
108+
let Some(Argument::StringLiteral(str)) = call_expr.arguments.first() else {
109+
return;
110+
};
111+
112+
if str.value != "iframe" {
113+
return;
114+
}
115+
116+
if let Some(Argument::ObjectExpression(obj_expr)) = call_expr.arguments.get(1) {
117+
obj_expr
118+
.properties
119+
.iter()
120+
.find_map(|prop| {
121+
if let ObjectPropertyKind::ObjectProperty(prop) = prop {
122+
if prop.key.is_specific_static_name("sandbox") {
123+
return Some(prop);
124+
}
125+
}
126+
127+
None
128+
})
129+
.map_or_else(
130+
|| {
131+
ctx.diagnostic(missing_sandbox_prop(obj_expr.span));
132+
},
133+
|sandbox_prop| {
134+
validate_sandbox_property(sandbox_prop, ctx);
135+
},
136+
);
137+
} else {
138+
ctx.diagnostic(missing_sandbox_prop(call_expr.span));
139+
}
140+
}
141+
}
142+
_ => {}
143+
}
144+
}
145+
}
146+
fn validate_sandbox_value(literal: &StringLiteral, ctx: &LintContext) {
147+
let attrs = literal.value.split(' ');
148+
let mut has_allow_same_origin = false;
149+
let mut has_allow_scripts = false;
150+
for trimmed_atr in attrs.into_iter().map(str::trim) {
151+
if !ALLOWED_VALUES.contains(trimmed_atr) {
152+
ctx.diagnostic(invalid_sandbox_prop(literal.span, trimmed_atr));
153+
}
154+
if trimmed_atr == "allow-scripts" {
155+
has_allow_scripts = true;
156+
}
157+
if trimmed_atr == "allow-same-origin" {
158+
has_allow_same_origin = true;
159+
}
160+
}
161+
if has_allow_scripts && has_allow_same_origin {
162+
ctx.diagnostic(invalid_sandbox_combination_prop(literal.span));
163+
}
164+
}
165+
166+
fn validate_sandbox_property(object_property: &ObjectProperty, ctx: &LintContext) {
167+
if let Expression::StringLiteral(str) = object_property.value.without_parentheses() {
168+
validate_sandbox_value(str, ctx);
169+
}
170+
}
171+
fn validate_sandbox_attribute(jsx_el: &JSXAttributeItem, ctx: &LintContext) {
172+
if let Some(JSXAttributeValue::StringLiteral(str)) = get_prop_value(jsx_el) {
173+
validate_sandbox_value(str, ctx);
174+
}
175+
}
176+
177+
#[test]
178+
fn test() {
179+
use crate::tester::Tester;
180+
181+
let pass = vec![
182+
r#"<div sandbox="__unknown__" />;"#,
183+
r#"<iframe sandbox="" />;"#,
184+
r#"<iframe sandbox={""} />"#,
185+
r#"React.createElement("iframe", { sandbox: "" });"#,
186+
r#"<iframe src="foo.htm" sandbox></iframe>"#,
187+
r#"React.createElement("iframe", { src: "foo.htm", sandbox: true })"#,
188+
r#"<iframe src="foo.htm" sandbox sandbox></iframe>"#,
189+
r#"<iframe sandbox="allow-forms"></iframe>"#,
190+
r#"<iframe sandbox="allow-modals"></iframe>"#,
191+
r#"<iframe sandbox="allow-orientation-lock"></iframe>"#,
192+
r#"<iframe sandbox="allow-pointer-lock"></iframe>"#,
193+
r#"<iframe sandbox="allow-popups"></iframe>"#,
194+
r#"<iframe sandbox="allow-popups-to-escape-sandbox"></iframe>"#,
195+
r#"<iframe sandbox="allow-presentation"></iframe>"#,
196+
r#"<iframe sandbox="allow-same-origin"></iframe>"#,
197+
r#"<iframe sandbox="allow-scripts"></iframe>"#,
198+
r#"<iframe sandbox="allow-top-navigation"></iframe>"#,
199+
r#"<iframe sandbox="allow-top-navigation-by-user-activation"></iframe>"#,
200+
r#"<iframe sandbox="allow-forms allow-modals"></iframe>"#,
201+
r#"<iframe sandbox="allow-popups allow-popups-to-escape-sandbox allow-pointer-lock allow-same-origin allow-top-navigation"></iframe>"#,
202+
r#"React.createElement("iframe", { sandbox: "allow-forms" })"#,
203+
r#"React.createElement("iframe", { sandbox: "allow-modals" })"#,
204+
r#"React.createElement("iframe", { sandbox: "allow-orientation-lock" })"#,
205+
r#"React.createElement("iframe", { sandbox: "allow-pointer-lock" })"#,
206+
r#"React.createElement("iframe", { sandbox: "allow-popups" })"#,
207+
r#"React.createElement("iframe", { sandbox: "allow-popups-to-escape-sandbox" })"#,
208+
r#"React.createElement("iframe", { sandbox: "allow-presentation" })"#,
209+
r#"React.createElement("iframe", { sandbox: "allow-same-origin" })"#,
210+
r#"React.createElement("iframe", { sandbox: "allow-scripts" })"#,
211+
r#"React.createElement("iframe", { sandbox: "allow-top-navigation" })"#,
212+
r#"React.createElement("iframe", { sandbox: "allow-top-navigation-by-user-activation" })"#,
213+
r#"React.createElement("iframe", { sandbox: "allow-forms allow-modals" })"#,
214+
r#"React.createElement("iframe", { sandbox: "allow-popups allow-popups-to-escape-sandbox allow-pointer-lock allow-same-origin allow-top-navigation" })"#,
215+
];
216+
217+
let fail = vec![
218+
"<iframe></iframe>;",
219+
"<iframe/>;",
220+
r#"React.createElement("iframe");"#,
221+
r#"React.createElement("iframe", {});"#,
222+
r#"React.createElement("iframe", null);"#,
223+
r#"<iframe sandbox="__unknown__"></iframe>"#,
224+
r#"React.createElement("iframe", { sandbox: "__unknown__" })"#,
225+
r#"<iframe sandbox="allow-popups __unknown__"/>"#,
226+
r#"<iframe sandbox="__unknown__ allow-popups"/>"#,
227+
r#"<iframe sandbox=" allow-forms __unknown__ allow-popups __unknown__ "/>"#,
228+
r#"<iframe sandbox="allow-scripts allow-same-origin"></iframe>;"#,
229+
r#"<iframe sandbox="allow-same-origin allow-scripts"/>;"#,
230+
];
231+
232+
Tester::new(IframeMissingSandbox::NAME, pass, fail).test_and_snapshot();
233+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
---
2+
source: crates/oxc_linter/src/tester.rs
3+
---
4+
eslint-plugin-react(iframe-missing-sandbox): An iframe element is missing a sandbox attribute
5+
╭─[iframe_missing_sandbox.tsx:1:2]
6+
1 │ <iframe></iframe>;
7+
· ──────
8+
╰────
9+
help: Add a `sandbox` attribute to the `iframe` element.
10+
11+
eslint-plugin-react(iframe-missing-sandbox): An iframe element is missing a sandbox attribute
12+
╭─[iframe_missing_sandbox.tsx:1:2]
13+
1<iframe/>;
14+
· ──────
15+
╰────
16+
help: Add a `sandbox` attribute to the `iframe` element.
17+
18+
eslint-plugin-react(iframe-missing-sandbox): An iframe element is missing a sandbox attribute
19+
╭─[iframe_missing_sandbox.tsx:1:1]
20+
1React.createElement("iframe");
21+
· ─────────────────────────────
22+
╰────
23+
help: Add a `sandbox` attribute to the `iframe` element.
24+
25+
eslint-plugin-react(iframe-missing-sandbox): An iframe element is missing a sandbox attribute
26+
╭─[iframe_missing_sandbox.tsx:1:31]
27+
1React.createElement("iframe", {});
28+
· ──
29+
╰────
30+
help: Add a `sandbox` attribute to the `iframe` element.
31+
32+
eslint-plugin-react(iframe-missing-sandbox): An iframe element is missing a sandbox attribute
33+
╭─[iframe_missing_sandbox.tsx:1:1]
34+
1React.createElement("iframe", null);
35+
· ───────────────────────────────────
36+
╰────
37+
help: Add a `sandbox` attribute to the `iframe` element.
38+
39+
eslint-plugin-react(iframe-missing-sandbox): An iframe element defines a sandbox attribute with invalid value: __unknown__
40+
╭─[iframe_missing_sandbox.tsx:1:17]
41+
1<iframe sandbox="__unknown__"></iframe>
42+
· ─────────────
43+
╰────
44+
help: Check this link for the valid values of `sandbox` attribute: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#sandbox.
45+
46+
eslint-plugin-react(iframe-missing-sandbox): An iframe element defines a sandbox attribute with invalid value: __unknown__
47+
╭─[iframe_missing_sandbox.tsx:1:42]
48+
1React.createElement("iframe", { sandbox: "__unknown__" })
49+
· ─────────────
50+
╰────
51+
help: Check this link for the valid values of `sandbox` attribute: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#sandbox.
52+
53+
eslint-plugin-react(iframe-missing-sandbox): An iframe element defines a sandbox attribute with invalid value: __unknown__
54+
╭─[iframe_missing_sandbox.tsx:1:17]
55+
1<iframe sandbox="allow-popups __unknown__"/>
56+
· ──────────────────────────
57+
╰────
58+
help: Check this link for the valid values of `sandbox` attribute: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#sandbox.
59+
60+
eslint-plugin-react(iframe-missing-sandbox): An iframe element defines a sandbox attribute with invalid value: __unknown__
61+
╭─[iframe_missing_sandbox.tsx:1:17]
62+
1<iframe sandbox="__unknown__ allow-popups"/>
63+
· ──────────────────────────
64+
╰────
65+
help: Check this link for the valid values of `sandbox` attribute: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#sandbox.
66+
67+
eslint-plugin-react(iframe-missing-sandbox): An iframe element defines a sandbox attribute with invalid value: __unknown__
68+
╭─[iframe_missing_sandbox.tsx:1:17]
69+
1<iframe sandbox=" allow-forms __unknown__ allow-popups __unknown__ "/>
70+
· ─────────────────────────────────────────────────────
71+
╰────
72+
help: Check this link for the valid values of `sandbox` attribute: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#sandbox.
73+
74+
eslint-plugin-react(iframe-missing-sandbox): An iframe element defines a sandbox attribute with invalid value: __unknown__
75+
╭─[iframe_missing_sandbox.tsx:1:17]
76+
1<iframe sandbox=" allow-forms __unknown__ allow-popups __unknown__ "/>
77+
· ─────────────────────────────────────────────────────
78+
╰────
79+
help: Check this link for the valid values of `sandbox` attribute: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#sandbox.
80+
81+
eslint-plugin-react(iframe-missing-sandbox): An `iframe` element defines a sandbox attribute with both allow-scripts and allow-same-origin which is invalid
82+
╭─[iframe_missing_sandbox.tsx:1:17]
83+
1<iframe sandbox="allow-scripts allow-same-origin"></iframe>;
84+
· ─────────────────────────────────
85+
╰────
86+
help: Remove `allow-scripts` or `allow-same-origin`.
87+
88+
eslint-plugin-react(iframe-missing-sandbox): An `iframe` element defines a sandbox attribute with both allow-scripts and allow-same-origin which is invalid
89+
╭─[iframe_missing_sandbox.tsx:1:17]
90+
1<iframe sandbox="allow-same-origin allow-scripts"/>;
91+
· ─────────────────────────────────
92+
╰────
93+
help: Remove `allow-scripts` or `allow-same-origin`.

0 commit comments

Comments
 (0)