Skip to content

Commit ff31004

Browse files
committed
feat: implement useIframeSandbox
1 parent 322675e commit ff31004

20 files changed

Lines changed: 476 additions & 16 deletions

File tree

.changeset/fluffy-ways-punch.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
5+
Added the nursery rule [`useIframeSandbox`](https://biomejs.dev/linter/rules/use-iframe-sandbox), which enforces the "sandbox" attribute for iframe tags.

crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs

Lines changed: 24 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/biome_configuration/src/analyzer/linter/rules.rs

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/biome_configuration/src/generated/linter_options_check.rs

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/biome_diagnostics_categories/src/categories.rs

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/biome_html_analyze/src/lint/a11y/use_iframe_title.rs

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use biome_analyze::{
33
};
44
use biome_console::markup;
55
use biome_diagnostics::Severity;
6-
use biome_html_syntax::AnyHtmlElement;
6+
use biome_html_syntax::{AnyHtmlElement, HtmlFileSource};
77
use biome_rowan::{AstNode, TextRange};
88
use biome_rule_options::use_iframe_title::UseIframeTitleOptions;
99

@@ -59,9 +59,8 @@ impl Rule for UseIframeTitle {
5959

6060
fn run(ctx: &RuleContext<Self>) -> Self::Signals {
6161
let element = ctx.query();
62-
let file_extension = ctx.file_path().extension()?;
6362

64-
if !is_iframe_element(element, file_extension) {
63+
if !is_iframe_element(element, ctx) {
6564
return None;
6665
}
6766

@@ -88,17 +87,19 @@ impl Rule for UseIframeTitle {
8887
}
8988
}
9089

91-
/// Checks if the element is an iframe element.
92-
///
93-
/// - In `.html` files, matching is case-insensitive.
94-
/// - In component-based frameworks, only lowercase `iframe` is matched to avoid flagging custom components like `<Iframe>`.
95-
fn is_iframe_element(element: &AnyHtmlElement, file_extension: &str) -> bool {
96-
element.name().is_some_and(|token_text| {
97-
let is_html_file = file_extension == "html";
98-
if is_html_file {
99-
token_text.eq_ignore_ascii_case("iframe")
100-
} else {
101-
token_text == "iframe"
102-
}
103-
})
90+
fn is_iframe_element(element: &AnyHtmlElement, ctx: &RuleContext<UseIframeTitle>) -> bool {
91+
let Some(element_name) = element.name() else {
92+
return false;
93+
};
94+
95+
let source_type = ctx.source_type::<HtmlFileSource>();
96+
97+
// In HTML files: case-insensitive (IFRAME, Iframe, iframe all match)
98+
// In component frameworks (Vue, Svelte, Astro): case-sensitive (only "iframe" matches)
99+
// This means <Iframe> in Vue/Svelte is treated as a component and ignored
100+
if source_type.is_html() {
101+
element_name.text().eq_ignore_ascii_case("iframe")
102+
} else {
103+
element_name.text() == "iframe"
104+
}
104105
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
use biome_analyze::{
2+
Ast, Rule, RuleDiagnostic, RuleSource, context::RuleContext, declare_lint_rule,
3+
};
4+
use biome_console::markup;
5+
use biome_diagnostics::Severity;
6+
use biome_html_syntax::{AnyHtmlElement, HtmlFileSource};
7+
use biome_rowan::AstNode;
8+
use biome_rule_options::use_iframe_sandbox::UseIframeSandboxOptions;
9+
10+
declare_lint_rule! {
11+
/// Enforce the 'sandbox' attribute for 'iframe' elements.
12+
///
13+
/// The sandbox attribute enables an extra set of restrictions for the content in the iframe.
14+
/// Using the sandbox attribute is considered a good security practice.
15+
///
16+
/// See [the Mozilla docs](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/iframe#sandbox) for details.
17+
///
18+
/// ## Examples
19+
///
20+
/// ### Invalid
21+
///
22+
/// ```html,expect_diagnostic
23+
/// <iframe src="https://example.com"></iframe>
24+
/// ```
25+
///
26+
/// ### Valid
27+
///
28+
/// ```html
29+
/// <iframe src="https://example.com" sandbox="allow-popups"></iframe>
30+
/// ```
31+
///
32+
pub UseIframeSandbox {
33+
version: "next",
34+
name: "useIframeSandbox",
35+
language: "html",
36+
recommended: false,
37+
severity: Severity::Warning,
38+
sources: &[RuleSource::EslintReactDom("no-missing-iframe-sandbox").same(), RuleSource::EslintReactXyz("dom-no-missing-iframe-sandbox").same()],
39+
}
40+
}
41+
42+
impl Rule for UseIframeSandbox {
43+
type Query = Ast<AnyHtmlElement>;
44+
type State = ();
45+
type Signals = Option<Self::State>;
46+
type Options = UseIframeSandboxOptions;
47+
48+
fn run(ctx: &RuleContext<Self>) -> Self::Signals {
49+
let element = ctx.query();
50+
51+
if !is_iframe_element(element, ctx) {
52+
return None;
53+
}
54+
55+
if element
56+
.find_attribute_by_name("sandbox")
57+
.is_none_or(|sandbox_attribute| sandbox_attribute.value().is_none())
58+
{
59+
return Some(());
60+
}
61+
62+
None
63+
}
64+
65+
fn diagnostic(ctx: &RuleContext<Self>, _state: &Self::State) -> Option<RuleDiagnostic> {
66+
let node = ctx.query();
67+
Some(
68+
RuleDiagnostic::new(
69+
rule_category!(),
70+
node.range(),
71+
markup! {
72+
"Iframe is missing "<Emphasis>"sandbox"</Emphasis>" attribute."
73+
}
74+
)
75+
.note(markup! {
76+
"The sandbox attribute enables an extra set of restrictions for the content in the iframe, protecting against malicious scripts and other security threats."
77+
})
78+
.note(markup! {
79+
"Provide a "<Emphasis>"sandbox"</Emphasis>" attribute when using iframe elements."
80+
}),
81+
)
82+
}
83+
}
84+
85+
fn is_iframe_element(element: &AnyHtmlElement, ctx: &RuleContext<UseIframeSandbox>) -> bool {
86+
let Some(element_name) = element.name() else {
87+
return false;
88+
};
89+
90+
let source_type = ctx.source_type::<HtmlFileSource>();
91+
92+
// In HTML files: case-insensitive (IFRAME, Iframe, iframe all match)
93+
// In component frameworks (Vue, Svelte, Astro): case-sensitive (only "iframe" matches)
94+
// This means <Iframe> in Vue/Svelte is treated as a component and ignored
95+
if source_type.is_html() {
96+
element_name.text().eq_ignore_ascii_case("iframe")
97+
} else {
98+
element_name.text() == "iframe"
99+
}
100+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<!-- should generate diagnostics -->
2+
<iframe></iframe>
3+
<iframe sandbox></iframe>
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
---
2+
source: crates/biome_html_analyze/tests/spec_tests.rs
3+
expression: invalid.html
4+
---
5+
# Input
6+
```html
7+
<!-- should generate diagnostics -->
8+
<iframe></iframe>
9+
<iframe sandbox></iframe>
10+
11+
```
12+
13+
# Diagnostics
14+
```
15+
invalid.html:2:1 lint/nursery/useIframeSandbox ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
16+
17+
! Iframe is missing sandbox attribute.
18+
19+
1 │ <!-- should generate diagnostics -->
20+
> 2 │ <iframe></iframe>
21+
│ ^^^^^^^^^^^^^^^^^
22+
3 │ <iframe sandbox></iframe>
23+
4 │
24+
25+
i The sandbox attribute enables an extra set of restrictions for the content in the iframe, protecting against malicious scripts and other security threats.
26+
27+
i Provide a sandbox attribute when using iframe elements.
28+
29+
i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information.
30+
31+
32+
```
33+
34+
```
35+
invalid.html:3:1 lint/nursery/useIframeSandbox ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
36+
37+
! Iframe is missing sandbox attribute.
38+
39+
1 │ <!-- should generate diagnostics -->
40+
2 │ <iframe></iframe>
41+
> 3 │ <iframe sandbox></iframe>
42+
│ ^^^^^^^^^^^^^^^^^^^^^^^^^
43+
4 │
44+
45+
i The sandbox attribute enables an extra set of restrictions for the content in the iframe, protecting against malicious scripts and other security threats.
46+
47+
i Provide a sandbox attribute when using iframe elements.
48+
49+
i This rule belongs to the nursery group, which means it is not yet stable and may change in the future. Visit https://biomejs.dev/linter/#nursery for more information.
50+
51+
52+
```
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<!-- should not generate diagnostics -->
2+
<a></a>
3+
<span></span>
4+
<button type="button">Click me</button>
5+
<iframe sandbox=""></iframe>
6+
<iframe sandbox="allow-downloads"></iframe>
7+
<iframe sandbox="allow-downloads allow-scripts"></iframe>
8+
<iframe sandbox="allow-downloads allow-scripts allow-forms"></iframe>

0 commit comments

Comments
 (0)