Skip to content

Commit f0643c4

Browse files
feat(linter): implement jsx-no-script-url (#6995)
#1022 --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 5d65656 commit f0643c4

File tree

3 files changed

+369
-0
lines changed

3 files changed

+369
-0
lines changed

crates/oxc_linter/src/rules.rs

+2
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,7 @@ mod react {
250250
pub mod jsx_key;
251251
pub mod jsx_no_comment_textnodes;
252252
pub mod jsx_no_duplicate_props;
253+
pub mod jsx_no_script_url;
253254
pub mod jsx_no_target_blank;
254255
pub mod jsx_no_undef;
255256
pub mod jsx_no_useless_fragment;
@@ -805,6 +806,7 @@ oxc_macros::declare_all_lint_rules! {
805806
react::jsx_key,
806807
react::jsx_no_comment_textnodes,
807808
react::jsx_no_duplicate_props,
809+
react::jsx_no_script_url,
808810
react::jsx_no_target_blank,
809811
react::jsx_no_undef,
810812
react::jsx_no_useless_fragment,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
use crate::context::ContextHost;
2+
use crate::{context::LintContext, rule::Rule, AstNode};
3+
use lazy_static::lazy_static;
4+
use oxc_ast::ast::JSXAttributeItem;
5+
use oxc_ast::AstKind;
6+
use oxc_diagnostics::OxcDiagnostic;
7+
use oxc_macros::declare_oxc_lint;
8+
use oxc_span::{CompactStr, GetSpan, Span};
9+
use regex::Regex;
10+
use rustc_hash::FxHashMap;
11+
use serde_json::Value;
12+
13+
fn jsx_no_script_url_diagnostic(span: Span) -> OxcDiagnostic {
14+
// See <https://oxc.rs/docs/contribute/linter/adding-rules.html#diagnostics> for details
15+
OxcDiagnostic::warn("A future version of React will block javascript: URLs as a security precaution.")
16+
.with_help("Use event handlers instead if you can. If you need to generate unsafe HTML, try using dangerouslySetInnerHTML instead.")
17+
.with_label(span)
18+
}
19+
20+
lazy_static! {
21+
static ref JS_SCRIPT_REGEX: Regex =
22+
Regex::new(r"(j|J)[\r\n\t]*(a|A)[\r\n\t]*(v|V)[\r\n\t]*(a|A)[\r\n\t]*(s|S)[\r\n\t]*(c|C)[\r\n\t]*(r|R)[\r\n\t]*(i|I)[\r\n\t]*(p|P)[\r\n\t]*(t|T)[\r\n\t]*:").unwrap();
23+
}
24+
25+
#[derive(Debug, Default, Clone)]
26+
pub struct JsxNoScriptUrl(Box<JsxNoScriptUrlConfig>);
27+
28+
#[derive(Debug, Default, Clone)]
29+
pub struct JsxNoScriptUrlConfig {
30+
include_from_settings: bool,
31+
components: FxHashMap<String, Vec<String>>,
32+
}
33+
34+
impl std::ops::Deref for JsxNoScriptUrl {
35+
type Target = JsxNoScriptUrlConfig;
36+
37+
fn deref(&self) -> &Self::Target {
38+
&self.0
39+
}
40+
}
41+
42+
declare_oxc_lint!(
43+
/// ### What it does
44+
///
45+
/// Disallow usage of `javascript:` URLs
46+
///
47+
/// ### Why is this bad?
48+
///
49+
/// URLs starting with javascript: are a dangerous attack surface because it’s easy to accidentally include unsanitized output in a tag like <a href> and create a security hole.
50+
/// In React 16.9 any URLs starting with javascript: scheme log a warning.
51+
/// In a future major release, React will throw an error if it encounters a javascript: URL.
52+
///
53+
/// ### Examples
54+
///
55+
/// Examples of **incorrect** code for this rule:
56+
/// ```jsx
57+
/// <a href="javascript:void(0)">Test</a>
58+
/// ```
59+
///
60+
/// Examples of **correct** code for this rule:
61+
/// ```jsx
62+
/// <Foo test="javascript:void(0)" />
63+
/// ```
64+
JsxNoScriptUrl,
65+
suspicious,
66+
pending
67+
);
68+
69+
fn is_link_attribute(tag_name: &str, prop_value_literal: String, ctx: &LintContext) -> bool {
70+
tag_name == "a"
71+
|| ctx.settings().react.get_link_component_attrs(tag_name).is_some_and(
72+
|link_component_attrs| {
73+
link_component_attrs.contains(&CompactStr::from(prop_value_literal))
74+
},
75+
)
76+
}
77+
78+
impl JsxNoScriptUrl {
79+
fn is_link_tag(&self, tag_name: &str, ctx: &LintContext) -> bool {
80+
if !self.include_from_settings {
81+
return tag_name == "a";
82+
}
83+
if tag_name == "a" {
84+
return true;
85+
}
86+
ctx.settings().react.get_link_component_attrs(tag_name).is_some()
87+
}
88+
}
89+
90+
impl Rule for JsxNoScriptUrl {
91+
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
92+
if let AstKind::JSXOpeningElement(element) = node.kind() {
93+
let Some(component_name) = element.name.get_identifier_name() else {
94+
return;
95+
};
96+
if let Some(link_props) = self.components.get(component_name.as_str()) {
97+
for jsx_attribute in &element.attributes {
98+
if let JSXAttributeItem::Attribute(attr) = jsx_attribute {
99+
let Some(prop_value) = &attr.value else {
100+
return;
101+
};
102+
if prop_value.as_string_literal().is_some_and(|val| {
103+
link_props.contains(&attr.name.get_identifier().name.to_string())
104+
&& JS_SCRIPT_REGEX.captures(&val.value).is_some()
105+
}) {
106+
ctx.diagnostic(jsx_no_script_url_diagnostic(attr.span()));
107+
}
108+
}
109+
}
110+
} else if self.is_link_tag(component_name.as_str(), ctx) {
111+
for jsx_attribute in &element.attributes {
112+
if let JSXAttributeItem::Attribute(attr) = jsx_attribute {
113+
let Some(prop_value) = &attr.value else {
114+
return;
115+
};
116+
if prop_value.as_string_literal().is_some_and(|val| {
117+
is_link_attribute(
118+
component_name.as_str(),
119+
attr.name.get_identifier().name.to_string(),
120+
ctx,
121+
) && JS_SCRIPT_REGEX.captures(&val.value).is_some()
122+
}) {
123+
ctx.diagnostic(jsx_no_script_url_diagnostic(attr.span()));
124+
}
125+
}
126+
}
127+
}
128+
}
129+
}
130+
131+
fn from_configuration(value: Value) -> Self {
132+
let mut components: FxHashMap<String, Vec<String>> = FxHashMap::default();
133+
if let Some(arr) = value.get(0).and_then(Value::as_array) {
134+
for component in arr {
135+
let name = component.get("name").and_then(Value::as_str).unwrap_or("").to_string();
136+
let props =
137+
component.get("props").and_then(Value::as_array).map_or(vec![], |array| {
138+
array
139+
.iter()
140+
.map(|prop| prop.as_str().map_or(String::new(), String::from))
141+
.collect::<Vec<String>>()
142+
});
143+
components.insert(name, props);
144+
}
145+
Self(Box::new(JsxNoScriptUrlConfig {
146+
include_from_settings: value.get(1).is_some_and(|conf| {
147+
conf.get("includeFromSettings").and_then(Value::as_bool).is_some_and(|v| v)
148+
}),
149+
components,
150+
}))
151+
} else {
152+
Self(Box::new(JsxNoScriptUrlConfig {
153+
include_from_settings: value.get(0).is_some_and(|conf| {
154+
conf.get("includeFromSettings").and_then(Value::as_bool).is_some_and(|v| v)
155+
}),
156+
components: FxHashMap::default(),
157+
}))
158+
}
159+
}
160+
161+
fn should_run(&self, ctx: &ContextHost) -> bool {
162+
ctx.source_type().is_jsx()
163+
}
164+
}
165+
166+
#[test]
167+
fn test() {
168+
use crate::tester::Tester;
169+
170+
let pass = vec![
171+
(r#"<a href="https://reactjs.org"></a>"#, None, None),
172+
(r#"<a href="mailto:foo@bar.com"></a>"#, None, None),
173+
(r##"<a href="#"></a>"##, None, None),
174+
(r#"<a href=""></a>"#, None, None),
175+
(r#"<a name="foo"></a>"#, None, None),
176+
(r#"<a href={"javascript:"}></a>"#, None, None),
177+
(r#"<Foo href="javascript:"></Foo>"#, None, None),
178+
("<a href />", None, None),
179+
(
180+
r#"<Foo other="javascript:"></Foo>"#,
181+
Some(serde_json::json!([ [{ "name": "Foo", "props": ["to", "href"] }], ])),
182+
None,
183+
),
184+
(
185+
r#"<Foo href="javascript:"></Foo>"#,
186+
None,
187+
Some(
188+
serde_json::json!({ "settings": {"react": {"linkComponents": [{ "name": "Foo", "linkAttribute": ["to", "href"] }]} } }),
189+
),
190+
),
191+
(
192+
r#"<Foo other="javascript:"></Foo>"#,
193+
Some(serde_json::json!([[], { "includeFromSettings": true }])),
194+
Some(
195+
serde_json::json!({ "settings": {"react": {"linkComponents": [{ "name": "Foo", "linkAttribute": ["to", "href"] }]} } }),
196+
),
197+
),
198+
(
199+
r#"<Foo href="javascript:"></Foo>"#,
200+
Some(serde_json::json!([[], { "includeFromSettings": false }])),
201+
Some(
202+
serde_json::json!({ "settings": {"react": {"linkComponents": [{ "name": "Foo", "linkAttribute": ["to", "href"] }]} } }),
203+
),
204+
),
205+
];
206+
207+
let fail = vec![
208+
(r#"<a href="javascript:"></a>"#, None, None),
209+
(r#"<a href="javascript:void(0)"></a>"#, None, None),
210+
(
211+
r#"<a href="j
212+
213+
214+
a
215+
v ascript:"></a>"#,
216+
None,
217+
None,
218+
),
219+
(
220+
r#"<Foo to="javascript:"></Foo>"#,
221+
Some(serde_json::json!([ [{ "name": "Foo", "props": ["to", "href"] }], ])),
222+
None,
223+
),
224+
(
225+
r#"<Foo href="javascript:"></Foo>"#,
226+
Some(serde_json::json!([ [{ "name": "Foo", "props": ["to", "href"] }], ])),
227+
None,
228+
),
229+
(
230+
r#"<a href="javascript:void(0)"></a>"#,
231+
Some(serde_json::json!([ [{ "name": "Foo", "props": ["to", "href"] }], ])),
232+
None,
233+
),
234+
(
235+
r#"<Foo to="javascript:"></Foo>"#,
236+
Some(
237+
serde_json::json!([ [{ "name": "Bar", "props": ["to", "href"] }], { "includeFromSettings": true }, ]),
238+
),
239+
Some(
240+
serde_json::json!({ "settings": {"react": {"linkComponents": [{ "name": "Foo", "linkAttribute": "to" }]}}}),
241+
),
242+
),
243+
(
244+
r#"<Foo href="javascript:"></Foo>"#,
245+
Some(serde_json::json!([{ "includeFromSettings": true }])),
246+
Some(
247+
serde_json::json!({ "settings": {"react": {"linkComponents": [{ "name": "Foo", "linkAttribute": ["to", "href"] }]} }}),
248+
),
249+
),
250+
(
251+
r#"
252+
<div>
253+
<Foo href="javascript:"></Foo>
254+
<Bar link="javascript:"></Bar>
255+
</div>
256+
"#,
257+
Some(
258+
serde_json::json!([ [{ "name": "Bar", "props": ["link"] }], { "includeFromSettings": true }, ]),
259+
),
260+
Some(
261+
serde_json::json!({ "settings": {"react": {"linkComponents": [{ "name": "Foo", "linkAttribute": ["to", "href"] }]}} }),
262+
),
263+
),
264+
(
265+
r#"
266+
<div>
267+
<Foo href="javascript:"></Foo>
268+
<Bar link="javascript:"></Bar>
269+
</div>
270+
"#,
271+
Some(serde_json::json!([ [{ "name": "Bar", "props": ["link"] }], ])),
272+
Some(
273+
serde_json::json!({ "settings": {"react": {"linkComponents": [{ "name": "Foo", "linkAttribute": ["to", "href"] }]}} }),
274+
),
275+
),
276+
];
277+
278+
Tester::new(JsxNoScriptUrl::NAME, pass, fail).test_and_snapshot();
279+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
---
2+
source: crates/oxc_linter/src/tester.rs
3+
---
4+
eslint-plugin-react(jsx-no-script-url): A future version of React will block javascript: URLs as a security precaution.
5+
╭─[jsx_no_script_url.tsx:1:4]
6+
1<a href="javascript:"></a>
7+
· ──────────────────
8+
╰────
9+
help: Use event handlers instead if you can. If you need to generate unsafe HTML, try using dangerouslySetInnerHTML instead.
10+
11+
eslint-plugin-react(jsx-no-script-url): A future version of React will block javascript: URLs as a security precaution.
12+
╭─[jsx_no_script_url.tsx:1:4]
13+
1<a href="javascript:void(0)"></a>
14+
· ─────────────────────────
15+
╰────
16+
help: Use event handlers instead if you can. If you need to generate unsafe HTML, try using dangerouslySetInnerHTML instead.
17+
18+
eslint-plugin-react(jsx-no-script-url): A future version of React will block javascript: URLs as a security precaution.
19+
╭─[jsx_no_script_url.tsx:1:4]
20+
1 │ ╭─▶ <a href="j
21+
2 │ │
22+
3 │ │
23+
4 │ │ a
24+
5 │ ╰─▶ v ascript:"></a>
25+
╰────
26+
help: Use event handlers instead if you can. If you need to generate unsafe HTML, try using dangerouslySetInnerHTML instead.
27+
28+
eslint-plugin-react(jsx-no-script-url): A future version of React will block javascript: URLs as a security precaution.
29+
╭─[jsx_no_script_url.tsx:1:6]
30+
1<Foo to="javascript:"></Foo>
31+
· ────────────────
32+
╰────
33+
help: Use event handlers instead if you can. If you need to generate unsafe HTML, try using dangerouslySetInnerHTML instead.
34+
35+
eslint-plugin-react(jsx-no-script-url): A future version of React will block javascript: URLs as a security precaution.
36+
╭─[jsx_no_script_url.tsx:1:6]
37+
1<Foo href="javascript:"></Foo>
38+
· ──────────────────
39+
╰────
40+
help: Use event handlers instead if you can. If you need to generate unsafe HTML, try using dangerouslySetInnerHTML instead.
41+
42+
eslint-plugin-react(jsx-no-script-url): A future version of React will block javascript: URLs as a security precaution.
43+
╭─[jsx_no_script_url.tsx:1:4]
44+
1<a href="javascript:void(0)"></a>
45+
· ─────────────────────────
46+
╰────
47+
help: Use event handlers instead if you can. If you need to generate unsafe HTML, try using dangerouslySetInnerHTML instead.
48+
49+
eslint-plugin-react(jsx-no-script-url): A future version of React will block javascript: URLs as a security precaution.
50+
╭─[jsx_no_script_url.tsx:1:6]
51+
1<Foo to="javascript:"></Foo>
52+
· ────────────────
53+
╰────
54+
help: Use event handlers instead if you can. If you need to generate unsafe HTML, try using dangerouslySetInnerHTML instead.
55+
56+
eslint-plugin-react(jsx-no-script-url): A future version of React will block javascript: URLs as a security precaution.
57+
╭─[jsx_no_script_url.tsx:1:6]
58+
1<Foo href="javascript:"></Foo>
59+
· ──────────────────
60+
╰────
61+
help: Use event handlers instead if you can. If you need to generate unsafe HTML, try using dangerouslySetInnerHTML instead.
62+
63+
eslint-plugin-react(jsx-no-script-url): A future version of React will block javascript: URLs as a security precaution.
64+
╭─[jsx_no_script_url.tsx:3:17]
65+
2 │ <div>
66+
3 │ <Foo href="javascript:"></Foo>
67+
· ──────────────────
68+
4 │ <Bar link="javascript:"></Bar>
69+
╰────
70+
help: Use event handlers instead if you can. If you need to generate unsafe HTML, try using dangerouslySetInnerHTML instead.
71+
72+
⚠ eslint-plugin-react(jsx-no-script-url): A future version of React will block javascript: URLs as a security precaution.
73+
╭─[jsx_no_script_url.tsx:4:17]
74+
3 │ <Foo href="javascript:"></Foo>
75+
4 │ <Bar link="javascript:"></Bar>
76+
· ──────────────────
77+
5 │ </div>
78+
╰────
79+
help: Use event handlers instead if you can. If you need to generate unsafe HTML, try using dangerouslySetInnerHTML instead.
80+
81+
eslint-plugin-react(jsx-no-script-url): A future version of React will block javascript: URLs as a security precaution.
82+
╭─[jsx_no_script_url.tsx:4:17]
83+
3<Foo href="javascript:"></Foo>
84+
4<Bar link="javascript:"></Bar>
85+
· ──────────────────
86+
5</div>
87+
╰────
88+
help: Use event handlers instead if you can. If you need to generate unsafe HTML, try using dangerouslySetInnerHTML instead.

0 commit comments

Comments
 (0)