mirror of
https://github.com/danbulant/oxc
synced 2026-05-19 04:08:41 +00:00
feat(linter): implement jsx-no-script-url (#6995)
https://github.com/oxc-project/oxc/issues/1022 --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
5d65656517
commit
f0643c4484
3 changed files with 369 additions and 0 deletions
|
|
@ -250,6 +250,7 @@ mod react {
|
|||
pub mod jsx_key;
|
||||
pub mod jsx_no_comment_textnodes;
|
||||
pub mod jsx_no_duplicate_props;
|
||||
pub mod jsx_no_script_url;
|
||||
pub mod jsx_no_target_blank;
|
||||
pub mod jsx_no_undef;
|
||||
pub mod jsx_no_useless_fragment;
|
||||
|
|
@ -805,6 +806,7 @@ oxc_macros::declare_all_lint_rules! {
|
|||
react::jsx_key,
|
||||
react::jsx_no_comment_textnodes,
|
||||
react::jsx_no_duplicate_props,
|
||||
react::jsx_no_script_url,
|
||||
react::jsx_no_target_blank,
|
||||
react::jsx_no_undef,
|
||||
react::jsx_no_useless_fragment,
|
||||
|
|
|
|||
279
crates/oxc_linter/src/rules/react/jsx_no_script_url.rs
Normal file
279
crates/oxc_linter/src/rules/react/jsx_no_script_url.rs
Normal file
|
|
@ -0,0 +1,279 @@
|
|||
use crate::context::ContextHost;
|
||||
use crate::{context::LintContext, rule::Rule, AstNode};
|
||||
use lazy_static::lazy_static;
|
||||
use oxc_ast::ast::JSXAttributeItem;
|
||||
use oxc_ast::AstKind;
|
||||
use oxc_diagnostics::OxcDiagnostic;
|
||||
use oxc_macros::declare_oxc_lint;
|
||||
use oxc_span::{CompactStr, GetSpan, Span};
|
||||
use regex::Regex;
|
||||
use rustc_hash::FxHashMap;
|
||||
use serde_json::Value;
|
||||
|
||||
fn jsx_no_script_url_diagnostic(span: Span) -> OxcDiagnostic {
|
||||
// See <https://oxc.rs/docs/contribute/linter/adding-rules.html#diagnostics> for details
|
||||
OxcDiagnostic::warn("A future version of React will block javascript: URLs as a security precaution.")
|
||||
.with_help("Use event handlers instead if you can. If you need to generate unsafe HTML, try using dangerouslySetInnerHTML instead.")
|
||||
.with_label(span)
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref JS_SCRIPT_REGEX: Regex =
|
||||
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();
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct JsxNoScriptUrl(Box<JsxNoScriptUrlConfig>);
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct JsxNoScriptUrlConfig {
|
||||
include_from_settings: bool,
|
||||
components: FxHashMap<String, Vec<String>>,
|
||||
}
|
||||
|
||||
impl std::ops::Deref for JsxNoScriptUrl {
|
||||
type Target = JsxNoScriptUrlConfig;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
declare_oxc_lint!(
|
||||
/// ### What it does
|
||||
///
|
||||
/// Disallow usage of `javascript:` URLs
|
||||
///
|
||||
/// ### Why is this bad?
|
||||
///
|
||||
/// 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.
|
||||
/// In React 16.9 any URLs starting with javascript: scheme log a warning.
|
||||
/// In a future major release, React will throw an error if it encounters a javascript: URL.
|
||||
///
|
||||
/// ### Examples
|
||||
///
|
||||
/// Examples of **incorrect** code for this rule:
|
||||
/// ```jsx
|
||||
/// <a href="javascript:void(0)">Test</a>
|
||||
/// ```
|
||||
///
|
||||
/// Examples of **correct** code for this rule:
|
||||
/// ```jsx
|
||||
/// <Foo test="javascript:void(0)" />
|
||||
/// ```
|
||||
JsxNoScriptUrl,
|
||||
suspicious,
|
||||
pending
|
||||
);
|
||||
|
||||
fn is_link_attribute(tag_name: &str, prop_value_literal: String, ctx: &LintContext) -> bool {
|
||||
tag_name == "a"
|
||||
|| ctx.settings().react.get_link_component_attrs(tag_name).is_some_and(
|
||||
|link_component_attrs| {
|
||||
link_component_attrs.contains(&CompactStr::from(prop_value_literal))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
impl JsxNoScriptUrl {
|
||||
fn is_link_tag(&self, tag_name: &str, ctx: &LintContext) -> bool {
|
||||
if !self.include_from_settings {
|
||||
return tag_name == "a";
|
||||
}
|
||||
if tag_name == "a" {
|
||||
return true;
|
||||
}
|
||||
ctx.settings().react.get_link_component_attrs(tag_name).is_some()
|
||||
}
|
||||
}
|
||||
|
||||
impl Rule for JsxNoScriptUrl {
|
||||
fn run<'a>(&self, node: &AstNode<'a>, ctx: &LintContext<'a>) {
|
||||
if let AstKind::JSXOpeningElement(element) = node.kind() {
|
||||
let Some(component_name) = element.name.get_identifier_name() else {
|
||||
return;
|
||||
};
|
||||
if let Some(link_props) = self.components.get(component_name.as_str()) {
|
||||
for jsx_attribute in &element.attributes {
|
||||
if let JSXAttributeItem::Attribute(attr) = jsx_attribute {
|
||||
let Some(prop_value) = &attr.value else {
|
||||
return;
|
||||
};
|
||||
if prop_value.as_string_literal().is_some_and(|val| {
|
||||
link_props.contains(&attr.name.get_identifier().name.to_string())
|
||||
&& JS_SCRIPT_REGEX.captures(&val.value).is_some()
|
||||
}) {
|
||||
ctx.diagnostic(jsx_no_script_url_diagnostic(attr.span()));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if self.is_link_tag(component_name.as_str(), ctx) {
|
||||
for jsx_attribute in &element.attributes {
|
||||
if let JSXAttributeItem::Attribute(attr) = jsx_attribute {
|
||||
let Some(prop_value) = &attr.value else {
|
||||
return;
|
||||
};
|
||||
if prop_value.as_string_literal().is_some_and(|val| {
|
||||
is_link_attribute(
|
||||
component_name.as_str(),
|
||||
attr.name.get_identifier().name.to_string(),
|
||||
ctx,
|
||||
) && JS_SCRIPT_REGEX.captures(&val.value).is_some()
|
||||
}) {
|
||||
ctx.diagnostic(jsx_no_script_url_diagnostic(attr.span()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn from_configuration(value: Value) -> Self {
|
||||
let mut components: FxHashMap<String, Vec<String>> = FxHashMap::default();
|
||||
if let Some(arr) = value.get(0).and_then(Value::as_array) {
|
||||
for component in arr {
|
||||
let name = component.get("name").and_then(Value::as_str).unwrap_or("").to_string();
|
||||
let props =
|
||||
component.get("props").and_then(Value::as_array).map_or(vec![], |array| {
|
||||
array
|
||||
.iter()
|
||||
.map(|prop| prop.as_str().map_or(String::new(), String::from))
|
||||
.collect::<Vec<String>>()
|
||||
});
|
||||
components.insert(name, props);
|
||||
}
|
||||
Self(Box::new(JsxNoScriptUrlConfig {
|
||||
include_from_settings: value.get(1).is_some_and(|conf| {
|
||||
conf.get("includeFromSettings").and_then(Value::as_bool).is_some_and(|v| v)
|
||||
}),
|
||||
components,
|
||||
}))
|
||||
} else {
|
||||
Self(Box::new(JsxNoScriptUrlConfig {
|
||||
include_from_settings: value.get(0).is_some_and(|conf| {
|
||||
conf.get("includeFromSettings").and_then(Value::as_bool).is_some_and(|v| v)
|
||||
}),
|
||||
components: FxHashMap::default(),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
fn should_run(&self, ctx: &ContextHost) -> bool {
|
||||
ctx.source_type().is_jsx()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test() {
|
||||
use crate::tester::Tester;
|
||||
|
||||
let pass = vec![
|
||||
(r#"<a href="https://reactjs.org"></a>"#, None, None),
|
||||
(r#"<a href="mailto:foo@bar.com"></a>"#, None, None),
|
||||
(r##"<a href="#"></a>"##, None, None),
|
||||
(r#"<a href=""></a>"#, None, None),
|
||||
(r#"<a name="foo"></a>"#, None, None),
|
||||
(r#"<a href={"javascript:"}></a>"#, None, None),
|
||||
(r#"<Foo href="javascript:"></Foo>"#, None, None),
|
||||
("<a href />", None, None),
|
||||
(
|
||||
r#"<Foo other="javascript:"></Foo>"#,
|
||||
Some(serde_json::json!([ [{ "name": "Foo", "props": ["to", "href"] }], ])),
|
||||
None,
|
||||
),
|
||||
(
|
||||
r#"<Foo href="javascript:"></Foo>"#,
|
||||
None,
|
||||
Some(
|
||||
serde_json::json!({ "settings": {"react": {"linkComponents": [{ "name": "Foo", "linkAttribute": ["to", "href"] }]} } }),
|
||||
),
|
||||
),
|
||||
(
|
||||
r#"<Foo other="javascript:"></Foo>"#,
|
||||
Some(serde_json::json!([[], { "includeFromSettings": true }])),
|
||||
Some(
|
||||
serde_json::json!({ "settings": {"react": {"linkComponents": [{ "name": "Foo", "linkAttribute": ["to", "href"] }]} } }),
|
||||
),
|
||||
),
|
||||
(
|
||||
r#"<Foo href="javascript:"></Foo>"#,
|
||||
Some(serde_json::json!([[], { "includeFromSettings": false }])),
|
||||
Some(
|
||||
serde_json::json!({ "settings": {"react": {"linkComponents": [{ "name": "Foo", "linkAttribute": ["to", "href"] }]} } }),
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
let fail = vec![
|
||||
(r#"<a href="javascript:"></a>"#, None, None),
|
||||
(r#"<a href="javascript:void(0)"></a>"#, None, None),
|
||||
(
|
||||
r#"<a href="j
|
||||
|
||||
|
||||
a
|
||||
v ascript:"></a>"#,
|
||||
None,
|
||||
None,
|
||||
),
|
||||
(
|
||||
r#"<Foo to="javascript:"></Foo>"#,
|
||||
Some(serde_json::json!([ [{ "name": "Foo", "props": ["to", "href"] }], ])),
|
||||
None,
|
||||
),
|
||||
(
|
||||
r#"<Foo href="javascript:"></Foo>"#,
|
||||
Some(serde_json::json!([ [{ "name": "Foo", "props": ["to", "href"] }], ])),
|
||||
None,
|
||||
),
|
||||
(
|
||||
r#"<a href="javascript:void(0)"></a>"#,
|
||||
Some(serde_json::json!([ [{ "name": "Foo", "props": ["to", "href"] }], ])),
|
||||
None,
|
||||
),
|
||||
(
|
||||
r#"<Foo to="javascript:"></Foo>"#,
|
||||
Some(
|
||||
serde_json::json!([ [{ "name": "Bar", "props": ["to", "href"] }], { "includeFromSettings": true }, ]),
|
||||
),
|
||||
Some(
|
||||
serde_json::json!({ "settings": {"react": {"linkComponents": [{ "name": "Foo", "linkAttribute": "to" }]}}}),
|
||||
),
|
||||
),
|
||||
(
|
||||
r#"<Foo href="javascript:"></Foo>"#,
|
||||
Some(serde_json::json!([{ "includeFromSettings": true }])),
|
||||
Some(
|
||||
serde_json::json!({ "settings": {"react": {"linkComponents": [{ "name": "Foo", "linkAttribute": ["to", "href"] }]} }}),
|
||||
),
|
||||
),
|
||||
(
|
||||
r#"
|
||||
<div>
|
||||
<Foo href="javascript:"></Foo>
|
||||
<Bar link="javascript:"></Bar>
|
||||
</div>
|
||||
"#,
|
||||
Some(
|
||||
serde_json::json!([ [{ "name": "Bar", "props": ["link"] }], { "includeFromSettings": true }, ]),
|
||||
),
|
||||
Some(
|
||||
serde_json::json!({ "settings": {"react": {"linkComponents": [{ "name": "Foo", "linkAttribute": ["to", "href"] }]}} }),
|
||||
),
|
||||
),
|
||||
(
|
||||
r#"
|
||||
<div>
|
||||
<Foo href="javascript:"></Foo>
|
||||
<Bar link="javascript:"></Bar>
|
||||
</div>
|
||||
"#,
|
||||
Some(serde_json::json!([ [{ "name": "Bar", "props": ["link"] }], ])),
|
||||
Some(
|
||||
serde_json::json!({ "settings": {"react": {"linkComponents": [{ "name": "Foo", "linkAttribute": ["to", "href"] }]}} }),
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
Tester::new(JsxNoScriptUrl::NAME, pass, fail).test_and_snapshot();
|
||||
}
|
||||
88
crates/oxc_linter/src/snapshots/jsx_no_script_url.snap
Normal file
88
crates/oxc_linter/src/snapshots/jsx_no_script_url.snap
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
---
|
||||
source: crates/oxc_linter/src/tester.rs
|
||||
---
|
||||
⚠ eslint-plugin-react(jsx-no-script-url): A future version of React will block javascript: URLs as a security precaution.
|
||||
╭─[jsx_no_script_url.tsx:1:4]
|
||||
1 │ <a href="javascript:"></a>
|
||||
· ──────────────────
|
||||
╰────
|
||||
help: Use event handlers instead if you can. If you need to generate unsafe HTML, try using dangerouslySetInnerHTML instead.
|
||||
|
||||
⚠ eslint-plugin-react(jsx-no-script-url): A future version of React will block javascript: URLs as a security precaution.
|
||||
╭─[jsx_no_script_url.tsx:1:4]
|
||||
1 │ <a href="javascript:void(0)"></a>
|
||||
· ─────────────────────────
|
||||
╰────
|
||||
help: Use event handlers instead if you can. If you need to generate unsafe HTML, try using dangerouslySetInnerHTML instead.
|
||||
|
||||
⚠ eslint-plugin-react(jsx-no-script-url): A future version of React will block javascript: URLs as a security precaution.
|
||||
╭─[jsx_no_script_url.tsx:1:4]
|
||||
1 │ ╭─▶ <a href="j
|
||||
2 │ │
|
||||
3 │ │
|
||||
4 │ │ a
|
||||
5 │ ╰─▶ v ascript:"></a>
|
||||
╰────
|
||||
help: Use event handlers instead if you can. If you need to generate unsafe HTML, try using dangerouslySetInnerHTML instead.
|
||||
|
||||
⚠ eslint-plugin-react(jsx-no-script-url): A future version of React will block javascript: URLs as a security precaution.
|
||||
╭─[jsx_no_script_url.tsx:1:6]
|
||||
1 │ <Foo to="javascript:"></Foo>
|
||||
· ────────────────
|
||||
╰────
|
||||
help: Use event handlers instead if you can. If you need to generate unsafe HTML, try using dangerouslySetInnerHTML instead.
|
||||
|
||||
⚠ eslint-plugin-react(jsx-no-script-url): A future version of React will block javascript: URLs as a security precaution.
|
||||
╭─[jsx_no_script_url.tsx:1:6]
|
||||
1 │ <Foo href="javascript:"></Foo>
|
||||
· ──────────────────
|
||||
╰────
|
||||
help: Use event handlers instead if you can. If you need to generate unsafe HTML, try using dangerouslySetInnerHTML instead.
|
||||
|
||||
⚠ eslint-plugin-react(jsx-no-script-url): A future version of React will block javascript: URLs as a security precaution.
|
||||
╭─[jsx_no_script_url.tsx:1:4]
|
||||
1 │ <a href="javascript:void(0)"></a>
|
||||
· ─────────────────────────
|
||||
╰────
|
||||
help: Use event handlers instead if you can. If you need to generate unsafe HTML, try using dangerouslySetInnerHTML instead.
|
||||
|
||||
⚠ eslint-plugin-react(jsx-no-script-url): A future version of React will block javascript: URLs as a security precaution.
|
||||
╭─[jsx_no_script_url.tsx:1:6]
|
||||
1 │ <Foo to="javascript:"></Foo>
|
||||
· ────────────────
|
||||
╰────
|
||||
help: Use event handlers instead if you can. If you need to generate unsafe HTML, try using dangerouslySetInnerHTML instead.
|
||||
|
||||
⚠ eslint-plugin-react(jsx-no-script-url): A future version of React will block javascript: URLs as a security precaution.
|
||||
╭─[jsx_no_script_url.tsx:1:6]
|
||||
1 │ <Foo href="javascript:"></Foo>
|
||||
· ──────────────────
|
||||
╰────
|
||||
help: Use event handlers instead if you can. If you need to generate unsafe HTML, try using dangerouslySetInnerHTML instead.
|
||||
|
||||
⚠ eslint-plugin-react(jsx-no-script-url): A future version of React will block javascript: URLs as a security precaution.
|
||||
╭─[jsx_no_script_url.tsx:3:17]
|
||||
2 │ <div>
|
||||
3 │ <Foo href="javascript:"></Foo>
|
||||
· ──────────────────
|
||||
4 │ <Bar link="javascript:"></Bar>
|
||||
╰────
|
||||
help: Use event handlers instead if you can. If you need to generate unsafe HTML, try using dangerouslySetInnerHTML instead.
|
||||
|
||||
⚠ eslint-plugin-react(jsx-no-script-url): A future version of React will block javascript: URLs as a security precaution.
|
||||
╭─[jsx_no_script_url.tsx:4:17]
|
||||
3 │ <Foo href="javascript:"></Foo>
|
||||
4 │ <Bar link="javascript:"></Bar>
|
||||
· ──────────────────
|
||||
5 │ </div>
|
||||
╰────
|
||||
help: Use event handlers instead if you can. If you need to generate unsafe HTML, try using dangerouslySetInnerHTML instead.
|
||||
|
||||
⚠ eslint-plugin-react(jsx-no-script-url): A future version of React will block javascript: URLs as a security precaution.
|
||||
╭─[jsx_no_script_url.tsx:4:17]
|
||||
3 │ <Foo href="javascript:"></Foo>
|
||||
4 │ <Bar link="javascript:"></Bar>
|
||||
· ──────────────────
|
||||
5 │ </div>
|
||||
╰────
|
||||
help: Use event handlers instead if you can. If you need to generate unsafe HTML, try using dangerouslySetInnerHTML instead.
|
||||
Loading…
Reference in a new issue