Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/fluffy-ways-punch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@biomejs/biome": patch
---

Added the nursery rule [`useIframeSandbox`](https://biomejs.dev/linter/rules/use-iframe-sandbox), which enforces the `sandbox` attribute for `iframe` tags.

**Invalid**:

```html
<iframe></iframe>
```
28 changes: 28 additions & 0 deletions crates/biome_cli/src/execute/migrate/eslint_any_rule_to_biome.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions crates/biome_configuration/src/analyzer/linter/rules.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/biome_diagnostics_categories/src/categories.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

33 changes: 17 additions & 16 deletions crates/biome_html_analyze/src/lint/a11y/use_iframe_title.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use biome_analyze::{
};
use biome_console::markup;
use biome_diagnostics::Severity;
use biome_html_syntax::AnyHtmlElement;
use biome_html_syntax::{AnyHtmlElement, HtmlFileSource};
use biome_rowan::{AstNode, TextRange};
use biome_rule_options::use_iframe_title::UseIframeTitleOptions;

Expand Down Expand Up @@ -59,9 +59,8 @@ impl Rule for UseIframeTitle {

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

if !is_iframe_element(element, file_extension) {
if !is_iframe_element(element, ctx) {
return None;
}

Expand All @@ -88,17 +87,19 @@ impl Rule for UseIframeTitle {
}
}

/// Checks if the element is an iframe element.
///
/// - In `.html` files, matching is case-insensitive.
/// - In component-based frameworks, only lowercase `iframe` is matched to avoid flagging custom components like `<Iframe>`.
fn is_iframe_element(element: &AnyHtmlElement, file_extension: &str) -> bool {
element.name().is_some_and(|token_text| {
let is_html_file = file_extension == "html";
if is_html_file {
token_text.eq_ignore_ascii_case("iframe")
} else {
token_text == "iframe"
}
})
fn is_iframe_element(element: &AnyHtmlElement, ctx: &RuleContext<UseIframeTitle>) -> bool {
let Some(element_name) = element.name() else {
return false;
};

let source_type = ctx.source_type::<HtmlFileSource>();

// In HTML files: case-insensitive (IFRAME, Iframe, iframe all match)
// In component frameworks (Vue, Svelte, Astro): case-sensitive (only "iframe" matches)
// This means <Iframe> in Vue/Svelte is treated as a component and ignored
if source_type.is_html() {
element_name.text().eq_ignore_ascii_case("iframe")
} else {
element_name.text() == "iframe"
}
}
Comment on lines +90 to 105
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will extract this function in a different PR, because it saw more components that could use this. But it will be a bit much for this PR

94 changes: 94 additions & 0 deletions crates/biome_html_analyze/src/lint/nursery/use_iframe_sandbox.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
use biome_analyze::{Ast, Rule, RuleDiagnostic, context::RuleContext, declare_lint_rule};
use biome_console::markup;
use biome_diagnostics::Severity;
use biome_html_syntax::{AnyHtmlElement, HtmlFileSource};
use biome_rowan::AstNode;
use biome_rule_options::use_iframe_sandbox::UseIframeSandboxOptions;

declare_lint_rule! {
/// Enforce the 'sandbox' attribute for 'iframe' elements.
///
/// The sandbox attribute enables an extra set of restrictions for the content in the iframe.
/// Using the sandbox attribute is considered a good security practice.
///
/// See [the Mozilla docs](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/iframe#sandbox) for details.
///
/// ## Examples
///
/// ### Invalid
///
/// ```html,expect_diagnostic
/// <iframe src="https://example.com"></iframe>
/// ```
///
/// ### Valid
///
/// ```html
/// <iframe src="https://example.com" sandbox="allow-popups"></iframe>
/// ```
///
pub UseIframeSandbox {
version: "next",
name: "useIframeSandbox",
language: "html",
recommended: false,
severity: Severity::Warning,
}
}

impl Rule for UseIframeSandbox {
type Query = Ast<AnyHtmlElement>;
type State = ();
type Signals = Option<Self::State>;
type Options = UseIframeSandboxOptions;

fn run(ctx: &RuleContext<Self>) -> Self::Signals {
let element = ctx.query();

if !is_iframe_element(element, ctx) {
return None;
}

if element.find_attribute_by_name("sandbox").is_none() {
return Some(());
}

None
}

fn diagnostic(ctx: &RuleContext<Self>, _state: &Self::State) -> Option<RuleDiagnostic> {
let node = ctx.query();
Some(
RuleDiagnostic::new(
rule_category!(),
node.range(),
markup! {
"Iframe doesn't have the "<Emphasis>"sandbox"</Emphasis>" attribute."
}
)
.note(markup! {
"The sandbox attribute enables an extra set of restrictions for the content in the iframe, protecting against malicious scripts and other security threats."
})
.note(markup! {
"Provide a "<Emphasis>"sandbox"</Emphasis>" attribute when using iframe elements."
}),
)
}
}

fn is_iframe_element(element: &AnyHtmlElement, ctx: &RuleContext<UseIframeSandbox>) -> bool {
let Some(element_name) = element.name() else {
return false;
};

let source_type = ctx.source_type::<HtmlFileSource>();

// In HTML files: case-insensitive (IFRAME, Iframe, iframe all match)
// In component frameworks (Vue, Svelte, Astro): case-sensitive (only "iframe" matches)
// This means <Iframe> in Vue/Svelte is treated as a component and ignored
if source_type.is_html() {
element_name.text().eq_ignore_ascii_case("iframe")
} else {
element_name.text() == "iframe"
}
}
Comment on lines +79 to +94
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same

Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<!-- should generate diagnostics -->
<iframe></iframe>
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
source: crates/biome_html_analyze/tests/spec_tests.rs
expression: invalid.html
---
# Input
```html
<!-- should generate diagnostics -->
<iframe></iframe>

```

# Diagnostics
```
invalid.html:2:1 lint/nursery/useIframeSandbox ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

! Iframe doesn't have the sandbox attribute.

1 │ <!-- should generate diagnostics -->
> 2 │ <iframe></iframe>
│ ^^^^^^^^^^^^^^^^^
3 │

i The sandbox attribute enables an extra set of restrictions for the content in the iframe, protecting against malicious scripts and other security threats.

i Provide a sandbox attribute when using iframe elements.

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.


```
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<!-- should not generate diagnostics -->
<a></a>
<span></span>
<button type="button">Click me</button>
<iframe sandbox></iframe>
<iframe sandbox=""></iframe>
<iframe sandbox="allow-downloads"></iframe>
<iframe sandbox="allow-downloads allow-scripts"></iframe>
<iframe sandbox="allow-downloads allow-scripts allow-forms"></iframe>
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
source: crates/biome_html_analyze/tests/spec_tests.rs
expression: valid.html
---
# Input
```html
<!-- should not generate diagnostics -->
<a></a>
<span></span>
<button type="button">Click me</button>
<iframe sandbox></iframe>
<iframe sandbox=""></iframe>
<iframe sandbox="allow-downloads"></iframe>
<iframe sandbox="allow-downloads allow-scripts"></iframe>
<iframe sandbox="allow-downloads allow-scripts allow-forms"></iframe>

```
Loading
Loading