From 28c2a286107dd2dc6488f3edcb36ebef4ba10e01 Mon Sep 17 00:00:00 2001 From: Radu Baston Date: Tue, 29 Oct 2024 09:56:08 +0200 Subject: [PATCH 1/6] feat(linter): implement jsx-no-script-url --- crates/oxc_linter/src/rules.rs | 2 + .../src/rules/react/jsx_no_script_url.rs | 266 ++++++++++++++++++ .../src/snapshots/jsx_no_script_url.snap | 77 +++++ 3 files changed, 345 insertions(+) create mode 100644 crates/oxc_linter/src/rules/react/jsx_no_script_url.rs create mode 100644 crates/oxc_linter/src/snapshots/jsx_no_script_url.snap diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index ffe855e5f884f..6cb9bfcaa1aec 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -240,6 +240,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; @@ -778,6 +779,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, diff --git a/crates/oxc_linter/src/rules/react/jsx_no_script_url.rs b/crates/oxc_linter/src/rules/react/jsx_no_script_url.rs new file mode 100644 index 0000000000000..f83adb8c49c52 --- /dev/null +++ b/crates/oxc_linter/src/rules/react/jsx_no_script_url.rs @@ -0,0 +1,266 @@ +use crate::context::ContextHost; +use crate::{context::LintContext, rule::Rule, AstNode}; +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 for details + OxcDiagnostic::warn("A future version of React will block javascript: URLs as a security precaution. Use event handlers instead if you can. If you need to generate unsafe HTML, try using dangerouslySetInnerHTML instead.") + .with_label(span) +} + +const IS_JAVA_SCRIPT_PROTOCOL: &str = 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]*:"; + +#[derive(Debug, Default, Clone)] +pub struct JsxNoScriptUrl(Box); + +#[derive(Debug, Default, Clone)] +pub struct JsxNoScriptUrlConfig { + include_from_settings: bool, + components: FxHashMap>, +} + +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? + /// + /// In React 16.9 any URLs starting with javascript: scheme log a warning. React considers the pattern as a dangerous attack surface, see details. 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 + /// Test + /// ``` + /// + /// Examples of **correct** code for this rule: + /// ```jsx + /// + /// ``` + JsxNoScriptUrl, + suspicious, + pending +); + +impl JsxNoScriptUrl { + fn check_is_link(&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(literal) = &attr.value else { + return; + }; + if literal.as_string_literal().is_some_and(|val| { + let re = Regex::new(IS_JAVA_SCRIPT_PROTOCOL).unwrap(); + link_props.contains(&attr.name.get_identifier().name.to_string()) + && re.captures(&val.value).is_some() + }) { + ctx.diagnostic(jsx_no_script_url_diagnostic(attr.span())); + } + } + } + } else if self.check_is_link(component_name.as_str(), ctx) { + for jsx_attribute in &element.attributes { + if let JSXAttributeItem::Attribute(attr) = jsx_attribute { + let Some(literal) = &attr.value else { + return; + }; + if literal.as_string_literal().is_some_and(|val| { + let re = Regex::new(IS_JAVA_SCRIPT_PROTOCOL).unwrap(); + (component_name.as_str() == "a" + || ctx + .settings() + .react + .get_link_component_attrs(component_name.as_str()) + .is_some_and(|link_component_attrs| { + link_component_attrs.contains(&CompactStr::from( + attr.name.get_identifier().name.to_string(), + )) + })) + && re.captures(&val.value).is_some() + }) { + ctx.diagnostic(jsx_no_script_url_diagnostic(attr.span())); + } + } + } + } + } + } + + fn from_configuration(value: Value) -> Self { + let mut components: FxHashMap> = 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::>() + }); + 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#""#, None, None), + (r#""#, None, None), + (r##""##, None, None), + (r#""#, None, None), + (r#""#, None, None), + (r#""#, None, None), + (r#""#, None, None), + ("", None, None), + ( + r#""#, + None, + Some( + serde_json::json!({ "settings": {"react": {"linkComponents": [{ "name": "Foo", "linkAttribute": ["to", "href"] }]} } }), + ), + ), + ( + r#""#, + Some(serde_json::json!([[], { "includeFromSettings": true }])), + Some( + serde_json::json!({ "settings": {"react": {"linkComponents": [{ "name": "Foo", "linkAttribute": ["to", "href"] }]} } }), + ), + ), + ( + r#""#, + Some(serde_json::json!([[], { "includeFromSettings": false }])), + Some( + serde_json::json!({ "settings": {"react": {"linkComponents": [{ "name": "Foo", "linkAttribute": ["to", "href"] }]} } }), + ), + ), + ]; + + let fail = vec![ + (r#""#, None, None), + (r#""#, None, None), + ( + r#""#, + None, + None, + ), + ( + r#""#, + Some(serde_json::json!([ [{ "name": "Foo", "props": ["to", "href"] }], ])), + None, + ), + ( + r#""#, + Some(serde_json::json!([ [{ "name": "Foo", "props": ["to", "href"] }], ])), + None, + ), + ( + r#""#, + Some(serde_json::json!([ [{ "name": "Foo", "props": ["to", "href"] }], ])), + None, + ), + ( + r#""#, + Some( + serde_json::json!([ [{ "name": "Bar", "props": ["to", "href"] }], { "includeFromSettings": true }, ]), + ), + Some( + serde_json::json!({ "settings": {"react": {"linkComponents": [{ "name": "Foo", "linkAttribute": "to" }]}}}), + ), + ), + ( + r#""#, + Some(serde_json::json!([{ "includeFromSettings": true }])), + Some( + serde_json::json!({ "settings": {"react": {"linkComponents": [{ "name": "Foo", "linkAttribute": ["to", "href"] }]} }}), + ), + ), + ( + r#" +
+ + +
+ "#, + Some( + serde_json::json!([ [{ "name": "Bar", "props": ["link"] }], { "includeFromSettings": true }, ]), + ), + Some( + serde_json::json!({ "settings": {"react": {"linkComponents": [{ "name": "Foo", "linkAttribute": ["to", "href"] }]}} }), + ), + ), + ( + r#" +
+ + +
+ "#, + 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(); +} diff --git a/crates/oxc_linter/src/snapshots/jsx_no_script_url.snap b/crates/oxc_linter/src/snapshots/jsx_no_script_url.snap new file mode 100644 index 0000000000000..86b3eb6bdbb07 --- /dev/null +++ b/crates/oxc_linter/src/snapshots/jsx_no_script_url.snap @@ -0,0 +1,77 @@ +--- +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. Use event handlers instead if you can. If you need to generate unsafe HTML, try using dangerouslySetInnerHTML instead. + ╭─[jsx_no_script_url.tsx:1:4] + 1 │ + · ────────────────── + ╰──── + + ⚠ eslint-plugin-react(jsx-no-script-url): A future version of React will block javascript: URLs as a security precaution. Use event handlers instead if you can. If you need to generate unsafe HTML, try using dangerouslySetInnerHTML instead. + ╭─[jsx_no_script_url.tsx:1:4] + 1 │ + · ───────────────────────── + ╰──── + + ⚠ eslint-plugin-react(jsx-no-script-url): A future version of React will block javascript: URLs as a security precaution. Use event handlers instead if you can. If you need to generate unsafe HTML, try using dangerouslySetInnerHTML instead. + ╭─[jsx_no_script_url.tsx:1:4] + 1 │ ╭─▶ + ╰──── + + ⚠ eslint-plugin-react(jsx-no-script-url): A future version of React will block javascript: URLs as a security precaution. Use event handlers instead if you can. If you need to generate unsafe HTML, try using dangerouslySetInnerHTML instead. + ╭─[jsx_no_script_url.tsx:1:6] + 1 │ + · ──────────────── + ╰──── + + ⚠ eslint-plugin-react(jsx-no-script-url): A future version of React will block javascript: URLs as a security precaution. Use event handlers instead if you can. If you need to generate unsafe HTML, try using dangerouslySetInnerHTML instead. + ╭─[jsx_no_script_url.tsx:1:6] + 1 │ + · ────────────────── + ╰──── + + ⚠ eslint-plugin-react(jsx-no-script-url): A future version of React will block javascript: URLs as a security precaution. Use event handlers instead if you can. If you need to generate unsafe HTML, try using dangerouslySetInnerHTML instead. + ╭─[jsx_no_script_url.tsx:1:4] + 1 │ + · ───────────────────────── + ╰──── + + ⚠ eslint-plugin-react(jsx-no-script-url): A future version of React will block javascript: URLs as a security precaution. Use event handlers instead if you can. If you need to generate unsafe HTML, try using dangerouslySetInnerHTML instead. + ╭─[jsx_no_script_url.tsx:1:6] + 1 │ + · ──────────────── + ╰──── + + ⚠ eslint-plugin-react(jsx-no-script-url): A future version of React will block javascript: URLs as a security precaution. Use event handlers instead if you can. If you need to generate unsafe HTML, try using dangerouslySetInnerHTML instead. + ╭─[jsx_no_script_url.tsx:1:6] + 1 │ + · ────────────────── + ╰──── + + ⚠ eslint-plugin-react(jsx-no-script-url): A future version of React will block javascript: URLs as a security precaution. Use event handlers instead if you can. If you need to generate unsafe HTML, try using dangerouslySetInnerHTML instead. + ╭─[jsx_no_script_url.tsx:3:17] + 2 │
+ 3 │ + · ────────────────── + 4 │ + ╰──── + + ⚠ eslint-plugin-react(jsx-no-script-url): A future version of React will block javascript: URLs as a security precaution. Use event handlers instead if you can. If you need to generate unsafe HTML, try using dangerouslySetInnerHTML instead. + ╭─[jsx_no_script_url.tsx:4:17] + 3 │ + 4 │ + · ────────────────── + 5 │
+ ╰──── + + ⚠ eslint-plugin-react(jsx-no-script-url): A future version of React will block javascript: URLs as a security precaution. Use event handlers instead if you can. If you need to generate unsafe HTML, try using dangerouslySetInnerHTML instead. + ╭─[jsx_no_script_url.tsx:4:17] + 3 │ + 4 │ + · ────────────────── + 5 │ + ╰──── From 2adc6fd81d394c4602e9430106a9946d7a885e2f Mon Sep 17 00:00:00 2001 From: Radu Baston Date: Tue, 29 Oct 2024 15:38:36 +0200 Subject: [PATCH 2/6] Address comments and small refactorizations --- .../src/rules/react/jsx_no_script_url.rs | 39 +++++++++++-------- .../src/snapshots/jsx_no_script_url.snap | 33 ++++++++++------ 2 files changed, 45 insertions(+), 27 deletions(-) diff --git a/crates/oxc_linter/src/rules/react/jsx_no_script_url.rs b/crates/oxc_linter/src/rules/react/jsx_no_script_url.rs index f83adb8c49c52..929db29a00459 100644 --- a/crates/oxc_linter/src/rules/react/jsx_no_script_url.rs +++ b/crates/oxc_linter/src/rules/react/jsx_no_script_url.rs @@ -11,7 +11,8 @@ use serde_json::Value; fn jsx_no_script_url_diagnostic(span: Span) -> OxcDiagnostic { // See for details - OxcDiagnostic::warn("A future version of React will block javascript: URLs as a security precaution. Use event handlers instead if you can. If you need to generate unsafe HTML, try using dangerouslySetInnerHTML instead.") + 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) } @@ -41,7 +42,9 @@ declare_oxc_lint!( /// /// ### Why is this bad? /// - /// In React 16.9 any URLs starting with javascript: scheme log a warning. React considers the pattern as a dangerous attack surface, see details. In a future major release, React will throw an error if it encounters a javascript: URL. + /// URLs starting with javascript: are a dangerous attack surface because it’s easy to accidentally include unsanitized output in a tag like 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 /// @@ -69,6 +72,19 @@ impl JsxNoScriptUrl { } ctx.settings().react.get_link_component_attrs(tag_name).is_some() } + + fn check_is_link_attribute(&self, 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 Rule for JsxNoScriptUrl { @@ -80,10 +96,10 @@ impl Rule for JsxNoScriptUrl { 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(literal) = &attr.value else { + let Some(prop_value) = &attr.value else { return; }; - if literal.as_string_literal().is_some_and(|val| { + if prop_value.as_string_literal().is_some_and(|val| { let re = Regex::new(IS_JAVA_SCRIPT_PROTOCOL).unwrap(); link_props.contains(&attr.name.get_identifier().name.to_string()) && re.captures(&val.value).is_some() @@ -95,21 +111,12 @@ impl Rule for JsxNoScriptUrl { } else if self.check_is_link(component_name.as_str(), ctx) { for jsx_attribute in &element.attributes { if let JSXAttributeItem::Attribute(attr) = jsx_attribute { - let Some(literal) = &attr.value else { + let Some(prop_value) = &attr.value else { return; }; - if literal.as_string_literal().is_some_and(|val| { + if prop_value.as_string_literal().is_some_and(|val| { let re = Regex::new(IS_JAVA_SCRIPT_PROTOCOL).unwrap(); - (component_name.as_str() == "a" - || ctx - .settings() - .react - .get_link_component_attrs(component_name.as_str()) - .is_some_and(|link_component_attrs| { - link_component_attrs.contains(&CompactStr::from( - attr.name.get_identifier().name.to_string(), - )) - })) + self.check_is_link_attribute(component_name.as_str(), attr.name.get_identifier().name.to_string(), ctx) && re.captures(&val.value).is_some() }) { ctx.diagnostic(jsx_no_script_url_diagnostic(attr.span())); diff --git a/crates/oxc_linter/src/snapshots/jsx_no_script_url.snap b/crates/oxc_linter/src/snapshots/jsx_no_script_url.snap index 86b3eb6bdbb07..68dc58dbf73e8 100644 --- a/crates/oxc_linter/src/snapshots/jsx_no_script_url.snap +++ b/crates/oxc_linter/src/snapshots/jsx_no_script_url.snap @@ -1,19 +1,21 @@ --- 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. 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 │ · ────────────────── ╰──── + 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. 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 │ · ───────────────────────── ╰──── + 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. 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 │ ╭─▶ ╰──── + 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. 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 │ · ──────────────── ╰──── + 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. 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 │ · ────────────────── ╰──── + 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. 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 │ · ───────────────────────── ╰──── + 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. 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 │ · ──────────────── ╰──── + 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. 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 │ · ────────────────── ╰──── + 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. 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 │
3 │ · ────────────────── 4 │ ╰──── + 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. 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 │ 4 │ · ────────────────── 5 │
╰──── + 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. 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 │ 4 │ · ────────────────── 5 │ ╰──── + help: Use event handlers instead if you can. If you need to generate unsafe HTML, try using dangerouslySetInnerHTML instead. From 8a64c52a3b579f13aeabcd85eebbe3b21cf29757 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 29 Oct 2024 13:39:59 +0000 Subject: [PATCH 3/6] [autofix.ci] apply automated fixes --- .../src/rules/react/jsx_no_script_url.rs | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/crates/oxc_linter/src/rules/react/jsx_no_script_url.rs b/crates/oxc_linter/src/rules/react/jsx_no_script_url.rs index 929db29a00459..66f9aae0ada6b 100644 --- a/crates/oxc_linter/src/rules/react/jsx_no_script_url.rs +++ b/crates/oxc_linter/src/rules/react/jsx_no_script_url.rs @@ -73,17 +73,18 @@ impl JsxNoScriptUrl { ctx.settings().react.get_link_component_attrs(tag_name).is_some() } - fn check_is_link_attribute(&self, tag_name: &str, prop_value_literal: String, ctx: &LintContext) -> bool { + fn check_is_link_attribute( + &self, + 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, - )) - }) + || ctx.settings().react.get_link_component_attrs(tag_name).is_some_and( + |link_component_attrs| { + link_component_attrs.contains(&CompactStr::from(prop_value_literal)) + }, + ) } } @@ -116,8 +117,11 @@ impl Rule for JsxNoScriptUrl { }; if prop_value.as_string_literal().is_some_and(|val| { let re = Regex::new(IS_JAVA_SCRIPT_PROTOCOL).unwrap(); - self.check_is_link_attribute(component_name.as_str(), attr.name.get_identifier().name.to_string(), ctx) - && re.captures(&val.value).is_some() + self.check_is_link_attribute( + component_name.as_str(), + attr.name.get_identifier().name.to_string(), + ctx, + ) && re.captures(&val.value).is_some() }) { ctx.diagnostic(jsx_no_script_url_diagnostic(attr.span())); } From 1c6df4694f6cad0cd434af25a8c831fdd456df34 Mon Sep 17 00:00:00 2001 From: Radu Baston Date: Tue, 29 Oct 2024 15:44:24 +0200 Subject: [PATCH 4/6] Fix linter --- .../src/rules/react/jsx_no_script_url.rs | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/crates/oxc_linter/src/rules/react/jsx_no_script_url.rs b/crates/oxc_linter/src/rules/react/jsx_no_script_url.rs index 66f9aae0ada6b..11348a4f93b00 100644 --- a/crates/oxc_linter/src/rules/react/jsx_no_script_url.rs +++ b/crates/oxc_linter/src/rules/react/jsx_no_script_url.rs @@ -62,6 +62,15 @@ declare_oxc_lint!( pending ); +fn check_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 check_is_link(&self, tag_name: &str, ctx: &LintContext) -> bool { if !self.include_from_settings { @@ -72,20 +81,6 @@ impl JsxNoScriptUrl { } ctx.settings().react.get_link_component_attrs(tag_name).is_some() } - - fn check_is_link_attribute( - &self, - 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 Rule for JsxNoScriptUrl { @@ -117,7 +112,7 @@ impl Rule for JsxNoScriptUrl { }; if prop_value.as_string_literal().is_some_and(|val| { let re = Regex::new(IS_JAVA_SCRIPT_PROTOCOL).unwrap(); - self.check_is_link_attribute( + check_is_link_attribute( component_name.as_str(), attr.name.get_identifier().name.to_string(), ctx, From 6775cf76b2cf290b8b73d6a5f39d6fa7e6480148 Mon Sep 17 00:00:00 2001 From: Radu Baston Date: Wed, 30 Oct 2024 14:04:54 +0200 Subject: [PATCH 5/6] Use lazy_static! for regex evaluation --- .../src/rules/react/jsx_no_script_url.rs | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/crates/oxc_linter/src/rules/react/jsx_no_script_url.rs b/crates/oxc_linter/src/rules/react/jsx_no_script_url.rs index 11348a4f93b00..0159bf348c612 100644 --- a/crates/oxc_linter/src/rules/react/jsx_no_script_url.rs +++ b/crates/oxc_linter/src/rules/react/jsx_no_script_url.rs @@ -1,5 +1,6 @@ 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; @@ -16,7 +17,10 @@ fn jsx_no_script_url_diagnostic(span: Span) -> OxcDiagnostic { .with_label(span) } -const IS_JAVA_SCRIPT_PROTOCOL: &str = 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]*:"; +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); @@ -62,7 +66,7 @@ declare_oxc_lint!( pending ); -fn check_is_link_attribute(tag_name: &str, prop_value_literal: String, ctx: &LintContext) -> bool { +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| { @@ -72,7 +76,7 @@ fn check_is_link_attribute(tag_name: &str, prop_value_literal: String, ctx: &Lin } impl JsxNoScriptUrl { - fn check_is_link(&self, tag_name: &str, ctx: &LintContext) -> bool { + fn is_link_tag(&self, tag_name: &str, ctx: &LintContext) -> bool { if !self.include_from_settings { return tag_name == "a"; } @@ -96,27 +100,25 @@ impl Rule for JsxNoScriptUrl { return; }; if prop_value.as_string_literal().is_some_and(|val| { - let re = Regex::new(IS_JAVA_SCRIPT_PROTOCOL).unwrap(); link_props.contains(&attr.name.get_identifier().name.to_string()) - && re.captures(&val.value).is_some() + && JS_SCRIPT_REGEX.captures(&val.value).is_some() }) { ctx.diagnostic(jsx_no_script_url_diagnostic(attr.span())); } } } - } else if self.check_is_link(component_name.as_str(), ctx) { + } 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| { - let re = Regex::new(IS_JAVA_SCRIPT_PROTOCOL).unwrap(); - check_is_link_attribute( + is_link_attribute( component_name.as_str(), attr.name.get_identifier().name.to_string(), ctx, - ) && re.captures(&val.value).is_some() + ) && JS_SCRIPT_REGEX.captures(&val.value).is_some() }) { ctx.diagnostic(jsx_no_script_url_diagnostic(attr.span())); } From 47ccbcd52764b6f3990387b83dbdb37eb654256c Mon Sep 17 00:00:00 2001 From: Radu Baston Date: Mon, 4 Nov 2024 13:46:56 +0200 Subject: [PATCH 6/6] Improve tests --- crates/oxc_linter/src/rules/react/jsx_no_script_url.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/oxc_linter/src/rules/react/jsx_no_script_url.rs b/crates/oxc_linter/src/rules/react/jsx_no_script_url.rs index 0159bf348c612..40ebfa68a5a36 100644 --- a/crates/oxc_linter/src/rules/react/jsx_no_script_url.rs +++ b/crates/oxc_linter/src/rules/react/jsx_no_script_url.rs @@ -176,6 +176,11 @@ fn test() { (r#""#, None, None), (r#""#, None, None), ("", None, None), + ( + r#""#, + Some(serde_json::json!([ [{ "name": "Foo", "props": ["to", "href"] }], ])), + None, + ), ( r#""#, None,