Skip to content
2 changes: 2 additions & 0 deletions crates/oxc_linter/src/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ mod jest {
mod react {
pub mod button_has_type;
pub mod checked_requires_onchange_or_readonly;
pub mod iframe_missing_sandbox;
pub mod jsx_boolean_value;
pub mod jsx_curly_brace_presence;
pub mod jsx_key;
Expand Down Expand Up @@ -771,6 +772,7 @@ oxc_macros::declare_all_lint_rules! {
promise::valid_params,
react::button_has_type,
react::checked_requires_onchange_or_readonly,
react::iframe_missing_sandbox,
react::jsx_boolean_value,
react::jsx_curly_brace_presence,
react::jsx_key,
Expand Down
232 changes: 232 additions & 0 deletions crates/oxc_linter/src/rules/react/iframe_missing_sandbox.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
use oxc_ast::ast::{
Argument, Expression, JSXAttributeItem, JSXAttributeValue, JSXElementName, ObjectProperty,
ObjectPropertyKind, StringLiteral,
};
use oxc_ast::AstKind;
use oxc_diagnostics::OxcDiagnostic;
use oxc_macros::declare_oxc_lint;
use oxc_span::Span;

use crate::utils::{get_prop_value, has_jsx_prop_ignore_case, is_create_element_call};
use crate::{context::LintContext, rule::Rule, AstNode};

fn missing_sandbox_prop(span: Span) -> OxcDiagnostic {
OxcDiagnostic::warn("An iframe element is missing a sandbox attribute")
.with_help("Add a `sandbox` attribute to the `iframe` element.")
.with_label(span)
}

fn invalid_sandbox_prop(span: Span, value: &str) -> OxcDiagnostic {
OxcDiagnostic::warn(format!("An iframe element defines a sandbox attribute with invalid value: {value}"))
.with_help("Change the `sandbox` attribute to one of the allowed values: ``, `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`.")
.with_label(span)
}

fn invalid_sandbox_combination_prop(span: Span) -> OxcDiagnostic {
OxcDiagnostic::warn("An `iframe` element defines a sandbox attribute with both allow-scripts and allow-same-origin which is invalid")
.with_label(span)
}

#[derive(Debug, Default, Clone)]
pub struct IframeMissingSandbox;

declare_oxc_lint!(
/// ### What it does
///
/// Enforce sandbox attribute on iframe elements
///
/// ### Why is this bad?
///
/// The sandbox attribute enables an extra set of restrictions for the content in the iframe. Using sandbox attribute is considered a good security practice.
///
/// ### Examples
///
/// Examples of **incorrect** code for this rule:
/// ```jsx
/// <iframe/>;
/// <iframe sandbox="invalid-value" />;
/// ```
///
/// Examples of **correct** code for this rule:
/// ```jsx
/// <iframe sandbox="" />;
/// <iframe sandbox="allow-origin" />;
/// ```
IframeMissingSandbox,
correctness
);

impl Rule for IframeMissingSandbox {
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
match node.kind() {
AstKind::JSXOpeningElement(jsx_el) => {
let JSXElementName::Identifier(identifier) = &jsx_el.name else {
return;
};

let name = identifier.name.as_str();
if name != "iframe" {
return;
}

has_jsx_prop_ignore_case(jsx_el, "sandbox").map_or_else(
|| {
ctx.diagnostic(missing_sandbox_prop(identifier.span));
},
|sandbox_prop| {
self.validate_sandbox_attribute(sandbox_prop, ctx);
},
);
}
AstKind::CallExpression(call_expr) => {
if is_create_element_call(call_expr) {
let Some(Argument::StringLiteral(str)) = call_expr.arguments.first() else {
return;
};

if str.value.as_str() != "iframe" {
return;
}

if let Some(Argument::ObjectExpression(obj_expr)) = call_expr.arguments.get(1) {
obj_expr
.properties
.iter()
.find_map(|prop| {
if let ObjectPropertyKind::ObjectProperty(prop) = prop {
if prop.key.is_specific_static_name("sandbox") {
return Some(prop);
}
}

None
})
.map_or_else(
|| {
ctx.diagnostic(missing_sandbox_prop(obj_expr.span));
},
|sandbox_prop| {
self.validate_sandbox_property(sandbox_prop, ctx);
},
);
} else {
ctx.diagnostic(missing_sandbox_prop(call_expr.span));
}
}
}
_ => {}
}
}
}

impl IframeMissingSandbox {
#[allow(clippy::unused_self)]
fn get_allowed_values(&self) -> Vec<&str> {
vec![
"",
"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",
]
}

fn validate_sandbox_value(&self, literal: &StringLiteral, ctx: &LintContext) {
let attrs = literal.value.as_str().split(' ').collect::<Vec<&str>>();
let mut allow_same_origin = false;
let mut allow_scripts = false;
for trimmed_atr in attrs.into_iter().map(str::trim) {
if !self.get_allowed_values().contains(&trimmed_atr) {
ctx.diagnostic(invalid_sandbox_prop(literal.span, trimmed_atr));
}
if trimmed_atr == "allow-scripts" {
allow_scripts = true;
}
if trimmed_atr == "allow-same-origin" {
allow_same_origin = true;
}
}
if allow_scripts && allow_same_origin {
ctx.diagnostic(invalid_sandbox_combination_prop(literal.span));
}
}

fn validate_sandbox_property(&self, object_property: &ObjectProperty, ctx: &LintContext) {
if let Expression::StringLiteral(str) = object_property.value.without_parentheses() {
self.validate_sandbox_value(str, ctx);
}
}
fn validate_sandbox_attribute(&self, jsx_el: &JSXAttributeItem, ctx: &LintContext) {
if let Some(JSXAttributeValue::StringLiteral(str)) = get_prop_value(jsx_el) {
self.validate_sandbox_value(str, ctx);
}
}
}

#[test]
fn test() {
use crate::tester::Tester;

let pass = vec![
r#"<div sandbox="__unknown__" />;"#,
r#"<iframe sandbox="" />;"#,
r#"<iframe sandbox={""} />"#,
r#"React.createElement("iframe", { sandbox: "" });"#,
r#"<iframe src="foo.htm" sandbox></iframe>"#,
r#"React.createElement("iframe", { src: "foo.htm", sandbox: true })"#,
r#"<iframe src="foo.htm" sandbox sandbox></iframe>"#,
r#"<iframe sandbox="allow-forms"></iframe>"#,
r#"<iframe sandbox="allow-modals"></iframe>"#,
r#"<iframe sandbox="allow-orientation-lock"></iframe>"#,
r#"<iframe sandbox="allow-pointer-lock"></iframe>"#,
r#"<iframe sandbox="allow-popups"></iframe>"#,
r#"<iframe sandbox="allow-popups-to-escape-sandbox"></iframe>"#,
r#"<iframe sandbox="allow-presentation"></iframe>"#,
r#"<iframe sandbox="allow-same-origin"></iframe>"#,
r#"<iframe sandbox="allow-scripts"></iframe>"#,
r#"<iframe sandbox="allow-top-navigation"></iframe>"#,
r#"<iframe sandbox="allow-top-navigation-by-user-activation"></iframe>"#,
r#"<iframe sandbox="allow-forms allow-modals"></iframe>"#,
r#"<iframe sandbox="allow-popups allow-popups-to-escape-sandbox allow-pointer-lock allow-same-origin allow-top-navigation"></iframe>"#,
r#"React.createElement("iframe", { sandbox: "allow-forms" })"#,
r#"React.createElement("iframe", { sandbox: "allow-modals" })"#,
r#"React.createElement("iframe", { sandbox: "allow-orientation-lock" })"#,
r#"React.createElement("iframe", { sandbox: "allow-pointer-lock" })"#,
r#"React.createElement("iframe", { sandbox: "allow-popups" })"#,
r#"React.createElement("iframe", { sandbox: "allow-popups-to-escape-sandbox" })"#,
r#"React.createElement("iframe", { sandbox: "allow-presentation" })"#,
r#"React.createElement("iframe", { sandbox: "allow-same-origin" })"#,
r#"React.createElement("iframe", { sandbox: "allow-scripts" })"#,
r#"React.createElement("iframe", { sandbox: "allow-top-navigation" })"#,
r#"React.createElement("iframe", { sandbox: "allow-top-navigation-by-user-activation" })"#,
r#"React.createElement("iframe", { sandbox: "allow-forms allow-modals" })"#,
r#"React.createElement("iframe", { sandbox: "allow-popups allow-popups-to-escape-sandbox allow-pointer-lock allow-same-origin allow-top-navigation" })"#,
];

let fail = vec![
"<iframe></iframe>;",
"<iframe/>;",
r#"React.createElement("iframe");"#,
r#"React.createElement("iframe", {});"#,
r#"React.createElement("iframe", null);"#,
r#"<iframe sandbox="__unknown__"></iframe>"#,
r#"React.createElement("iframe", { sandbox: "__unknown__" })"#,
r#"<iframe sandbox="allow-popups __unknown__"/>"#,
r#"<iframe sandbox="__unknown__ allow-popups"/>"#,
r#"<iframe sandbox=" allow-forms __unknown__ allow-popups __unknown__ "/>"#,
r#"<iframe sandbox="allow-scripts allow-same-origin"></iframe>;"#,
r#"<iframe sandbox="allow-same-origin allow-scripts"/>;"#,
];

Tester::new(IframeMissingSandbox::NAME, pass, fail).test_and_snapshot();
}
97 changes: 97 additions & 0 deletions crates/oxc_linter/src/snapshots/iframe_missing_sandbox.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
---
source: crates/oxc_linter/src/tester.rs
---
⚠ eslint-plugin-react(iframe-missing-sandbox): An iframe element is missing a sandbox attribute
╭─[iframe_missing_sandbox.tsx:1:2]
1 │ <iframe></iframe>;
· ──────
╰────
help: Add a `sandbox` attribute to the `iframe` element.

⚠ eslint-plugin-react(iframe-missing-sandbox): An iframe element is missing a sandbox attribute
╭─[iframe_missing_sandbox.tsx:1:2]
1 │ <iframe/>;
· ──────
╰────
help: Add a `sandbox` attribute to the `iframe` element.

⚠ eslint-plugin-react(iframe-missing-sandbox): An iframe element is missing a sandbox attribute
╭─[iframe_missing_sandbox.tsx:1:1]
1 │ React.createElement("iframe");
· ─────────────────────────────
╰────
help: Add a `sandbox` attribute to the `iframe` element.

⚠ eslint-plugin-react(iframe-missing-sandbox): An iframe element is missing a sandbox attribute
╭─[iframe_missing_sandbox.tsx:1:31]
1 │ React.createElement("iframe", {});
· ──
╰────
help: Add a `sandbox` attribute to the `iframe` element.

⚠ eslint-plugin-react(iframe-missing-sandbox): An iframe element is missing a sandbox attribute
╭─[iframe_missing_sandbox.tsx:1:1]
1 │ React.createElement("iframe", null);
· ───────────────────────────────────
╰────
help: Add a `sandbox` attribute to the `iframe` element.

⚠ eslint-plugin-react(iframe-missing-sandbox): An iframe element defines a sandbox attribute with invalid value: __unknown__
╭─[iframe_missing_sandbox.tsx:1:17]
1 │ <iframe sandbox="__unknown__"></iframe>
· ─────────────
╰────
help: Change the `sandbox` attribute to one of the allowed values: ``, `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`.

⚠ eslint-plugin-react(iframe-missing-sandbox): An iframe element defines a sandbox attribute with invalid value: __unknown__
╭─[iframe_missing_sandbox.tsx:1:42]
1 │ React.createElement("iframe", { sandbox: "__unknown__" })
· ─────────────
╰────
help: Change the `sandbox` attribute to one of the allowed values: ``, `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`.

⚠ eslint-plugin-react(iframe-missing-sandbox): An iframe element defines a sandbox attribute with invalid value: __unknown__
╭─[iframe_missing_sandbox.tsx:1:17]
1 │ <iframe sandbox="allow-popups __unknown__"/>
· ──────────────────────────
╰────
help: Change the `sandbox` attribute to one of the allowed values: ``, `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`.

⚠ eslint-plugin-react(iframe-missing-sandbox): An iframe element defines a sandbox attribute with invalid value: __unknown__
╭─[iframe_missing_sandbox.tsx:1:17]
1 │ <iframe sandbox="__unknown__ allow-popups"/>
· ──────────────────────────
╰────
help: Change the `sandbox` attribute to one of the allowed values: ``, `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`.

⚠ eslint-plugin-react(iframe-missing-sandbox): An iframe element defines a sandbox attribute with invalid value: __unknown__
╭─[iframe_missing_sandbox.tsx:1:17]
1 │ <iframe sandbox=" allow-forms __unknown__ allow-popups __unknown__ "/>
· ─────────────────────────────────────────────────────
╰────
help: Change the `sandbox` attribute to one of the allowed values: ``, `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`.

⚠ eslint-plugin-react(iframe-missing-sandbox): An iframe element defines a sandbox attribute with invalid value: __unknown__
╭─[iframe_missing_sandbox.tsx:1:17]
1 │ <iframe sandbox=" allow-forms __unknown__ allow-popups __unknown__ "/>
· ─────────────────────────────────────────────────────
╰────
help: Change the `sandbox` attribute to one of the allowed values: ``, `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`.

⚠ eslint-plugin-react(iframe-missing-sandbox): An `iframe` element defines a sandbox attribute with both allow-scripts and allow-same-origin which is invalid
╭─[iframe_missing_sandbox.tsx:1:17]
1 │ <iframe sandbox="allow-scripts allow-same-origin"></iframe>;
· ─────────────────────────────────
╰────

⚠ eslint-plugin-react(iframe-missing-sandbox): An `iframe` element defines a sandbox attribute with both allow-scripts and allow-same-origin which is invalid
╭─[iframe_missing_sandbox.tsx:1:17]
1 │ <iframe sandbox="allow-same-origin allow-scripts"/>;
· ─────────────────────────────────
╰────