Skip to content

Commit e0ba71d

Browse files
Netailautofix-ci[bot]ematipico
authored
feat: implement useIframeSandbox (#9949)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Emanuele Stoppa <my.burning@gmail.com>
1 parent d6bdf4a commit e0ba71d

File tree

20 files changed

+459
-16
lines changed

20 files changed

+459
-16
lines changed

.changeset/fluffy-ways-punch.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
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.
6+
7+
**Invalid**:
8+
9+
```html
10+
<iframe></iframe>
11+
```

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

Lines changed: 28 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: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
use biome_analyze::{Ast, Rule, RuleDiagnostic, context::RuleContext, declare_lint_rule};
2+
use biome_console::markup;
3+
use biome_diagnostics::Severity;
4+
use biome_html_syntax::{AnyHtmlElement, HtmlFileSource};
5+
use biome_rowan::AstNode;
6+
use biome_rule_options::use_iframe_sandbox::UseIframeSandboxOptions;
7+
8+
declare_lint_rule! {
9+
/// Enforce the 'sandbox' attribute for 'iframe' elements.
10+
///
11+
/// The sandbox attribute enables an extra set of restrictions for the content in the iframe.
12+
/// Using the sandbox attribute is considered a good security practice.
13+
///
14+
/// See [the Mozilla docs](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/iframe#sandbox) for details.
15+
///
16+
/// ## Examples
17+
///
18+
/// ### Invalid
19+
///
20+
/// ```html,expect_diagnostic
21+
/// <iframe src="https://example.com"></iframe>
22+
/// ```
23+
///
24+
/// ### Valid
25+
///
26+
/// ```html
27+
/// <iframe src="https://example.com" sandbox="allow-popups"></iframe>
28+
/// ```
29+
///
30+
pub UseIframeSandbox {
31+
version: "next",
32+
name: "useIframeSandbox",
33+
language: "html",
34+
recommended: false,
35+
severity: Severity::Warning,
36+
}
37+
}
38+
39+
impl Rule for UseIframeSandbox {
40+
type Query = Ast<AnyHtmlElement>;
41+
type State = ();
42+
type Signals = Option<Self::State>;
43+
type Options = UseIframeSandboxOptions;
44+
45+
fn run(ctx: &RuleContext<Self>) -> Self::Signals {
46+
let element = ctx.query();
47+
48+
if !is_iframe_element(element, ctx) {
49+
return None;
50+
}
51+
52+
if element.find_attribute_by_name("sandbox").is_none() {
53+
return Some(());
54+
}
55+
56+
None
57+
}
58+
59+
fn diagnostic(ctx: &RuleContext<Self>, _state: &Self::State) -> Option<RuleDiagnostic> {
60+
let node = ctx.query();
61+
Some(
62+
RuleDiagnostic::new(
63+
rule_category!(),
64+
node.range(),
65+
markup! {
66+
"Iframe doesn't have the "<Emphasis>"sandbox"</Emphasis>" attribute."
67+
}
68+
)
69+
.note(markup! {
70+
"The sandbox attribute enables an extra set of restrictions for the content in the iframe, protecting against malicious scripts and other security threats."
71+
})
72+
.note(markup! {
73+
"Provide a "<Emphasis>"sandbox"</Emphasis>" attribute when using iframe elements."
74+
}),
75+
)
76+
}
77+
}
78+
79+
fn is_iframe_element(element: &AnyHtmlElement, ctx: &RuleContext<UseIframeSandbox>) -> bool {
80+
let Some(element_name) = element.name() else {
81+
return false;
82+
};
83+
84+
let source_type = ctx.source_type::<HtmlFileSource>();
85+
86+
// In HTML files: case-insensitive (IFRAME, Iframe, iframe all match)
87+
// In component frameworks (Vue, Svelte, Astro): case-sensitive (only "iframe" matches)
88+
// This means <Iframe> in Vue/Svelte is treated as a component and ignored
89+
if source_type.is_html() {
90+
element_name.text().eq_ignore_ascii_case("iframe")
91+
} else {
92+
element_name.text() == "iframe"
93+
}
94+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
<!-- should generate diagnostics -->
2+
<iframe></iframe>
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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+
10+
```
11+
12+
# Diagnostics
13+
```
14+
invalid.html:2:1 lint/nursery/useIframeSandbox ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
15+
16+
! Iframe doesn't have the sandbox attribute.
17+
18+
1 │ <!-- should generate diagnostics -->
19+
> 2 │ <iframe></iframe>
20+
│ ^^^^^^^^^^^^^^^^^
21+
3 │
22+
23+
i The sandbox attribute enables an extra set of restrictions for the content in the iframe, protecting against malicious scripts and other security threats.
24+
25+
i Provide a sandbox attribute when using iframe elements.
26+
27+
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.
28+
29+
30+
```
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
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=""></iframe>
7+
<iframe sandbox="allow-downloads"></iframe>
8+
<iframe sandbox="allow-downloads allow-scripts"></iframe>
9+
<iframe sandbox="allow-downloads allow-scripts allow-forms"></iframe>

0 commit comments

Comments
 (0)