Skip to content

Commit 84c9e08

Browse files
ruidosujeiraautofix-ci[bot]dyc3ematipicoNetail
authored
feat: implement noScriptUrl rule (#8232)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Carson McManus <[email protected]> Co-authored-by: Emanuele Stoppa <[email protected]> Co-authored-by: Maikel van Dort <[email protected]>
1 parent cc2a62e commit 84c9e08

File tree

33 files changed

+1178
-90
lines changed

33 files changed

+1178
-90
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
"@biomejs/biome": patch
3+
---
4+
Added the nursery rule [`noScriptUrl`](https://biomejs.dev/linter/rules/no-script-url/).
5+
6+
This rule disallows the use of `javascript:` URLs, which are considered a form of `eval` and can pose security risks such as XSS vulnerabilities.
7+
8+
```jsx
9+
<a href="javascript:alert('XSS')">Click me</a>
10+
```

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

Lines changed: 60 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: 108 additions & 86 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
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,7 @@ define_categories! {
253253
"lint/security/noDangerouslySetInnerHtml": "https://biomejs.dev/linter/rules/no-dangerously-set-inner-html",
254254
"lint/security/noDangerouslySetInnerHtmlWithChildren": "https://biomejs.dev/linter/rules/no-dangerously-set-inner-html-with-children",
255255
"lint/security/noGlobalEval": "https://biomejs.dev/linter/rules/no-global-eval",
256+
"lint/nursery/noScriptUrl": "https://biomejs.dev/linter/rules/no-script-url",
256257
"lint/security/noSecrets": "https://biomejs.dev/linter/rules/no-secrets",
257258
"lint/style/noCommonJs": "https://biomejs.dev/linter/rules/no-common-js",
258259
"lint/style/noDefaultExport": "https://biomejs.dev/linter/rules/no-default-export",

crates/biome_html_analyze/src/lint/nursery.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
//! Generated file, do not edit by hand, see `xtask/codegen`
44
55
use biome_analyze::declare_lint_group;
6+
pub mod no_script_url;
67
pub mod no_sync_scripts;
78
pub mod no_vue_v_if_with_v_for;
89
pub mod use_vue_hyphenated_attributes;
@@ -13,4 +14,4 @@ pub mod use_vue_valid_v_html;
1314
pub mod use_vue_valid_v_if;
1415
pub mod use_vue_valid_v_on;
1516
pub mod use_vue_valid_v_text;
16-
declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_sync_scripts :: NoSyncScripts , self :: no_vue_v_if_with_v_for :: NoVueVIfWithVFor , self :: use_vue_hyphenated_attributes :: UseVueHyphenatedAttributes , self :: use_vue_valid_v_bind :: UseVueValidVBind , self :: use_vue_valid_v_else :: UseVueValidVElse , self :: use_vue_valid_v_else_if :: UseVueValidVElseIf , self :: use_vue_valid_v_html :: UseVueValidVHtml , self :: use_vue_valid_v_if :: UseVueValidVIf , self :: use_vue_valid_v_on :: UseVueValidVOn , self :: use_vue_valid_v_text :: UseVueValidVText ,] } }
17+
declare_lint_group! { pub Nursery { name : "nursery" , rules : [self :: no_script_url :: NoScriptUrl , self :: no_sync_scripts :: NoSyncScripts , self :: no_vue_v_if_with_v_for :: NoVueVIfWithVFor , self :: use_vue_hyphenated_attributes :: UseVueHyphenatedAttributes , self :: use_vue_valid_v_bind :: UseVueValidVBind , self :: use_vue_valid_v_else :: UseVueValidVElse , self :: use_vue_valid_v_else_if :: UseVueValidVElseIf , self :: use_vue_valid_v_html :: UseVueValidVHtml , self :: use_vue_valid_v_if :: UseVueValidVIf , self :: use_vue_valid_v_on :: UseVueValidVOn , self :: use_vue_valid_v_text :: UseVueValidVText ,] } }
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
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::{AnyHtmlAttributeInitializer, HtmlOpeningElement, inner_string_text};
7+
use biome_rowan::{AstNode, TextRange};
8+
use biome_rule_options::no_script_url::NoScriptUrlOptions;
9+
use biome_string_case::StrOnlyExtension;
10+
11+
declare_lint_rule! {
12+
/// Disallow `javascript:` URLs in HTML.
13+
///
14+
/// Using `javascript:` URLs is considered a form of `eval` and can be a security risk.
15+
/// These URLs can execute arbitrary JavaScript code, which can lead to cross-site scripting (XSS) vulnerabilities.
16+
///
17+
/// ## Examples
18+
///
19+
/// ### Invalid
20+
///
21+
/// ```html,expect_diagnostic
22+
/// <a href="javascript:void(0)">Click me</a>
23+
/// ```
24+
///
25+
/// ```html,expect_diagnostic
26+
/// <a href="javascript:alert('XSS')">Click me</a>
27+
/// ```
28+
///
29+
/// ### Valid
30+
///
31+
/// ```html
32+
/// <a href="https://example.com">Click me</a>
33+
/// <a href="/path/to/page">Click me</a>
34+
/// <a href="#section">Click me</a>
35+
/// <span href="javascript:void(0)">Not a real href</span>
36+
/// ```
37+
///
38+
pub NoScriptUrl {
39+
version: "next",
40+
name: "noScriptUrl",
41+
language: "html",
42+
// Show equivalents from related ecosystems
43+
sources: &[
44+
RuleSource::Eslint("no-script-url").same(),
45+
RuleSource::EslintReact("jsx-no-script-url").same(),
46+
RuleSource::EslintQwik("jsx-no-script-url").same(),
47+
RuleSource::EslintSolid("jsx-no-script-url").same(),
48+
RuleSource::EslintReactXyz("dom-no-script-url").same(),
49+
],
50+
recommended: true,
51+
severity: Severity::Error,
52+
}
53+
}
54+
55+
impl Rule for NoScriptUrl {
56+
type Query = Ast<HtmlOpeningElement>;
57+
type State = TextRange;
58+
type Signals = Option<Self::State>;
59+
type Options = NoScriptUrlOptions;
60+
61+
fn run(ctx: &RuleContext<Self>) -> Option<Self::State> {
62+
let element = ctx.query();
63+
64+
// Only check <a> elements for HTML (unlike JSX where components/custom elements exist)
65+
let name = element.name().ok()?;
66+
let token = name.value_token().ok()?;
67+
let tag = token.text_trimmed();
68+
if !tag.eq_ignore_ascii_case("a") {
69+
return None;
70+
}
71+
72+
let attrs = element.attributes();
73+
let attr = attrs.find_by_name("href")?;
74+
let initializer = attr.initializer()?;
75+
let value = initializer.value().ok()?;
76+
77+
if let AnyHtmlAttributeInitializer::HtmlString(html_string) = value
78+
&& let Ok(token) = html_string.value_token()
79+
{
80+
let inner = inner_string_text(&token);
81+
if inner.trim().to_lowercase_cow().starts_with("javascript:") {
82+
return Some(initializer.range());
83+
}
84+
}
85+
86+
None
87+
}
88+
89+
fn diagnostic(_ctx: &RuleContext<Self>, range: &Self::State) -> Option<RuleDiagnostic> {
90+
Some(
91+
RuleDiagnostic::new(
92+
rule_category!(),
93+
*range,
94+
markup! {
95+
"Avoid using "<Emphasis>"javascript:"</Emphasis>" URLs, as they can be a security risk."
96+
},
97+
)
98+
.note(markup! {
99+
"Using "<Emphasis>"javascript:"</Emphasis>" URLs can lead to security vulnerabilities such as cross-site scripting (XSS)."
100+
})
101+
.note(markup! {
102+
"Consider using regular URLs, or if you need to handle click events, use event handlers instead."
103+
}),
104+
)
105+
}
106+
}

crates/biome_html_analyze/tests/spec_tests.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ use camino::Utf8Path;
1313
use std::ops::Deref;
1414
use std::{fs::read_to_string, slice};
1515

16-
tests_macros::gen_tests! {"tests/specs/**/*.{html,vue,json,jsonc}", crate::run_test, "module"}
17-
tests_macros::gen_tests! {"tests/suppression/**/*.{html,vue,json,jsonc}", crate::run_suppression_test, "module"}
16+
tests_macros::gen_tests! {"tests/specs/**/*.{html,vue,astro,svelte,json,jsonc}", crate::run_test, "module"}
17+
tests_macros::gen_tests! {"tests/suppression/**/*.{html,vue,astro,svelte,json,jsonc}", crate::run_suppression_test, "module"}
1818

1919
fn run_test(input: &'static str, _: &str, _: &str, _: &str) {
2020
register_leak_checker();
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
// Astro invalid cases - should trigger the rule
3+
---
4+
5+
<a href="javascript:void(0)">Void</a>
6+
<a href=" javascript:prompt('XSS') ">Prompt</a>
7+
<a href="JAVASCRIPT:doSomething()">Uppercase</a>
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
---
2+
source: crates/biome_html_analyze/tests/spec_tests.rs
3+
expression: invalid.astro
4+
---
5+
# Input
6+
```html
7+
---
8+
// Astro invalid cases - should trigger the rule
9+
---
10+
11+
<a href="javascript:void(0)">Void</a>
12+
<a href=" javascript:prompt('XSS') ">Prompt</a>
13+
<a href="JAVASCRIPT:doSomething()">Uppercase</a>
14+
15+
```
16+
17+
# Diagnostics
18+
```
19+
invalid.astro:5:8 lint/nursery/noScriptUrl ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
20+
21+
× Avoid using javascript: URLs, as they can be a security risk.
22+
23+
3 │ ---
24+
4 │
25+
> 5 │ <a href="javascript:void(0)">Void</a>
26+
│ ^^^^^^^^^^^^^^^^^^^^^
27+
6 │ <a href=" javascript:prompt('XSS') ">Prompt</a>
28+
7 │ <a href="JAVASCRIPT:doSomething()">Uppercase</a>
29+
30+
i Using javascript: URLs can lead to security vulnerabilities such as cross-site scripting (XSS).
31+
32+
i Consider using regular URLs, or if you need to handle click events, use event handlers instead.
33+
34+
35+
```
36+
37+
```
38+
invalid.astro:6:8 lint/nursery/noScriptUrl ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
39+
40+
× Avoid using javascript: URLs, as they can be a security risk.
41+
42+
5 │ <a href="javascript:void(0)">Void</a>
43+
> 6 │ <a href=" javascript:prompt('XSS') ">Prompt</a>
44+
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
45+
7 │ <a href="JAVASCRIPT:doSomething()">Uppercase</a>
46+
8 │
47+
48+
i Using javascript: URLs can lead to security vulnerabilities such as cross-site scripting (XSS).
49+
50+
i Consider using regular URLs, or if you need to handle click events, use event handlers instead.
51+
52+
53+
```
54+
55+
```
56+
invalid.astro:7:8 lint/nursery/noScriptUrl ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
57+
58+
× Avoid using javascript: URLs, as they can be a security risk.
59+
60+
5 │ <a href="javascript:void(0)">Void</a>
61+
6 │ <a href=" javascript:prompt('XSS') ">Prompt</a>
62+
> 7 │ <a href="JAVASCRIPT:doSomething()">Uppercase</a>
63+
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^
64+
8 │
65+
66+
i Using javascript: URLs can lead to security vulnerabilities such as cross-site scripting (XSS).
67+
68+
i Consider using regular URLs, or if you need to handle click events, use event handlers instead.
69+
70+
71+
```
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<!-- Invalid cases - should trigger the rule -->
2+
3+
<a href="javascript:void(0)">Link</a>
4+
5+
<a href="javascript:alert('XSS')">Link</a>
6+
7+
<a href=" javascript:void(0)">Link</a>
8+
9+
<a href="JAVASCRIPT:void(0)">Link</a>

0 commit comments

Comments
 (0)